exportFacts
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
- tcmts.cli.npm.ModuleShape
- tcmts.cli.npm.NpmFacts
- tcmts.cli.npm.SIDE_CONDITIONS
- tcmts.cli.npm.Unit
- tcmts.cli.npm.emptyUnit
- tcmts.cli.npm.moduleShape
- tcmts.cli.npm.resolveSpecifier
- tcmts.cli.npm.shapes
- tcmts.cli.placement.NODE_GLOBALS
- tcmts.cli.placement.isDomGlobal
- tcmts.core.effects.EFFECT_EXPORTS
- tcmts.core.effects.EFFECT_GLOBALS
- tcmts.core.effects.EFFECT_MEMBERS
- tcmts.core.effects.EFFECT_MODULES
- tcmts.core.effects.Effect
- tcmts.core.effects.QUIET_PROCESS