checkMirror

tcmts.cli.mirror.checkMirror · #xlinfolk · term · runs on the server

uses node:path

What doesn't match between the mirror's files and the codebase, re-analyzing the files as import does. With keepText, when they match, the codebase keeps each definition's code as the files spell it, so the next write spells references the way the files do (not as the scratch file it was edited in did).

import type { CodebaseCore } from "#codebase/tcmts/cli/codebase";
import { fileDefinition, filesIn } from "#codebase/tcmts/cli/files";
import {
  assetFiles, assetFormat, assetName, isAsset, joinName, namesIn, namespaceForFile, sourceFiles,
} from "#codebase/tcmts/cli/mirror";
import type { Mirror } from "#codebase/tcmts/cli/mirror";
import fs from "node:fs";
import path from "node:path";

export function checkMirror(cb: CodebaseCore, m: Mirror, keepText = false): string[] {
  const files = sourceFiles(
    m.dir,
    new Set(m.exclude.map((e) => path.resolve(m.dir, e))),
    m.tests,
  );
  const a = cb.analyzeProject(
    files,
    (file, name) => cb.lineageSalt(joinName(m.into, namespaceForFile(m.dir, file), name)),
    [],
    cb.compilerOptions(m.dir, "import"),
  );
  const problems = [...a.errors, ...a.diagnostics.map((d) => d.message)];
  if (problems.length) return problems;
  const out: string[] = [];
  const seen = new Set<string>();
  for (const d of a.defs) {
    const p = joinName(m.into, namespaceForFile(m.dir, d.file), d.nameHint);
    seen.add(p);
    const hs = cb.store.hashesAt(p);
    if (!hs.includes(d.hash))
      out.push(
        `${p}: the file's code hashes to #${d.hash.slice(0, 8)}, the codebase has ${hs.length ? hs.map((h) => `#${h.slice(0, 8)}`).join(", ") : "nothing there"}`,
      );
  }
  for (const [rel, f] of Object.entries(m.files)) {
    if (isAsset(rel, m.dir)) continue;
    for (const leaf of Object.keys(namesIn(cb, f.ns)))
      if (!seen.has(joinName(f.ns, leaf)))
        out.push(`${joinName(f.ns, leaf)} is in the codebase but not in the files`);
  }
  // files of the codebase that aren't code: the same content
  for (const file of assetFiles(m.dir, new Set(m.exclude.map((e) => path.resolve(m.dir, e))))) {
    const name = assetName(m.dir, m.into, file);
    seen.add(name);
    const { def } = fileDefinition(cb, name, assetFormat(file), fs.readFileSync(file));
    const hs = cb.store.hashesAt(name);
    if (!hs.includes(def.hash))
      out.push(
        `${name}: the file hashes to #${def.hash.slice(0, 8)}, the codebase has ${hs.length ? hs.map((h) => `#${h.slice(0, 8)}`).join(", ") : "nothing there"}`,
      );
  }
  for (const name of filesIn(cb, m.into).keys())
    if (!seen.has(name)) out.push(`${name} is in the codebase but not in the files`);
  // (on the mirror's branch, with the command that wrote the files: the text a branch shows is in its history)
  if (keepText && !out.length)
    cb.store.transaction(() => {
      for (const d of a.defs) {
        const was = cb.store.getDef(d.hash);
        if (was && was.sourceText !== d.sourceText)
          cb.store.setText({ ...d, doc: was.doc, docRefs: was.docRefs }, "mirror");
      }
    });
  return out;
}

uses

used by