From ab1cdbb9dbb807cbc852d5cb6c4350580f65e0ef Mon Sep 17 00:00:00 2001 From: Marat Dyatko Date: Thu, 23 Apr 2026 14:41:33 +0200 Subject: [PATCH] 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 Made-with: Cursor --- .../add-no-self-package-imports-lint-rule.md | 5 + .changeset/fix-circular-self-imports.md | 10 + packages/catalog-model/report-alpha.api.md | 2 +- .../catalog-model/src/entity/AlphaEntity.ts | 4 +- packages/core-plugin-api/src/app/useApp.tsx | 2 +- .../docs/rules/no-self-package-imports.md | 106 +++++++ packages/eslint-plugin/index.js | 2 + .../rules/no-self-package-imports.js | 273 ++++++++++++++++++ .../packages/self-import-pkg/package.json | 12 + .../self-import-pkg/src/alpha/index.ts | 19 ++ .../self-import-pkg/src/alpha/refs.ts | 17 ++ .../packages/self-import-pkg/src/index.ts | 18 ++ .../packages/self-import-pkg/src/next/foo.ts | 17 ++ .../self-import-pkg/src/next/index.ts | 17 ++ .../packages/self-import-pkg/src/orphan.ts | 17 ++ .../packages/self-import-pkg/src/shared.ts | 17 ++ .../packages/self-import-pkg/src/testUtils.ts | 17 ++ .../self-import-pkg/src/testUtils/helper.ts | 17 ++ .../packages/self-import-pkg/src/util.ts | 17 ++ .../src/no-self-package-imports.test.ts | 206 +++++++++++++ .../blueprints/AppRootElementBlueprint.tsx | 2 +- plugins/catalog-node/report-alpha.api.md | 2 +- plugins/catalog-node/report.api.md | 10 +- plugins/catalog-node/src/extensions.ts | 2 +- plugins/kubernetes-common/report.api.md | 4 +- .../src/error-detection/error-detection.ts | 2 +- .../kubernetes-common/src/util/response.ts | 2 +- plugins/kubernetes-node/report.api.md | 14 +- plugins/kubernetes-node/src/extensions.ts | 2 +- 29 files changed, 810 insertions(+), 25 deletions(-) create mode 100644 .changeset/add-no-self-package-imports-lint-rule.md create mode 100644 .changeset/fix-circular-self-imports.md create mode 100644 packages/eslint-plugin/docs/rules/no-self-package-imports.md create mode 100644 packages/eslint-plugin/rules/no-self-package-imports.js create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/package.json create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/index.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/refs.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/index.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/foo.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/index.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/orphan.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/shared.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils/helper.ts create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/util.ts create mode 100644 packages/eslint-plugin/src/no-self-package-imports.test.ts diff --git a/.changeset/add-no-self-package-imports-lint-rule.md b/.changeset/add-no-self-package-imports-lint-rule.md new file mode 100644 index 0000000000..743c5d73d0 --- /dev/null +++ b/.changeset/add-no-self-package-imports-lint-rule.md @@ -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`. diff --git a/.changeset/fix-circular-self-imports.md b/.changeset/fix-circular-self-imports.md new file mode 100644 index 0000000000..b30dbc1b80 --- /dev/null +++ b/.changeset/fix-circular-self-imports.md @@ -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. diff --git a/packages/catalog-model/report-alpha.api.md b/packages/catalog-model/report-alpha.api.md index c323e4be5c..7483d0b6a6 100644 --- a/packages/catalog-model/report-alpha.api.md +++ b/packages/catalog-model/report-alpha.api.md @@ -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'; diff --git a/packages/catalog-model/src/entity/AlphaEntity.ts b/packages/catalog-model/src/entity/AlphaEntity.ts index 6f5c9d4fa7..4f3f9d55fc 100644 --- a/packages/catalog-model/src/entity/AlphaEntity.ts +++ b/packages/catalog-model/src/entity/AlphaEntity.ts @@ -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'; /** diff --git a/packages/core-plugin-api/src/app/useApp.tsx b/packages/core-plugin-api/src/app/useApp.tsx index 5d789689cd..16818c038a 100644 --- a/packages/core-plugin-api/src/app/useApp.tsx +++ b/packages/core-plugin-api/src/app/useApp.tsx @@ -27,7 +27,7 @@ import { FrontendPlugin, ApiHolder, } from '@backstage/frontend-plugin-api'; -import { +import type { AppComponents, IconComponent, BackstagePlugin, diff --git a/packages/eslint-plugin/docs/rules/no-self-package-imports.md b/packages/eslint-plugin/docs/rules/no-self-package-imports.md new file mode 100644 index 0000000000..4b383ed59c --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-self-package-imports.md @@ -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. diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index d911eee1ef..e31091ace3 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -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'), }, }; diff --git a/packages/eslint-plugin/rules/no-self-package-imports.js b/packages/eslint-plugin/rules/no-self-package-imports.js new file mode 100644 index 0000000000..3e42953125 --- /dev/null +++ b/packages/eslint-plugin/rules/no-self-package-imports.js @@ -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>>} */ +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>} + */ +function buildFileToEntriesMap(pkgDir, entries) { + /** @type {Map>} */ + const fileToEntries = new Map(); + for (const entry of entries) { + const sourceFile = path.join(pkgDir, entry.sourceFile); + if (!fs.existsSync(sourceFile)) continue; + /** @type {Set} */ + 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>} + */ +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, + }, + }); + } + }); + }, +}; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/package.json new file mode 100644 index 0000000000..7e97cf807a --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/package.json @@ -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" + } +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/index.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/index.ts new file mode 100644 index 0000000000..3a1f81dd58 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/index.ts @@ -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'; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/refs.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/refs.ts new file mode 100644 index 0000000000..f79af46aaf --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/alpha/refs.ts @@ -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; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/index.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/index.ts new file mode 100644 index 0000000000..38a032a9d9 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/index.ts @@ -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'; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/foo.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/foo.ts new file mode 100644 index 0000000000..98c11f1fda --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/foo.ts @@ -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; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/index.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/index.ts new file mode 100644 index 0000000000..7971b0e2cd --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/next/index.ts @@ -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'; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/orphan.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/orphan.ts new file mode 100644 index 0000000000..9e2273e1dd --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/orphan.ts @@ -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; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/shared.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/shared.ts new file mode 100644 index 0000000000..78d0377307 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/shared.ts @@ -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; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils.ts new file mode 100644 index 0000000000..f180d591e0 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils.ts @@ -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'; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils/helper.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils/helper.ts new file mode 100644 index 0000000000..20e9b4a712 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/testUtils/helper.ts @@ -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; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/util.ts b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/util.ts new file mode 100644 index 0000000000..70f0e721b9 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/self-import-pkg/src/util.ts @@ -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; diff --git a/packages/eslint-plugin/src/no-self-package-imports.test.ts b/packages/eslint-plugin/src/no-self-package-imports.test.ts new file mode 100644 index 0000000000..773752503a --- /dev/null +++ b/packages/eslint-plugin/src/no-self-package-imports.test.ts @@ -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')], + }, + ], +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.tsx index 955abce136..730af779ab 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.tsx @@ -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'; /** diff --git a/plugins/catalog-node/report-alpha.api.md b/plugins/catalog-node/report-alpha.api.md index f0598ece59..4e96265f0e 100644 --- a/plugins/catalog-node/report-alpha.api.md +++ b/plugins/catalog-node/report-alpha.api.md @@ -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'; diff --git a/plugins/catalog-node/report.api.md b/plugins/catalog-node/report.api.md index 8a01810e4c..edbbdd2296 100644 --- a/plugins/catalog-node/report.api.md +++ b/plugins/catalog-node/report.api.md @@ -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'; diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 223a18be05..ac41d383d5 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -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, diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index da989ceeca..7f90727bed 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -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'; diff --git a/plugins/kubernetes-common/src/error-detection/error-detection.ts b/plugins/kubernetes-common/src/error-detection/error-detection.ts index 6fa9211db4..e81f417a7c 100644 --- a/plugins/kubernetes-common/src/error-detection/error-detection.ts +++ b/plugins/kubernetes-common/src/error-detection/error-detection.ts @@ -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'; diff --git a/plugins/kubernetes-common/src/util/response.ts b/plugins/kubernetes-common/src/util/response.ts index 7f7aacfb0b..e99a92b8d7 100644 --- a/plugins/kubernetes-common/src/util/response.ts +++ b/plugins/kubernetes-common/src/util/response.ts @@ -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 */ diff --git a/plugins/kubernetes-node/report.api.md b/plugins/kubernetes-node/report.api.md index 901d1ff782..09a1bd91f8 100644 --- a/plugins/kubernetes-node/report.api.md +++ b/plugins/kubernetes-node/report.api.md @@ -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 { diff --git a/plugins/kubernetes-node/src/extensions.ts b/plugins/kubernetes-node/src/extensions.ts index 50b529d05b..e993b9ecc6 100644 --- a/plugins/kubernetes-node/src/extensions.ts +++ b/plugins/kubernetes-node/src/extensions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { +import type { AuthenticationStrategy, CustomResource, ObjectToFetch,