#!/usr/bin/env bun /** * Fixes broken relative imports inside files that were just migrated from * src/.ts to src//.ts. * * The migrate-feature-layout script moved files one level deeper but only * rewrote the imports OF the moved files, not imports WITHIN them. Any * `./X.js` (or `./X/index.js`) inside a moved file now points at * `src//X.js` (which doesn't exist) instead of the intended * `src/X.js`. * * For each `src//{.ts, .test.ts}`, walks its * relative imports and rewrites `./X` → `../X` when the target resolves at * the package's src/ root (one level up). Leaves intra-folder imports * alone (`./.types.js` etc.) since those are still correct. * * Idempotent: a second run finds nothing to fix. */ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; const PACKAGES_DIR = "packages"; interface MovedFile { readonly file: string; readonly srcDir: string; readonly featureDir: string; readonly feature: string; } function listMovedFiles(): MovedFile[] { const out: MovedFile[] = []; for (const cat of readdirSync(PACKAGES_DIR)) { const catDir = join(PACKAGES_DIR, cat); if (!statSync(catDir).isDirectory()) continue; for (const plugin of readdirSync(catDir)) { const pluginDir = join(catDir, plugin); if (!statSync(pluginDir).isDirectory()) continue; for (const pkg of readdirSync(pluginDir)) { const pkgDir = join(pluginDir, pkg); if (!statSync(pkgDir).isDirectory()) continue; const srcDir = join(pkgDir, "src"); try { statSync(srcDir); } catch { continue; } for (const entry of readdirSync(srcDir)) { const featureDir = join(srcDir, entry); try { if (!statSync(featureDir).isDirectory()) continue; } catch { continue; } const feature = entry; const impl = join(featureDir, `${feature}.ts`); const test = join(featureDir, `${feature}.test.ts`); if (existsSync(impl)) out.push({ file: impl, srcDir, featureDir, feature }); if (existsSync(test)) out.push({ file: test, srcDir, featureDir, feature }); } } } } return out; } function targetResolves(targetBase: string, lookupDir: string): boolean { // targetBase is e.g. "./errors/serialize-error" — strip leading ./, then // look for matching candidates from lookupDir. const stripped = targetBase.replace(/^\.\//, ""); const candidates = [ join(lookupDir, `${stripped}.ts`), join(lookupDir, `${stripped}.tsx`), join(lookupDir, stripped, "index.ts"), join(lookupDir, stripped, "index.tsx"), join(lookupDir, stripped), // for asset / json imports ]; return candidates.some((c) => { try { statSync(c); return true; } catch { return false; } }); } function fixFile(moved: MovedFile): number { const src = readFileSync(moved.file, "utf8"); // Match every `"./.js"` whether in import or side-effect. const re = /"(\.\/[^"]+\.js)"/g; let updated = src; const touches = new Set(); for (const m of src.matchAll(re)) { const fullPath = m[1] as string; // e.g. "./foo.js" or "./foo/bar.js" const withoutJs = fullPath.replace(/\.js$/, ""); const base = withoutJs.replace(/\/index$/, ""); // Skip imports that target THE feature folder itself or its siblings // — those resolve correctly inside the new feature dir. const intraFolder = base === `./${moved.feature}` || base.startsWith(`./${moved.feature}.`) || // sibling files inside the same feature folder !base.includes("/", 2); // Step 1: does it currently resolve from the file's location? const fileDir = dirname(moved.file); if (targetResolves(base, fileDir)) continue; // Step 2: does it resolve at src/ root (one level up)? if (targetResolves(base, moved.srcDir)) { const newPath = fullPath.replace(/^\.\//, "../"); updated = updated.split(`"${fullPath}"`).join(`"${newPath}"`); touches.add(fullPath); continue; } // Avoid lint warning about unused variable void intraFolder; } if (updated !== src) { writeFileSync(moved.file, updated); return touches.size; } return 0; } function main(): void { const moved = listMovedFiles(); let filesChanged = 0; let importsRewritten = 0; for (const m of moved) { const n = fixFile(m); if (n > 0) { filesChanged++; importsRewritten += n; console.log(`FIX ${m.file} (${n} imports)`); } } console.log( `\nFixed ${importsRewritten} import path${importsRewritten === 1 ? "" : "s"} across ${filesChanged} file${filesChanged === 1 ? "" : "s"}.` ); } main();