@backstage/eslint-plugin 0.2.3 → 0.3.0-next.0
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/CHANGELOG.md +6 -0
- package/docs/rules/no-self-package-imports.md +106 -0
- package/index.js +2 -0
- package/lib/visitImports.js +1 -1
- package/package.json +2 -2
- package/rules/no-self-package-imports.js +284 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/package.json +12 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/index.ts +20 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/refs.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/typeRef.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/index.ts +18 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/foo.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/index.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/orphan.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/shared.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils/helper.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils.ts +17 -0
- package/src/__fixtures__/monorepo/packages/self-import-pkg/src/util.ts +17 -0
- package/src/no-self-package-imports.test.ts +229 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @backstage/eslint-plugin
|
|
2
2
|
|
|
3
|
+
## 0.3.0-next.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ab1cdbb: Added a new `no-self-package-imports` lint rule, enabled as `error` in the recommended config, that reports when a package imports itself by its own name instead of using a relative path. This pattern causes circular initialization errors in bundled ESM and with `jest.requireActual`.
|
|
8
|
+
|
|
3
9
|
## 0.2.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @backstage/no-self-package-imports
|
|
2
|
+
|
|
3
|
+
This rule prevents a package from importing itself by its own name when the
|
|
4
|
+
imported entry point bundles the current file. Self-package imports in that
|
|
5
|
+
situation create a circular dependency through the bundled barrel, which can
|
|
6
|
+
surface as runtime errors such as
|
|
7
|
+
`Cannot access 'X' before initialization` when the package is loaded in an
|
|
8
|
+
environment that triggers eager re-evaluation (for example
|
|
9
|
+
`jest.requireActual`, or ESM consumers that follow the cycle).
|
|
10
|
+
|
|
11
|
+
The rule understands your package's `exports` map and follows the
|
|
12
|
+
relative import graph from each entry's source file to determine which files
|
|
13
|
+
are actually bundled into each entry. It then reports:
|
|
14
|
+
|
|
15
|
+
- **Same-entry self-imports**: the current file is part of the same bundle
|
|
16
|
+
as the entry it imports, so the cycle is real.
|
|
17
|
+
- **Cross-entry self-imports** (optional): the file is part of a different
|
|
18
|
+
entry's bundle than the one it imports. Cross-entry self-imports don't
|
|
19
|
+
always cycle, but they still couple unrelated entry points at initialization
|
|
20
|
+
time and are worth avoiding.
|
|
21
|
+
|
|
22
|
+
Files that aren't reachable from any published entry (tests, scripts,
|
|
23
|
+
orphans) are skipped, as self-imports from them can't affect a published
|
|
24
|
+
bundle.
|
|
25
|
+
|
|
26
|
+
Imports declared with `import type` (or `export type`) are erased at runtime
|
|
27
|
+
and are always allowed, since they can't cause circular initialization.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Add the rule as follows:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
"@backstage/no-self-package-imports": ["error"]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This errors on same-entry self-imports. Cross-entry self-imports are allowed
|
|
38
|
+
by default; opt in to reporting them with `allowCrossEntry: false`:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
"@backstage/no-self-package-imports": ["error", { "allowCrossEntry": false }]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Rule Details
|
|
45
|
+
|
|
46
|
+
Given this `package.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"name": "@backstage/plugin-foo",
|
|
51
|
+
"exports": {
|
|
52
|
+
".": "./src/index.ts",
|
|
53
|
+
"./alpha": "./src/alpha.ts",
|
|
54
|
+
"./package.json": "./package.json"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
and `src/index.ts` that re-exports `./blueprint`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export * from './blueprint';
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Fail
|
|
66
|
+
|
|
67
|
+
Importing `@backstage/plugin-foo` from a file that is also reachable from
|
|
68
|
+
`src/index.ts` creates a cycle:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// src/blueprint.ts
|
|
72
|
+
import { helper } from '@backstage/plugin-foo';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Pass
|
|
76
|
+
|
|
77
|
+
Use a relative import instead:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// src/blueprint.ts
|
|
81
|
+
import { helper } from './helper';
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or, if only the type is used, `import type` is erased at runtime and is
|
|
85
|
+
always allowed:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// src/blueprint.ts
|
|
89
|
+
import type { Helper } from '@backstage/plugin-foo';
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Importing `package.json` is always allowed:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { version } from '@backstage/plugin-foo/package.json';
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Options
|
|
99
|
+
|
|
100
|
+
### `allowCrossEntry`
|
|
101
|
+
|
|
102
|
+
- Type: `boolean`
|
|
103
|
+
- Default: `true`
|
|
104
|
+
|
|
105
|
+
When `false`, the rule also reports self-imports that target a different
|
|
106
|
+
entry from the one the current file is part of.
|
package/index.js
CHANGED
|
@@ -24,6 +24,7 @@ module.exports = {
|
|
|
24
24
|
'@backstage/no-undeclared-imports': 'error',
|
|
25
25
|
'@backstage/no-mixed-plugin-imports': 'warn',
|
|
26
26
|
'@backstage/no-ui-css-imports-in-non-frontend': 'error',
|
|
27
|
+
'@backstage/no-self-package-imports': 'error',
|
|
27
28
|
},
|
|
28
29
|
},
|
|
29
30
|
},
|
|
@@ -34,5 +35,6 @@ module.exports = {
|
|
|
34
35
|
'no-top-level-material-ui-4-imports': require('./rules/no-top-level-material-ui-4-imports'),
|
|
35
36
|
'no-mixed-plugin-imports': require('./rules/no-mixed-plugin-imports'),
|
|
36
37
|
'no-ui-css-imports-in-non-frontend': require('./rules/no-ui-css-imports-in-non-frontend'),
|
|
38
|
+
'no-self-package-imports': require('./rules/no-self-package-imports'),
|
|
37
39
|
},
|
|
38
40
|
};
|
package/lib/visitImports.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-next.0",
|
|
4
4
|
"description": "Backstage ESLint plugin",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"minimatch": "^10.2.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@backstage/cli": "
|
|
25
|
+
"@backstage/cli": "0.36.2-next.1",
|
|
26
26
|
"@types/estree": "^1.0.5",
|
|
27
27
|
"eslint": "^8.33.0"
|
|
28
28
|
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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 fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const visitImports = require('../lib/visitImports');
|
|
22
|
+
const getPackages = require('../lib/getPackages');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef EntryInfo
|
|
26
|
+
* @type {object}
|
|
27
|
+
* @property {string} key - The exports key, e.g. '.' or './alpha'.
|
|
28
|
+
* @property {string} sourceFile - The source file for the entry, relative to the package dir.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const SOURCE_EXTENSIONS = [
|
|
32
|
+
'.ts',
|
|
33
|
+
'.tsx',
|
|
34
|
+
'.mts',
|
|
35
|
+
'.cts',
|
|
36
|
+
'.js',
|
|
37
|
+
'.jsx',
|
|
38
|
+
'.mjs',
|
|
39
|
+
'.cjs',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Cache the per-package analysis across files lint invocations. The key is the
|
|
43
|
+
// absolute package dir; the value is a Map from absolute file path to the set
|
|
44
|
+
// of entry keys whose bundle reaches that file.
|
|
45
|
+
/** @type {Map<string, Map<string, Set<string>>>} */
|
|
46
|
+
const bundleCache = new Map();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a list of entries from the package.json exports field. Entries that
|
|
50
|
+
* don't point to a script (e.g. `./package.json`) are ignored.
|
|
51
|
+
*
|
|
52
|
+
* @param {unknown} exportsField
|
|
53
|
+
* @returns {EntryInfo[]}
|
|
54
|
+
*/
|
|
55
|
+
function readEntries(exportsField) {
|
|
56
|
+
if (!exportsField || typeof exportsField !== 'object') {
|
|
57
|
+
return [{ key: '.', sourceFile: 'src/index.ts' }];
|
|
58
|
+
}
|
|
59
|
+
/** @type {EntryInfo[]} */
|
|
60
|
+
const entries = [];
|
|
61
|
+
for (const [key, value] of Object.entries(exportsField)) {
|
|
62
|
+
if (typeof value !== 'string') continue;
|
|
63
|
+
if (key === './package.json') continue;
|
|
64
|
+
const rel = value.replace(/^\.\//, '');
|
|
65
|
+
entries.push({ key, sourceFile: rel });
|
|
66
|
+
}
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a relative module specifier against a containing file.
|
|
72
|
+
* Tries source extensions and index files, in that order.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} fromFile
|
|
75
|
+
* @param {string} specifier
|
|
76
|
+
* @returns {string | undefined}
|
|
77
|
+
*/
|
|
78
|
+
function resolveSourcePath(fromFile, specifier) {
|
|
79
|
+
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
80
|
+
// If the specifier already carries an extension, only try the exact path.
|
|
81
|
+
if (/\.[cm]?[jt]sx?$/i.test(specifier)) {
|
|
82
|
+
return fs.existsSync(base) ? base : undefined;
|
|
83
|
+
}
|
|
84
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
85
|
+
const candidate = base + ext;
|
|
86
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
87
|
+
}
|
|
88
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
89
|
+
const candidate = path.join(base, 'index' + ext);
|
|
90
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Matches `from '...'` specifiers in `import`/`export ... from` statements.
|
|
96
|
+
// Uses a non-greedy body so multi-line imports match cleanly. The negative
|
|
97
|
+
// lookahead skips `import type` / `export type` statements because type-only
|
|
98
|
+
// edges are erased at runtime and can't pull files into a runtime bundle.
|
|
99
|
+
const FROM_SPEC_RE =
|
|
100
|
+
/(?:^|[\s;}])(?:import|export)\b(?!\s+type\b)[^"';]*?\bfrom\s*["']([^"']+)["']/gm;
|
|
101
|
+
// Matches side-effect imports: `import '...';`.
|
|
102
|
+
const SIDE_EFFECT_IMPORT_RE = /(?:^|[\s;}])import\s*["']([^"']+)["']/gm;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract relative module specifiers from a source file's contents.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} contents
|
|
108
|
+
* @returns {string[]}
|
|
109
|
+
*/
|
|
110
|
+
function collectRelativeSpecifiers(contents) {
|
|
111
|
+
const specs = new Set();
|
|
112
|
+
for (const m of contents.matchAll(FROM_SPEC_RE)) {
|
|
113
|
+
if (m[1].startsWith('.')) specs.add(m[1]);
|
|
114
|
+
}
|
|
115
|
+
for (const m of contents.matchAll(SIDE_EFFECT_IMPORT_RE)) {
|
|
116
|
+
if (m[1].startsWith('.')) specs.add(m[1]);
|
|
117
|
+
}
|
|
118
|
+
return [...specs];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Walk the relative import/export graph of each entry's source file and build
|
|
123
|
+
* a map of absolute file paths to the set of entries whose bundles include
|
|
124
|
+
* them. Files that aren't reachable from any entry (e.g. tests, scripts)
|
|
125
|
+
* aren't present in the map.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} pkgDir
|
|
128
|
+
* @param {EntryInfo[]} entries
|
|
129
|
+
* @returns {Map<string, Set<string>>}
|
|
130
|
+
*/
|
|
131
|
+
function buildFileToEntriesMap(pkgDir, entries) {
|
|
132
|
+
/** @type {Map<string, Set<string>>} */
|
|
133
|
+
const fileToEntries = new Map();
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const sourceFile = path.join(pkgDir, entry.sourceFile);
|
|
136
|
+
if (!fs.existsSync(sourceFile)) continue;
|
|
137
|
+
/** @type {Set<string>} */
|
|
138
|
+
const visited = new Set();
|
|
139
|
+
const queue = [sourceFile];
|
|
140
|
+
while (queue.length > 0) {
|
|
141
|
+
const current = /** @type {string} */ (queue.pop());
|
|
142
|
+
if (visited.has(current)) continue;
|
|
143
|
+
visited.add(current);
|
|
144
|
+
|
|
145
|
+
let set = fileToEntries.get(current);
|
|
146
|
+
if (!set) {
|
|
147
|
+
set = new Set();
|
|
148
|
+
fileToEntries.set(current, set);
|
|
149
|
+
}
|
|
150
|
+
set.add(entry.key);
|
|
151
|
+
|
|
152
|
+
let contents;
|
|
153
|
+
try {
|
|
154
|
+
contents = fs.readFileSync(current, 'utf8');
|
|
155
|
+
} catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const spec of collectRelativeSpecifiers(contents)) {
|
|
160
|
+
const resolved = resolveSourcePath(current, spec);
|
|
161
|
+
if (resolved) queue.push(resolved);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return fileToEntries;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} pkgDir
|
|
170
|
+
* @param {EntryInfo[]} entries
|
|
171
|
+
* @returns {Map<string, Set<string>>}
|
|
172
|
+
*/
|
|
173
|
+
function getFileToEntriesMap(pkgDir, entries) {
|
|
174
|
+
let cached = bundleCache.get(pkgDir);
|
|
175
|
+
if (!cached) {
|
|
176
|
+
cached = buildFileToEntriesMap(pkgDir, entries);
|
|
177
|
+
bundleCache.set(pkgDir, cached);
|
|
178
|
+
}
|
|
179
|
+
return cached;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Find which entry an import targets based on its subpath. Returns undefined
|
|
184
|
+
* when the import doesn't match any declared entry.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} subPath - The part after the package name, without leading slash. Empty for the root entry.
|
|
187
|
+
* @param {EntryInfo[]} entries
|
|
188
|
+
* @returns {EntryInfo | undefined}
|
|
189
|
+
*/
|
|
190
|
+
function findEntryForImport(subPath, entries) {
|
|
191
|
+
const key = subPath ? `./${subPath}` : '.';
|
|
192
|
+
return entries.find(e => e.key === key);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
196
|
+
module.exports = {
|
|
197
|
+
meta: {
|
|
198
|
+
type: 'problem',
|
|
199
|
+
messages: {
|
|
200
|
+
sameEntrySelfImport:
|
|
201
|
+
"Do not import from your own package '{{packageName}}'. This causes a circular dependency because '{{entry}}' re-exports this file. Switch to a relative import, or use `import type` if only types are needed (type-only imports are erased at runtime).",
|
|
202
|
+
crossEntrySelfImport:
|
|
203
|
+
"Avoid importing from your own package '{{packageName}}' via '{{importPath}}'. Even across entry points this can lead to subtle circular initialization issues. Prefer a relative import, or use `import type` if only types are needed (type-only imports are erased at runtime).",
|
|
204
|
+
},
|
|
205
|
+
docs: {
|
|
206
|
+
description:
|
|
207
|
+
'Disallow a package from importing itself by its own name, which causes circular initialization issues in bundled ESM.',
|
|
208
|
+
url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-self-package-imports.md',
|
|
209
|
+
},
|
|
210
|
+
schema: [
|
|
211
|
+
{
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
allowCrossEntry: {
|
|
215
|
+
type: 'boolean',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
create(context) {
|
|
223
|
+
const options = context.options[0] || {};
|
|
224
|
+
const allowCrossEntry = options.allowCrossEntry !== false;
|
|
225
|
+
|
|
226
|
+
const packages = getPackages(context.getCwd());
|
|
227
|
+
const filename = context.getFilename();
|
|
228
|
+
const selfPkg = packages?.byPath(filename);
|
|
229
|
+
if (!selfPkg) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
const selfName = selfPkg.packageJson.name;
|
|
233
|
+
const entries = readEntries(selfPkg.packageJson.exports);
|
|
234
|
+
const fileToEntries = getFileToEntriesMap(selfPkg.dir, entries);
|
|
235
|
+
const fileEntries = fileToEntries.get(filename);
|
|
236
|
+
|
|
237
|
+
// If the file isn't part of any entry's bundle (tests, scripts, orphans),
|
|
238
|
+
// a self-package import from it can't create a circular-initialization
|
|
239
|
+
// problem in a published bundle. Skip.
|
|
240
|
+
if (!fileEntries || fileEntries.size === 0) {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return visitImports(context, (node, imp) => {
|
|
245
|
+
if (imp.type !== 'internal') return;
|
|
246
|
+
if (imp.packageName !== selfName) return;
|
|
247
|
+
// Type-only imports are erased at runtime and can't cause circular init.
|
|
248
|
+
if (imp.kind === 'type') return;
|
|
249
|
+
// Importing a non-script asset (e.g. `package.json`) doesn't go through
|
|
250
|
+
// the module barrel, so it can't cause circular init issues.
|
|
251
|
+
if (imp.path === 'package.json' || imp.path.endsWith('/package.json')) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const importEntry = findEntryForImport(imp.path, entries);
|
|
256
|
+
if (!importEntry) return;
|
|
257
|
+
|
|
258
|
+
const importPath = imp.path ? `${selfName}/${imp.path}` : selfName;
|
|
259
|
+
|
|
260
|
+
if (fileEntries.has(importEntry.key)) {
|
|
261
|
+
context.report({
|
|
262
|
+
node,
|
|
263
|
+
messageId: 'sameEntrySelfImport',
|
|
264
|
+
data: {
|
|
265
|
+
packageName: selfName,
|
|
266
|
+
entry: importEntry.key,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!allowCrossEntry) {
|
|
273
|
+
context.report({
|
|
274
|
+
node,
|
|
275
|
+
messageId: 'crossEntrySelfImport',
|
|
276
|
+
data: {
|
|
277
|
+
packageName: selfName,
|
|
278
|
+
importPath,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export * from './refs';
|
|
18
|
+
export * from '../shared';
|
|
19
|
+
export * from '../next';
|
|
20
|
+
export type { TypeOnly } from './typeRef';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export const refs = 3;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export type TypeOnly = { value: number };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export * from './util';
|
|
18
|
+
export * from './shared';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export const foo = 4;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export * from './foo';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export const orphan = 6;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export const shared = 2;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export const helper = 5;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export * from './testUtils/helper';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
export const util = 1;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
import { RuleTester } from 'eslint';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import rule from '../rules/no-self-package-imports';
|
|
20
|
+
|
|
21
|
+
const RULE = 'no-self-package-imports';
|
|
22
|
+
const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo');
|
|
23
|
+
const PKG_DIR = path.join(FIXTURE, 'packages/self-import-pkg');
|
|
24
|
+
|
|
25
|
+
const sameEntryErr = (entry = '.') => ({
|
|
26
|
+
messageId: 'sameEntrySelfImport',
|
|
27
|
+
data: { packageName: '@internal/self-import-pkg', entry },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const crossEntryErr = (importPath: string) => ({
|
|
31
|
+
messageId: 'crossEntrySelfImport',
|
|
32
|
+
data: { packageName: '@internal/self-import-pkg', importPath },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const origDir = process.cwd();
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
process.chdir(origDir);
|
|
38
|
+
});
|
|
39
|
+
process.chdir(FIXTURE);
|
|
40
|
+
|
|
41
|
+
const ruleTester = new RuleTester({
|
|
42
|
+
parser: require.resolve('@typescript-eslint/parser'),
|
|
43
|
+
parserOptions: {
|
|
44
|
+
sourceType: 'module',
|
|
45
|
+
ecmaVersion: 2021,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
ruleTester.run(RULE, rule, {
|
|
50
|
+
valid: [
|
|
51
|
+
// Relative imports are always fine.
|
|
52
|
+
{
|
|
53
|
+
code: `import { foo } from './local'`,
|
|
54
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
55
|
+
},
|
|
56
|
+
// Imports of other packages are unaffected.
|
|
57
|
+
{
|
|
58
|
+
code: `import { foo } from '@internal/bar'`,
|
|
59
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
code: `import { foo } from 'react'`,
|
|
63
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
64
|
+
},
|
|
65
|
+
// `src/alpha/refs.ts` is only in the `./alpha` bundle; importing from the
|
|
66
|
+
// root entry `.` is a cross-entry reference, which is allowed by default.
|
|
67
|
+
{
|
|
68
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
69
|
+
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
|
70
|
+
},
|
|
71
|
+
// `src/index.ts` is only in the `.` bundle; importing from `./alpha` is
|
|
72
|
+
// cross-entry.
|
|
73
|
+
{
|
|
74
|
+
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
|
75
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
code: `import { foo } from '@internal/self-import-pkg/testUtils'`,
|
|
79
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
80
|
+
},
|
|
81
|
+
// `src/next/foo.ts` is physically under `src/` but only re-exported from
|
|
82
|
+
// `./alpha` (via `src/alpha/index.ts` → `../next`). The rule follows the
|
|
83
|
+
// actual barrel graph rather than the directory layout, so importing
|
|
84
|
+
// from the root entry is correctly classified as cross-entry.
|
|
85
|
+
{
|
|
86
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
87
|
+
filename: path.join(PKG_DIR, 'src/next/foo.ts'),
|
|
88
|
+
},
|
|
89
|
+
// `package.json` imports are exempt since they don't go through the
|
|
90
|
+
// module barrel.
|
|
91
|
+
{
|
|
92
|
+
code: `import pkg from '@internal/self-import-pkg/package.json'`,
|
|
93
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
94
|
+
},
|
|
95
|
+
// Files that aren't reachable from any entry (tests, scripts, orphans)
|
|
96
|
+
// can't cause circular-init errors in the published bundle, so they're
|
|
97
|
+
// skipped entirely.
|
|
98
|
+
{
|
|
99
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
100
|
+
filename: path.join(PKG_DIR, 'src/index.test.ts'),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
|
104
|
+
filename: path.join(PKG_DIR, 'src/index.test.ts'),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
108
|
+
filename: path.join(PKG_DIR, 'src/orphan.ts'),
|
|
109
|
+
},
|
|
110
|
+
// Dynamic imports in a test file still count as orphan, since the test
|
|
111
|
+
// file itself isn't part of any entry's bundle.
|
|
112
|
+
{
|
|
113
|
+
code: `const m = import('@internal/self-import-pkg')`,
|
|
114
|
+
filename: path.join(PKG_DIR, 'src/alpha/refs.test.ts'),
|
|
115
|
+
},
|
|
116
|
+
// `import type` is erased at runtime and can't create circular
|
|
117
|
+
// initialization issues, so it's always allowed.
|
|
118
|
+
{
|
|
119
|
+
code: `import type { Foo } from '@internal/self-import-pkg'`,
|
|
120
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
code: `import type { Foo } from '@internal/self-import-pkg/alpha'`,
|
|
124
|
+
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
|
125
|
+
},
|
|
126
|
+
// `export type { ... } from` is also a type-only statement: the TS AST
|
|
127
|
+
// marks it with `exportKind: 'type'`, and the emitted JS has no runtime
|
|
128
|
+
// edge. Both same-entry and cross-entry forms must be skipped.
|
|
129
|
+
{
|
|
130
|
+
code: `export type { Foo } from '@internal/self-import-pkg'`,
|
|
131
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
code: `export type { Foo } from '@internal/self-import-pkg/alpha'`,
|
|
135
|
+
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
|
136
|
+
},
|
|
137
|
+
// `src/alpha/typeRef.ts` is only reachable from the `./alpha` barrel via
|
|
138
|
+
// an `export type { ... } from './typeRef'` edge. Since type-only edges
|
|
139
|
+
// are erased at runtime, the file isn't part of any entry's bundle and
|
|
140
|
+
// self-imports from it must be skipped as orphans.
|
|
141
|
+
{
|
|
142
|
+
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
|
143
|
+
filename: path.join(PKG_DIR, 'src/alpha/typeRef.ts'),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
147
|
+
filename: path.join(PKG_DIR, 'src/alpha/typeRef.ts'),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
invalid: [
|
|
151
|
+
// Same-entry self-imports are always errors because they create circular
|
|
152
|
+
// module graphs inside a bundle.
|
|
153
|
+
{
|
|
154
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
155
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
156
|
+
errors: [sameEntryErr('.')],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
160
|
+
filename: path.join(PKG_DIR, 'src/util.ts'),
|
|
161
|
+
errors: [sameEntryErr('.')],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
code: `export { foo } from '@internal/self-import-pkg'`,
|
|
165
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
166
|
+
errors: [sameEntryErr('.')],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
code: `const x = require('@internal/self-import-pkg')`,
|
|
170
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
171
|
+
errors: [sameEntryErr('.')],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
code: `const m = import('@internal/self-import-pkg')`,
|
|
175
|
+
filename: path.join(PKG_DIR, 'src/util.ts'),
|
|
176
|
+
errors: [sameEntryErr('.')],
|
|
177
|
+
},
|
|
178
|
+
// Files in a non-root entry's bundle importing that same entry are
|
|
179
|
+
// flagged.
|
|
180
|
+
{
|
|
181
|
+
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
|
182
|
+
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
|
183
|
+
errors: [sameEntryErr('./alpha')],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
code: `import { foo } from '@internal/self-import-pkg/testUtils'`,
|
|
187
|
+
filename: path.join(PKG_DIR, 'src/testUtils.ts'),
|
|
188
|
+
errors: [sameEntryErr('./testUtils')],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
code: `import { foo } from '@internal/self-import-pkg/testUtils'`,
|
|
192
|
+
filename: path.join(PKG_DIR, 'src/testUtils/helper.ts'),
|
|
193
|
+
errors: [sameEntryErr('./testUtils')],
|
|
194
|
+
},
|
|
195
|
+
// `src/next/foo.ts` is actually in the `./alpha` bundle via barrel
|
|
196
|
+
// re-exports, so importing `./alpha` from it is same-entry — even though
|
|
197
|
+
// the directory layout might suggest otherwise.
|
|
198
|
+
{
|
|
199
|
+
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
|
200
|
+
filename: path.join(PKG_DIR, 'src/next/foo.ts'),
|
|
201
|
+
errors: [sameEntryErr('./alpha')],
|
|
202
|
+
},
|
|
203
|
+
// `src/shared.ts` is re-exported by both the root and alpha entries, so
|
|
204
|
+
// either target is same-entry.
|
|
205
|
+
{
|
|
206
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
207
|
+
filename: path.join(PKG_DIR, 'src/shared.ts'),
|
|
208
|
+
errors: [sameEntryErr('.')],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
|
212
|
+
filename: path.join(PKG_DIR, 'src/shared.ts'),
|
|
213
|
+
errors: [sameEntryErr('./alpha')],
|
|
214
|
+
},
|
|
215
|
+
// With `allowCrossEntry: false`, cross-entry imports are also flagged.
|
|
216
|
+
{
|
|
217
|
+
code: `import { foo } from '@internal/self-import-pkg'`,
|
|
218
|
+
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
|
219
|
+
options: [{ allowCrossEntry: false }],
|
|
220
|
+
errors: [crossEntryErr('@internal/self-import-pkg')],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
|
224
|
+
filename: path.join(PKG_DIR, 'src/index.ts'),
|
|
225
|
+
options: [{ allowCrossEntry: false }],
|
|
226
|
+
errors: [crossEntryErr('@internal/self-import-pkg/alpha')],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
});
|