exportFacts

tcmts.cli.npm.exportFacts · #53pqje4z · term · runs on the server

uses node:path

What name (an export; *, all of the module) of file reaches, on a side (its conditions resolve the packages it imports), following everything it uses (NpmFacts). Kept in cacheDir/npm-facts, by the package's installed directory (which is its version's, and never changes).

import { emptyUnit, moduleShape, resolveSpecifier, shapes, SIDE_CONDITIONS } from "#codebase/tcmts/cli/npm";
import type { ModuleShape, NpmFacts, Unit } from "#codebase/tcmts/cli/npm";
import { isDomGlobal, NODE_GLOBALS } from "#codebase/tcmts/cli/placement";
import {
  EFFECT_EXPORTS, EFFECT_GLOBALS, EFFECT_MEMBERS, EFFECT_MODULES, QUIET_PROCESS,
} from "#codebase/tcmts/core/effects";
import type { Effect } from "#codebase/tcmts/core/effects";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";

export function exportFacts(
  file: string,
  name: string,
  side: keyof typeof SIDE_CONDITIONS,
  cacheDir: string,
): NpmFacts {
  const conditions = SIDE_CONDITIONS[side];
  // (by the package's directory, and by this code's hash: another tcm works them out again)
  const by =
    (exportFacts as unknown as Record<symbol, string | undefined>)[Symbol.for("tcm.hash")] ?? "";
  const keepFile = path.join(
    cacheDir,
    "npm-facts",
    `${createHash("sha256")
      .update(`${by}\n${path.dirname(file)}`)
      .digest("hex")
      .slice(0, 24)}.json`,
  );
  let kept: Record<string, NpmFacts> = {};
  try {
    kept = JSON.parse(fs.readFileSync(keepFile, "utf8")) as Record<string, NpmFacts>;
  } catch {}
  const key = `${side}|${path.basename(file)}|${name}`;
  if (kept[key]) return kept[key]!;
  const effects = new Set<Effect>();
  const node = new Set<string>();
  const dom = new Set<string>();
  const client = new Set<string>();
  const opaque = new Set<string>();
  let state = false;
  const seenUnits = new Set<Unit>();
  const seenExports = new Set<string>();
  const ran = new Set<string>();
  const shapeOf = (f: string) => {
    let s = shapes.get(f);
    if (!s) {
      s = f.endsWith(".json")
        ? { units: {}, top: emptyUnit(), exports: {}, stars: [], cjs: false }
        : moduleShape(f, fs.readFileSync(f, "utf8"));
      shapes.set(f, s);
    }
    return s;
  };
  const addUnit = (f: string, s: ModuleShape, u: Unit | undefined) => {
    if (!u || seenUnits.has(u)) return;
    seenUnits.add(u);
    state ||= u.state;
    if (u.clock) effects.add("clock");
    for (const o of u.opaque) opaque.add(o);
    for (const g of u.globals) {
      const e = EFFECT_GLOBALS[g];
      if (e) effects.add(e);
      if (isDomGlobal(g)) dom.add(g);
      // (`process.env.NODE_ENV` is what a bundler writes in: not the environment)
      if (NODE_GLOBALS.has(g) && g !== "require") {
        const quiet =
          g === "process" &&
          u.members
            .filter((m) => m.startsWith("process."))
            .every((m) => m === "process.env" || QUIET_PROCESS.has(m.slice(8)));
        if (!quiet) node.add(g);
      }
    }
    for (const m of u.members) {
      const e = EFFECT_MEMBERS[m];
      if (e) effects.add(e);
    }
    for (const l of u.locals) addUnit(f, s, s.units[l]);
    for (const i of u.imports) visitImport(f, i.spec, i.name);
  };
  const visitImport = (from: string, spec: string, what: string) => {
    // React's, which only a component the browser keeps alive has
    if (
      /^react(-dom)?$/.test(spec) &&
      /^(use[A-Z]\w*|createContext|createPortal|flushSync|Component|PureComponent)$/.test(what)
    )
      client.add(`uses ${what}`);
    const r = resolveSpecifier(from, spec, conditions);
    if (!r) {
      opaque.add(`${spec} isn't found`);
      return;
    }
    if ("builtin" in r) {
      node.add(r.builtin);
      const whole = EFFECT_MODULES[r.builtin];
      if (whole) effects.add(whole);
      const one = EFFECT_EXPORTS[`${r.builtin}#${what}`];
      if (one) effects.add(one);
      return;
    }
    visitExport(r.file, what);
  };
  const visitExport = (f: string, what: string) => {
    const k = `${f}#${what}`;
    if (seenExports.has(k)) return;
    seenExports.add(k);
    const s = shapeOf(f);
    // importing it runs it
    if (!ran.has(f)) {
      ran.add(f);
      addUnit(f, s, s.top);
      // (the directive, after any comments)
      if (
        /^(?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*["']use client["']/.test(
          fs.readFileSync(f, "utf8").slice(0, 2000),
        )
      )
        client.add(`says "use client"`);
    }
    if (s.cjs || what === "") return;
    if (what === "*") {
      for (const u of Object.values(s.units)) addUnit(f, s, u);
      for (const [n, e] of Object.entries(s.exports))
        if (e.from) visitImport(f, e.from.spec, e.from.name);
      for (const star of s.stars) visitImport(f, star, "*");
      return;
    }
    const e = s.exports[what];
    if (e?.local) addUnit(f, s, s.units[e.local]);
    else if (e?.from) visitImport(f, e.from.spec, e.from.name);
    else for (const star of s.stars) visitImport(f, star, what);
  };
  visitExport(file, name);
  const facts: NpmFacts = {
    effects: [...effects].sort(),
    node: [...node].sort(),
    dom: [...dom].sort(),
    client: [...client].sort(),
    state,
    opaque: [...opaque].sort(),
  };
  kept[key] = facts;
  try {
    fs.mkdirSync(path.dirname(keepFile), { recursive: true });
    fs.writeFileSync(keepFile, JSON.stringify(kept));
  } catch {}
  return facts;
}

uses

used by