Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/barrel-metric.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions codemods/debarrel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions codemods/debarrel/codemod.yaml
Original file line number Diff line number Diff line change
@@ -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 <mo@codemod.com>"
license: "MIT"
Expand All @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions codemods/debarrel/scripts/codemod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import {
rewriteMockCalls,
type BarrelMockInfo,
} from "./utils/mocks.ts";
import { emitMetricsForBarrelSibling } from "./utils/metrics.ts";

const codemod: Codemod<Language> = async (root) => {
const rootNode = root.root();
const filename = root.filename();
const edits: Edit[] = [];
const barrelRewrites = new Map<string, BarrelMockInfo>();

emitMetricsForBarrelSibling(filename);

for (const importStmt of rootNode.findAll({
rule: { kind: "import_statement" },
})) {
Expand Down
13 changes: 12 additions & 1 deletion codemods/debarrel/scripts/utils/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,16 @@ import type { Language } from "./language.ts";

export function getStringContent(node: SgNode<Language>): 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;
}
Loading
Loading