keepValues

tcmts.cli.kept.keepValues · #jena6vm7 · term · runs on the server

uses node:path

Evaluate pure constants once (DefSource.keep), and keep their values with them: constants whose initializer computes something, from code that reaches nothing outside the codebase (reaches) and uses no npm packages, with a value that can be written as code the same (valueSource), and nothing it uses holding state (stateless). All at once, in one process, from their modules (written already). Each is decided once: the rest are kept as not kept.

import type { CodebaseCore } from "#codebase/tcmts/cli/codebase";
import { reaches } from "#codebase/tcmts/cli/effects";
import { computedInitializer, isFixed, keptKey, keptValue, stateless, usesPackages } from "#codebase/tcmts/cli/kept";
import { isPureExport, parseExtRef } from "#codebase/tcmts/cli/npm";
import { exportId } from "#codebase/tcmts/core/hash";
import { moduleFile, nodeArgs, valueSource } from "#codebase/tcmts/loader";
import type { DefSource, Kept } from "#codebase/tcmts/loader";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path_1 from "node:path";

export function keepValues(cb: CodebaseCore, hashes: string[], src: DefSource): void {
  const key = keptKey();
  const decided = new Map<string, Kept | null>();
  const evaluate: { hash: string; at: { start: number; end: number } }[] = [];
  const outside = reaches(cb, hashes, src);
  // (an npm package's export whose code is pure, worked out from it, is as good as the codebase's own)
  const pureRef = (ref: string) => {
    const e = parseExtRef(ref);
    return !!e && isPureExport(cb.lay.cacheDir, e);
  };
  for (const h of new Set(hashes)) {
    const rec = src.get(h);
    const at = rec && computedInitializer(rec);
    if (!rec || !at || outside.get(h)?.length || usesPackages(src, rec.componentHash, pureRef))
      decided.set(h, null);
    else evaluate.push({ hash: h, at });
  }
  if (evaluate.length) {
    const module = (h: string) => `./${moduleFile(src.get(h)!.componentHash, src)}`;
    // what else each one's code holds: its value can't share an object with them (a copy would be another object)
    const around = (h: string) => {
      const comps = new Set<string>();
      const own = src.get(h)!.componentHash;
      for (const queue = [h]; queue.length;) {
        const r = src.get(queue.pop()!);
        if (!r || comps.has(r.componentHash)) continue;
        comps.add(r.componentHash);
        for (const m of src.members(r.componentHash))
          for (const d of m.deps)
            if (d.pos === "value" && d.ref.startsWith("#")) queue.push(d.ref.slice(1));
      }
      comps.delete(own);
      return [...comps].map((c) => `./${moduleFile(c, src)}`);
    };
    const list = evaluate.map((e) => [e.hash, module(e.hash), exportId(e.hash), around(e.hash)]);
    const runner = path_1.join(cb.lay.cacheDir, `keep_${process.pid}.ts`);
    fs.writeFileSync(
      runner,
      // (the serializer, as code: it uses nothing, and isn't in the codebase whose values these are)
      `const valueSource = ${valueSource.toString()};\n` +
        `const list = ${JSON.stringify(list)} as [string, string, string, string[]][];\n` +
        `const out: Record<string, string | null> = {};\n` +
        `const hold = (x: unknown, into: Set<unknown>) => {\n` +
        `  if (into.size > 100000 || (typeof x !== "object" && typeof x !== "function") || x === null || into.has(x)) return;\n` +
        `  into.add(x);\n` +
        `  if (x instanceof Map) for (const [k, v] of x) (hold(k, into), hold(v, into));\n` +
        `  else if (x instanceof Set) for (const v of x) hold(v, into);\n` +
        `  else if (typeof x === "object" && (Array.isArray(x) || Object.getPrototypeOf(x) === Object.prototype)) for (const v of Object.values(x)) hold(v, into);\n` +
        `};\n` +
        `for (const [h, file, name, others] of list) {\n` +
        `  try {\n` +
        `    const held = new Set<unknown>();\n` +
        `    for (const f of others) for (const x of Object.values(await import(f) as object)) hold(x, held);\n` +
        `    out[h] = valueSource((await import(file) as Record<string, unknown>)[name], held) ?? null;\n` +
        `  } catch {\n` +
        `    out[h] = null;\n` +
        `  }\n` +
        `}\n` +
        `process.stdout.write(JSON.stringify(out));\n`,
    );
    const r = spawnSync(process.execPath, [...nodeArgs(cb.lay), runner], {
      encoding: "utf8",
      cwd: cb.lay.root,
    });
    if (process.env.TCM_DEBUG)
      console.error(`keeping ${evaluate.length} value(s): ${r.status}\n${r.stderr}`);
    else fs.rmSync(runner, { force: true });
    let values: Record<string, string | null> = {};
    try {
      if (r.status === 0) values = JSON.parse(r.stdout) as Record<string, string | null>;
    } catch {}
    // (one the evaluation didn't get to, it failing, isn't kept either: it isn't tried every time)
    const fixed = (h: string) => {
      const v = values[h] ?? keptValue(cb, src.get(h))?.value;
      return typeof v === "string" ? isFixed(v) : undefined;
    };
    for (const e of evaluate) {
      const value = values[e.hash];
      // and one whose code reads state something else could change first isn't: its value could differ in a run
      decided.set(
        e.hash,
        typeof value === "string" && stateless(src, e.hash, fixed) ? { ...e.at, value } : null,
      );
    }
  }
  if (decided.size)
    cb.store.transaction(() => {
      for (const [h, k] of decided) cb.store.setMetadata(h, key, JSON.stringify(k));
    });
}

uses

used by