@backstage/eslint-plugin 0.0.0-nightly-20230207022622

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js ADDED
@@ -0,0 +1,34 @@
1
+ /*
2
+ * Copyright 2023 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
+
17
+ // Custom lint config here since source is ES5
18
+ module.exports = {
19
+ plugins: ['@backstage'],
20
+ env: {
21
+ node: true,
22
+ es2021: true,
23
+ },
24
+ parser: '@typescript-eslint/parser',
25
+ parserOptions: {
26
+ ecmaVersion: 2018,
27
+ sourceType: 'module',
28
+ lib: require('@backstage/cli/config/tsconfig.json').compilerOptions.lib,
29
+ },
30
+ rules: {
31
+ '@backstage/no-undeclared-imports': ['error'],
32
+ 'no-unused-expressions': 'off',
33
+ },
34
+ };
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @backstage/eslint-plugin
2
+
3
+ ## 0.0.0-nightly-20230207022622
4
+
5
+ ### Minor Changes
6
+
7
+ - 179d301518: Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @backstage/eslint-plugin
2
+
3
+ A collection of ESLint rules useful to Backstage projects.
4
+
5
+ ## Usage
6
+
7
+ This ESLint plugin is part of the default lint configuration provided by the [Backstage CLI](https://www.npmjs.com/package/@backstage/cli), so you generally do not need to install it manually.
8
+
9
+ If you do wish to install this plugin manually, start by adding it as a development dependency to your project:
10
+
11
+ ```sh
12
+ yarn add --dev @backstage/eslint-plugin
13
+ ```
14
+
15
+ Then add it to your ESLint configuration:
16
+
17
+ ```js
18
+ extends: [
19
+ 'plugin:@backstage/recommended',
20
+ ],
21
+ ```
22
+
23
+ Alternatively, if you want to install in individual rules manually:
24
+
25
+ ```js
26
+ plugins: [
27
+ '@backstage',
28
+ ],
29
+ rules: {
30
+ '@backstage/no-forbidden-package-imports': 'error',
31
+ }
32
+ ```
33
+
34
+ ## Rules
35
+
36
+ The following rules are provided by this plugin:
37
+
38
+ | Rule | Description |
39
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
40
+ | [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md) | Disallow internal monorepo imports from package subpaths that are not exported. |
41
+ | [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md) | Forbid relative imports that reach outside of the package in a monorepo. |
42
+ | [@backstage/no-undeclared-imports](./docs/rules/no-undeclared-imports.md) | Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. |
@@ -0,0 +1,52 @@
1
+ # @backstage/no-forbidden-package-imports
2
+
3
+ Disallow internal monorepo imports from package subpaths that are not exported.
4
+
5
+ ## Usage
6
+
7
+ Add the rules as follows, it has no options:
8
+
9
+ ```js
10
+ "@backstage/no-forbidden-package-imports": ["error"]
11
+ ```
12
+
13
+ ## Rule Details
14
+
15
+ Given the following two target packages:
16
+
17
+ ```json
18
+ {
19
+ "name": "@backstage/plugin-foo",
20
+ "files": ["dist", "type-utils"]
21
+ }
22
+ ```
23
+
24
+ ```json
25
+ {
26
+ "name": "@backstage/plugin-bar",
27
+ "exports": {
28
+ ".": "./src/index.ts",
29
+ "./testUtils": "./src/testUtils/index.ts",
30
+ "./package.json": "./package.json"
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### Fail
36
+
37
+ ```ts
38
+ import { FooCard } from '@backstage/plugin-foo/src/components';
39
+ import { BarCard } from '@backstage/plugin-bar/src/components';
40
+ ```
41
+
42
+ ### Pass
43
+
44
+ ```ts
45
+ import { FooCard } from '@backstage/plugin-foo';
46
+ import { FooType } from '@backstage/plugin-foo/type-utils';
47
+ import fooPkg from '@backstage/plugin-foo/package.json';
48
+
49
+ import { BarCard } from '@backstage/plugin-bar';
50
+ import { renderBarCardExtension } from '@backstage/plugin-bar/testUtils';
51
+ import barPkg from '@backstage/plugin-bar/package.json';
52
+ ```
@@ -0,0 +1,43 @@
1
+ # @backstage/no-relative-monorepo-imports
2
+
3
+ Forbid relative imports that reach outside of the package in a monorepo.
4
+
5
+ ## Usage
6
+
7
+ Add the rules as follows, it has no options:
8
+
9
+ ```js
10
+ "@backstage/no-relative-monorepo-imports": ["error"]
11
+ ```
12
+
13
+ The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies:
14
+
15
+ ```python
16
+ !src/** # Any files outside of src are considered dev files
17
+ src/**/*.test.*
18
+ src/**/*.stories.*
19
+ src/**/__testUtils__/**
20
+ src/**/__mocks__/**
21
+ src/setupTests.*
22
+ ```
23
+
24
+ ## Rule Details
25
+
26
+ Assuming an import from for example `plugins/bar/src/index.ts`:
27
+
28
+ ### Fail
29
+
30
+ ```ts
31
+ import { FooCard } from '../../foo';
32
+
33
+ import { FooCard } from '../../foo/src/components/FooCard';
34
+ ```
35
+
36
+ ### Pass
37
+
38
+ ```ts
39
+ import { FooCard } from '@internal/plugin-foo';
40
+
41
+ // This is allowed by this rule, but not by no-forbidden-package-imports
42
+ import { FooCard } from '@internal/plugin-foo/src/components/FooCard';
43
+ ```
@@ -0,0 +1,84 @@
1
+ # @backstage/no-undeclared-imports
2
+
3
+ Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`.
4
+
5
+ ## Usage
6
+
7
+ Add the rules as follows, it has no options:
8
+
9
+ ```js
10
+ "@backstage/no-undeclared-imports": ["error"]
11
+ ```
12
+
13
+ The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies:
14
+
15
+ ```python
16
+ !src/** # Any files outside of src are considered dev files
17
+ src/**/*.test.*
18
+ src/**/*.stories.*
19
+ src/**/__testUtils__/**
20
+ src/**/__mocks__/**
21
+ src/setupTests.*
22
+ ```
23
+
24
+ ## Rule Details
25
+
26
+ Given the following `package.json`:
27
+
28
+ ```json
29
+ {
30
+ "name": "@backstage/plugin-foo",
31
+ "backstage": {
32
+ "role": "frontend-plugin"
33
+ },
34
+ "dependencies": {
35
+ "react": "^17.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@backstage/core-plugin-api": "^1.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@backstage/config": "*"
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Fail
47
+
48
+ Inside `src/my-plugin.ts`:
49
+
50
+ ```ts
51
+ // Should be declared as a dependency
52
+ const _ = require('lodash');
53
+ import _ from 'lodash';
54
+
55
+ // React should be a peer dependency in frontend plugins
56
+ import react from 'react';
57
+
58
+ // Should be declared as a dependency, not a dev dependency
59
+ import { useApi } from '@backstage/core-plugin-api';
60
+ ```
61
+
62
+ Inside `src/my-plugin.test.ts` (a test file):
63
+
64
+ ```ts
65
+ // Should be declared as a dev dependency
66
+ const _ = require('lodash');
67
+ import _ from 'lodash';
68
+ ```
69
+
70
+ ### Pass
71
+
72
+ Inside `src/my-plugin.ts`:
73
+
74
+ ```ts
75
+ // Declared in peerDependencies, so it is allowed
76
+ import { ConfigReader } from '@backstage/config';
77
+ ```
78
+
79
+ Inside `src/my-plugin.test.ts` (a test file):
80
+
81
+ ```ts
82
+ // Declared as a dev dependency inside a test file
83
+ import { useApi } from '@backstage/core-plugin-api';
84
+ ```
package/index.js ADDED
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Copyright 2023 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
+
17
+ module.exports = {
18
+ configs: {
19
+ recommended: {
20
+ plugins: ['@backstage'],
21
+ rules: {
22
+ '@backstage/no-forbidden-package-imports': 'error',
23
+ '@backstage/no-relative-monorepo-imports': 'error',
24
+ '@backstage/no-undeclared-imports': 'error',
25
+ },
26
+ },
27
+ },
28
+ rules: {
29
+ 'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'),
30
+ 'no-relative-monorepo-imports': require('./rules/no-relative-monorepo-imports'),
31
+ 'no-undeclared-imports': require('./rules/no-undeclared-imports'),
32
+ },
33
+ };
@@ -0,0 +1,71 @@
1
+ /*
2
+ * Copyright 2023 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
+
17
+ // @ts-check
18
+
19
+ const path = require('path');
20
+ const manypkg = require('@manypkg/get-packages');
21
+
22
+ /**
23
+ * @typedef ExtendedPackage
24
+ * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record<string, string>, files?: Array<string> }}} packageJson
25
+ */
26
+
27
+ /**
28
+ * @typedef PackageMap
29
+ * @type object
30
+ *
31
+ * @property {ExtendedPackage} root
32
+ * @property {ExtendedPackage[]} list
33
+ * @property {Map<string, ExtendedPackage>} map
34
+ * @property {(path: string) => ExtendedPackage | undefined} byPath
35
+ */
36
+
37
+ // Loads all packages in the monorepo once, and caches the result
38
+ module.exports = (function () {
39
+ /** @type {PackageMap | undefined} */
40
+ let result = undefined;
41
+ /** @type {number} */
42
+ let lastLoadAt = 0;
43
+
44
+ /** @returns {PackageMap | undefined} */
45
+ return function getPackages(/** @type {string} */ dir) {
46
+ if (result) {
47
+ // Only cache for 5 seconds, to avoid the need to reload ESLint servers
48
+ if (Date.now() - lastLoadAt > 5000) {
49
+ result = undefined;
50
+ } else {
51
+ return result;
52
+ }
53
+ }
54
+ const packages = manypkg.getPackagesSync(dir);
55
+ if (!packages) {
56
+ return undefined;
57
+ }
58
+ result = {
59
+ map: new Map(packages.packages.map(pkg => [pkg.packageJson.name, pkg])),
60
+ list: packages.packages,
61
+ root: packages.root,
62
+ byPath(filePath) {
63
+ return packages.packages.find(
64
+ pkg => !path.relative(pkg.dir, filePath).startsWith('..'),
65
+ );
66
+ },
67
+ };
68
+ lastLoadAt = Date.now();
69
+ return result;
70
+ };
71
+ })();
@@ -0,0 +1,173 @@
1
+ /*
2
+ * Copyright 2023 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
+
17
+ // @ts-check
18
+
19
+ const { builtinModules } = require('module');
20
+ const getPackages = require('./getPackages');
21
+
22
+ /**
23
+ * @typedef LocalImport
24
+ * @type {object}
25
+ * @property {'local'} type
26
+ * @property {'value' | 'type'} kind
27
+ * @property {string} path
28
+ */
29
+
30
+ /**
31
+ * @typedef InternalImport
32
+ * @type {object}
33
+ * @property {'internal'} type
34
+ * @property {'value' | 'type'} kind
35
+ * @property {string} path
36
+ * @property {import('./getPackages').ExtendedPackage} package
37
+ */
38
+
39
+ /**
40
+ * @typedef ExternalImport
41
+ * @type {object}
42
+ * @property {'external'} type
43
+ * @property {'value' | 'type'} kind
44
+ * @property {string} path
45
+ * @property {string} packageName
46
+ */
47
+
48
+ /**
49
+ * @typedef BuiltinImport
50
+ * @type {object}
51
+ * @property {'builtin'} type
52
+ * @property {'value' | 'type'} kind
53
+ * @property {string} path
54
+ * @property {string} packageName
55
+ */
56
+
57
+ /**
58
+ * @callback ImportVisitor
59
+ * @param {ConsideredNode} node
60
+ * @param {LocalImport | InternalImport | ExternalImport | BuiltinImport} import
61
+ */
62
+
63
+ /**
64
+ * @typedef ConsideredNode
65
+ * @type {import('estree').ImportDeclaration | import('estree').ExportAllDeclaration | import('estree').ExportNamedDeclaration | import('estree').ImportExpression | import('estree').SimpleCallExpression}
66
+ */
67
+
68
+ /**
69
+ * @param {ConsideredNode} node
70
+ * @returns {undefined | {path: string, kind: 'type' | 'value'}}
71
+ */
72
+ function getImportInfo(node) {
73
+ /** @type {import('estree').Expression | import('estree').SpreadElement | undefined | null} */
74
+ let pathNode;
75
+
76
+ if (node.type === 'CallExpression') {
77
+ if (
78
+ node.callee.type === 'Identifier' &&
79
+ node.callee.name == 'require' &&
80
+ node.arguments.length === 1
81
+ ) {
82
+ pathNode = node.arguments[0];
83
+ }
84
+ } else {
85
+ pathNode = node.source;
86
+ }
87
+
88
+ if (pathNode?.type !== 'Literal') {
89
+ return undefined;
90
+ }
91
+ if (typeof pathNode.value !== 'string') {
92
+ return undefined;
93
+ }
94
+
95
+ /** @type {any} */
96
+ const anyNode = node;
97
+ return { path: pathNode.value, kind: anyNode.importKind ?? 'value' };
98
+ }
99
+
100
+ /**
101
+ * @param visitor - Visitor callback
102
+ * @param {import('eslint').Rule.RuleContext} context
103
+ * @param {ImportVisitor} visitor
104
+ */
105
+ module.exports = function visitImports(context, visitor) {
106
+ const packages = getPackages(context.getCwd());
107
+ if (!packages) {
108
+ return;
109
+ }
110
+
111
+ /**
112
+ * @param {ConsideredNode} node
113
+ */
114
+ function visit(node) {
115
+ const info = getImportInfo(node);
116
+ if (!info) {
117
+ return;
118
+ }
119
+
120
+ if (info.path[0] === '.') {
121
+ return visitor(node, { type: 'local', ...info });
122
+ }
123
+
124
+ const pathParts = info.path.split('/');
125
+
126
+ // Check for match with plain name, then namespaced name
127
+ let packageName;
128
+ let subPath;
129
+ if (info.path[0] === '@') {
130
+ packageName = pathParts.slice(0, 2).join('/');
131
+ subPath = pathParts.slice(2).join('/');
132
+ } else {
133
+ packageName = pathParts[0];
134
+ subPath = pathParts.slice(1).join('/');
135
+ }
136
+ const pkg = packages?.map.get(packageName);
137
+ if (!pkg) {
138
+ if (
139
+ packageName.startsWith('node:') ||
140
+ builtinModules.includes(packageName)
141
+ ) {
142
+ return visitor(node, {
143
+ type: 'builtin',
144
+ kind: info.kind,
145
+ path: subPath,
146
+ packageName,
147
+ });
148
+ }
149
+
150
+ return visitor(node, {
151
+ type: 'external',
152
+ kind: info.kind,
153
+ path: subPath,
154
+ packageName,
155
+ });
156
+ }
157
+
158
+ return visitor(node, {
159
+ type: 'internal',
160
+ kind: info.kind,
161
+ path: subPath,
162
+ package: pkg,
163
+ });
164
+ }
165
+
166
+ return {
167
+ ImportDeclaration: visit,
168
+ ExportAllDeclaration: visit,
169
+ ExportNamedDeclaration: visit,
170
+ ImportExpression: visit,
171
+ CallExpression: visit,
172
+ };
173
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@backstage/eslint-plugin",
3
+ "description": "Backstage ESLint plugin",
4
+ "version": "0.0.0-nightly-20230207022622",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "homepage": "https://backstage.io",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/backstage/backstage",
12
+ "directory": "packages/eslint-plugin"
13
+ },
14
+ "license": "Apache-2.0",
15
+ "main": "./index.js",
16
+ "scripts": {
17
+ "lint": "backstage-cli package lint",
18
+ "test": "backstage-cli package test"
19
+ },
20
+ "dependencies": {
21
+ "@manypkg/get-packages": "^1.1.3",
22
+ "minimatch": "^5.1.2"
23
+ },
24
+ "devDependencies": {
25
+ "@backstage/cli": "^0.0.0-nightly-20230207022622",
26
+ "eslint": "^8.33.0"
27
+ }
28
+ }
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright 2023 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
+
17
+ // @ts-check
18
+
19
+ const visitImports = require('../lib/visitImports');
20
+
21
+ /** @type {import('eslint').Rule.RuleModule} */
22
+ module.exports = {
23
+ meta: {
24
+ type: 'problem',
25
+ messages: {
26
+ forbidden: '{{packageName}} does not export {{subPath}}',
27
+ },
28
+ docs: {
29
+ description:
30
+ 'Disallow internal monorepo imports from package subpaths that are not exported.',
31
+ url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md',
32
+ },
33
+ },
34
+ create(context) {
35
+ return visitImports(context, (node, imp) => {
36
+ if (imp.type !== 'internal') {
37
+ return;
38
+ }
39
+ // Empty subpaths are always allowed
40
+ if (!imp.path) {
41
+ return;
42
+ }
43
+
44
+ // If the import is listed in the package.json exports field, we allow it
45
+ const exp = imp.package.packageJson.exports;
46
+ if (exp && (exp[imp.path] || exp['./' + imp.path])) {
47
+ return;
48
+ }
49
+ if (!exp) {
50
+ // If there's no exports field, we allow anything listed in files, except dist
51
+ const files = imp.package.packageJson.files;
52
+ if (
53
+ !files ||
54
+ files.some(f => !f.startsWith('dist') && imp.path.startsWith(f))
55
+ ) {
56
+ return;
57
+ }
58
+ // And also package.json
59
+ if (imp.path === 'package.json') {
60
+ return;
61
+ }
62
+ }
63
+
64
+ context.report({
65
+ node: node,
66
+ messageId: 'forbidden',
67
+ data: {
68
+ packageName: imp.package.packageJson.name || imp.package.dir,
69
+ subPath: imp.path,
70
+ },
71
+ });
72
+ });
73
+ },
74
+ };