followMoves
uses node:fs
Imports and re-exports of moved definitions, pointed at the files they're in now: { x } from the old file
becomes an import from the new one ({ y as x } if renamed too, so the code using it reads the same), the new
file stops importing it, and the old file imports it if its other code still uses it. A namespace import
(a.x) can't follow: that's a problem. Returns the text of the files outside mirrored it changed, as they
were.
import { applyEdits, importedNames, importsAt, mentions, specifierFor } from "#codebase/tcmts/cli/mirror"; import type { Edit, Move } from "#codebase/tcmts/cli/mirror"; import { isTypeOnlyImport, parse } from "#codebase/tcmts/core/ts"; import fs from "node:fs"; import path from "node:path"; import { isExportDeclaration as ts_isExportDeclaration, isImportDeclaration as ts_isImportDeclaration, isNamedExports as ts_isNamedExports, isNamespaceImport as ts_isNamespaceImport, isStringLiteral as ts_isStringLiteral, } from "typescript/ast"; import type { ExportSpecifier as ts_ExportSpecifier, ImportSpecifier as ts_ImportSpecifier } from "typescript/ast"; export function followMoves(files: string[], mirrored: Set<string>, moves: Move[], problems: string[]): Map<string, string> { const outside = new Map<string, string>(); for (const file of new Set(files)) { const text = fs.readFileSync(file, "utf8"); const sf = parse(file, text); const edits: Edit[] = []; const added: string[] = []; for (const st of sf.statements) { const spec = (ts_isImportDeclaration(st) || ts_isExportDeclaration(st)) && st.moduleSpecifier && ts_isStringLiteral(st.moduleSpecifier) ? st.moduleSpecifier.text : undefined; if (!spec?.startsWith(".")) continue; const target = path.resolve(path.dirname(file), spec); const here = moves.filter((v) => v.from === target); if (!here.length) continue; const clause = ts_isImportDeclaration(st) ? st.importClause : undefined; const bindings = clause?.namedBindings ?? (ts_isExportDeclaration(st) && st.exportClause && ts_isNamedExports(st.exportClause) ? st.exportClause : undefined); if (bindings && ts_isNamespaceImport(bindings)) { for (const v of here) if (new RegExp(`\\b${bindings.name.text}\\.${v.leaf}\\b`).test(text)) problems.push(`${path.relative(process.cwd(), file)} uses ${bindings.name.text}.${v.leaf}, which moved to ${path.relative(path.dirname(file), v.to)}; change it there by hand`); continue; } if (!bindings) continue; const elements = bindings.elements as readonly (ts_ImportSpecifier | ts_ExportSpecifier)[]; const moving = (e: ts_ImportSpecifier | ts_ExportSpecifier) => here.find((v) => (e.propertyName ?? e.name).text === v.leaf); if (!elements.some(moving)) continue; const keep = elements.filter((e) => !moving(e)); const kw = ts_isImportDeclaration(st) ? "import" : "export"; const type = (ts_isImportDeclaration(st) ? isTypeOnlyImport(clause) : ts_isExportDeclaration(st) && st.isTypeOnly) ? " type" : ""; // what stays imported from the old file const dflt = clause?.name ? `${clause.name.text}${keep.length ? ", " : ""}` : ""; const end = text[st.end] === "\n" ? st.end + 1 : st.end; edits.push({ start: st.getStart(sf), end, text: keep.length || dflt ? `${kw}${type} ${dflt}${keep.length ? `{ ${keep.map((e) => e.getText(sf)).join(", ")} }` : ""} from ${JSON.stringify(spec)};\n` : "" }); // and what's imported from the new ones (nothing, in the file it's in now) for (const e of elements) { const v = moving(e); if (!v || v.to === file) continue; const local = e.name.text; const name = v.as === local && !e.propertyName ? local : `${v.as} as ${local}`; added.push(`${kw}${type} { ${e.isTypeOnly ? "type " : ""}${name} } from ${JSON.stringify(specifierFor(file, v.to, v.as))};`); } } // the old file's code that still uses what moved imports it for (const v of moves) { if (v.from !== file || importedNames(file, text).includes(v.leaf) || !mentions(sf, v.leaf)) continue; added.push(`import { ${v.as === v.leaf ? v.leaf : `${v.as} as ${v.leaf}`} } from ${JSON.stringify(specifierFor(file, v.to, v.as))};`); } if (added.length) { const at = importsAt(file, text); edits.push({ start: at, end: at, text: (at ? "\n" : "") + added.join("\n") + (at ? "" : "\n") }); } if (!edits.length) continue; if (!mirrored.has(file)) outside.set(file, text); fs.writeFileSync(file, applyEdits(text, edits)); } return outside; }
uses
- tcmts.cli.mirror.Edit
- tcmts.cli.mirror.Move
- tcmts.cli.mirror.applyEdits
- tcmts.cli.mirror.importedNames
- tcmts.cli.mirror.importsAt
- tcmts.cli.mirror.mentions
- tcmts.cli.mirror.specifierFor
- tcmts.core.ts.isTypeOnlyImport
- tcmts.core.ts.parse