commit f560271492ed555cc6a83b10423d83972e06babe
parent f991e35b2a78ad01740bfb7b4cc8e5225ef9d070
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 19:59:31 -0400
scripts: a guard — no umtool app module joins a cwd-derived path without turbopackIgnore
scripts/umtool-build-trace.test.mjs (run by test:scripts, 3 tests)
scans umtool's app, components, lib, report-to-video and song/paths.mjs
per module: a path or fs call whose arguments carry a value derived in
that file from process.cwd(), import.meta.url/dirname/filename or
__dirname must open with the opt-out (a nested opted-out call covers
its caller). With main's lib/paths.mjs it fails and names the defect:
`paths.mjs:118 : path.join(REPO_ROOT, "transcripts", "channels")`.
A worktree build cannot catch this -- it has no corpus to walk.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 195 insertions(+), 0 deletions(-)
diff --git a/scripts/umtool-build-trace.test.mjs b/scripts/umtool-build-trace.test.mjs
@@ -0,0 +1,195 @@
+// umtool's modules must not hand Turbopack a directory to bundle.
+//
+// Turbopack evaluates `process.cwd()` (and a module's own `import.meta.url` /
+// `__dirname`) statically, as a path in the project, and a `path.join` /
+// `path.resolve` / fs call on such a value becomes an ASSET REFERENCE: to a
+// file, or, when the joined path is a directory, to EVERY file under it. Release
+// 12 slice Q wrote `path.join(REPO_ROOT, "transcripts", "channels")` with
+// `REPO_ROOT = findRepoRoot(process.cwd())`, and `next build` in the primary
+// checkout walked the whole corpus (hundreds of GB, `data/` symlinked to another
+// drive) until the kernel killed it -- while a worktree, which has no
+// `transcripts/`, built in 30 s. So no build-in-a-worktree gate can see this.
+//
+// The rule, checked statically and per module (Turbopack's value analysis is
+// per module; an imported binding is opaque to it): every path or fs call whose
+// arguments carry a value derived IN THAT FILE from `process.cwd()`,
+// `import.meta.url`, `import.meta.dirname|filename` or `__dirname` must open its
+// argument list with the documented opt-out, `/* turbopackIgnore: true */`.
+// The comment changes nothing at run time. `os.homedir()` is NOT a source: the
+// tracer does not follow it (a build with HOME pointed at a synthetic home full
+// of out-of-root symlinks inside the project succeeds), and neither is
+// `process.env.*`.
+//
+// Run with: pnpm test:scripts
+import assert from "node:assert/strict";
+import { readdirSync, readFileSync } from "node:fs";
+import path from "node:path";
+import test from "node:test";
+import { fileURLToPath } from "node:url";
+
+const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const UMTOOL = path.join(REPO, "umtool");
+
+const SOURCE = /process\.cwd\(\)|import\.meta\.(?:url|dirname|filename)|\b__dirname\b/;
+const MARK = "__TURBOPACK_IGNORE__";
+const IGNORE_COMMENT = /\/\*\s*turbopackIgnore\s*:\s*true\s*\*\//g;
+
+// path ops, and fs calls either bare (`existsSync(`) or on a namespace
+// (`fs.readdir(`, `fsp.stat(`). A method on anything else (`obj.stat(`) is not
+// one.
+const SINK =
+ /(?:\bpath\.(?:join|resolve|dirname|relative)|(?<![\w$.])(?:fs\.|fsp\.|promises\.)?(?:existsSync|readFileSync|readdirSync|statSync|lstatSync|realpathSync|opendirSync|readFile|readdir|stat|lstat|opendir|createReadStream))\s*\(/g;
+
+/** Comments out, except the opt-out, which becomes a marker. Strings stay. */
+function prepare(text) {
+ let s = text.replace(IGNORE_COMMENT, ` ${MARK} `);
+ s = s.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
+ // A line comment: `//` not preceded by `:` (URLs, `file://` templates).
+ s = s.replace(/(^|[^:\\])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
+ return s;
+}
+
+/** Index just past the bracket that closes the one at `open`. */
+function closeOf(s, open) {
+ let depth = 0;
+ let quote = null;
+ for (let i = open; i < s.length; i += 1) {
+ const c = s[i];
+ if (quote) {
+ if (c === "\\") i += 1;
+ else if (c === quote) quote = null;
+ continue;
+ }
+ if (c === '"' || c === "'" || c === "`") quote = c;
+ else if (c === "(" || c === "[" || c === "{") depth += 1;
+ else if (c === ")" || c === "]" || c === "}") {
+ depth -= 1;
+ if (depth === 0) return i + 1;
+ }
+ }
+ return s.length;
+}
+
+/** Names assigned, in this file, from a source or from another such name. */
+function taintedNames(s) {
+ const decls = [];
+ const re = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g;
+ for (let m; (m = re.exec(s)); ) {
+ // The right-hand side runs to the first `;` or newline at depth 0 -- good
+ // enough for this repo's formatter, which ends statements with `;`.
+ let depth = 0;
+ let end = s.length;
+ for (let i = re.lastIndex; i < s.length; i += 1) {
+ const c = s[i];
+ if (c === "(" || c === "[" || c === "{") depth += 1;
+ else if (c === ")" || c === "]" || c === "}") depth -= 1;
+ else if (c === ";" && depth <= 0) {
+ end = i;
+ break;
+ }
+ }
+ decls.push({ name: m[1], rhs: s.slice(re.lastIndex, end) });
+ }
+ const names = new Set();
+ for (let grew = true; grew; ) {
+ grew = false;
+ for (const { name, rhs } of decls) {
+ if (names.has(name)) continue;
+ if (SOURCE.test(rhs) || [...names].some((n) => new RegExp(`(?<![\\w$.])${n.replace(/\$/g, "\\$")}\\b`).test(rhs))) {
+ names.add(name);
+ grew = true;
+ }
+ }
+ }
+ return names;
+}
+
+/** Every path/fs call on a cwd-derived value that does not opt out. */
+export function untracedCalls(text) {
+ const s = prepare(text);
+ const names = taintedNames(s);
+ const carries = (args) =>
+ SOURCE.test(args) ||
+ [...names].some((n) => new RegExp(`(?<![\\w$.])${n.replace(/\$/g, "\\$")}\\b(?!\\s*:)`).test(args));
+ // A call nested in these arguments that opts out is opaque to this one too:
+ // `readFileSync(path.join(/* turbopackIgnore: true */ HERE, "a.json"))`. Its
+ // own arguments are cut out before asking whether this call carries a source.
+ const withoutOptedOut = (args) => {
+ let out = args;
+ for (let i = out.search(new RegExp(`\\(\\s*${MARK}`)); i !== -1; i = out.search(new RegExp(`\\(\\s*${MARK}`))) {
+ out = out.slice(0, i) + out.slice(closeOf(out, i));
+ }
+ return out;
+ };
+ const out = [];
+ for (let m; (m = SINK.exec(s)); ) {
+ const open = SINK.lastIndex - 1;
+ const args = s.slice(open + 1, closeOf(s, open) - 1);
+ if (args.trimStart().startsWith(MARK)) continue;
+ if (!carries(withoutOptedOut(args))) continue;
+ const line = s.slice(0, m.index).split("\n").length;
+ out.push({ line, call: text.split("\n")[line - 1].trim() });
+ }
+ return out;
+}
+
+/** umtool's modules that a Next build can reach: the app, its libs, the pipeline. */
+function appModules() {
+ const out = [];
+ const walk = (dir) => {
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p);
+ else if (/\.(?:mjs|js|ts|tsx)$/.test(e.name) && !/\.test\./.test(e.name) && !e.name.endsWith(".d.ts")) out.push(p);
+ }
+ };
+ for (const d of ["app", "components", "lib"]) walk(path.join(UMTOOL, d));
+ for (const e of readdirSync(path.join(UMTOOL, "report-to-video"))) {
+ if (e.endsWith(".mjs") && !e.includes(".test.")) out.push(path.join(UMTOOL, "report-to-video", e));
+ }
+ // song/paths.mjs is imported by lib/paths.mjs; the other song scripts are CLIs.
+ out.push(path.join(UMTOOL, "song", "paths.mjs"));
+ return out;
+}
+
+test("the check flags slice Q's join and passes the opted-out form", () => {
+ const bad = [
+ 'const REPO_ROOT = findRepoRoot(process.cwd());',
+ 'export const CHANNELS_DIR = path.resolve(',
+ ' process.env.CHANNELS_DIR ?? path.join(REPO_ROOT, "transcripts", "channels"),',
+ ');',
+ ].join("\n");
+ const found = untracedCalls(bad);
+ assert.ok(found.some((f) => f.call.includes('path.join(REPO_ROOT, "transcripts"')), JSON.stringify(found));
+
+ const good = bad
+ .replace("path.resolve(", "path.resolve(/* turbopackIgnore: true */")
+ .replace("path.join(REPO_ROOT", "path.join(/* turbopackIgnore: true */ REPO_ROOT");
+ assert.deepEqual(untracedCalls(good), []);
+});
+
+test("sources: cwd, import.meta, __dirname; not homedir, env, or a comment", () => {
+ assert.equal(untracedCalls('const X = path.join(process.cwd(), "song");').length, 1);
+ assert.equal(untracedCalls("const H = path.dirname(fileURLToPath(import.meta.url));").length, 1);
+ assert.equal(untracedCalls('readFileSync(path.join(__dirname, "a.json"));').length, 2);
+ assert.equal(untracedCalls('const R = path.join(os.homedir(), "reports");').length, 0);
+ assert.equal(untracedCalls('const R = path.join(process.env.X, "channels");').length, 0);
+ assert.equal(untracedCalls('// path.join(process.cwd(), "x")\nconst y = 1;').length, 0);
+ // An object key named like a tainted value is not a use of it.
+ assert.equal(untracedCalls('const cwd = path.join(/* turbopackIgnore: true */ process.cwd(), "s");\nf(path.join(a, { cwd: 1 }));').length, 0);
+});
+
+test("no umtool module the app can import joins a cwd-derived path without opting out", () => {
+ const bad = [];
+ for (const file of appModules()) {
+ for (const f of untracedCalls(readFileSync(file, "utf8"))) {
+ bad.push(`${path.relative(REPO, file)}:${f.line} ${f.call}`);
+ }
+ }
+ assert.deepEqual(
+ bad,
+ [],
+ "add /* turbopackIgnore: true */ as the first argument (see this file's header):\n" + bad.join("\n"),
+ );
+});