writeShim

tcmts.loader.writeShim · #p3dqdsda · term · runs on the server

uses node:path

Regenerate .tcm/codebase/** so scratch files can import { x } from "#codebase/ns": a file per namespace. With only, just those namespaces' files (the ones whose names changed), and only the modules they import that aren't there yet: the rest of the shim, and every module already written, are as they were.

import { exportId } from "#codebase/tcmts/core/hash";
import { isConflicted } from "#codebase/tcmts/core/types";
import {
  augmentationRoot, isAugmentation, lastSegment, materialize, moduleFile, writeIfChanged, writePairModule,
} from "#codebase/tcmts/loader";
import type { DefSource, Layout, ShimName } from "#codebase/tcmts/loader";
import fs from "node:fs";
import path_1 from "node:path";

export function writeShim(
  names: ShimName[],
  src: DefSource,
  lay: Layout,
  only?: Set<string>,
): void {
  const nsOf = (p: string) => (p.includes(".") ? p.slice(0, p.lastIndexOf(".")) : "");
  const fileOf = (ns: string) =>
    path_1.join(lay.shimDir, ns ? `${ns.replaceAll(".", "/")}.ts` : "index.ts");
  const byPath = new Map<string, string[]>();
  for (const n of names)
    if (!only || only.has(nsOf(n.path)))
      byPath.set(n.path, [...(byPath.get(n.path) ?? []), n.hash]);
  const files = new Map<string, string[]>();
  const hashes: string[] = [];
  // an augmentation has no name to export: a type package each instead, which every check lists (`augmentationRoot`)
  const augmentations = names.filter(
    (n) => ["global", "augments"].includes(lastSegment(n.path)) && isAugmentation(src.get(n.hash)!),
  );
  const augmented = new Set(augmentations.map((n) => n.path));
  const augRoot = augmentationRoot(lay);
  for (const n of augmentations) {
    const file = path_1.join(augRoot, n.path, "index.d.ts");
    const module = path_1.join(lay.cacheDir, moduleFile(src.get(n.hash)!.componentHash, src));
    files.set(file, [
      `import "${path_1.relative(path_1.dirname(file), module).split(path_1.sep).join("/")}";`,
    ]);
    hashes.push(n.hash);
  }
  for (const [name, hs] of byPath) {
    if (augmented.has(name)) continue;
    // a file whose own name isn't an identifier (`getting-started`) is exported by its name in quotes
    const leaf = lastSegment(name);
    const exported = /^[A-Za-z_$][\w$]*$/.test(leaf) ? leaf : JSON.stringify(leaf);
    const file = fileOf(nsOf(name));
    const lines = files.get(file) ?? [];
    files.set(file, lines);
    const relTo = (f: string) => {
      const rel = path_1
        .relative(path_1.dirname(file), path_1.join(lay.cacheDir, f))
        .split(path_1.sep)
        .join("/");
      return rel.startsWith(".") ? rel : "./" + rel;
    };
    const recs = hs.map((h) => src.get(h)!);
    if (isConflicted(recs.map((r) => r.kind))) {
      lines.push(
        `// ${lastSegment(name)}: conflicted (${hs.length} definitions) — see \`tcm todo\``,
      );
      continue;
    }
    hashes.push(...hs);
    if (recs.length === 2 && exported === leaf) {
      lines.push(
        `export { ${lastSegment(name)} } from "${relTo(writePairModule(lastSegment(name), recs, src, lay))}";`,
      );
      continue;
    }
    const rec = recs[0]!;
    lines.push(
      `export ${rec.kind === "type" ? "type " : ""}{ ${exportId(rec.hash)} as ${exported} } from "${relTo(moduleFile(rec.componentHash, src))}";`,
    );
  }
  materialize(hashes, src, lay, only ? { trustExisting: true } : {});
  // (the root's file is always there: it's what `#codebase` itself imports)
  const index = fileOf("");
  if (!only || only.has("")) files.set(index, files.get(index) ?? []);
  for (const [file, lines] of files)
    writeIfChanged(
      file,
      `// generated by tcm — do not edit\n${lines.sort().join("\n")}\nexport {};\n`,
    );
  for (const d of fs.existsSync(augRoot) ? fs.readdirSync(augRoot) : [])
    if (!augmented.has(d)) fs.rmSync(path_1.join(augRoot, d), { recursive: true, force: true });
  // a namespace with no names left has no file
  if (only) {
    for (const ns of only) if (ns && !files.has(fileOf(ns))) fs.rmSync(fileOf(ns), { force: true });
    return;
  }
  const walk = (dir: string): string[] =>
    fs.existsSync(dir)
      ? fs
          .readdirSync(dir, { withFileTypes: true })
          .flatMap((e) =>
            e.isDirectory() ? walk(path_1.join(dir, e.name)) : [path_1.join(dir, e.name)],
          )
      : [];
  for (const f of walk(lay.shimDir)) if (!files.has(f)) fs.rmSync(f);
}

uses

used by