importMissing
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
- tcmts.cli.codebase.Codebase
- tcmts.cli.main.addToScratch
- tcmts.cli.main.builtinImport
- tcmts.cli.main.firstSyntaxError
- tcmts.cli.main.importedFrom
- tcmts.cli.mirror.joinName
- tcmts.cli.style.yellow