fileDefinition

tcmts.cli.files.fileDefinition · #cexey7sf · term · runs on the server

uses node:crypto

A file kept in the codebase as written, as a definition named name. Markdown's {@link}s to code are references by hash, so they read with current names and follow updates; its links to other files are kept as written, and checked: one to a file of the codebase that isn't there (and isn't among alsoNew) is a warning. A binary format's bytes are kept as base64. format is a hidden file's after a dot (.gitignore is ".", .mcp.json ".json"), kept so in its IR, as it's written back.

import type { CodebaseCore } from "#codebase/tcmts/cli/codebase";
import { BINARY_FORMATS, isText, linkedCode, linkedName, markdownLinks } from "#codebase/tcmts/cli/files";
import { hashIR, serializeIR } from "#codebase/tcmts/core/hash";
import type { IR } from "#codebase/tcmts/core/hash";
import type { DefRecord, Span } from "#codebase/tcmts/core/types";
import { createHash } from "node:crypto";

export function fileDefinition(
  cb: CodebaseCore,
  name: string,
  format: string,
  content: Buffer,
  alsoNew = new Set<string>(),
): { def: DefRecord; problems: string[]; warnings: string[] } {
  const leaf = name.slice(name.lastIndexOf(".") + 1);
  const ext = format.replace(/^\./, "");
  const ns = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : "";
  const problems: string[] = [];
  const warnings: string[] = [];
  const record = (ir: IR, sourceText: string, spans: Span[]): DefRecord => {
    const hash = hashIR(ir);
    return {
      hash,
      kind: "file",
      componentHash: hash,
      componentIndex: 0,
      componentSize: 1,
      ir: serializeIR(ir),
      sourceText,
      spans,
      reservedNames: [],
      initKind: "hoisted",
      nameHint: leaf,
      nominal: false,
      deps: [...new Set(spans.map((s) => s.hash!))].map((h) => ({
        ref: `#${h}`,
        pos: "value" as const,
        strict: false,
      })),
      // (a file with no extension: text, or bytes)
      signature: ext || ((ir as unknown[][])[2]?.[0] === "Bytes" ? "bytes" : "text"),
    };
  };
  // kept as bytes by its format, or because it isn't text (UTF-8)
  if (BINARY_FORMATS.has(ext) || !isText(content))
    return {
      def: record(
        ["File", format, ["Bytes", createHash("sha256").update(content).digest("hex")]],
        content.toString("base64"),
        [],
      ),
      problems,
      warnings,
    };
  const text = content.toString("utf8");
  const ir: IR[] = ["File", format];
  const spans: Span[] = [];
  let at = 0;
  if (ext === "md") {
    for (const l of markdownLinks(text)) {
      if (l.code) {
        const h = linkedCode(cb, ns, l.target);
        if (!h) {
          problems.push(`{@link ${l.target}}: there's no ${l.target} in the codebase`);
          continue;
        }
        ir.push(text.slice(at, l.start), ["Ref", h]);
        spans.push({ start: l.start, end: l.end, pos: "value", hash: h });
        at = l.end;
        continue;
      }
      const to = linkedName(name, l.target);
      if (
        to &&
        !alsoNew.has(to.name) &&
        !cb.store.hashesAt(to.name).some((h) => cb.store.getDef(h)?.kind === "file")
      )
        warnings.push(`${name} links to ${l.target}, and there's no ${to.name} in the codebase`);
    }
  }
  ir.push(text.slice(at));
  return { def: record(ir, text, spans), problems, warnings };
}

uses

used by