Address review feedback on no-self-package-imports rule

- `visitImports` now also reads `exportKind` so `export type { … } from`
  statements are classified as type-only, fixing a false positive in the
  self-import rule (and correctly skipping them in `no-undeclared-imports`
  too).
- The reachability-graph regex in `no-self-package-imports` skips
  `import type` / `export type` edges so files reachable only via
  type-only re-exports aren't pulled into a runtime bundle and no longer
  get false-positive same-entry errors.
- `SOURCE_EXTENSIONS` now includes `.mts` and `.cts` so entries and
  barrels using those extensions are followed correctly.
- The ESLint plugin changeset wording matches the `error` severity of
  the recommended config.
- Adds regression fixtures and RuleTester cases for `export type …` at
  both entries and for a file only reachable via a type-only edge.

Signed-off-by: Marat Dyatko <maratd@spotify.com>
Made-with: Cursor
This commit is contained in:
Marat Dyatko
2026-04-23 16:28:20 +02:00
parent ab1cdbb9db
commit f1e26b8ed7
6 changed files with 57 additions and 5 deletions
@@ -17,3 +17,4 @@
export * from './refs';
export * from '../shared';
export * from '../next';
export type { TypeOnly } from './typeRef';
@@ -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 type TypeOnly = { value: number };
@@ -123,6 +123,29 @@ ruleTester.run(RULE, rule, {
code: `import type { Foo } from '@internal/self-import-pkg/alpha'`,
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
},
// `export type { ... } from` is also a type-only statement: the TS AST
// marks it with `exportKind: 'type'`, and the emitted JS has no runtime
// edge. Both same-entry and cross-entry forms must be skipped.
{
code: `export type { Foo } from '@internal/self-import-pkg'`,
filename: path.join(PKG_DIR, 'src/index.ts'),
},
{
code: `export type { Foo } from '@internal/self-import-pkg/alpha'`,
filename: path.join(PKG_DIR, 'src/alpha/refs.ts'),
},
// `src/alpha/typeRef.ts` is only reachable from the `./alpha` barrel via
// an `export type { ... } from './typeRef'` edge. Since type-only edges
// are erased at runtime, the file isn't part of any entry's bundle and
// self-imports from it must be skipped as orphans.
{
code: `import { foo } from '@internal/self-import-pkg/alpha'`,
filename: path.join(PKG_DIR, 'src/alpha/typeRef.ts'),
},
{
code: `import { foo } from '@internal/self-import-pkg'`,
filename: path.join(PKG_DIR, 'src/alpha/typeRef.ts'),
},
],
invalid: [
// Same-entry self-imports are always errors because they create circular