Fix circular self-imports and add no-self-package-imports lint rule
- Fixes the `Cannot access '_AppRootElementBlueprintesm' before initialization` crash in `@backstage/frontend-plugin-api` caused by a self-referential import in the packaged ESM. - Cleans up similar self-imports in `@backstage/catalog-model`, `@backstage/core-plugin-api`, `@backstage/plugin-catalog-node`, `@backstage/plugin-kubernetes-common`, and `@backstage/plugin-kubernetes-node`. Value imports switch to relative paths; type-only imports use `import type` so they're erased at runtime. - Adds a new `@backstage/no-self-package-imports` ESLint rule. It reads each package's `exports` map, traverses the relative import graph from every entry's source file, and only reports imports where the current file is in the same bundle as the target entry (same-entry). Files that aren't reachable from any entry (tests, scripts, orphans) are skipped. `import type`, `package.json` imports, and cross-entry self-imports are allowed by default; cross-entry can be opted into with `allowCrossEntry: false`. Signed-off-by: Marat Dyatko <maratd@spotify.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/eslint-plugin': minor
|
||||
---
|
||||
|
||||
Added a new `no-self-package-imports` lint rule that warns 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`.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
'@backstage/catalog-model': patch
|
||||
'@backstage/core-plugin-api': patch
|
||||
'@backstage/plugin-catalog-node': patch
|
||||
'@backstage/plugin-kubernetes-common': patch
|
||||
'@backstage/plugin-kubernetes-node': patch
|
||||
---
|
||||
|
||||
Removed a handful of internal imports that referenced the package by its own name. Value imports were switched to relative paths, and type-only imports to `import type`. These self-referential imports could trigger circular initialization errors in bundled ESM and when the package was loaded via `jest.requireActual` — most visibly `Cannot access '_AppRootElementBlueprintesm' before initialization` from `@backstage/frontend-plugin-api`. There are no user-facing API changes.
|
||||
@@ -3,7 +3,7 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { SerializedError } from '@backstage/errors';
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO(Rugvip): Figure out best way to allow this import
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { EntityStatus } from './EntityStatus';
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
FrontendPlugin,
|
||||
ApiHolder,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
import type {
|
||||
AppComponents,
|
||||
IconComponent,
|
||||
BackstagePlugin,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# @backstage/no-self-package-imports
|
||||
|
||||
This rule prevents a package from importing itself by its own name when the
|
||||
imported entry point bundles the current file. Self-package imports in that
|
||||
situation create a circular dependency through the bundled barrel, which can
|
||||
surface as runtime errors such as
|
||||
`Cannot access 'X' before initialization` when the package is loaded in an
|
||||
environment that triggers eager re-evaluation (for example
|
||||
`jest.requireActual`, or ESM consumers that follow the cycle).
|
||||
|
||||
The rule understands your package's `exports` map and follows the
|
||||
relative import graph from each entry's source file to determine which files
|
||||
are actually bundled into each entry. It then reports:
|
||||
|
||||
- **Same-entry self-imports**: the current file is part of the same bundle
|
||||
as the entry it imports, so the cycle is real.
|
||||
- **Cross-entry self-imports** (optional): the file is part of a different
|
||||
entry's bundle than the one it imports. Cross-entry self-imports don't
|
||||
always cycle, but they still couple unrelated entry points at initialization
|
||||
time and are worth avoiding.
|
||||
|
||||
Files that aren't reachable from any published entry (tests, scripts,
|
||||
orphans) are skipped, as self-imports from them can't affect a published
|
||||
bundle.
|
||||
|
||||
Imports declared with `import type` (or `export type`) are erased at runtime
|
||||
and are always allowed, since they can't cause circular initialization.
|
||||
|
||||
## Usage
|
||||
|
||||
Add the rule as follows:
|
||||
|
||||
```js
|
||||
"@backstage/no-self-package-imports": ["error"]
|
||||
```
|
||||
|
||||
This errors on same-entry self-imports. Cross-entry self-imports are allowed
|
||||
by default; opt in to reporting them with `allowCrossEntry: false`:
|
||||
|
||||
```js
|
||||
"@backstage/no-self-package-imports": ["error", { "allowCrossEntry": false }]
|
||||
```
|
||||
|
||||
## Rule Details
|
||||
|
||||
Given this `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@backstage/plugin-foo",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
and `src/index.ts` that re-exports `./blueprint`:
|
||||
|
||||
```ts
|
||||
export * from './blueprint';
|
||||
```
|
||||
|
||||
### Fail
|
||||
|
||||
Importing `@backstage/plugin-foo` from a file that is also reachable from
|
||||
`src/index.ts` creates a cycle:
|
||||
|
||||
```ts
|
||||
// src/blueprint.ts
|
||||
import { helper } from '@backstage/plugin-foo';
|
||||
```
|
||||
|
||||
### Pass
|
||||
|
||||
Use a relative import instead:
|
||||
|
||||
```ts
|
||||
// src/blueprint.ts
|
||||
import { helper } from './helper';
|
||||
```
|
||||
|
||||
Or, if only the type is used, `import type` is erased at runtime and is
|
||||
always allowed:
|
||||
|
||||
```ts
|
||||
// src/blueprint.ts
|
||||
import type { Helper } from '@backstage/plugin-foo';
|
||||
```
|
||||
|
||||
Importing `package.json` is always allowed:
|
||||
|
||||
```ts
|
||||
import { version } from '@backstage/plugin-foo/package.json';
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### `allowCrossEntry`
|
||||
|
||||
- Type: `boolean`
|
||||
- Default: `true`
|
||||
|
||||
When `false`, the rule also reports self-imports that target a different
|
||||
entry from the one the current file is part of.
|
||||
@@ -24,6 +24,7 @@ module.exports = {
|
||||
'@backstage/no-undeclared-imports': 'error',
|
||||
'@backstage/no-mixed-plugin-imports': 'warn',
|
||||
'@backstage/no-ui-css-imports-in-non-frontend': 'error',
|
||||
'@backstage/no-self-package-imports': 'error',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -34,5 +35,6 @@ module.exports = {
|
||||
'no-top-level-material-ui-4-imports': require('./rules/no-top-level-material-ui-4-imports'),
|
||||
'no-mixed-plugin-imports': require('./rules/no-mixed-plugin-imports'),
|
||||
'no-ui-css-imports-in-non-frontend': require('./rules/no-ui-css-imports-in-non-frontend'),
|
||||
'no-self-package-imports': require('./rules/no-self-package-imports'),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const visitImports = require('../lib/visitImports');
|
||||
const getPackages = require('../lib/getPackages');
|
||||
|
||||
/**
|
||||
* @typedef EntryInfo
|
||||
* @type {object}
|
||||
* @property {string} key - The exports key, e.g. '.' or './alpha'.
|
||||
* @property {string} sourceFile - The source file for the entry, relative to the package dir.
|
||||
*/
|
||||
|
||||
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
||||
|
||||
// Cache the per-package analysis across files lint invocations. The key is the
|
||||
// absolute package dir; the value is a Map from absolute file path to the set
|
||||
// of entry keys whose bundle reaches that file.
|
||||
/** @type {Map<string, Map<string, Set<string>>>} */
|
||||
const bundleCache = new Map();
|
||||
|
||||
/**
|
||||
* Build a list of entries from the package.json exports field. Entries that
|
||||
* don't point to a script (e.g. `./package.json`) are ignored.
|
||||
*
|
||||
* @param {unknown} exportsField
|
||||
* @returns {EntryInfo[]}
|
||||
*/
|
||||
function readEntries(exportsField) {
|
||||
if (!exportsField || typeof exportsField !== 'object') {
|
||||
return [{ key: '.', sourceFile: 'src/index.ts' }];
|
||||
}
|
||||
/** @type {EntryInfo[]} */
|
||||
const entries = [];
|
||||
for (const [key, value] of Object.entries(exportsField)) {
|
||||
if (typeof value !== 'string') continue;
|
||||
if (key === './package.json') continue;
|
||||
const rel = value.replace(/^\.\//, '');
|
||||
entries.push({ key, sourceFile: rel });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a relative module specifier against a containing file.
|
||||
* Tries source extensions and index files, in that order.
|
||||
*
|
||||
* @param {string} fromFile
|
||||
* @param {string} specifier
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function resolveSourcePath(fromFile, specifier) {
|
||||
const base = path.resolve(path.dirname(fromFile), specifier);
|
||||
// If the specifier already carries an extension, only try the exact path.
|
||||
if (/\.[cm]?[jt]sx?$/i.test(specifier)) {
|
||||
return fs.existsSync(base) ? base : undefined;
|
||||
}
|
||||
for (const ext of SOURCE_EXTENSIONS) {
|
||||
const candidate = base + ext;
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
}
|
||||
for (const ext of SOURCE_EXTENSIONS) {
|
||||
const candidate = path.join(base, 'index' + ext);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Matches `from '...'` specifiers in `import`/`export ... from` statements.
|
||||
// Uses a non-greedy body so multi-line imports match cleanly.
|
||||
const FROM_SPEC_RE =
|
||||
/(?:^|[\s;}])(?:import|export)\b[^"';]*?\bfrom\s*["']([^"']+)["']/gm;
|
||||
// Matches side-effect imports: `import '...';`.
|
||||
const SIDE_EFFECT_IMPORT_RE = /(?:^|[\s;}])import\s*["']([^"']+)["']/gm;
|
||||
|
||||
/**
|
||||
* Extract relative module specifiers from a source file's contents.
|
||||
*
|
||||
* @param {string} contents
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function collectRelativeSpecifiers(contents) {
|
||||
const specs = new Set();
|
||||
for (const m of contents.matchAll(FROM_SPEC_RE)) {
|
||||
if (m[1].startsWith('.')) specs.add(m[1]);
|
||||
}
|
||||
for (const m of contents.matchAll(SIDE_EFFECT_IMPORT_RE)) {
|
||||
if (m[1].startsWith('.')) specs.add(m[1]);
|
||||
}
|
||||
return [...specs];
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the relative import/export graph of each entry's source file and build
|
||||
* a map of absolute file paths to the set of entries whose bundles include
|
||||
* them. Files that aren't reachable from any entry (e.g. tests, scripts)
|
||||
* aren't present in the map.
|
||||
*
|
||||
* @param {string} pkgDir
|
||||
* @param {EntryInfo[]} entries
|
||||
* @returns {Map<string, Set<string>>}
|
||||
*/
|
||||
function buildFileToEntriesMap(pkgDir, entries) {
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
const fileToEntries = new Map();
|
||||
for (const entry of entries) {
|
||||
const sourceFile = path.join(pkgDir, entry.sourceFile);
|
||||
if (!fs.existsSync(sourceFile)) continue;
|
||||
/** @type {Set<string>} */
|
||||
const visited = new Set();
|
||||
const queue = [sourceFile];
|
||||
while (queue.length > 0) {
|
||||
const current = /** @type {string} */ (queue.pop());
|
||||
if (visited.has(current)) continue;
|
||||
visited.add(current);
|
||||
|
||||
let set = fileToEntries.get(current);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
fileToEntries.set(current, set);
|
||||
}
|
||||
set.add(entry.key);
|
||||
|
||||
let contents;
|
||||
try {
|
||||
contents = fs.readFileSync(current, 'utf8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const spec of collectRelativeSpecifiers(contents)) {
|
||||
const resolved = resolveSourcePath(current, spec);
|
||||
if (resolved) queue.push(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileToEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pkgDir
|
||||
* @param {EntryInfo[]} entries
|
||||
* @returns {Map<string, Set<string>>}
|
||||
*/
|
||||
function getFileToEntriesMap(pkgDir, entries) {
|
||||
let cached = bundleCache.get(pkgDir);
|
||||
if (!cached) {
|
||||
cached = buildFileToEntriesMap(pkgDir, entries);
|
||||
bundleCache.set(pkgDir, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find which entry an import targets based on its subpath. Returns undefined
|
||||
* when the import doesn't match any declared entry.
|
||||
*
|
||||
* @param {string} subPath - The part after the package name, without leading slash. Empty for the root entry.
|
||||
* @param {EntryInfo[]} entries
|
||||
* @returns {EntryInfo | undefined}
|
||||
*/
|
||||
function findEntryForImport(subPath, entries) {
|
||||
const key = subPath ? `./${subPath}` : '.';
|
||||
return entries.find(e => e.key === key);
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
sameEntrySelfImport:
|
||||
"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).",
|
||||
crossEntrySelfImport:
|
||||
"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).",
|
||||
},
|
||||
docs: {
|
||||
description:
|
||||
'Disallow a package from importing itself by its own name, which causes circular initialization issues in bundled ESM.',
|
||||
url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-self-package-imports.md',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowCrossEntry: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const allowCrossEntry = options.allowCrossEntry !== false;
|
||||
|
||||
const packages = getPackages(context.getCwd());
|
||||
const filename = context.getFilename();
|
||||
const selfPkg = packages?.byPath(filename);
|
||||
if (!selfPkg) {
|
||||
return {};
|
||||
}
|
||||
const selfName = selfPkg.packageJson.name;
|
||||
const entries = readEntries(selfPkg.packageJson.exports);
|
||||
const fileToEntries = getFileToEntriesMap(selfPkg.dir, entries);
|
||||
const fileEntries = fileToEntries.get(filename);
|
||||
|
||||
// If the file isn't part of any entry's bundle (tests, scripts, orphans),
|
||||
// a self-package import from it can't create a circular-initialization
|
||||
// problem in a published bundle. Skip.
|
||||
if (!fileEntries || fileEntries.size === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return visitImports(context, (node, imp) => {
|
||||
if (imp.type !== 'internal') return;
|
||||
if (imp.packageName !== selfName) return;
|
||||
// Type-only imports are erased at runtime and can't cause circular init.
|
||||
if (imp.kind === 'type') return;
|
||||
// Importing a non-script asset (e.g. `package.json`) doesn't go through
|
||||
// the module barrel, so it can't cause circular init issues.
|
||||
if (imp.path === 'package.json' || imp.path.endsWith('/package.json')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importEntry = findEntryForImport(imp.path, entries);
|
||||
if (!importEntry) return;
|
||||
|
||||
const importPath = imp.path ? `${selfName}/${imp.path}` : selfName;
|
||||
|
||||
if (fileEntries.has(importEntry.key)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'sameEntrySelfImport',
|
||||
data: {
|
||||
packageName: selfName,
|
||||
entry: importEntry.key,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowCrossEntry) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'crossEntrySelfImport',
|
||||
data: {
|
||||
packageName: selfName,
|
||||
importPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@internal/self-import-pkg",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha/index.ts",
|
||||
"./testUtils": "./src/testUtils.ts",
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './refs';
|
||||
export * from '../shared';
|
||||
export * from '../next';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const refs = 3;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './util';
|
||||
export * from './shared';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const foo = 4;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './foo';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const orphan = 6;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const shared = 2;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './testUtils/helper';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const helper = 5;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const util = 1;
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import path from 'node:path';
|
||||
import rule from '../rules/no-self-package-imports';
|
||||
|
||||
const RULE = 'no-self-package-imports';
|
||||
const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo');
|
||||
const PKG_DIR = path.join(FIXTURE, 'packages/self-import-pkg');
|
||||
|
||||
const sameEntryErr = (entry = '.') => ({
|
||||
messageId: 'sameEntrySelfImport',
|
||||
data: { packageName: '@internal/self-import-pkg', entry },
|
||||
});
|
||||
|
||||
const crossEntryErr = (importPath: string) => ({
|
||||
messageId: 'crossEntrySelfImport',
|
||||
data: { packageName: '@internal/self-import-pkg', importPath },
|
||||
});
|
||||
|
||||
const origDir = process.cwd();
|
||||
afterAll(() => {
|
||||
process.chdir(origDir);
|
||||
});
|
||||
process.chdir(FIXTURE);
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2021,
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run(RULE, rule, {
|
||||
valid: [
|
||||
// Relative imports are always fine.
|
||||
{
|
||||
code: `import { foo } from './local'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
},
|
||||
// Imports of other packages are unaffected.
|
||||
{
|
||||
code: `import { foo } from '@internal/bar'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
},
|
||||
{
|
||||
code: `import { foo } from 'react'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
},
|
||||
// `src/alpha/refs.ts` is only in the `./alpha` bundle; importing from the
|
||||
// root entry `.` is a cross-entry reference, which is allowed by default.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
||||
},
|
||||
// `src/index.ts` is only in the `.` bundle; importing from `./alpha` is
|
||||
// cross-entry.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/testUtils'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
},
|
||||
// `src/next/foo.ts` is physically under `src/` but only re-exported from
|
||||
// `./alpha` (via `src/alpha/index.ts` → `../next`). The rule follows the
|
||||
// actual barrel graph rather than the directory layout, so importing
|
||||
// from the root entry is correctly classified as cross-entry.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/next/foo.ts'),
|
||||
},
|
||||
// `package.json` imports are exempt since they don't go through the
|
||||
// module barrel.
|
||||
{
|
||||
code: `import pkg from '@internal/self-import-pkg/package.json'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
},
|
||||
// Files that aren't reachable from any entry (tests, scripts, orphans)
|
||||
// can't cause circular-init errors in the published bundle, so they're
|
||||
// skipped entirely.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.test.ts'),
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.test.ts'),
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/orphan.ts'),
|
||||
},
|
||||
// Dynamic imports in a test file still count as orphan, since the test
|
||||
// file itself isn't part of any entry's bundle.
|
||||
{
|
||||
code: `const m = import('@internal/self-import-pkg')`,
|
||||
filename: path.join(PKG_DIR, 'src/alpha/refs.test.ts'),
|
||||
},
|
||||
// `import type` is erased at runtime and can't create circular
|
||||
// initialization issues, so it's always allowed.
|
||||
{
|
||||
code: `import type { Foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
},
|
||||
{
|
||||
code: `import type { Foo } from '@internal/self-import-pkg/alpha'`,
|
||||
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Same-entry self-imports are always errors because they create circular
|
||||
// module graphs inside a bundle.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
errors: [sameEntryErr('.')],
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/util.ts'),
|
||||
errors: [sameEntryErr('.')],
|
||||
},
|
||||
{
|
||||
code: `export { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
errors: [sameEntryErr('.')],
|
||||
},
|
||||
{
|
||||
code: `const x = require('@internal/self-import-pkg')`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
errors: [sameEntryErr('.')],
|
||||
},
|
||||
{
|
||||
code: `const m = import('@internal/self-import-pkg')`,
|
||||
filename: path.join(PKG_DIR, 'src/util.ts'),
|
||||
errors: [sameEntryErr('.')],
|
||||
},
|
||||
// Files in a non-root entry's bundle importing that same entry are
|
||||
// flagged.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
||||
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
||||
errors: [sameEntryErr('./alpha')],
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/testUtils'`,
|
||||
filename: path.join(PKG_DIR, 'src/testUtils.ts'),
|
||||
errors: [sameEntryErr('./testUtils')],
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/testUtils'`,
|
||||
filename: path.join(PKG_DIR, 'src/testUtils/helper.ts'),
|
||||
errors: [sameEntryErr('./testUtils')],
|
||||
},
|
||||
// `src/next/foo.ts` is actually in the `./alpha` bundle via barrel
|
||||
// re-exports, so importing `./alpha` from it is same-entry — even though
|
||||
// the directory layout might suggest otherwise.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
||||
filename: path.join(PKG_DIR, 'src/next/foo.ts'),
|
||||
errors: [sameEntryErr('./alpha')],
|
||||
},
|
||||
// `src/shared.ts` is re-exported by both the root and alpha entries, so
|
||||
// either target is same-entry.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/shared.ts'),
|
||||
errors: [sameEntryErr('.')],
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
||||
filename: path.join(PKG_DIR, 'src/shared.ts'),
|
||||
errors: [sameEntryErr('./alpha')],
|
||||
},
|
||||
// With `allowCrossEntry: false`, cross-entry imports are also flagged.
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg'`,
|
||||
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
|
||||
options: [{ allowCrossEntry: false }],
|
||||
errors: [crossEntryErr('@internal/self-import-pkg')],
|
||||
},
|
||||
{
|
||||
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
|
||||
filename: path.join(PKG_DIR, 'src/index.ts'),
|
||||
options: [{ allowCrossEntry: false }],
|
||||
errors: [crossEntryErr('@internal/self-import-pkg/alpha')],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExtensionBoundary } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionBoundary } from '../components/ExtensionBoundary';
|
||||
import { coreExtensionData, createExtensionBlueprint } from '../wiring';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { CatalogModelLayer } from '@backstage/catalog-model/alpha';
|
||||
import { CatalogModelSource } from '@backstage/catalog-model/alpha';
|
||||
import { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
|
||||
import type { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
|
||||
import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
|
||||
@@ -9,10 +9,10 @@ import { AnalyzeLocationExistingEntity } from '@backstage/plugin-catalog-common'
|
||||
import { AnalyzeLocationRequest } from '@backstage/plugin-catalog-common';
|
||||
import { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { CatalogProcessor as CatalogProcessor_2 } from '@backstage/plugin-catalog-node';
|
||||
import type { CatalogProcessor as CatalogProcessor_2 } from '@backstage/plugin-catalog-node';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider as EntityProvider_2 } from '@backstage/plugin-catalog-node';
|
||||
import type { EntityProvider as EntityProvider_2 } from '@backstage/plugin-catalog-node';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { GetEntitiesByRefsRequest } from '@backstage/catalog-client';
|
||||
import { GetEntitiesByRefsResponse } from '@backstage/catalog-client';
|
||||
@@ -25,16 +25,16 @@ import { GetEntityFacetsResponse } from '@backstage/catalog-client';
|
||||
import { GetLocationsResponse } from '@backstage/catalog-client';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { Location as Location_2 } from '@backstage/catalog-client';
|
||||
import { LocationAnalyzer as LocationAnalyzer_2 } from '@backstage/plugin-catalog-node';
|
||||
import type { LocationAnalyzer as LocationAnalyzer_2 } from '@backstage/plugin-catalog-node';
|
||||
import { LocationEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common';
|
||||
import { PlaceholderResolver as PlaceholderResolver_2 } from '@backstage/plugin-catalog-node';
|
||||
import type { PlaceholderResolver as PlaceholderResolver_2 } from '@backstage/plugin-catalog-node';
|
||||
import { QueryEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { QueryEntitiesResponse } from '@backstage/catalog-client';
|
||||
import { QueryLocationsInitialRequest } from '@backstage/catalog-client';
|
||||
import { QueryLocationsRequest } from '@backstage/catalog-client';
|
||||
import { QueryLocationsResponse } from '@backstage/catalog-client';
|
||||
import { ScmLocationAnalyzer as ScmLocationAnalyzer_2 } from '@backstage/plugin-catalog-node';
|
||||
import type { ScmLocationAnalyzer as ScmLocationAnalyzer_2 } from '@backstage/plugin-catalog-node';
|
||||
import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
import { StreamEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { ValidateEntityResponse } from '@backstage/catalog-client';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { createExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { Entity, Validators } from '@backstage/catalog-model';
|
||||
import { CatalogModelSource } from '@backstage/catalog-model/alpha';
|
||||
import {
|
||||
import type {
|
||||
CatalogProcessor,
|
||||
CatalogProcessorParser,
|
||||
EntityProvider,
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
```ts
|
||||
import { BasicPermission } from '@backstage/plugin-permission-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { FetchResponse as FetchResponse_2 } from '@backstage/plugin-kubernetes-common';
|
||||
import type { FetchResponse as FetchResponse_2 } from '@backstage/plugin-kubernetes-common';
|
||||
import type { JsonObject } from '@backstage/types';
|
||||
import type { JsonValue } from '@backstage/types';
|
||||
import { ObjectsByEntityResponse as ObjectsByEntityResponse_2 } from '@backstage/plugin-kubernetes-common';
|
||||
import type { ObjectsByEntityResponse as ObjectsByEntityResponse_2 } from '@backstage/plugin-kubernetes-common';
|
||||
import type { PodStatus } from '@kubernetes/client-node';
|
||||
import type { V1ConfigMap } from '@kubernetes/client-node';
|
||||
import type { V1CronJob } from '@kubernetes/client-node';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { DetectedError, DetectedErrorsByCluster } from './types';
|
||||
import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { groupResponses } from '../util';
|
||||
import { detectErrorsInPods } from './pods';
|
||||
import { detectErrorsInDeployments } from './deployments';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FetchResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import type { FetchResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { GroupedResponses } from '../types';
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import type { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { CustomResource as CustomResource_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import type { CustomResource as CustomResource_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import type express from 'express';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { FetchResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import { KubernetesFetcher as KubernetesFetcher_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import type { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import type { KubernetesFetcher as KubernetesFetcher_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesObjectsProvider as KubernetesObjectsProvider_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import type { KubernetesObjectsProvider as KubernetesObjectsProvider_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesServiceLocator as KubernetesServiceLocator_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import type { KubernetesServiceLocator as KubernetesServiceLocator_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { ObjectToFetch as ObjectToFetch_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import type { ObjectToFetch as ObjectToFetch_2 } from '@backstage/plugin-kubernetes-node';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface AuthenticationStrategy {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
import type {
|
||||
AuthenticationStrategy,
|
||||
CustomResource,
|
||||
ObjectToFetch,
|
||||
|
||||
Reference in New Issue
Block a user