UNPKG

8.89 kBJavaScriptView Raw
1/*
2 * Copyright 2020 The Backstage Authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17const { join: joinPath } = require('path');
18
19/**
20 * Creates a ESLint configuration that extends the base Backstage configuration.
21 * In addition to the standard ESLint configuration options, the `extraConfig`
22 * parameter also accepts the following keys:
23 *
24 * - `tsRules`: Additional ESLint rules to apply to TypeScript
25 * - `testRules`: Additional ESLint rules to apply to tests
26 * - `restrictedImports`: Additional paths to add to no-restricted-imports
27 * - `restrictedSrcImports`: Additional paths to add to no-restricted-imports in src files
28 * - `restrictedTestImports`: Additional paths to add to no-restricted-imports in test files
29 * - `restrictedSyntax`: Additional patterns to add to no-restricted-syntax
30 * - `restrictedSrcSyntax`: Additional patterns to add to no-restricted-syntax in src files
31 * - `restrictedTestSyntax`: Additional patterns to add to no-restricted-syntax in test files
32 */
33function createConfig(dir, extraConfig = {}) {
34 const {
35 extends: extraExtends,
36 plugins,
37 env,
38 parserOptions,
39 ignorePatterns,
40 overrides,
41
42 rules,
43 tsRules,
44 testRules,
45
46 restrictedImports,
47 restrictedSrcImports,
48 restrictedTestImports,
49 restrictedSyntax,
50 restrictedSrcSyntax,
51 restrictedTestSyntax,
52
53 ...otherConfig
54 } = extraConfig;
55
56 return {
57 ...otherConfig,
58 extends: [
59 '@spotify/eslint-config-base',
60 '@spotify/eslint-config-typescript',
61 'prettier',
62 'plugin:jest/recommended',
63 'plugin:monorepo/recommended',
64 ...(extraExtends ?? []),
65 ],
66 parser: '@typescript-eslint/parser',
67 plugins: ['import', ...(plugins ?? [])],
68 env: {
69 jest: true,
70 ...env,
71 },
72 parserOptions: {
73 ecmaVersion: 2018,
74 sourceType: 'module',
75 lib: require('./tsconfig.json').compilerOptions.lib,
76 ...parserOptions,
77 },
78 ignorePatterns: [
79 '.eslintrc.js',
80 '**/dist/**',
81 '**/dist-types/**',
82 ...(ignorePatterns ?? []),
83 ],
84 rules: {
85 'no-shadow': 'off',
86 'no-redeclare': 'off',
87 '@typescript-eslint/no-shadow': 'error',
88 '@typescript-eslint/no-redeclare': 'error',
89 'no-undef': 'off',
90 'import/newline-after-import': 'error',
91 'import/no-extraneous-dependencies': [
92 'error',
93 {
94 devDependencies: dir
95 ? [
96 `!${joinPath(dir, 'src/**')}`,
97 joinPath(dir, 'src/**/*.test.*'),
98 joinPath(dir, 'src/**/*.stories.*'),
99 joinPath(dir, 'src/setupTests.*'),
100 ]
101 : [
102 // Legacy config for packages that don't provide a dir
103 '**/*.test.*',
104 '**/*.stories.*',
105 '**/src/setupTests.*',
106 '**/dev/**',
107 ],
108 optionalDependencies: true,
109 peerDependencies: true,
110 bundledDependencies: true,
111 },
112 ],
113 'no-unused-expressions': 'off',
114 '@typescript-eslint/no-unused-expressions': 'error',
115 '@typescript-eslint/consistent-type-assertions': 'error',
116 '@typescript-eslint/no-unused-vars': [
117 'warn',
118 {
119 vars: 'all',
120 args: 'after-used',
121 ignoreRestSiblings: true,
122 argsIgnorePattern: '^_',
123 },
124 ],
125
126 'no-restricted-imports': [
127 2,
128 {
129 paths: [
130 ...(restrictedImports ?? []),
131 ...(restrictedSrcImports ?? []),
132 ],
133 patterns: [
134 // Avoid cross-package imports
135 '**/../../**/*/src/**',
136 '**/../../**/*/src',
137 // Prevent imports of stories or tests
138 '*.stories*',
139 '*.test*',
140 ],
141 },
142 ],
143
144 'no-restricted-syntax': [
145 'error',
146 ...(restrictedSyntax ?? []),
147 ...(restrictedSrcSyntax ?? []),
148 ],
149
150 ...rules,
151 },
152 overrides: [
153 {
154 files: ['**/*.ts?(x)'],
155 rules: {
156 '@typescript-eslint/no-unused-vars': 'off',
157 'no-undef': 'off',
158 ...tsRules,
159 },
160 },
161 {
162 files: ['**/*.test.*', '**/*.stories.*', 'src/setupTests.*', '!src/**'],
163 rules: {
164 ...testRules,
165 'no-restricted-syntax': [
166 'error',
167 ...(restrictedSyntax ?? []),
168 ...(restrictedTestSyntax ?? []),
169 ],
170 'no-restricted-imports': [
171 2,
172 {
173 paths: [
174 ...(restrictedImports ?? []),
175 ...(restrictedTestImports ?? []),
176 ],
177 // Avoid cross-package imports
178 patterns: ['**/../../**/*/src/**', '**/../../**/*/src'],
179 },
180 ],
181 },
182 },
183 ...(overrides ?? []),
184 ],
185 };
186}
187
188/**
189 * Creates a ESLint configuration for the given package role.
190 * The `extraConfig` parameter accepts the same keys as for the `createConfig` function.
191 */
192function createConfigForRole(dir, role, extraConfig = {}) {
193 switch (role) {
194 case 'common-library':
195 return createConfig(dir, extraConfig);
196
197 case 'web-library':
198 case 'frontend':
199 case 'frontend-plugin':
200 case 'frontend-plugin-module':
201 return createConfig(dir, {
202 ...extraConfig,
203 extends: [
204 '@spotify/eslint-config-react',
205 ...(extraConfig.extends ?? []),
206 ],
207 parserOptions: {
208 ecmaFeatures: {
209 jsx: true,
210 },
211 ...extraConfig.parserOptions,
212 },
213 settings: {
214 react: {
215 version: 'detect',
216 },
217 ...extraConfig.settings,
218 },
219 restrictedImports: [
220 {
221 // Importing the entire MUI icons packages kills build performance as the list of icons is huge.
222 name: '@material-ui/icons',
223 message: "Please import '@material-ui/icons/<Icon>' instead.",
224 },
225 {
226 name: '@material-ui/icons/', // because this is possible too ._.
227 message: "Please import '@material-ui/icons/<Icon>' instead.",
228 },
229 ...require('module').builtinModules,
230 ...(extraConfig.restrictedImports ?? []),
231 ],
232 tsRules: {
233 'react/prop-types': 0,
234 ...extraConfig.tsRules,
235 },
236 });
237
238 case 'cli':
239 case 'node-library':
240 case 'backend':
241 case 'backend-plugin':
242 case 'backend-plugin-module':
243 return createConfig(dir, {
244 ...extraConfig,
245 globals: {
246 __non_webpack_require__: 'readonly',
247 ...extraConfig.globals,
248 },
249 rules: {
250 'no-console': 0, // Permitted in console programs
251 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()'
252 ...extraConfig.rules,
253 },
254 restrictedSyntax: [
255 {
256 message:
257 'Default import from winston is not allowed, import `* as winston` instead.',
258 selector:
259 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier',
260 },
261 ...(extraConfig.restrictedSyntax ?? []),
262 ],
263 restrictedSrcSyntax: [
264 {
265 message:
266 "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.",
267 selector: 'Identifier[name="__dirname"]',
268 },
269 ...(extraConfig.restrictedSrcSyntax ?? []),
270 ],
271 });
272 default:
273 throw new Error(`Unknown package role ${role}`);
274 }
275}
276
277/**
278 * Creates a ESLint configuration for the package in the given directory.
279 * The `extraConfig` parameter accepts the same keys as for the `createConfig` function.
280 */
281function createPackageConfig(dir, extraConfig) {
282 const pkg = require(joinPath(dir, 'package.json'));
283 const role = pkg.backstage?.role;
284 if (!role) {
285 throw new Error(`Package ${pkg.name} does not have a backstage.role`);
286 }
287
288 return createConfigForRole(dir, role, extraConfig);
289}
290
291module.exports = createPackageConfig; // Alias
292module.exports.createConfig = createConfig;
293module.exports.createConfigForRole = createConfigForRole;
294module.exports.createPackageConfig = createPackageConfig;