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:
Marat Dyatko
2026-04-23 14:41:33 +02:00
parent 427d5219a6
commit ab1cdbb9db
29 changed files with 810 additions and 25 deletions
@@ -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`.
+10
View File
@@ -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.
+1 -1
View File
@@ -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';
/**
+1 -1
View File
@@ -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.
+2
View File
@@ -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,
},
});
}
});
},
};
@@ -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"
}
}
@@ -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';
@@ -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;
@@ -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';
@@ -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;
@@ -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';
@@ -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;
@@ -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;
@@ -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';
@@ -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';
/**
+1 -1
View File
@@ -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';
+5 -5
View File
@@ -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';
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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 */
+7 -7
View File
@@ -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 {
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createExtensionPoint } from '@backstage/backend-plugin-api';
import {
import type {
AuthenticationStrategy,
CustomResource,
ObjectToFetch,