mirror of
https://github.com/mksglu/context-mode.git
synced 2026-09-19 03:27:16 +08:00
e9a7e69629
CI run 25655545561 windows-latest failed on
tests/scripts/assert-bundle.test.ts > exits 0 on a clean fixture bundle
tests/scripts/assert-bundle.test.ts > exits 1 when given polluted fixture
with "expected '' to match /OK/" and "expected +0 to be 1" — assertions
that only fire if the script produces no output and exits 0 by default.
Root cause: the direct-invocation check at the bottom of the script used
string equality between two values that diverge on Windows:
import.meta.url = "file:///C:/path/to/assert-bundle.mjs"
`file://${process.argv[1]}` = "file://C:\\path\\to\\assert-bundle.mjs"
The first has triple-slash + forward separators (URL form). The second
has double-slash + backslashes (template-literal of the OS path). They
never compare equal on Windows, the fallback `endsWith` likewise never
matches (URL has `/`, argv has `\`), so `isDirectInvocation` was always
false → main() never ran → script exited 0 silently.
The G3 invariant check (`npm run assert-bundle`) was therefore a no-op
on the windows-latest runner — bundles could ship polluted with the
`Dynamic require of` shim and the guardrail wouldn't catch it. This
also explained why the test "current production bundles pass the
assert-bundle clean check" passed-by-accident on Windows: exit 0 from
a silent no-op satisfies `expect(r.status).toBe(0)`.
Fix: use `pathToFileURL(process.argv[1]).href` so the entry-point
comparison is OS-agnostic. Both sides are now normalized to the
canonical `file:///C:/...` form on Windows and `file:///...` on POSIX.
Verified locally on macOS:
$ npm run bundle && npx vitest run tests/scripts/assert-bundle.test.ts
Test Files 1 passed (1)
Tests 4 passed (4)
Bundles are intentionally not rebuilt here — CI step `npm run bundle`
regenerates them from source on every run.
105 lines
3.8 KiB
JavaScript
105 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// G3 — post-build bundle invariant assertion.
|
|
//
|
|
// Issue #511 class: esbuild rewrites bare `require("node:...")` calls into a
|
|
// `__require` shim that throws `Dynamic require of "..." is not supported`
|
|
// under Node ESM / Bun. Detecting the shim text in the produced bundle is the
|
|
// single invariant signal — the variable name is renamed by the minifier, but
|
|
// the embedded error literal is stable.
|
|
//
|
|
// This script is invoked by `npm run assert-bundle` (chained from `build`)
|
|
// and by the CI workflows. It scans every passed file for forbidden patterns
|
|
// and exits 1 with a violations report if any hit, exits 0 otherwise.
|
|
//
|
|
// Usage:
|
|
// node scripts/assert-bundle.mjs <file> [<file>...]
|
|
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
/** @type {Array<{ name: string; pattern: RegExp; reason: string }>} */
|
|
const FORBIDDEN_PATTERNS = [
|
|
{
|
|
name: "esbuild-throwing-require-shim",
|
|
pattern: /Dynamic require of/,
|
|
reason:
|
|
"esbuild emitted the throwing __require shim. This means a bare require() of a node: module reached the bundle and will throw under Node ESM / Bun. Use createRequire(import.meta.url) at module top instead. (Issue #511 class.)",
|
|
},
|
|
{
|
|
name: "shimmed-node-builtin-call",
|
|
pattern: /__require\s*\(\s*["'`]\s*node:/,
|
|
reason:
|
|
"Bundle contains a __require('node:...') call site, which routes through the throwing shim. Replace with createRequire(import.meta.url) at module top.",
|
|
},
|
|
{
|
|
name: "raw-bare-require-node-builtin",
|
|
pattern: /\brequire\s*\(\s*["'`]\s*node:/,
|
|
reason:
|
|
"Bundle contains a bare require('node:...') call. esbuild ESM output cannot resolve this at runtime. Use createRequire(import.meta.url). (Pattern catches single, double, and template-literal quote forms with optional whitespace.)",
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Scan a single bundle file for forbidden patterns.
|
|
* @param {string} filePath
|
|
* @returns {{ clean: boolean; violations: string[] }}
|
|
*/
|
|
export function assertBundleClean(filePath) {
|
|
if (!existsSync(filePath)) {
|
|
return {
|
|
clean: false,
|
|
violations: [`File not found: ${filePath}`],
|
|
};
|
|
}
|
|
const content = readFileSync(filePath, "utf-8");
|
|
const violations = [];
|
|
for (const { name, pattern, reason } of FORBIDDEN_PATTERNS) {
|
|
const match = content.match(pattern);
|
|
if (match) {
|
|
violations.push(
|
|
`[${name}] matched ${JSON.stringify(match[0])} — ${reason}`,
|
|
);
|
|
}
|
|
}
|
|
return { clean: violations.length === 0, violations };
|
|
}
|
|
|
|
function main() {
|
|
const files = process.argv.slice(2);
|
|
if (files.length === 0) {
|
|
console.error(
|
|
"assert-bundle: no bundle paths provided.\nUsage: node scripts/assert-bundle.mjs <file> [<file>...]",
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
let failed = false;
|
|
for (const f of files) {
|
|
const abs = resolve(f);
|
|
const { clean, violations } = assertBundleClean(abs);
|
|
if (clean) {
|
|
console.log(`assert-bundle: OK ${f}`);
|
|
} else {
|
|
failed = true;
|
|
console.error(`assert-bundle: FAIL ${f}`);
|
|
for (const v of violations) console.error(` - ${v}`);
|
|
}
|
|
}
|
|
process.exit(failed ? 1 : 0);
|
|
}
|
|
|
|
// Run only when invoked directly, not when imported.
|
|
// On Windows, `import.meta.url` is `file:///C:/...` (forward slashes),
|
|
// while `process.argv[1]` is `C:\...` (backslashes). A literal-string
|
|
// `file://${argv[1]}` template never matches, so the prior comparison
|
|
// silently skipped main() on Windows and the script exited 0 with empty
|
|
// stdout — making the assert-bundle CI guardrail vacuous. Use
|
|
// `pathToFileURL` so the entry-point comparison is OS-agnostic.
|
|
const isDirectInvocation =
|
|
process.argv[1] != null &&
|
|
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
if (isDirectInvocation) {
|
|
main();
|
|
}
|