importMissing

tcmts.cli.main.importMissing · #47g6ql7o · term · runs on the server

uses node:fs

Names a scratch file uses but doesn't have (TypeScript can't find them), imported: from the codebase, the one in near's namespace; else from a package or module the file imports other names from already, if it exports it (cli from @bomb.sh/router); else the only definition by that name; from outside it (spawnSync, createRequire, import os from "node:os"), where the codebase's own code imports it from most; else a page or other file, if it's the only one by that name. The imports added (write: else only worked out, the file left as it is); a name with several candidates is left to say so.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { addToScratch, builtinImport, firstSyntaxError, importedFrom } from "#codebase/tcmts/cli/main";
import { joinName } from "#codebase/tcmts/cli/mirror";
import { yellow } from "#codebase/tcmts/cli/style";
import fs from "node:fs";
import { isImportDeclaration as ts_isImportDeclaration, isStringLiteral as ts_isStringLiteral } from "typescript/ast";
import { SymbolFlags as ts_SymbolFlags } from "typescript/sync";

export function importMissing(cb: Codebase, file: string, near: string, write = true): string[] {
  cb.refreshShim();
  const a = cb.analyze(file, cb.scratchSalts());
  // (code that doesn't parse is read wrong: what it seems to use isn't)
  if (firstSyntaxError(a, file)) return [];
  const missing = new Set(
    a.diagnostics
      // (2662 and 2663: not found, with a class member by that name, as a method calling the function it's named for)
      .filter((d) => [2304, 2552, 2662, 2663].includes(d.code))
      .map((d) => /'([^']+)'/.exec(d.message)?.[1])
      .filter((n): n is string => !!n),
  );
  if (!missing.size) return [];
  const ns = near.slice(0, Math.max(0, near.lastIndexOf(".")));
  // what the modules it imports from outside the codebase export (a type only as a type)
  const exported = new Map<string, string>();
  for (const st of a.program.getSourceFile(file)?.statements ?? []) {
    if (!ts_isImportDeclaration(st) || !ts_isStringLiteral(st.moduleSpecifier)) continue;
    const spec = st.moduleSpecifier.text;
    if (spec === "#codebase" || spec.startsWith("#codebase/")) continue;
    const mod = a.checker.getSymbolAtLocation(st.moduleSpecifier);
    for (const e of mod ? a.checker.getExportsOfModule(mod) : []) {
      if (exported.has(e.name)) continue;
      const s = e.flags & ts_SymbolFlags.Alias ? a.checker.getAliasedSymbol(e) : e;
      const type = !(s.flags & ts_SymbolFlags.Value);
      exported.set(
        e.name,
        `import ${type ? "type " : ""}{ ${e.name} } from ${JSON.stringify(spec)};`,
      );
    }
  }
  const lines: string[] = [];
  for (const n of missing) {
    // (files too: a page or stylesheet is imported like a definition)
    const found = cb.store
      .names(undefined, true)
      .filter((x) => x.path.slice(x.path.lastIndexOf(".") + 1) === n);
    const nearest = found.find((x) => x.path === joinName(ns, n));
    const fromImported = exported.get(n);
    if (!nearest && fromImported) {
      lines.push(fromImported);
      continue;
    }
    const code = found.filter((x) => x.kind !== "file");
    const outside = () => importedFrom(cb, n) ?? builtinImport(n);
    // (a page by that name only if nothing outside the codebase has one)
    if (!nearest && !code.length && found.length === 1) {
      const line = outside();
      if (line) {
        lines.push(line);
        continue;
      }
    }
    const pick =
      nearest ?? (found.length === 1 ? found[0] : code.length === 1 ? code[0] : undefined);
    if (!pick) {
      // several by that name and none nearer: which is the agent's (or person's) to say
      if (found.length) {
        console.log(
          yellow(
            `  ${n} isn't imported, and the codebase has ${found.length} by that name: ${found.map((x) => x.path).join(", ")}; import the one you mean`,
          ),
        );
        continue;
      }
      // (else one of tcm's own modules', or Node's, if one has it)
      const line = outside();
      if (line) lines.push(line);
      continue;
    }
    const from = pick.path.slice(0, Math.max(0, pick.path.lastIndexOf(".")));
    lines.push(
      `import ${pick.kind === "type" ? "type " : ""}{ ${n} } from "#codebase${from ? `/${from.replaceAll(".", "/")}` : ""}";`,
    );
  }
  if (write && lines.length)
    fs.writeFileSync(
      file,
      addToScratch(file, fs.readFileSync(file, "utf8"), `${lines.join("\n")}\n`).trimEnd() + "\n",
    );
  return lines;
}

uses

used by