followMoves

tcmts.cli.mirror.followMoves · #strf63tb · term · runs on the server

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

used by