Files
suaveplan/scripts/lint/fix-migrated-imports.ts
2026-05-27 23:47:53 +01:00

158 lines
5.3 KiB
TypeScript

#!/usr/bin/env bun
/**
* Fixes broken relative imports inside files that were just migrated from
* src/<feature>.ts to src/<feature>/<feature>.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/<feature>/X.js` (which doesn't exist) instead of the intended
* `src/X.js`.
*
* For each `src/<feature>/{<feature>.ts, <feature>.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 (`./<feature>.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 `"./<something>.js"` whether in import or side-effect.
const re = /"(\.\/[^"]+\.js)"/g;
let updated = src;
const touches = new Set<string>();
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();