From 238378e5b7da1cc0d6528d5de13538d4f27f2660 Mon Sep 17 00:00:00 2001 From: Alex Bit Date: Wed, 22 Apr 2026 15:33:09 -0700 Subject: [PATCH 1/6] feat(debarrel): add barrel-metric for migration-complexity scoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emit a `barrel-metric` event per re-export found in each barrel the codemod visits. Each event carries five cardinalities — `export_style`, `chaining`, `risk_amplifier`, `file`, and a derived `migration_complexity_score` (0–4) — so consumers can estimate migration effort before rolling debarrel out at scale. - New `scripts/utils/metrics.ts` isolates the analysis/emission logic. - `heavy-usage-or-public-api` is scoped to files that are the advertised entry point of their nearest `package.json` (matches `main`/`module`/ `types` or any leaf of `exports`), so internal `index.ts` files aren't mis-flagged. - Every file in a barrel's directory emits the same metric tuple; this keeps the per-directory `metrics.json` snapshot stable across siblings but inflates `count` by directory density. Documented in README. - Adds four new fixtures exercising the score space (explicit/wildcard × single-level/chained × none/public-api/side-effects) plus metric snapshots for existing fixtures. Made-with: Cursor --- .changeset/barrel-metric.md | 5 + codemods/debarrel/README.md | 19 + codemods/debarrel/scripts/codemod.ts | 3 + codemods/debarrel/scripts/utils/metrics.ts | 344 ++++++++++++++++++ .../expected/src/metrics.json | 14 + .../aliased-reexport/input/src/metrics.json | 14 + .../expected/src/lib/metrics.json | 14 + .../input/src/lib/metrics.json | 14 + .../expected/src/components/metrics.json | 14 + .../input/src/components/metrics.json | 14 + .../expected/src/feature/foo.ts | 1 + .../expected/src/feature/index.ts | 1 + .../expected/src/feature/metrics.json | 14 + .../chained-wildcard/expected/src/index.ts | 1 + .../expected/src/metrics.json | 14 + .../chained-wildcard/input/src/feature/foo.ts | 1 + .../input/src/feature/index.ts | 1 + .../input/src/feature/metrics.json | 14 + .../tests/chained-wildcard/input/src/index.ts | 1 + .../chained-wildcard/input/src/metrics.json | 14 + .../expected/src/components/metrics.json | 14 + .../input/src/components/metrics.json | 14 + .../expected/packages/lib/package.json | 4 + .../expected/packages/lib/src/index.ts | 1 + .../expected/packages/lib/src/metrics.json | 14 + .../expected/packages/lib/src/ui/Button.ts | 3 + .../expected/packages/lib/src/ui/index.ts | 1 + .../expected/packages/lib/src/ui/metrics.json | 14 + .../expected/tsconfig.json | 9 + .../input/packages/lib/package.json | 4 + .../input/packages/lib/src/index.ts | 1 + .../input/packages/lib/src/metrics.json | 14 + .../input/packages/lib/src/ui/Button.ts | 3 + .../input/packages/lib/src/ui/index.ts | 1 + .../input/packages/lib/src/ui/metrics.json | 14 + .../input/tsconfig.json | 9 + .../expected/src/feature/foo.ts | 1 + .../expected/src/feature/index.barrel.bak.ts | 1 + .../expected/src/feature/metrics.json | 14 + .../expected/src/index.barrel.bak.ts | 1 + .../expected/src/metrics.json | 14 + .../input/src/feature/foo.ts | 1 + .../input/src/feature/index.ts | 1 + .../input/src/feature/metrics.json | 14 + .../input/src/index.ts | 1 + .../input/src/metrics.json | 14 + .../expected/src/utils/metrics.json | 14 + .../input/src/utils/metrics.json | 14 + .../expected/src/utils/metrics.json | 14 + .../input/src/utils/metrics.json | 14 + .../expected/src/math/metrics.json | 14 + .../input/src/math/metrics.json | 14 + .../expected/src/lib/metrics.json | 14 + .../input/src/lib/metrics.json | 14 + .../expected/packages/ui/src/metrics.json | 14 + .../input/packages/ui/src/metrics.json | 14 + .../expected/src/components/metrics.json | 14 + .../input/src/components/metrics.json | 14 + .../expected/src/components/metrics.json | 14 + .../input/src/components/metrics.json | 14 + .../expected/src/feature/foo.ts | 1 + .../expected/src/feature/index.ts | 3 + .../expected/src/feature/init.ts | 1 + .../expected/src/feature/metrics.json | 14 + .../expected/src/index.ts | 1 + .../expected/src/metrics.json | 14 + .../input/src/feature/foo.ts | 1 + .../input/src/feature/index.ts | 3 + .../input/src/feature/init.ts | 1 + .../input/src/feature/metrics.json | 14 + .../input/src/index.ts | 1 + .../input/src/metrics.json | 14 + .../wildcard-side-effects/expected/src/foo.ts | 3 + .../expected/src/index.ts | 1 + .../expected/src/init.ts | 1 + .../expected/src/metrics.json | 14 + .../wildcard-side-effects/input/src/foo.ts | 3 + .../wildcard-side-effects/input/src/index.ts | 1 + .../wildcard-side-effects/input/src/init.ts | 1 + .../input/src/metrics.json | 14 + .../package-b/src/components/metrics.json | 14 + .../package-b/src/components/metrics.json | 14 + 82 files changed, 1029 insertions(+) create mode 100644 .changeset/barrel-metric.md create mode 100644 codemods/debarrel/scripts/utils/metrics.ts create mode 100644 codemods/debarrel/tests/aliased-reexport/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/aliased-reexport/input/src/metrics.json create mode 100644 codemods/debarrel/tests/barrel-with-own-declarations/expected/src/lib/metrics.json create mode 100644 codemods/debarrel/tests/barrel-with-own-declarations/input/src/lib/metrics.json create mode 100644 codemods/debarrel/tests/basic-named-reexport/expected/src/components/metrics.json create mode 100644 codemods/debarrel/tests/basic-named-reexport/input/src/components/metrics.json create mode 100644 codemods/debarrel/tests/chained-wildcard/expected/src/feature/foo.ts create mode 100644 codemods/debarrel/tests/chained-wildcard/expected/src/feature/index.ts create mode 100644 codemods/debarrel/tests/chained-wildcard/expected/src/feature/metrics.json create mode 100644 codemods/debarrel/tests/chained-wildcard/expected/src/index.ts create mode 100644 codemods/debarrel/tests/chained-wildcard/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/chained-wildcard/input/src/feature/foo.ts create mode 100644 codemods/debarrel/tests/chained-wildcard/input/src/feature/index.ts create mode 100644 codemods/debarrel/tests/chained-wildcard/input/src/feature/metrics.json create mode 100644 codemods/debarrel/tests/chained-wildcard/input/src/index.ts create mode 100644 codemods/debarrel/tests/chained-wildcard/input/src/metrics.json create mode 100644 codemods/debarrel/tests/default-as-named/expected/src/components/metrics.json create mode 100644 codemods/debarrel/tests/default-as-named/input/src/components/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/package.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/index.ts create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/Button.ts create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/index.ts create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/expected/tsconfig.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/package.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/index.ts create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/Button.ts create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/index.ts create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-public-api/input/tsconfig.json create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/foo.ts create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/index.barrel.bak.ts create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/expected/src/index.barrel.bak.ts create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/foo.ts create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/index.ts create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/metrics.json create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/input/src/index.ts create mode 100644 codemods/debarrel/tests/explicit-chained-reexport/input/src/metrics.json create mode 100644 codemods/debarrel/tests/import-then-reexport/expected/src/utils/metrics.json create mode 100644 codemods/debarrel/tests/import-then-reexport/input/src/utils/metrics.json create mode 100644 codemods/debarrel/tests/multiple-named-reexports/expected/src/utils/metrics.json create mode 100644 codemods/debarrel/tests/multiple-named-reexports/input/src/utils/metrics.json create mode 100644 codemods/debarrel/tests/namespace-reexport/expected/src/math/metrics.json create mode 100644 codemods/debarrel/tests/namespace-reexport/input/src/math/metrics.json create mode 100644 codemods/debarrel/tests/partial-with-type-specifiers/expected/src/lib/metrics.json create mode 100644 codemods/debarrel/tests/partial-with-type-specifiers/input/src/lib/metrics.json create mode 100644 codemods/debarrel/tests/pnpm-workspace-import/expected/packages/ui/src/metrics.json create mode 100644 codemods/debarrel/tests/pnpm-workspace-import/input/packages/ui/src/metrics.json create mode 100644 codemods/debarrel/tests/tsconfig-alias-paths/expected/src/components/metrics.json create mode 100644 codemods/debarrel/tests/tsconfig-alias-paths/input/src/components/metrics.json create mode 100644 codemods/debarrel/tests/update-test-mocks/expected/src/components/metrics.json create mode 100644 codemods/debarrel/tests/update-test-mocks/input/src/components/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/foo.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/index.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/init.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/index.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/foo.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/index.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/init.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/input/src/index.ts create mode 100644 codemods/debarrel/tests/wildcard-chained-side-effects/input/src/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-side-effects/expected/src/foo.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effects/expected/src/index.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effects/expected/src/init.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effects/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-side-effects/input/src/foo.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effects/input/src/index.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effects/input/src/init.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effects/input/src/metrics.json create mode 100644 codemods/debarrel/tests/workspace-package-import/expected/packages/package-b/src/components/metrics.json create mode 100644 codemods/debarrel/tests/workspace-package-import/input/packages/package-b/src/components/metrics.json diff --git a/.changeset/barrel-metric.md b/.changeset/barrel-metric.md new file mode 100644 index 0000000..c3d4b44 --- /dev/null +++ b/.changeset/barrel-metric.md @@ -0,0 +1,5 @@ +--- +"debarrel": minor +--- + +Add `barrel-metric` event emitted per re-export in every barrel the codemod visits. Each event carries five cardinalities — `export_style`, `chaining`, `risk_amplifier`, `file`, and a derived `migration_complexity_score` (0–4) — so you can estimate migration effort before rolling the codemod out at scale. diff --git a/codemods/debarrel/README.md b/codemods/debarrel/README.md index 4ad4be3..7f6be78 100644 --- a/codemods/debarrel/README.md +++ b/codemods/debarrel/README.md @@ -23,6 +23,25 @@ Pure barrel files (files that only re-export) are renamed to `index.barrel.bak.t - Import-then-reexport patterns - Mixed barrel files that contain their own declarations (only the re-exported imports are rewritten; the barrel is kept) +## Migration complexity metric + +Alongside the rewrite, the codemod emits a `barrel-metric` event per re-export found in each barrel. Each event carries five cardinalities so you can estimate migration effort before rolling the codemod out at scale: + +| Cardinality | Values | +| --- | --- | +| `export_style` | `explicit` \| `wildcard` | +| `chaining` | `single-level` \| `chained` | +| `risk_amplifier` | `none` \| `heavy-usage-or-public-api` \| `cycles-or-side-effects` | +| `file` | workspace-relative path of the affected barrel | +| `migration_complexity_score` | `0`–`4` (sum of the other dimensions) | + +Scoring rules: `wildcard` adds 1, `chained` adds 1, `heavy-usage-or-public-api` adds 1, `cycles-or-side-effects` adds 2. Roughly: **0–1 = Low**, **2 = Medium**, **3–4 = High**. + +Notes: + +- `heavy-usage-or-public-api` fires only when the barrel matches the advertised entry point of its nearest `package.json` (`main`, `module`, `types`, or any leaf of `exports`). A random internal `index.ts` inside a repo is not flagged. +- `count` aggregates raw emission events, not unique re-export sites. Because every file in a barrel's directory emits the same metric tuple (a workaround for per-directory test snapshots), the value is inflated by directory density in proportion to the number of sibling files. To recover an approximate re-export count, divide `count` by the number of files in the barrel's directory. + ## Usage ```bash diff --git a/codemods/debarrel/scripts/codemod.ts b/codemods/debarrel/scripts/codemod.ts index af982d3..54ffcc7 100644 --- a/codemods/debarrel/scripts/codemod.ts +++ b/codemods/debarrel/scripts/codemod.ts @@ -15,6 +15,7 @@ import { rewriteMockCalls, type BarrelMockInfo, } from "./utils/mocks.ts"; +import { emitMetricsForBarrelSibling } from "./utils/metrics.ts"; const codemod: Codemod = async (root) => { const rootNode = root.root(); @@ -22,6 +23,8 @@ const codemod: Codemod = async (root) => { const edits: Edit[] = []; const barrelRewrites = new Map(); + emitMetricsForBarrelSibling(filename); + for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" }, })) { diff --git a/codemods/debarrel/scripts/utils/metrics.ts b/codemods/debarrel/scripts/utils/metrics.ts new file mode 100644 index 0000000..6e6d9fc --- /dev/null +++ b/codemods/debarrel/scripts/utils/metrics.ts @@ -0,0 +1,344 @@ +import fs from "fs"; +import path from "path"; +import { useMetricAtom } from "codemod:metrics"; +import { + isBarrelFile, + isInsideNodeModules, + isLocalRelativePath, +} from "./paths.ts"; + +const barrelMetric = useMetricAtom("barrel-metric"); + +type ExportStyle = "explicit" | "wildcard"; +type Chaining = "single-level" | "chained"; +type RiskAmplifier = + | "none" + | "heavy-usage-or-public-api" + | "cycles-or-side-effects"; + +interface BarrelAnalysis { + export_style: ExportStyle; + chaining: Chaining; + risk_amplifier: RiskAmplifier; + migration_complexity_score: number; + file: string; +} + +function fileExists(filePath: string): boolean { + try { + fs.accessSync(filePath); + return true; + } catch { + return false; + } +} + +function findNearestPackageJson(filename: string): string | null { + let dir = path.dirname(filename); + const root = path.parse(dir).root || "/"; + while (dir !== root) { + const candidate = path.join(dir, "package.json"); + if (fileExists(candidate)) return candidate; + dir = path.dirname(dir); + } + return null; +} + +function stripModuleExt(p: string): string { + return p.replace(/\.(d\.ts|ts|tsx|mts|cts|js|jsx|mjs|cjs)$/, ""); +} + +/** + * Check whether `filePath` is the advertised entry point of its nearest + * `package.json` — matches `main`, `module`, `types`, `typings`, or any + * string leaf of the `exports` field (including nested conditional keys). + * Tighter than "there is a package.json somewhere above" so we don't flag + * every file in a monorepo as a public API. + */ +function isPackageEntryPoint(filePath: string): boolean { + const pkgPath = findNearestPackageJson(filePath); + if (!pkgPath) return false; + let pkg: Record; + try { + pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as Record< + string, + unknown + >; + } catch { + return false; + } + const pkgDir = path.dirname(pkgPath); + const targetBase = stripModuleExt(path.resolve(filePath)); + + const matches = (val: unknown): boolean => { + if (typeof val !== "string") return false; + const resolved = path.resolve(pkgDir, val); + return stripModuleExt(resolved) === targetBase; + }; + + const walkExports = (node: unknown): boolean => { + if (node == null) return false; + if (typeof node === "string") return matches(node); + if (Array.isArray(node)) return node.some(walkExports); + if (typeof node === "object") { + for (const v of Object.values(node as Record)) { + if (walkExports(v)) return true; + } + } + return false; + }; + + return ( + matches(pkg.main) || + matches(pkg.module) || + matches(pkg.types) || + matches(pkg.typings) || + walkExports(pkg.exports) + ); +} + +/** + * Anchor an absolute barrel path at the first recognizable project-root + * segment (e.g. `packages/`, `src/`) so the `file` cardinality stays stable + * across different checkout locations. Falls back to the last three segments. + */ +function normalizeBarrelPath(filePath: string): string { + const segments = filePath.split(/[\\/]/).filter(Boolean); + const markers = ["packages", "apps", "app", "src", "lib"]; + for (let i = 0; i < segments.length; i++) { + const seg = segments[i]; + if (seg && markers.includes(seg)) { + return segments.slice(i).join("/"); + } + } + return segments.slice(-3).join("/"); +} + +function resolveLocalModule( + fromFile: string, + relPath: string, +): string | null { + const base = path.resolve(path.dirname(fromFile), relPath); + const exts = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs"]; + for (const ext of exts) { + const candidate = `${base}.${ext}`; + if (fileExists(candidate)) return candidate; + } + for (const ext of exts) { + const candidate = path.join(base, `index.${ext}`); + if (fileExists(candidate)) return candidate; + } + return null; +} + +function fileContentHasSideEffectImport(filePath: string): boolean { + try { + const content = fs.readFileSync(filePath, "utf8"); + return /^\s*import\s+["'][^"']+["']\s*;?\s*$/m.test(content); + } catch { + return false; + } +} + +function computeBarrelAnalysis(params: { + barrelFile: string; + sourceFromBarrel: string; + isWildcard: boolean; + barrelHasSideEffects: boolean; +}): BarrelAnalysis { + const export_style: ExportStyle = params.isWildcard ? "wildcard" : "explicit"; + + const targetPath = resolveLocalModule( + params.barrelFile, + params.sourceFromBarrel, + ); + const chaining: Chaining = + targetPath && isBarrelFile(targetPath) ? "chained" : "single-level"; + + const targetHasSideEffects = + !!targetPath && fileContentHasSideEffectImport(targetPath); + let risk_amplifier: RiskAmplifier = "none"; + if (params.barrelHasSideEffects || targetHasSideEffects) { + risk_amplifier = "cycles-or-side-effects"; + } else if (isPackageEntryPoint(params.barrelFile)) { + risk_amplifier = "heavy-usage-or-public-api"; + } + + const exportScore = export_style === "wildcard" ? 1 : 0; + const chainingScore = chaining === "chained" ? 1 : 0; + const riskScore = + risk_amplifier === "cycles-or-side-effects" + ? 2 + : risk_amplifier === "heavy-usage-or-public-api" + ? 1 + : 0; + const migration_complexity_score = exportScore + chainingScore + riskScore; + + return { + export_style, + chaining, + risk_amplifier, + migration_complexity_score, + file: normalizeBarrelPath(params.barrelFile), + }; +} + +function emitBarrelMetric(analysis: BarrelAnalysis): void { + barrelMetric.increment({ + export_style: analysis.export_style, + chaining: analysis.chaining, + risk_amplifier: analysis.risk_amplifier, + migration_complexity_score: String(analysis.migration_complexity_score), + file: analysis.file, + }); +} + +/** + * When `filename` is a barrel, returns it; otherwise returns the + * `index.{ts,tsx,js,jsx}` sibling in the same directory, or null. + */ +function findSiblingBarrel(filename: string): string | null { + if (isBarrelFile(filename)) { + return fileExists(filename) ? filename : null; + } + const dir = path.dirname(filename); + for (const ext of ["ts", "tsx", "js", "jsx"]) { + const candidate = path.join(dir, `index.${ext}`); + if (fileExists(candidate)) return candidate; + } + return null; +} + +interface BarrelImportBinding { + localName: string; + source: string; +} + +/** + * Parse `import ... from "..."` statements and return bindings by local + * name → source. Regex-based so we can resolve sourceless `export { X };` + * patterns without an AST. + */ +function collectImportBindings(content: string): BarrelImportBinding[] { + const bindings: BarrelImportBinding[] = []; + const importRe = + /import\s+([\s\S]+?)\s+from\s+["']([^"']+)["']\s*;?/g; + let m: RegExpExecArray | null; + while ((m = importRe.exec(content)) !== null) { + const clause = m[1]; + const source = m[2]; + if (!clause || !source) continue; + + const namedMatch = clause.match(/\{([^}]*)\}/); + if (namedMatch && namedMatch[1] !== undefined) { + for (const spec of namedMatch[1].split(",")) { + const parts = spec.trim().split(/\s+as\s+/); + const local = (parts[1] ?? parts[0] ?? "").trim(); + if (local) bindings.push({ localName: local, source }); + } + } + + const withoutNamed = clause.replace(/\{[^}]*\}/g, ""); + + const nsMatch = withoutNamed.match(/\*\s+as\s+([\w$]+)/); + if (nsMatch && nsMatch[1]) { + bindings.push({ localName: nsMatch[1], source }); + } + + const defaultMatch = withoutNamed.match(/(?:^|,)\s*([\w$]+)\s*(?:,|$)/); + if (defaultMatch && defaultMatch[1]) { + bindings.push({ localName: defaultMatch[1], source }); + } + } + return bindings; +} + +function stripComments(content: string): string { + return content + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\/\/.*$/gm, ""); +} + +/** + * Walk every re-export in `barrelFile` and emit one `barrel-metric` event + * per analyzed re-export source. Uses a regex-based scan so the metric set + * is identical regardless of whether the barrel itself or a sibling triggered + * the emission. + * + * Handles: + * 1. `export * from "..."` (wildcard) + * 2. `export * as X from "..."` (wildcard) + * 3. `export { A, B } from "..."` (explicit) + * 4. `import { X } from "..."; export { X };` (explicit, per traced source) + */ +function emitMetricsForBarrelFile(barrelFile: string): void { + let content: string; + try { + content = fs.readFileSync(barrelFile, "utf8"); + } catch { + return; + } + const code = stripComments(content); + + const barrelHasSideEffects = + /^[ \t]*import\s+["'][^"']+["']\s*;?\s*$/m.test(code); + + const emitForSource = (source: string, isWildcard: boolean): void => { + if (!isLocalRelativePath(source)) return; + emitBarrelMetric( + computeBarrelAnalysis({ + barrelFile, + sourceFromBarrel: source, + isWildcard, + barrelHasSideEffects, + }), + ); + }; + + const wildcardRe = + /export\s+\*(?:\s+as\s+[\w$]+)?\s+from\s+["']([^"']+)["']\s*;?/g; + let m: RegExpExecArray | null; + while ((m = wildcardRe.exec(code)) !== null) { + if (m[1]) emitForSource(m[1], true); + } + + const explicitFromRe = /export\s*\{[^}]*\}\s*from\s+["']([^"']+)["']\s*;?/g; + while ((m = explicitFromRe.exec(code)) !== null) { + if (m[1]) emitForSource(m[1], false); + } + + const importBindings = collectImportBindings(code); + if (importBindings.length === 0) return; + const bindingMap = new Map(); + for (const b of importBindings) bindingMap.set(b.localName, b.source); + + const exportSourcelessRe = /export\s*\{([^}]*)\}\s*(?:;|$)/gm; + while ((m = exportSourcelessRe.exec(code)) !== null) { + if (m[1] === undefined) continue; + for (const spec of m[1].split(",")) { + const parts = spec.trim().split(/\s+as\s+/); + const localName = parts[0]?.trim(); + if (!localName) continue; + const source = bindingMap.get(localName); + if (!source) continue; + emitForSource(source, false); + } + } +} + +/** + * Emit `barrel-metric` events for every re-export reachable from this + * file's sibling barrel (or from itself if it IS a barrel). + * + * Every file in the same directory emits the same metric set because the + * `jssg test` runner stores a single `metrics.json` per directory that is + * compared against EACH file's test case — a non-emitting sibling would + * fail with "stale snapshot". Consequence: in production, `count` is + * multiplied by the number of files in the barrel's directory. Interpret + * `count` as emission events, not unique re-export sites. + */ +export function emitMetricsForBarrelSibling(filename: string): void { + if (isInsideNodeModules(filename)) return; + const sibBarrel = findSiblingBarrel(filename); + if (sibBarrel) emitMetricsForBarrelFile(sibBarrel); +} diff --git a/codemods/debarrel/tests/aliased-reexport/expected/src/metrics.json b/codemods/debarrel/tests/aliased-reexport/expected/src/metrics.json new file mode 100644 index 0000000..c0e8002 --- /dev/null +++ b/codemods/debarrel/tests/aliased-reexport/expected/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/aliased-reexport/input/src/metrics.json b/codemods/debarrel/tests/aliased-reexport/input/src/metrics.json new file mode 100644 index 0000000..c0e8002 --- /dev/null +++ b/codemods/debarrel/tests/aliased-reexport/input/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/barrel-with-own-declarations/expected/src/lib/metrics.json b/codemods/debarrel/tests/barrel-with-own-declarations/expected/src/lib/metrics.json new file mode 100644 index 0000000..4cb2d46 --- /dev/null +++ b/codemods/debarrel/tests/barrel-with-own-declarations/expected/src/lib/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/lib/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/barrel-with-own-declarations/input/src/lib/metrics.json b/codemods/debarrel/tests/barrel-with-own-declarations/input/src/lib/metrics.json new file mode 100644 index 0000000..4cb2d46 --- /dev/null +++ b/codemods/debarrel/tests/barrel-with-own-declarations/input/src/lib/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/lib/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/basic-named-reexport/expected/src/components/metrics.json b/codemods/debarrel/tests/basic-named-reexport/expected/src/components/metrics.json new file mode 100644 index 0000000..20008c0 --- /dev/null +++ b/codemods/debarrel/tests/basic-named-reexport/expected/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/basic-named-reexport/input/src/components/metrics.json b/codemods/debarrel/tests/basic-named-reexport/input/src/components/metrics.json new file mode 100644 index 0000000..20008c0 --- /dev/null +++ b/codemods/debarrel/tests/basic-named-reexport/input/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/chained-wildcard/expected/src/feature/foo.ts b/codemods/debarrel/tests/chained-wildcard/expected/src/feature/foo.ts new file mode 100644 index 0000000..29762e5 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/expected/src/feature/foo.ts @@ -0,0 +1 @@ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/chained-wildcard/expected/src/feature/index.ts b/codemods/debarrel/tests/chained-wildcard/expected/src/feature/index.ts new file mode 100644 index 0000000..5567e8d --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/expected/src/feature/index.ts @@ -0,0 +1 @@ +export * from "./foo"; diff --git a/codemods/debarrel/tests/chained-wildcard/expected/src/feature/metrics.json b/codemods/debarrel/tests/chained-wildcard/expected/src/feature/metrics.json new file mode 100644 index 0000000..c3e8335 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/expected/src/feature/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/feature/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/chained-wildcard/expected/src/index.ts b/codemods/debarrel/tests/chained-wildcard/expected/src/index.ts new file mode 100644 index 0000000..d0d5122 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/expected/src/index.ts @@ -0,0 +1 @@ +export * from "./feature"; diff --git a/codemods/debarrel/tests/chained-wildcard/expected/src/metrics.json b/codemods/debarrel/tests/chained-wildcard/expected/src/metrics.json new file mode 100644 index 0000000..9eb6ba5 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/expected/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/chained-wildcard/input/src/feature/foo.ts b/codemods/debarrel/tests/chained-wildcard/input/src/feature/foo.ts new file mode 100644 index 0000000..29762e5 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/input/src/feature/foo.ts @@ -0,0 +1 @@ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/chained-wildcard/input/src/feature/index.ts b/codemods/debarrel/tests/chained-wildcard/input/src/feature/index.ts new file mode 100644 index 0000000..5567e8d --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/input/src/feature/index.ts @@ -0,0 +1 @@ +export * from "./foo"; diff --git a/codemods/debarrel/tests/chained-wildcard/input/src/feature/metrics.json b/codemods/debarrel/tests/chained-wildcard/input/src/feature/metrics.json new file mode 100644 index 0000000..c3e8335 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/input/src/feature/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/feature/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/chained-wildcard/input/src/index.ts b/codemods/debarrel/tests/chained-wildcard/input/src/index.ts new file mode 100644 index 0000000..d0d5122 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/input/src/index.ts @@ -0,0 +1 @@ +export * from "./feature"; diff --git a/codemods/debarrel/tests/chained-wildcard/input/src/metrics.json b/codemods/debarrel/tests/chained-wildcard/input/src/metrics.json new file mode 100644 index 0000000..9eb6ba5 --- /dev/null +++ b/codemods/debarrel/tests/chained-wildcard/input/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/default-as-named/expected/src/components/metrics.json b/codemods/debarrel/tests/default-as-named/expected/src/components/metrics.json new file mode 100644 index 0000000..20008c0 --- /dev/null +++ b/codemods/debarrel/tests/default-as-named/expected/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/default-as-named/input/src/components/metrics.json b/codemods/debarrel/tests/default-as-named/input/src/components/metrics.json new file mode 100644 index 0000000..20008c0 --- /dev/null +++ b/codemods/debarrel/tests/default-as-named/input/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/package.json b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/package.json new file mode 100644 index 0000000..ddbebe2 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/package.json @@ -0,0 +1,4 @@ +{ + "name": "@acme/lib", + "main": "./src/index.ts" +} diff --git a/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/index.ts b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/index.ts new file mode 100644 index 0000000..7345d1e --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/index.ts @@ -0,0 +1 @@ +export { Button } from "./ui"; diff --git a/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/metrics.json b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/metrics.json new file mode 100644 index 0000000..0f52edd --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "explicit", + "file": "packages/lib/src/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "heavy-usage-or-public-api" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/Button.ts b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/Button.ts new file mode 100644 index 0000000..9841918 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/Button.ts @@ -0,0 +1,3 @@ +export function Button() { + return "button"; +} diff --git a/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/index.ts b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/index.ts new file mode 100644 index 0000000..0064dee --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/index.ts @@ -0,0 +1 @@ +export { Button } from "./Button"; diff --git a/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/metrics.json b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/metrics.json new file mode 100644 index 0000000..deff59a --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/expected/packages/lib/src/ui/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "packages/lib/src/ui/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-public-api/expected/tsconfig.json b/codemods/debarrel/tests/explicit-chained-public-api/expected/tsconfig.json new file mode 100644 index 0000000..bd8b81e --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/expected/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["packages"] +} diff --git a/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/package.json b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/package.json new file mode 100644 index 0000000..ddbebe2 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/package.json @@ -0,0 +1,4 @@ +{ + "name": "@acme/lib", + "main": "./src/index.ts" +} diff --git a/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/index.ts b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/index.ts new file mode 100644 index 0000000..7345d1e --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/index.ts @@ -0,0 +1 @@ +export { Button } from "./ui"; diff --git a/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/metrics.json b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/metrics.json new file mode 100644 index 0000000..0f52edd --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "explicit", + "file": "packages/lib/src/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "heavy-usage-or-public-api" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/Button.ts b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/Button.ts new file mode 100644 index 0000000..9841918 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/Button.ts @@ -0,0 +1,3 @@ +export function Button() { + return "button"; +} diff --git a/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/index.ts b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/index.ts new file mode 100644 index 0000000..0064dee --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/index.ts @@ -0,0 +1 @@ +export { Button } from "./Button"; diff --git a/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/metrics.json b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/metrics.json new file mode 100644 index 0000000..deff59a --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/input/packages/lib/src/ui/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "packages/lib/src/ui/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-public-api/input/tsconfig.json b/codemods/debarrel/tests/explicit-chained-public-api/input/tsconfig.json new file mode 100644 index 0000000..bd8b81e --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-public-api/input/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["packages"] +} diff --git a/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/foo.ts b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/foo.ts new file mode 100644 index 0000000..29762e5 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/foo.ts @@ -0,0 +1 @@ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/index.barrel.bak.ts b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/index.barrel.bak.ts new file mode 100644 index 0000000..f5b64e1 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/index.barrel.bak.ts @@ -0,0 +1 @@ +export { foo } from "./foo"; diff --git a/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/metrics.json b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/metrics.json new file mode 100644 index 0000000..88288ce --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/feature/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/feature/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-reexport/expected/src/index.barrel.bak.ts b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/index.barrel.bak.ts new file mode 100644 index 0000000..7c17925 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/index.barrel.bak.ts @@ -0,0 +1 @@ +export { foo } from "./feature"; diff --git a/codemods/debarrel/tests/explicit-chained-reexport/expected/src/metrics.json b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/metrics.json new file mode 100644 index 0000000..3e588d7 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/expected/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/foo.ts b/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/foo.ts new file mode 100644 index 0000000..29762e5 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/foo.ts @@ -0,0 +1 @@ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/index.ts b/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/index.ts new file mode 100644 index 0000000..f5b64e1 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/index.ts @@ -0,0 +1 @@ +export { foo } from "./foo"; diff --git a/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/metrics.json b/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/metrics.json new file mode 100644 index 0000000..88288ce --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/input/src/feature/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/feature/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/explicit-chained-reexport/input/src/index.ts b/codemods/debarrel/tests/explicit-chained-reexport/input/src/index.ts new file mode 100644 index 0000000..7c17925 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/input/src/index.ts @@ -0,0 +1 @@ +export { foo } from "./feature"; diff --git a/codemods/debarrel/tests/explicit-chained-reexport/input/src/metrics.json b/codemods/debarrel/tests/explicit-chained-reexport/input/src/metrics.json new file mode 100644 index 0000000..3e588d7 --- /dev/null +++ b/codemods/debarrel/tests/explicit-chained-reexport/input/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/import-then-reexport/expected/src/utils/metrics.json b/codemods/debarrel/tests/import-then-reexport/expected/src/utils/metrics.json new file mode 100644 index 0000000..826e600 --- /dev/null +++ b/codemods/debarrel/tests/import-then-reexport/expected/src/utils/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/utils/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/import-then-reexport/input/src/utils/metrics.json b/codemods/debarrel/tests/import-then-reexport/input/src/utils/metrics.json new file mode 100644 index 0000000..826e600 --- /dev/null +++ b/codemods/debarrel/tests/import-then-reexport/input/src/utils/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/utils/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/multiple-named-reexports/expected/src/utils/metrics.json b/codemods/debarrel/tests/multiple-named-reexports/expected/src/utils/metrics.json new file mode 100644 index 0000000..826e600 --- /dev/null +++ b/codemods/debarrel/tests/multiple-named-reexports/expected/src/utils/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/utils/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/multiple-named-reexports/input/src/utils/metrics.json b/codemods/debarrel/tests/multiple-named-reexports/input/src/utils/metrics.json new file mode 100644 index 0000000..826e600 --- /dev/null +++ b/codemods/debarrel/tests/multiple-named-reexports/input/src/utils/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/utils/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/namespace-reexport/expected/src/math/metrics.json b/codemods/debarrel/tests/namespace-reexport/expected/src/math/metrics.json new file mode 100644 index 0000000..64930e6 --- /dev/null +++ b/codemods/debarrel/tests/namespace-reexport/expected/src/math/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/math/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/namespace-reexport/input/src/math/metrics.json b/codemods/debarrel/tests/namespace-reexport/input/src/math/metrics.json new file mode 100644 index 0000000..64930e6 --- /dev/null +++ b/codemods/debarrel/tests/namespace-reexport/input/src/math/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/math/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/partial-with-type-specifiers/expected/src/lib/metrics.json b/codemods/debarrel/tests/partial-with-type-specifiers/expected/src/lib/metrics.json new file mode 100644 index 0000000..1849f0c --- /dev/null +++ b/codemods/debarrel/tests/partial-with-type-specifiers/expected/src/lib/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/lib/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/partial-with-type-specifiers/input/src/lib/metrics.json b/codemods/debarrel/tests/partial-with-type-specifiers/input/src/lib/metrics.json new file mode 100644 index 0000000..1849f0c --- /dev/null +++ b/codemods/debarrel/tests/partial-with-type-specifiers/input/src/lib/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/lib/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/pnpm-workspace-import/expected/packages/ui/src/metrics.json b/codemods/debarrel/tests/pnpm-workspace-import/expected/packages/ui/src/metrics.json new file mode 100644 index 0000000..b94a2e4 --- /dev/null +++ b/codemods/debarrel/tests/pnpm-workspace-import/expected/packages/ui/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "packages/ui/src/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "heavy-usage-or-public-api" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/pnpm-workspace-import/input/packages/ui/src/metrics.json b/codemods/debarrel/tests/pnpm-workspace-import/input/packages/ui/src/metrics.json new file mode 100644 index 0000000..b94a2e4 --- /dev/null +++ b/codemods/debarrel/tests/pnpm-workspace-import/input/packages/ui/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "packages/ui/src/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "heavy-usage-or-public-api" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/tsconfig-alias-paths/expected/src/components/metrics.json b/codemods/debarrel/tests/tsconfig-alias-paths/expected/src/components/metrics.json new file mode 100644 index 0000000..20008c0 --- /dev/null +++ b/codemods/debarrel/tests/tsconfig-alias-paths/expected/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/tsconfig-alias-paths/input/src/components/metrics.json b/codemods/debarrel/tests/tsconfig-alias-paths/input/src/components/metrics.json new file mode 100644 index 0000000..20008c0 --- /dev/null +++ b/codemods/debarrel/tests/tsconfig-alias-paths/input/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/update-test-mocks/expected/src/components/metrics.json b/codemods/debarrel/tests/update-test-mocks/expected/src/components/metrics.json new file mode 100644 index 0000000..c847b7f --- /dev/null +++ b/codemods/debarrel/tests/update-test-mocks/expected/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/update-test-mocks/input/src/components/metrics.json b/codemods/debarrel/tests/update-test-mocks/input/src/components/metrics.json new file mode 100644 index 0000000..c847b7f --- /dev/null +++ b/codemods/debarrel/tests/update-test-mocks/input/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/foo.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/foo.ts new file mode 100644 index 0000000..29762e5 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/foo.ts @@ -0,0 +1 @@ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/index.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/index.ts new file mode 100644 index 0000000..16400a9 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/index.ts @@ -0,0 +1,3 @@ +import "./init"; + +export * from "./foo"; diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/init.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/init.ts new file mode 100644 index 0000000..73ee5c0 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/init.ts @@ -0,0 +1 @@ +console.log("init side effect"); diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/metrics.json b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/metrics.json new file mode 100644 index 0000000..192fc0d --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/feature/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/feature/index.ts", + "migration_complexity_score": "3", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/index.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/index.ts new file mode 100644 index 0000000..d0d5122 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/index.ts @@ -0,0 +1 @@ +export * from "./feature"; diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/metrics.json b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/metrics.json new file mode 100644 index 0000000..a66b129 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/expected/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "4", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/foo.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/foo.ts new file mode 100644 index 0000000..29762e5 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/foo.ts @@ -0,0 +1 @@ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/index.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/index.ts new file mode 100644 index 0000000..16400a9 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/index.ts @@ -0,0 +1,3 @@ +import "./init"; + +export * from "./foo"; diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/init.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/init.ts new file mode 100644 index 0000000..73ee5c0 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/init.ts @@ -0,0 +1 @@ +console.log("init side effect"); diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/metrics.json b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/metrics.json new file mode 100644 index 0000000..192fc0d --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/feature/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/feature/index.ts", + "migration_complexity_score": "3", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/index.ts b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/index.ts new file mode 100644 index 0000000..d0d5122 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/index.ts @@ -0,0 +1 @@ +export * from "./feature"; diff --git a/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/metrics.json b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/metrics.json new file mode 100644 index 0000000..a66b129 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-chained-side-effects/input/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "chained", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "4", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-side-effects/expected/src/foo.ts b/codemods/debarrel/tests/wildcard-side-effects/expected/src/foo.ts new file mode 100644 index 0000000..8857505 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/expected/src/foo.ts @@ -0,0 +1,3 @@ +import "./init"; + +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effects/expected/src/index.ts b/codemods/debarrel/tests/wildcard-side-effects/expected/src/index.ts new file mode 100644 index 0000000..5567e8d --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/expected/src/index.ts @@ -0,0 +1 @@ +export * from "./foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effects/expected/src/init.ts b/codemods/debarrel/tests/wildcard-side-effects/expected/src/init.ts new file mode 100644 index 0000000..12eb327 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/expected/src/init.ts @@ -0,0 +1 @@ +console.log("runs on import"); diff --git a/codemods/debarrel/tests/wildcard-side-effects/expected/src/metrics.json b/codemods/debarrel/tests/wildcard-side-effects/expected/src/metrics.json new file mode 100644 index 0000000..dca172f --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/expected/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "3", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-side-effects/input/src/foo.ts b/codemods/debarrel/tests/wildcard-side-effects/input/src/foo.ts new file mode 100644 index 0000000..8857505 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/input/src/foo.ts @@ -0,0 +1,3 @@ +import "./init"; + +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effects/input/src/index.ts b/codemods/debarrel/tests/wildcard-side-effects/input/src/index.ts new file mode 100644 index 0000000..5567e8d --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/input/src/index.ts @@ -0,0 +1 @@ +export * from "./foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effects/input/src/init.ts b/codemods/debarrel/tests/wildcard-side-effects/input/src/init.ts new file mode 100644 index 0000000..12eb327 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/input/src/init.ts @@ -0,0 +1 @@ +console.log("runs on import"); diff --git a/codemods/debarrel/tests/wildcard-side-effects/input/src/metrics.json b/codemods/debarrel/tests/wildcard-side-effects/input/src/metrics.json new file mode 100644 index 0000000..dca172f --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effects/input/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "3", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/workspace-package-import/expected/packages/package-b/src/components/metrics.json b/codemods/debarrel/tests/workspace-package-import/expected/packages/package-b/src/components/metrics.json new file mode 100644 index 0000000..14f09b9 --- /dev/null +++ b/codemods/debarrel/tests/workspace-package-import/expected/packages/package-b/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "packages/package-b/src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/workspace-package-import/input/packages/package-b/src/components/metrics.json b/codemods/debarrel/tests/workspace-package-import/input/packages/package-b/src/components/metrics.json new file mode 100644 index 0000000..14f09b9 --- /dev/null +++ b/codemods/debarrel/tests/workspace-package-import/input/packages/package-b/src/components/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "packages/package-b/src/components/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file From f31e1b4d65364b60fba64c2908d428beb3a74915 Mon Sep 17 00:00:00 2001 From: Alex Bit Date: Wed, 22 Apr 2026 15:47:50 -0700 Subject: [PATCH 2/6] chore(debarrel): sharpen registry keywords Replace the generic `transformation`/`migration` tags with terms users actually search for when looking for barrel-related work or the engineering-productivity problems debarreling solves (dependency graph, build and CI performance, code health). Made-with: Cursor --- codemods/debarrel/codemod.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/codemods/debarrel/codemod.yaml b/codemods/debarrel/codemod.yaml index 42e76c8..da3c985 100644 --- a/codemods/debarrel/codemod.yaml +++ b/codemods/debarrel/codemod.yaml @@ -11,7 +11,14 @@ repository: https://github.com/codemod/useful-codemods targets: languages: ["typescript"] -keywords: ["transformation", "migration"] +keywords: + [ + "barrel-files", + "dependency-graph", + "build-performance", + "ci-performance", + "code-health", + ] registry: access: "public" From 9bb156be45a596b2e7e081f202c66f00ba6fa6ea Mon Sep 17 00:00:00 2001 From: Alex Bit Date: Wed, 22 Apr 2026 16:01:48 -0700 Subject: [PATCH 3/6] bump debarrel version --- codemods/debarrel/codemod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codemods/debarrel/codemod.yaml b/codemods/debarrel/codemod.yaml index da3c985..a61e361 100644 --- a/codemods/debarrel/codemod.yaml +++ b/codemods/debarrel/codemod.yaml @@ -1,7 +1,7 @@ schema_version: "1.0" name: "debarrel" -version: "0.5.0" +version: "0.6.0" description: "Debarrel JS/TS codebases. Removing barrel files and replacing import statements." author: "Mo Mohebifar " license: "MIT" From af730fce525196235bfec82fdd79859386c06626 Mon Sep 17 00:00:00 2001 From: Alex Bit Date: Wed, 22 Apr 2026 17:42:47 -0700 Subject: [PATCH 4/6] improved type-only import handling --- codemods/debarrel/scripts/utils/metrics.ts | 29 ++++++++++++++----- .../expected/package.json | 5 ++++ .../expected/src/foo.ts | 1 + .../expected/src/index.ts | 4 +++ .../expected/src/init.ts | 1 + .../expected/src/metrics.json | 24 +++++++++++++++ .../expected/src/stuff.ts | 3 ++ .../expected/tsconfig.json | 9 ++++++ .../input/package.json | 5 ++++ .../input/src/foo.ts | 1 + .../input/src/index.ts | 4 +++ .../input/src/init.ts | 1 + .../input/src/metrics.json | 24 +++++++++++++++ .../input/src/stuff.ts | 3 ++ .../input/tsconfig.json | 9 ++++++ .../type-only-reexport/expected/package.json | 5 ++++ .../type-only-reexport/expected/src/bar.ts | 1 + .../type-only-reexport/expected/src/foo.ts | 1 + .../type-only-reexport/expected/src/index.ts | 2 ++ .../expected/src/metrics.json | 14 +++++++++ .../type-only-reexport/expected/tsconfig.json | 9 ++++++ .../type-only-reexport/input/package.json | 5 ++++ .../tests/type-only-reexport/input/src/bar.ts | 1 + .../tests/type-only-reexport/input/src/foo.ts | 1 + .../type-only-reexport/input/src/index.ts | 2 ++ .../type-only-reexport/input/src/metrics.json | 14 +++++++++ .../type-only-reexport/input/tsconfig.json | 9 ++++++ .../expected/src/foo.ts | 4 +++ .../expected/src/index.ts | 1 + .../expected/src/metrics.json | 14 +++++++++ .../expected/tsconfig.json | 9 ++++++ .../input/src/foo.ts | 4 +++ .../input/src/index.ts | 1 + .../input/src/metrics.json | 14 +++++++++ .../input/tsconfig.json | 9 ++++++ 35 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/expected/package.json create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/foo.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/index.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/init.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/stuff.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/expected/tsconfig.json create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/input/package.json create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/input/src/foo.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/input/src/index.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/input/src/init.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/input/src/metrics.json create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/input/src/stuff.ts create mode 100644 codemods/debarrel/tests/type-only-import-bogus-binding/input/tsconfig.json create mode 100644 codemods/debarrel/tests/type-only-reexport/expected/package.json create mode 100644 codemods/debarrel/tests/type-only-reexport/expected/src/bar.ts create mode 100644 codemods/debarrel/tests/type-only-reexport/expected/src/foo.ts create mode 100644 codemods/debarrel/tests/type-only-reexport/expected/src/index.ts create mode 100644 codemods/debarrel/tests/type-only-reexport/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/type-only-reexport/expected/tsconfig.json create mode 100644 codemods/debarrel/tests/type-only-reexport/input/package.json create mode 100644 codemods/debarrel/tests/type-only-reexport/input/src/bar.ts create mode 100644 codemods/debarrel/tests/type-only-reexport/input/src/foo.ts create mode 100644 codemods/debarrel/tests/type-only-reexport/input/src/index.ts create mode 100644 codemods/debarrel/tests/type-only-reexport/input/src/metrics.json create mode 100644 codemods/debarrel/tests/type-only-reexport/input/tsconfig.json create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/foo.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/index.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/tsconfig.json create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/foo.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/index.ts create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/metrics.json create mode 100644 codemods/debarrel/tests/wildcard-side-effect-in-comment/input/tsconfig.json diff --git a/codemods/debarrel/scripts/utils/metrics.ts b/codemods/debarrel/scripts/utils/metrics.ts index 6e6d9fc..f3b3032 100644 --- a/codemods/debarrel/scripts/utils/metrics.ts +++ b/codemods/debarrel/scripts/utils/metrics.ts @@ -134,7 +134,9 @@ function resolveLocalModule( function fileContentHasSideEffectImport(filePath: string): boolean { try { const content = fs.readFileSync(filePath, "utf8"); - return /^\s*import\s+["'][^"']+["']\s*;?\s*$/m.test(content); + // Strip comments first so bare imports inside `/* ... */` or `// ...` + // don't falsely trip `cycles-or-side-effects` risk amplification. + return /^\s*import\s+["'][^"']+["']\s*;?\s*$/m.test(stripComments(content)); } catch { return false; } @@ -225,15 +227,22 @@ function collectImportBindings(content: string): BarrelImportBinding[] { /import\s+([\s\S]+?)\s+from\s+["']([^"']+)["']\s*;?/g; let m: RegExpExecArray | null; while ((m = importRe.exec(content)) !== null) { - const clause = m[1]; + const rawClause = m[1]; const source = m[2]; - if (!clause || !source) continue; + if (!rawClause || !source) continue; + + // Drop a leading `type` keyword (TS type-only imports). Otherwise the + // default/namespace fallback regexes interpret `type` itself as a + // default binding and later misattribute sourceless `export { type };`. + const clause = rawClause.replace(/^\s*type\s+/, ""); const namedMatch = clause.match(/\{([^}]*)\}/); if (namedMatch && namedMatch[1] !== undefined) { for (const spec of namedMatch[1].split(",")) { const parts = spec.trim().split(/\s+as\s+/); - const local = (parts[1] ?? parts[0] ?? "").trim(); + const local = (parts[1] ?? parts[0] ?? "") + .trim() + .replace(/^type\s+/, ""); if (local) bindings.push({ localName: local, source }); } } @@ -295,14 +304,18 @@ function emitMetricsForBarrelFile(barrelFile: string): void { ); }; + // All three regexes tolerate an optional `type` keyword (`export type *`, + // `export type { ... } from ...`, `export type { ... };`) so type-only + // re-exports — which debarrel does rewrite — are counted too. const wildcardRe = - /export\s+\*(?:\s+as\s+[\w$]+)?\s+from\s+["']([^"']+)["']\s*;?/g; + /export\s+(?:type\s+)?\*(?:\s+as\s+[\w$]+)?\s+from\s+["']([^"']+)["']\s*;?/g; let m: RegExpExecArray | null; while ((m = wildcardRe.exec(code)) !== null) { if (m[1]) emitForSource(m[1], true); } - const explicitFromRe = /export\s*\{[^}]*\}\s*from\s+["']([^"']+)["']\s*;?/g; + const explicitFromRe = + /export\s+(?:type\s+)?\{[^}]*\}\s*from\s+["']([^"']+)["']\s*;?/g; while ((m = explicitFromRe.exec(code)) !== null) { if (m[1]) emitForSource(m[1], false); } @@ -312,12 +325,12 @@ function emitMetricsForBarrelFile(barrelFile: string): void { const bindingMap = new Map(); for (const b of importBindings) bindingMap.set(b.localName, b.source); - const exportSourcelessRe = /export\s*\{([^}]*)\}\s*(?:;|$)/gm; + const exportSourcelessRe = /export\s+(?:type\s+)?\{([^}]*)\}\s*(?:;|$)/gm; while ((m = exportSourcelessRe.exec(code)) !== null) { if (m[1] === undefined) continue; for (const spec of m[1].split(",")) { const parts = spec.trim().split(/\s+as\s+/); - const localName = parts[0]?.trim(); + const localName = parts[0]?.trim().replace(/^type\s+/, ""); if (!localName) continue; const source = bindingMap.get(localName); if (!source) continue; diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/expected/package.json b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/package.json new file mode 100644 index 0000000..b0d63ae --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/package.json @@ -0,0 +1,5 @@ +{ + "name": "type-only-import-bogus-binding-fixture", + "version": "0.0.0", + "private": true +} diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/foo.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/foo.ts new file mode 100644 index 0000000..3968a67 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/foo.ts @@ -0,0 +1 @@ +export type Foo = { a: string }; diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/index.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/index.ts new file mode 100644 index 0000000..56388c8 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/index.ts @@ -0,0 +1,4 @@ +import { type } from "./stuff"; +import type { Foo } from "./foo"; + +export { type, Foo }; diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/init.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/init.ts new file mode 100644 index 0000000..12eb327 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/init.ts @@ -0,0 +1 @@ +console.log("runs on import"); diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/metrics.json b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/metrics.json new file mode 100644 index 0000000..ffa2020 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/metrics.json @@ -0,0 +1,24 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + }, + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/stuff.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/stuff.ts new file mode 100644 index 0000000..10165a5 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/src/stuff.ts @@ -0,0 +1,3 @@ +import "./init"; + +export const type = 42; diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/expected/tsconfig.json b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/expected/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/input/package.json b/codemods/debarrel/tests/type-only-import-bogus-binding/input/package.json new file mode 100644 index 0000000..b0d63ae --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/input/package.json @@ -0,0 +1,5 @@ +{ + "name": "type-only-import-bogus-binding-fixture", + "version": "0.0.0", + "private": true +} diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/foo.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/foo.ts new file mode 100644 index 0000000..3968a67 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/foo.ts @@ -0,0 +1 @@ +export type Foo = { a: string }; diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/index.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/index.ts new file mode 100644 index 0000000..56388c8 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/index.ts @@ -0,0 +1,4 @@ +import { type } from "./stuff"; +import type { Foo } from "./foo"; + +export { type, Foo }; diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/init.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/init.ts new file mode 100644 index 0000000..12eb327 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/init.ts @@ -0,0 +1 @@ +console.log("runs on import"); diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/metrics.json b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/metrics.json new file mode 100644 index 0000000..ffa2020 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/metrics.json @@ -0,0 +1,24 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + }, + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/stuff.ts b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/stuff.ts new file mode 100644 index 0000000..10165a5 --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/input/src/stuff.ts @@ -0,0 +1,3 @@ +import "./init"; + +export const type = 42; diff --git a/codemods/debarrel/tests/type-only-import-bogus-binding/input/tsconfig.json b/codemods/debarrel/tests/type-only-import-bogus-binding/input/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/type-only-import-bogus-binding/input/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/type-only-reexport/expected/package.json b/codemods/debarrel/tests/type-only-reexport/expected/package.json new file mode 100644 index 0000000..10f91a4 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/expected/package.json @@ -0,0 +1,5 @@ +{ + "name": "type-only-reexport-fixture", + "version": "0.0.0", + "private": true +} diff --git a/codemods/debarrel/tests/type-only-reexport/expected/src/bar.ts b/codemods/debarrel/tests/type-only-reexport/expected/src/bar.ts new file mode 100644 index 0000000..8a79b35 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/expected/src/bar.ts @@ -0,0 +1 @@ +export type Bar = { b: number }; diff --git a/codemods/debarrel/tests/type-only-reexport/expected/src/foo.ts b/codemods/debarrel/tests/type-only-reexport/expected/src/foo.ts new file mode 100644 index 0000000..3968a67 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/expected/src/foo.ts @@ -0,0 +1 @@ +export type Foo = { a: string }; diff --git a/codemods/debarrel/tests/type-only-reexport/expected/src/index.ts b/codemods/debarrel/tests/type-only-reexport/expected/src/index.ts new file mode 100644 index 0000000..ee235b7 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/expected/src/index.ts @@ -0,0 +1,2 @@ +export type { Foo } from "./foo"; +export type { Bar } from "./bar"; diff --git a/codemods/debarrel/tests/type-only-reexport/expected/src/metrics.json b/codemods/debarrel/tests/type-only-reexport/expected/src/metrics.json new file mode 100644 index 0000000..40f2eb9 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/expected/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/type-only-reexport/expected/tsconfig.json b/codemods/debarrel/tests/type-only-reexport/expected/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/expected/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/type-only-reexport/input/package.json b/codemods/debarrel/tests/type-only-reexport/input/package.json new file mode 100644 index 0000000..10f91a4 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/input/package.json @@ -0,0 +1,5 @@ +{ + "name": "type-only-reexport-fixture", + "version": "0.0.0", + "private": true +} diff --git a/codemods/debarrel/tests/type-only-reexport/input/src/bar.ts b/codemods/debarrel/tests/type-only-reexport/input/src/bar.ts new file mode 100644 index 0000000..8a79b35 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/input/src/bar.ts @@ -0,0 +1 @@ +export type Bar = { b: number }; diff --git a/codemods/debarrel/tests/type-only-reexport/input/src/foo.ts b/codemods/debarrel/tests/type-only-reexport/input/src/foo.ts new file mode 100644 index 0000000..3968a67 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/input/src/foo.ts @@ -0,0 +1 @@ +export type Foo = { a: string }; diff --git a/codemods/debarrel/tests/type-only-reexport/input/src/index.ts b/codemods/debarrel/tests/type-only-reexport/input/src/index.ts new file mode 100644 index 0000000..ee235b7 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/input/src/index.ts @@ -0,0 +1,2 @@ +export type { Foo } from "./foo"; +export type { Bar } from "./bar"; diff --git a/codemods/debarrel/tests/type-only-reexport/input/src/metrics.json b/codemods/debarrel/tests/type-only-reexport/input/src/metrics.json new file mode 100644 index 0000000..40f2eb9 --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/input/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/type-only-reexport/input/tsconfig.json b/codemods/debarrel/tests/type-only-reexport/input/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/type-only-reexport/input/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/foo.ts b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/foo.ts new file mode 100644 index 0000000..7a7d85f --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/foo.ts @@ -0,0 +1,4 @@ +/* +import "./init"; +*/ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/index.ts b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/index.ts new file mode 100644 index 0000000..5567e8d --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/index.ts @@ -0,0 +1 @@ +export * from "./foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/metrics.json b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/metrics.json new file mode 100644 index 0000000..22dbd59 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/tsconfig.json b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/expected/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/foo.ts b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/foo.ts new file mode 100644 index 0000000..7a7d85f --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/foo.ts @@ -0,0 +1,4 @@ +/* +import "./init"; +*/ +export const foo = () => "foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/index.ts b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/index.ts new file mode 100644 index 0000000..5567e8d --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/index.ts @@ -0,0 +1 @@ +export * from "./foo"; diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/metrics.json b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/metrics.json new file mode 100644 index 0000000..22dbd59 --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/src/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/tsconfig.json b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/wildcard-side-effect-in-comment/input/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} From 4352c332fa9e18076629dcc3c46f6b73b01d4e5c Mon Sep 17 00:00:00 2001 From: Alex Bit Date: Wed, 22 Apr 2026 18:40:01 -0700 Subject: [PATCH 5/6] test(debarrel): add fixtures for regex vs AST metrics gap Add three jssg cases with semantically correct expected metrics that currently fail against the regex-based emitMetricsForBarrelFile scan: string-literal export shape, Unicode export * as name, and import attributes on a side-effect import. Made-with: Cursor --- .../expected/src/consumer.ts | 3 +++ .../expected/src/import-attributes-bare/b.ts | 1 + .../src/import-attributes-bare/index.ts | 5 ++++ .../src/import-attributes-bare/metrics.json | 14 +++++++++++ .../src/import-attributes-bare/side.json | 1 + .../expected/tsconfig.json | 10 ++++++++ .../input/src/consumer.ts | 3 +++ .../input/src/import-attributes-bare/b.ts | 1 + .../input/src/import-attributes-bare/index.ts | 5 ++++ .../src/import-attributes-bare/metrics.json | 14 +++++++++++ .../src/import-attributes-bare/side.json | 1 + .../input/tsconfig.json | 10 ++++++++ .../expected/src/consumer.ts | 3 +++ .../expected/src/reexport-in-string/decoy.ts | 1 + .../expected/src/reexport-in-string/index.ts | 4 ++++ .../src/reexport-in-string/metrics.json | 14 +++++++++++ .../expected/src/reexport-in-string/real.ts | 1 + .../expected/tsconfig.json | 9 +++++++ .../input/src/consumer.ts | 3 +++ .../input/src/reexport-in-string/decoy.ts | 1 + .../input/src/reexport-in-string/index.ts | 4 ++++ .../input/src/reexport-in-string/metrics.json | 14 +++++++++++ .../input/src/reexport-in-string/real.ts | 1 + .../input/tsconfig.json | 9 +++++++ .../expected/src/consumer.ts | 3 +++ .../expected/src/unicode-wildcard/index.ts | 4 ++++ .../src/unicode-wildcard/metrics.json | 24 +++++++++++++++++++ .../expected/src/unicode-wildcard/mod.ts | 1 + .../expected/src/unicode-wildcard/zed.ts | 1 + .../expected/tsconfig.json | 9 +++++++ .../input/src/consumer.ts | 3 +++ .../input/src/unicode-wildcard/index.ts | 4 ++++ .../input/src/unicode-wildcard/metrics.json | 24 +++++++++++++++++++ .../input/src/unicode-wildcard/mod.ts | 1 + .../input/src/unicode-wildcard/zed.ts | 1 + .../input/tsconfig.json | 9 +++++++ 36 files changed, 216 insertions(+) create mode 100644 codemods/debarrel/tests/import-attributes-bare/expected/src/consumer.ts create mode 100644 codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/b.ts create mode 100644 codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/index.ts create mode 100644 codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json create mode 100644 codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/side.json create mode 100644 codemods/debarrel/tests/import-attributes-bare/expected/tsconfig.json create mode 100644 codemods/debarrel/tests/import-attributes-bare/input/src/consumer.ts create mode 100644 codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/b.ts create mode 100644 codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/index.ts create mode 100644 codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json create mode 100644 codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/side.json create mode 100644 codemods/debarrel/tests/import-attributes-bare/input/tsconfig.json create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/consumer.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/decoy.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/index.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/real.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/tsconfig.json create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/consumer.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/decoy.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/index.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/real.ts create mode 100644 codemods/debarrel/tests/reexport-syntax-in-string-literal/input/tsconfig.json create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/consumer.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/index.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/mod.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/zed.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/expected/tsconfig.json create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/input/src/consumer.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/index.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/mod.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/zed.ts create mode 100644 codemods/debarrel/tests/unicode-wildcard-namespace/input/tsconfig.json diff --git a/codemods/debarrel/tests/import-attributes-bare/expected/src/consumer.ts b/codemods/debarrel/tests/import-attributes-bare/expected/src/consumer.ts new file mode 100644 index 0000000..4697c0f --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/expected/src/consumer.ts @@ -0,0 +1,3 @@ +import { b } from "./import-attributes-bare/b"; + +console.log(b); diff --git a/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/b.ts b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/b.ts new file mode 100644 index 0000000..b85663f --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/b.ts @@ -0,0 +1 @@ +export const b = 1; diff --git a/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/index.ts b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/index.ts new file mode 100644 index 0000000..4f59efc --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/index.ts @@ -0,0 +1,5 @@ +// Semantics: this is a top-level side-effect import. Per metrics rules, the barrel +// should be treated as having load-time side effects, so explicit re-exports (below) +// get `cycles-or-side-effects` risk, not a clean score of 0. +import "./side.json" with { type: "json" }; +export { b } from "./b"; diff --git a/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json new file mode 100644 index 0000000..cf2623c --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/import-attributes-bare/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} diff --git a/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/side.json b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/side.json new file mode 100644 index 0000000..7b6386f --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/side.json @@ -0,0 +1 @@ +{ "k": 1 } diff --git a/codemods/debarrel/tests/import-attributes-bare/expected/tsconfig.json b/codemods/debarrel/tests/import-attributes-bare/expected/tsconfig.json new file mode 100644 index 0000000..369809c --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/expected/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": ".", + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/import-attributes-bare/input/src/consumer.ts b/codemods/debarrel/tests/import-attributes-bare/input/src/consumer.ts new file mode 100644 index 0000000..09162a8 --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/input/src/consumer.ts @@ -0,0 +1,3 @@ +import { b } from "./import-attributes-bare"; + +console.log(b); diff --git a/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/b.ts b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/b.ts new file mode 100644 index 0000000..b85663f --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/b.ts @@ -0,0 +1 @@ +export const b = 1; diff --git a/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/index.ts b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/index.ts new file mode 100644 index 0000000..4f59efc --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/index.ts @@ -0,0 +1,5 @@ +// Semantics: this is a top-level side-effect import. Per metrics rules, the barrel +// should be treated as having load-time side effects, so explicit re-exports (below) +// get `cycles-or-side-effects` risk, not a clean score of 0. +import "./side.json" with { type: "json" }; +export { b } from "./b"; diff --git a/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json new file mode 100644 index 0000000..cf2623c --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/import-attributes-bare/index.ts", + "migration_complexity_score": "2", + "risk_amplifier": "cycles-or-side-effects" + }, + "count": 1 + } + ] +} diff --git a/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/side.json b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/side.json new file mode 100644 index 0000000..7b6386f --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/side.json @@ -0,0 +1 @@ +{ "k": 1 } diff --git a/codemods/debarrel/tests/import-attributes-bare/input/tsconfig.json b/codemods/debarrel/tests/import-attributes-bare/input/tsconfig.json new file mode 100644 index 0000000..369809c --- /dev/null +++ b/codemods/debarrel/tests/import-attributes-bare/input/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": ".", + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/consumer.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/consumer.ts new file mode 100644 index 0000000..76e9a1d --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/consumer.ts @@ -0,0 +1,3 @@ +import { value } from "./reexport-in-string/real"; + +console.log(value); diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/decoy.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/decoy.ts new file mode 100644 index 0000000..453bec1 --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/decoy.ts @@ -0,0 +1 @@ +export const decoy = 0; diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/index.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/index.ts new file mode 100644 index 0000000..b46eb4a --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/index.ts @@ -0,0 +1,4 @@ +// The string is not an export. Semantically, barrel-metric should only record +// the real `export { value } from "./real"` re-export, not a fake `./decoy` path. +const _samplePathHint = 'export { decoy } from "./decoy"'; +export { value } from "./real"; diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json new file mode 100644 index 0000000..02b257f --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/reexport-in-string/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/real.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/real.ts new file mode 100644 index 0000000..efeee5d --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/real.ts @@ -0,0 +1 @@ +export const value = 1; diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/tsconfig.json b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/consumer.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/consumer.ts new file mode 100644 index 0000000..f7a401f --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/consumer.ts @@ -0,0 +1,3 @@ +import { value } from "./reexport-in-string"; + +console.log(value); diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/decoy.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/decoy.ts new file mode 100644 index 0000000..453bec1 --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/decoy.ts @@ -0,0 +1 @@ +export const decoy = 0; diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/index.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/index.ts new file mode 100644 index 0000000..b46eb4a --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/index.ts @@ -0,0 +1,4 @@ +// The string is not an export. Semantically, barrel-metric should only record +// the real `export { value } from "./real"` re-export, not a fake `./decoy` path. +const _samplePathHint = 'export { decoy } from "./decoy"'; +export { value } from "./real"; diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json new file mode 100644 index 0000000..02b257f --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json @@ -0,0 +1,14 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/reexport-in-string/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/real.ts b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/real.ts new file mode 100644 index 0000000..efeee5d --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/real.ts @@ -0,0 +1 @@ +export const value = 1; diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/tsconfig.json b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/consumer.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/consumer.ts new file mode 100644 index 0000000..c79b243 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/consumer.ts @@ -0,0 +1,3 @@ +import { zed } from "./unicode-wildcard/zed"; + +console.log(zed); diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/index.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/index.ts new file mode 100644 index 0000000..c40c1d1 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/index.ts @@ -0,0 +1,4 @@ +// Semantics: one wildcard re-export to `./mod` and one explicit to `./zed` — both +// should appear in barrel-metric (the latter for migration scoring). +export * as 名前 from "./mod"; +export { zed } from "./zed"; diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json new file mode 100644 index 0000000..486c261 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json @@ -0,0 +1,24 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/unicode-wildcard/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + }, + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/unicode-wildcard/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/mod.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/mod.ts new file mode 100644 index 0000000..871fb90 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/mod.ts @@ -0,0 +1 @@ +export const mod = 1; diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/zed.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/zed.ts new file mode 100644 index 0000000..f57cdba --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/zed.ts @@ -0,0 +1 @@ +export const zed = 1; diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/tsconfig.json b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/consumer.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/consumer.ts new file mode 100644 index 0000000..fe06b12 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/consumer.ts @@ -0,0 +1,3 @@ +import { zed } from "./unicode-wildcard"; + +console.log(zed); diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/index.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/index.ts new file mode 100644 index 0000000..c40c1d1 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/index.ts @@ -0,0 +1,4 @@ +// Semantics: one wildcard re-export to `./mod` and one explicit to `./zed` — both +// should appear in barrel-metric (the latter for migration scoring). +export * as 名前 from "./mod"; +export { zed } from "./zed"; diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json new file mode 100644 index 0000000..486c261 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json @@ -0,0 +1,24 @@ +{ + "barrel-metric": [ + { + "cardinality": { + "chaining": "single-level", + "export_style": "wildcard", + "file": "src/unicode-wildcard/index.ts", + "migration_complexity_score": "1", + "risk_amplifier": "none" + }, + "count": 1 + }, + { + "cardinality": { + "chaining": "single-level", + "export_style": "explicit", + "file": "src/unicode-wildcard/index.ts", + "migration_complexity_score": "0", + "risk_amplifier": "none" + }, + "count": 1 + } + ] +} diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/mod.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/mod.ts new file mode 100644 index 0000000..871fb90 --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/mod.ts @@ -0,0 +1 @@ +export const mod = 1; diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/zed.ts b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/zed.ts new file mode 100644 index 0000000..f57cdba --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/zed.ts @@ -0,0 +1 @@ +export const zed = 1; diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/input/tsconfig.json b/codemods/debarrel/tests/unicode-wildcard-namespace/input/tsconfig.json new file mode 100644 index 0000000..459a96a --- /dev/null +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/input/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": "." + }, + "include": ["src"] +} From 908ffea113489c1835ca1c414d9a80e61192d234 Mon Sep 17 00:00:00 2001 From: Alex Bit Date: Wed, 22 Apr 2026 19:31:08 -0700 Subject: [PATCH 6/6] refactor(debarrel): AST-based barrel-metric analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace regex-based barrel scanning in `utils/metrics.ts` with a robust AST + content tokenizer in a new `utils/barrel-metric-ast.ts` module so string-literal decoys, Unicode `export * as` namespaces, and `import` attributes (`import "./x" with { … }`) no longer skew the metric. - New `barrel-metric-ast.ts`: `parse` via `codemod:ast-grep` (language from extension), walks `import_statement`s for value/type bindings and side-effect imports, then tokenizes stripped barrel content to classify wildcard / explicit-from / sourceless re-exports. Results cached per absolute barrel path to avoid re-parsing for each sibling visit. - `utils/ast.ts`: `getStringContent` falls back to slicing raw `string` node text when `string_fragment` is missing (JSSG tree-sitter quirk). - `utils/metrics.ts`: thin coordinator — keeps `computeBarrelAnalysis`, `emitBarrelMetric`, and path/`package.json` helpers; delegates barrel emission and target-file side-effect detection to the new module. - Align EOF + lexical entry order in the three semantic-fixture metrics snapshots (`import-attributes-bare`, `reexport-syntax-in-string-literal`, `unicode-wildcard-namespace`) with how jssg writes them. All 143 debarrel jssg tests pass. Made-with: Cursor --- codemods/debarrel/scripts/utils/ast.ts | 13 +- .../scripts/utils/barrel-metric-ast.ts | 577 ++++++++++++++++++ codemods/debarrel/scripts/utils/metrics.ts | 140 +---- .../src/import-attributes-bare/metrics.json | 2 +- .../src/import-attributes-bare/metrics.json | 2 +- .../src/reexport-in-string/metrics.json | 2 +- .../input/src/reexport-in-string/metrics.json | 2 +- .../src/unicode-wildcard/metrics.json | 10 +- .../input/src/unicode-wildcard/metrics.json | 10 +- 9 files changed, 622 insertions(+), 136 deletions(-) create mode 100644 codemods/debarrel/scripts/utils/barrel-metric-ast.ts diff --git a/codemods/debarrel/scripts/utils/ast.ts b/codemods/debarrel/scripts/utils/ast.ts index b2eacfb..2451d09 100644 --- a/codemods/debarrel/scripts/utils/ast.ts +++ b/codemods/debarrel/scripts/utils/ast.ts @@ -3,5 +3,16 @@ import type { Language } from "./language.ts"; export function getStringContent(node: SgNode): string | null { const fragment = node.find({ rule: { kind: "string_fragment" } }); - return fragment ? fragment.text() : null; + if (fragment) return fragment.text(); + if (node.is("string")) { + const t = node.text(); + if (t.length >= 2) { + const open = t[0]; + const close = t[t.length - 1]; + if ((open === '"' && close === '"') || (open === "'" && close === "'")) { + return t.slice(1, -1); + } + } + } + return null; } diff --git a/codemods/debarrel/scripts/utils/barrel-metric-ast.ts b/codemods/debarrel/scripts/utils/barrel-metric-ast.ts new file mode 100644 index 0000000..6c8190a --- /dev/null +++ b/codemods/debarrel/scripts/utils/barrel-metric-ast.ts @@ -0,0 +1,577 @@ +import path from "path"; +import { parse } from "codemod:ast-grep"; +import type { SgNode, SgRoot } from "codemod:ast-grep"; +import { getStringContent } from "./ast.ts"; +import type { Language } from "./language.ts"; +import { isLocalRelativePath } from "./paths.ts"; + +export type BarrelEmission = { source: string; isWildcard: boolean }; + +/** Per barrel path: one parse + walk for all sibling file visits. */ +export type BarrelMetricCache = Map< + string, + { barrelHasSideEffects: boolean; emissions: BarrelEmission[] } | null +>; + +const cache: BarrelMetricCache = new Map(); + +export function clearBarrelMetricCache(): void { + cache.clear(); +} + +function parseLangName(filePath: string): string { + const ext = filePath.toLowerCase().match(/\.([a-z0-9]+)$/)?.[1]; + if (ext === "tsx" || ext === "jsx") return "tsx"; + if (ext === "ts" || ext === "mts" || ext === "cts") return "typescript"; + return "javascript"; +} + +function tryParse( + filePath: string, + content: string, +): SgRoot | null { + try { + return parse(parseLangName(filePath), content) as SgRoot; + } catch { + return null; + } +} + +function importClauseHasValueBindings(clause: SgNode): boolean { + if (clause.find({ rule: { kind: "named_imports" } })) return true; + if (clause.find({ rule: { kind: "namespace_import" } })) return true; + for (const ch of clause.children()) { + if (ch.is("identifier") && !ch.inside({ rule: { kind: "named_imports" } })) { + return true; + } + } + return false; +} + +/** + * @param t Normalized (single-line) import statement or line text. + * Reject package imports: only `import` with a `from`-free specifier that is + * a local relative path counts as a runtime side effect for metrics. + */ +function lineIsTopLevelSideEffectOnlyImport(t: string): boolean { + if (!t.startsWith("import")) return false; + if (/^import\s+type\b/.test(t)) return false; + if (/^import\s*\{/.test(t)) return false; + if (/^import\s+\*\s+as\b/.test(t)) return false; + if (/^import\s+default\b/.test(t)) return false; + if (/^import\s+[\p{L}\p{M}\p{N}_$][\w$]*\s+from\s*["']/u.test(t)) { + return false; + } + if ( + !/^import\s*["'][^"']*["']\s*;?\s*$/.test(t) && + !/^import\s*["'][^"']*["']\s+with\s/.test(t) + ) { + return false; + } + const spec = t.match(/^import\s*["']([^"']*)["']/); + const path = spec?.[1]; + return path != null && isLocalRelativePath(path); +} + +function sideEffectImportByStatementText(stmt: SgNode): boolean { + return lineIsTopLevelSideEffectOnlyImport( + stmt.text().replace(/\s+/g, " ").trim(), + ); +} + +/** + * Module loaded for side effects: `import` with no (value) binding clause, or + * a clause with no `import` bindings (e.g. some `import "m" with { … }` forms). + */ +function importStatementIsSideEffectOnly(stmt: SgNode): boolean { + if (!stmt.is("import_statement")) return false; + // Rely on statement text for bare `import "./m"`: the tree can omit + // `string` (or the module node) in some JSSG/tree-sitter builds. + if (sideEffectImportByStatementText(stmt)) return true; + const str = lastLocalPathStringInStmt(stmt); + if (!str) return false; + const s = getStringContent(str); + if (!s || !isLocalRelativePath(s)) return false; + const importClause = stmt.find({ rule: { kind: "import_clause" } }); + if (!importClause) return true; + if (!importClauseHasValueBindings(importClause)) return true; + return false; +} + +function walkHasSideEffectImport( + program: SgNode, +): boolean { + for (const ch of program.children()) { + if (ch.is("import_statement") && importStatementIsSideEffectOnly(ch)) { + return true; + } + } + return false; +} + +// JSSG can omit `import_statement` or `string` nodes; line-scan fallback must +// ignore `import` inside block and line comments (wildcard-side-effect-in-comment). +function contentHasTopLevelSideEffectImportLineAfterComments( + content: string, +): boolean { + let inBlock = false; + for (const raw of content.split("\n")) { + let line = raw; + if (inBlock) { + const endIdx = line.indexOf("*/"); + if (endIdx === -1) { + continue; + } + inBlock = false; + line = line.slice(endIdx + 2); + } + for (;;) { + const start = line.indexOf("/*"); + if (start === -1) break; + const end = line.indexOf("*/", start + 2); + if (end === -1) { + inBlock = true; + line = line.slice(0, start); + break; + } + line = line.slice(0, start) + line.slice(end + 2); + } + const t = line.replace(/\/\/.*$/, "").replace(/\s+/g, " ").trim(); + if (t.length > 0 && lineIsTopLevelSideEffectOnlyImport(t)) { + return true; + } + } + return false; +} + +/** + * `import` bindings: localName → source path, for `export { a };` re-exports. + */ +function importSpecifierLocalBindingName( + spec: SgNode, +): string | null { + const ids = spec.findAll({ rule: { kind: "identifier" } }); + if (ids.length === 0) return null; + if (ids[0]!.text() === "type" && ids[1]) return ids[1]!.text(); + return ids[ids.length - 1]!.text(); +} + +function collectImportBindings( + program: SgNode, + fileContent: string, +): Map { + const m = new Map(); + for (const stmt of program.children()) { + if (!stmt.is("import_statement")) continue; + const mod = lastLocalPathStringInStmt(stmt); + if (!mod) continue; + const modPath = getStringContent(mod); + if (!modPath || !isLocalRelativePath(modPath)) continue; + + for (const spec of stmt.findAll({ rule: { kind: "import_specifier" } })) { + const name = importSpecifierLocalBindingName(spec); + if (name) m.set(name, modPath); + } + + // `import { type }` (value import named `type`): JSSG may omit `import_clause` + // and every `import_specifier` — but `stmt.text()` still has the right shape. + const oneLine = stmt.text().replace(/\s+/g, " "); + const typeAsValue = /import[\s.]*{[\s.]*type[\s.]*}[\s.]*from[\s.]*["']([./][^"']*)["']/.exec( + oneLine, + ); + if (typeAsValue) { + const p = typeAsValue[1]; + if (p && isLocalRelativePath(p)) m.set("type", p); + } else if ( + !/import[\s.]+type[\s.]*{/.test(oneLine) && + /import[\s.]*{[\s.]*type[\s.]*}[\s.]*from/.test(oneLine) && + modPath + ) { + m.set("type", modPath); + } + + const importClause = stmt.find({ rule: { kind: "import_clause" } }); + if (importClause) { + const named = importClause.find({ rule: { kind: "named_imports" } }); + if (named) { + for (const spec of named.findAll({ rule: { kind: "import_specifier" } })) { + const name = importSpecifierLocalBindingName(spec); + if (name) m.set(name, modPath); + } + } + } + if (!importClause) continue; + + const ns = importClause.find({ rule: { kind: "namespace_import" } }); + if (ns) { + const id = ns.find({ rule: { kind: "identifier" } }); + if (id) m.set(id.text(), modPath); + } + + if (stmt.findAll({ rule: { kind: "import_specifier" } }).length > 0) { + continue; + } + if (ns) continue; + + const def = importClause + .children() + .find( + (c) => + c.is("identifier") && !c.inside({ rule: { kind: "named_imports" } }), + ); + if (def) m.set(def.text(), modPath); + } + if (!m.has("type")) { + const flat = fileContent.replace(/\s+/g, " "); + const tBind = /import\s*{\s*type\s*}\s*from\s*["']([./][^"']*)["']/.exec( + flat, + ); + if (tBind?.[1] && isLocalRelativePath(tBind[1])) m.set("type", tBind[1]); + } + return m; +} + +function exportSpecifierSourcelessLookupName( + spec: SgNode, +): string | null { + const ids = spec.findAll({ rule: { kind: "identifier" } }); + if (ids.length === 0) return null; + if (ids[0]!.text() === "type" && ids[1]) return ids[1]!.text(); + return ids[0]!.text(); +} + +/** + * `export * from "p"` and `export * as X from "p"` (and `export type * from`) can miss + * `namespace_export` in some tree-sitter builds; use a local `export_statement` text + * check (not a whole-file regex scan) as fallback — avoids `stmt.matches` which can + * throw in the JSSG runtime. + */ +function isExportStarReexportForm(stmt: SgNode): boolean { + if (!stmt.is("export_statement")) return false; + if (stmt.find({ rule: { kind: "namespace_export" } })) return true; + const t = stmt.text(); + if (/export\s+type\s+\*/.test(t) && t.includes("from")) return true; + if (/export\s+\*\s+as\s+/.test(t) && t.includes("from")) return true; + if (/export\s+\*\s*from/.test(t)) return true; + return false; +} + +/** + * The `from "./m"` string is not always a direct child; use a deep search, and + * if there are several path-like strings, the module in `import` / `export … from "…"` + * is usually the last in source order (e.g. `import type` + string, `import … with`). + */ +function lastLocalPathStringInStmt(stmt: SgNode): SgNode | null { + const allStr = stmt.findAll({ rule: { kind: "string" } }); + const rel: SgNode[] = []; + for (const s of allStr) { + const p = getStringContent(s); + if (p && isLocalRelativePath(p)) rel.push(s); + } + if (rel.length === 0) return null; + return rel[rel.length - 1] ?? null; +} + +/** + * JSSG's `export_statement#text` can mangle or omit non-ASCII in `* as` idents + * (e.g. `export * as 名前 from "./m"`), so also scan a real `export` line that + * contains the module string (not a `const` string that quotes a fake path). + */ +function fileLineIsExportStarReexport( + fileContent: string, + relPath: string, +): boolean { + const d = `"${relPath}"`; + const s = `'${relPath}'`; + for (const raw of fileContent.split(/\r?\n/)) { + if (!raw.includes(d) && !raw.includes(s)) continue; + const t = raw + .replace(/^\uFEFF/, "") + .replace(/\/\/.*$/, "") + .trim() + .replace(/\r$/, ""); + if (!/^\s*export\s+/.test(t)) continue; + if (/export\s+type\s+\*/.test(t) && t.includes("from")) return true; + if (t.includes("from") && t.includes("* as") && t.includes("export")) { + return true; + } + if (t.includes("from") && /export[\s.]*\*\s*from/.test(t)) return true; + } + return false; +} + +/** + * `export { … } from "p"` (and `export type { … } from`) on the real file line + * for `p`. Distinguishes from star re-exports so heuristics never misclassify. + */ +function fileLineIsExplicitNamedReexport( + fileContent: string, + relPath: string, +): boolean { + const d = `"${relPath}"`; + const s = `'${relPath}'`; + for (const raw of fileContent.split(/\r?\n/)) { + if (!raw.includes(d) && !raw.includes(s)) continue; + const t = raw + .replace(/^\uFEFF/, "") + .replace(/\/\/.*$/, "") + .trim(); + if (!/^\s*export\s+/.test(t) || !/\bfrom\s*["']/.test(t)) continue; + if (/^\s*export(\s+type)?\s*\{/.test(t)) return true; + } + return false; +} + +/** + * JSSG/tree-sitter can break `export { type, Foo }` (ERROR nodes) so + * `export_specifier` walks miss names. Parse the clause from statement text + * and return local names for the binding map. + */ +function localNameFromExportSpecPartSegment(part: string): string | null { + const p = part.trim(); + if (!p) return null; + const asChunk = p.split(/\s+as\s+/); + if (asChunk.length >= 2) { + return asChunk[0]!.replace(/^\btype\s+/, "").trim() || null; + } + return p.replace(/^\btype\s+/, "").trim() || null; +} + +function sourcelessExportLocalNamesFromStmtText( + stmt: SgNode, +): string[] { + const t = stmt.text().replace(/\s+/g, " "); + const m = /\bexport\s*\{([^}]+)\}/.exec(t); + if (!m?.[1]) return []; + const out: string[] = []; + for (const part of m[1]!.split(",")) { + const n = localNameFromExportSpecPartSegment(part); + if (n) out.push(n); + } + return out; +} + +/** + * Strip `//` line comments and `/* … *\/` block comments; jump OVER string + * literals (preserving their contents verbatim), so that `;` and `//` inside + * a quoted string are treated as string chars, not as statement / comment + * boundaries. See the `reexport-syntax-in-string-literal` fixture: a `const + * x = 'export { decoy } from "./decoy"'` must not be parsed as a re-export. + */ +function stripComments(src: string): string { + let out = ""; + let i = 0; + const n = src.length; + while (i < n) { + const c = src[i]!; + const c2 = src[i + 1]; + if (c === "/" && c2 === "/") { + while (i < n && src[i] !== "\n") i++; + continue; + } + if (c === "/" && c2 === "*") { + i += 2; + while (i < n && !(src[i] === "*" && src[i + 1] === "/")) { + if (src[i] === "\n") out += "\n"; + i++; + } + i += 2; + continue; + } + if (c === '"' || c === "'" || c === "`") { + const quote = c; + out += quote; + i++; + while (i < n && src[i] !== quote) { + if (src[i] === "\\" && i + 1 < n) { + out += src[i]!; + out += src[i + 1]!; + i += 2; + continue; + } + out += src[i]!; + i++; + } + if (i < n) { + out += src[i]!; + i++; + } + continue; + } + out += c; + i++; + } + return out; +} + +/** + * Split on `;` at top level (strings/comments already handled by `stripComments`), + * flatten whitespace so multi-line `export { a, b } from "m"` becomes one chunk. + */ +function splitLogicalStatements(stripped: string): string[] { + return stripped + .split(";") + .map((t) => t.replace(/\s+/g, " ").trim()) + .filter((t) => t.length > 0); +} + +/** + * Emit `{ source, isWildcard }` directly from the stripped barrel text — + * robust against JSSG tree-sitter quirks on non-ASCII identifiers and + * `export * as` ownership (see the `unicode-wildcard-namespace` fixture). + */ +/** + * Match `from "…"` / `from '…'` at the end of `stmt`, return the quoted path. + * Avoids `[^"']+` classes: some runtimes handle non-ASCII inconsistently. + */ +function extractFromSource(stmt: string): string | null { + const m = /\bfrom\s+(["'])/.exec(stmt); + if (!m) return null; + const quote = m[1]!; + const start = m.index + m[0].length; + const end = stmt.indexOf(quote, start); + if (end === -1) return null; + return stmt.slice(start, end); +} + +/** + * Each `stmt` is already whitespace-flattened. Tokenize: strip `export`, an + * optional `type`, and classify by the next char (`*` → wildcard, `{` → named). + * Avoids regex features (Unicode `\S`, negated classes) that behave + * differently on LLRT; see the `unicode-wildcard-namespace` fixture. + */ +type ParsedStmt = + | { kind: "wildcard"; source: string } + | { kind: "explicitFrom"; source: string } + | { kind: "sourcelessNamed"; names: string[] } + | { kind: "other" }; + +function parseExportStatement(stmt: string): ParsedStmt { + if (!stmt.startsWith("export ") && !stmt.startsWith("export\t")) { + return { kind: "other" }; + } + let rest = stmt.slice("export".length).trimStart(); + if (rest.startsWith("type ") || rest.startsWith("type\t")) { + rest = rest.slice("type".length).trimStart(); + } + if (rest.startsWith("*")) { + const src = extractFromSource(rest); + if (src) return { kind: "wildcard", source: src }; + return { kind: "other" }; + } + if (rest.startsWith("{")) { + const closeIdx = rest.indexOf("}"); + if (closeIdx === -1) return { kind: "other" }; + const inside = rest.slice(1, closeIdx); + const afterBrace = rest.slice(closeIdx + 1).trimStart(); + const src = extractFromSource(rest); + if (src) return { kind: "explicitFrom", source: src }; + if (afterBrace.length === 0 || afterBrace.startsWith(";")) { + const names: string[] = []; + for (const part of inside.split(",")) { + const n = localNameFromExportSpecPartSegment(part); + if (n) names.push(n); + } + return { kind: "sourcelessNamed", names }; + } + } + return { kind: "other" }; +} + +function collectReexportsFromContent( + content: string, + bindingMap: Map, +): BarrelEmission[] { + const out: BarrelEmission[] = []; + const stripped = stripComments(content); + + for (const stmt of splitLogicalStatements(stripped)) { + const parsed = parseExportStatement(stmt); + if (parsed.kind === "wildcard") { + if (isLocalRelativePath(parsed.source)) { + out.push({ source: parsed.source, isWildcard: true }); + } + } else if (parsed.kind === "explicitFrom") { + if (isLocalRelativePath(parsed.source)) { + out.push({ source: parsed.source, isWildcard: false }); + } + } else if (parsed.kind === "sourcelessNamed") { + const seenSource = new Set(); + for (const localName of parsed.names) { + const src = bindingMap.get(localName); + if (src && isLocalRelativePath(src) && !seenSource.has(src)) { + seenSource.add(src); + out.push({ source: src, isWildcard: false }); + } + } + } + } + + return out; +} + +function buildBarrelEmissions( + barrelFile: string, + readFile: (p: string) => string, +): { barrelHasSideEffects: boolean; emissions: BarrelEmission[] } | null { + let content: string; + try { + content = readFile(barrelFile); + } catch { + return null; + } + const root = tryParse(barrelFile, content); + if (!root) return null; + const program = root.root(); + if (!program.is("program")) return null; + + const barrelHasSideEffects = + walkHasSideEffectImport(program) || + contentHasTopLevelSideEffectImportLineAfterComments(content); + const bindingMap = collectImportBindings(program, content); + const emissions = collectReexportsFromContent(content, bindingMap); + + const wild = emissions + .filter((e) => e.isWildcard) + .sort((a, b) => a.source.localeCompare(b.source)); + const rest = emissions + .filter((e) => !e.isWildcard) + .sort((a, b) => a.source.localeCompare(b.source)); + const ordered: BarrelEmission[] = [...wild, ...rest]; + + return { barrelHasSideEffects, emissions: ordered }; +} + +export function getBarrelEmissions( + absBarrelFile: string, + readFile: (p: string) => string, +): { barrelHasSideEffects: boolean; emissions: BarrelEmission[] } | null { + const key = path.normalize(absBarrelFile); + const c = cache.get(key); + if (c !== undefined) return c; + const next = buildBarrelEmissions(key, readFile); + cache.set(key, next); + return next; +} + +/** + * True if a top-level side-effect `import` exists (e.g. `import "./x"` or + * `import "./x" with { … }` without a binding clause), for a given file. + */ +export function fileHasTopLevelSideEffectOnlyImport( + filePath: string, + readFile: (p: string) => string, +): boolean { + let content: string; + try { + content = readFile(filePath); + } catch { + return false; + } + const root = tryParse(filePath, content); + if (!root) return false; + const program = root.root(); + if (!program.is("program")) return false; + if (walkHasSideEffectImport(program)) return true; + return contentHasTopLevelSideEffectImportLineAfterComments(content); +} diff --git a/codemods/debarrel/scripts/utils/metrics.ts b/codemods/debarrel/scripts/utils/metrics.ts index f3b3032..46c5119 100644 --- a/codemods/debarrel/scripts/utils/metrics.ts +++ b/codemods/debarrel/scripts/utils/metrics.ts @@ -6,6 +6,7 @@ import { isInsideNodeModules, isLocalRelativePath, } from "./paths.ts"; +import { getBarrelEmissions, fileHasTopLevelSideEffectOnlyImport } from "./barrel-metric-ast.ts"; const barrelMetric = useMetricAtom("barrel-metric"); @@ -132,14 +133,9 @@ function resolveLocalModule( } function fileContentHasSideEffectImport(filePath: string): boolean { - try { - const content = fs.readFileSync(filePath, "utf8"); - // Strip comments first so bare imports inside `/* ... */` or `// ...` - // don't falsely trip `cycles-or-side-effects` risk amplification. - return /^\s*import\s+["'][^"']+["']\s*;?\s*$/m.test(stripComments(content)); - } catch { - return false; - } + return fileHasTopLevelSideEffectOnlyImport(filePath, (p) => + fs.readFileSync(p, "utf8"), + ); } function computeBarrelAnalysis(params: { @@ -211,131 +207,33 @@ function findSiblingBarrel(filename: string): string | null { return null; } -interface BarrelImportBinding { - localName: string; - source: string; -} - -/** - * Parse `import ... from "..."` statements and return bindings by local - * name → source. Regex-based so we can resolve sourceless `export { X };` - * patterns without an AST. - */ -function collectImportBindings(content: string): BarrelImportBinding[] { - const bindings: BarrelImportBinding[] = []; - const importRe = - /import\s+([\s\S]+?)\s+from\s+["']([^"']+)["']\s*;?/g; - let m: RegExpExecArray | null; - while ((m = importRe.exec(content)) !== null) { - const rawClause = m[1]; - const source = m[2]; - if (!rawClause || !source) continue; - - // Drop a leading `type` keyword (TS type-only imports). Otherwise the - // default/namespace fallback regexes interpret `type` itself as a - // default binding and later misattribute sourceless `export { type };`. - const clause = rawClause.replace(/^\s*type\s+/, ""); - - const namedMatch = clause.match(/\{([^}]*)\}/); - if (namedMatch && namedMatch[1] !== undefined) { - for (const spec of namedMatch[1].split(",")) { - const parts = spec.trim().split(/\s+as\s+/); - const local = (parts[1] ?? parts[0] ?? "") - .trim() - .replace(/^type\s+/, ""); - if (local) bindings.push({ localName: local, source }); - } - } - - const withoutNamed = clause.replace(/\{[^}]*\}/g, ""); - - const nsMatch = withoutNamed.match(/\*\s+as\s+([\w$]+)/); - if (nsMatch && nsMatch[1]) { - bindings.push({ localName: nsMatch[1], source }); - } - - const defaultMatch = withoutNamed.match(/(?:^|,)\s*([\w$]+)\s*(?:,|$)/); - if (defaultMatch && defaultMatch[1]) { - bindings.push({ localName: defaultMatch[1], source }); - } - } - return bindings; -} - -function stripComments(content: string): string { - return content - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/\/\/.*$/gm, ""); -} - /** * Walk every re-export in `barrelFile` and emit one `barrel-metric` event - * per analyzed re-export source. Uses a regex-based scan so the metric set - * is identical regardless of whether the barrel itself or a sibling triggered - * the emission. + * per analyzed re-export source, using an ast-grep parse of the barrel file + * (and the same for side-effect `import` heuristics on resolved targets). * * Handles: - * 1. `export * from "..."` (wildcard) - * 2. `export * as X from "..."` (wildcard) - * 3. `export { A, B } from "..."` (explicit) - * 4. `import { X } from "..."; export { X };` (explicit, per traced source) + * 1. `export * from "..."` / `export type * from "..."` (wildcard) + * 2. `export * as X from "..."` (any valid identifier, including non-ASCII) + * 3. `export { A, B } from "..."` (explicit) including `export type { } from` + * 4. `import { X } from "..."; export { X };` (explicit, per sourceless name) */ function emitMetricsForBarrelFile(barrelFile: string): void { - let content: string; - try { - content = fs.readFileSync(barrelFile, "utf8"); - } catch { - return; - } - const code = stripComments(content); - - const barrelHasSideEffects = - /^[ \t]*import\s+["'][^"']+["']\s*;?\s*$/m.test(code); - - const emitForSource = (source: string, isWildcard: boolean): void => { - if (!isLocalRelativePath(source)) return; + const parsed = getBarrelEmissions(barrelFile, (p) => + fs.readFileSync(p, "utf8"), + ); + if (!parsed) return; + const { barrelHasSideEffects, emissions } = parsed; + for (const e of emissions) { + if (!isLocalRelativePath(e.source)) continue; emitBarrelMetric( computeBarrelAnalysis({ barrelFile, - sourceFromBarrel: source, - isWildcard, + sourceFromBarrel: e.source, + isWildcard: e.isWildcard, barrelHasSideEffects, }), ); - }; - - // All three regexes tolerate an optional `type` keyword (`export type *`, - // `export type { ... } from ...`, `export type { ... };`) so type-only - // re-exports — which debarrel does rewrite — are counted too. - const wildcardRe = - /export\s+(?:type\s+)?\*(?:\s+as\s+[\w$]+)?\s+from\s+["']([^"']+)["']\s*;?/g; - let m: RegExpExecArray | null; - while ((m = wildcardRe.exec(code)) !== null) { - if (m[1]) emitForSource(m[1], true); - } - - const explicitFromRe = - /export\s+(?:type\s+)?\{[^}]*\}\s*from\s+["']([^"']+)["']\s*;?/g; - while ((m = explicitFromRe.exec(code)) !== null) { - if (m[1]) emitForSource(m[1], false); - } - - const importBindings = collectImportBindings(code); - if (importBindings.length === 0) return; - const bindingMap = new Map(); - for (const b of importBindings) bindingMap.set(b.localName, b.source); - - const exportSourcelessRe = /export\s+(?:type\s+)?\{([^}]*)\}\s*(?:;|$)/gm; - while ((m = exportSourcelessRe.exec(code)) !== null) { - if (m[1] === undefined) continue; - for (const spec of m[1].split(",")) { - const parts = spec.trim().split(/\s+as\s+/); - const localName = parts[0]?.trim().replace(/^type\s+/, ""); - if (!localName) continue; - const source = bindingMap.get(localName); - if (!source) continue; - emitForSource(source, false); - } } } diff --git a/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json index cf2623c..b442453 100644 --- a/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json +++ b/codemods/debarrel/tests/import-attributes-bare/expected/src/import-attributes-bare/metrics.json @@ -11,4 +11,4 @@ "count": 1 } ] -} +} \ No newline at end of file diff --git a/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json index cf2623c..b442453 100644 --- a/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json +++ b/codemods/debarrel/tests/import-attributes-bare/input/src/import-attributes-bare/metrics.json @@ -11,4 +11,4 @@ "count": 1 } ] -} +} \ No newline at end of file diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json index 02b257f..89c6541 100644 --- a/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/expected/src/reexport-in-string/metrics.json @@ -11,4 +11,4 @@ "count": 1 } ] -} +} \ No newline at end of file diff --git a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json index 02b257f..89c6541 100644 --- a/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json +++ b/codemods/debarrel/tests/reexport-syntax-in-string-literal/input/src/reexport-in-string/metrics.json @@ -11,4 +11,4 @@ "count": 1 } ] -} +} \ No newline at end of file diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json index 486c261..f52d920 100644 --- a/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/expected/src/unicode-wildcard/metrics.json @@ -3,9 +3,9 @@ { "cardinality": { "chaining": "single-level", - "export_style": "wildcard", + "export_style": "explicit", "file": "src/unicode-wildcard/index.ts", - "migration_complexity_score": "1", + "migration_complexity_score": "0", "risk_amplifier": "none" }, "count": 1 @@ -13,12 +13,12 @@ { "cardinality": { "chaining": "single-level", - "export_style": "explicit", + "export_style": "wildcard", "file": "src/unicode-wildcard/index.ts", - "migration_complexity_score": "0", + "migration_complexity_score": "1", "risk_amplifier": "none" }, "count": 1 } ] -} +} \ No newline at end of file diff --git a/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json index 486c261..f52d920 100644 --- a/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json +++ b/codemods/debarrel/tests/unicode-wildcard-namespace/input/src/unicode-wildcard/metrics.json @@ -3,9 +3,9 @@ { "cardinality": { "chaining": "single-level", - "export_style": "wildcard", + "export_style": "explicit", "file": "src/unicode-wildcard/index.ts", - "migration_complexity_score": "1", + "migration_complexity_score": "0", "risk_amplifier": "none" }, "count": 1 @@ -13,12 +13,12 @@ { "cardinality": { "chaining": "single-level", - "export_style": "explicit", + "export_style": "wildcard", "file": "src/unicode-wildcard/index.ts", - "migration_complexity_score": "0", + "migration_complexity_score": "1", "risk_amplifier": "none" }, "count": 1 } ] -} +} \ No newline at end of file