Files
zip-js/scripts/verify-examples.mjs
zeekay a86165b198 feat: hello + validate-email + policy-eval examples
Three end-to-end examples mirroring zip-rs:
 - hello       — minimal name → greeting handler (1.5 KB built)
 - validate-email — port of the canonical validate fixture, byte-equivalent
                    JSON output to base/plugins/extbench/fixtures/goja-js (1.9 KB)
 - policy-eval — realistic IAM/gateway authz decision handler (2.0 KB)

Built JS is committed so test harnesses + downstream consumers don't
depend on the toolchain. scripts/build-example.mjs is the monorepo
build driver (aliases @hanzo/zip to the local dist); end users use
the published npx zip-js build instead.

scripts/verify-examples.mjs loads each built bundle into a fresh V8
context and asserts byte-for-byte JSON output matches the canonical
reference strings — the same shape extbench compares.
2026-05-18 22:04:15 -07:00

116 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Verify every built example by loading it into a fresh V8 context
* (Node's `vm` module) and calling the exported globals exactly the
* way the production goja host does:
*
* 1. Run the script once. Trailer sets `globalThis.<fn>`.
* 2. Call `globalThis.<fn>(payload)` where payload is a parsed JS
* value.
* 3. JSON.stringify the return. Compare bytes against the canonical
* reference output.
*
* V8's JSON.stringify and goja's stringify use the same key-insertion
* order for plain objects, so byte-equivalence holds across both hosts.
*/
import { promises as fs } from "node:fs";
import path from "node:path";
import url from "node:url";
import vm from "node:vm";
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const examplesDir = path.resolve(__dirname, "..", "examples");
/** Each row: [example dir, exported fn, payload object, expected JSON string]. */
const cases = [
["hello", "hello", { name: "world" }, '{"greeting":"hello, world!"}'],
["hello", "hello", { name: "" }, '{"ok":false,"error":"name required"}'],
["hello", "hello", {}, '{"ok":false,"error":"name required"}'],
["validate-email", "validate", { email: "[email protected] ", age: 25 }, '{"ok":true,"email":"[email protected]","age":25}'],
["validate-email", "validate", { email: "", age: 25 }, '{"ok":false,"error":"email required"}'],
["validate-email", "validate", { email: "[email protected]", age: -1 }, '{"ok":false,"error":"age out of range"}'],
["validate-email", "validate", { email: "[email protected]", age: 200 }, '{"ok":false,"error":"age out of range"}'],
["validate-email", "validate", { email: "bad", age: 30 }, '{"ok":false,"error":"email shape"}'],
["validate-email", "validate", { email: "a@nodot", age: 30 }, '{"ok":false,"error":"email domain"}'],
["validate-email", "validate", null, '{"ok":false,"error":"input must be an object"}'],
["policy-eval", "evaluate", { subject: "alice", action: "read", resource: "public:docs" }, '{"allow":true,"reason":"public resource"}'],
["policy-eval", "evaluate", { subject: "alice", action: "write", resource: "alice:doc1" }, '{"allow":true,"reason":"owner"}'],
["policy-eval", "evaluate", { subject: "bob", action: "delete", resource: "alice:doc1" }, '{"allow":false,"reason":"not owner"}'],
["policy-eval", "evaluate", { subject: "root", action: "x", resource: "y" }, '{"allow":true,"reason":"admin override"}'],
["policy-eval", "evaluate", { subject: "alice", action: "list", resource: "anything" }, '{"allow":false,"reason":"no rule matched"}'],
];
async function loadModule(dir) {
const manifestText = await fs.readFile(path.join(dir, "extension.json"), "utf8");
const manifest = JSON.parse(manifestText);
const modulePath = path.join(dir, manifest.module ?? `${manifest.name}.js`);
const source = await fs.readFile(modulePath, "utf8");
return { manifest, source, modulePath };
}
let failures = 0;
let passes = 0;
// Group cases by example so each module is compiled exactly once per
// scenario set — matches the goja host's "one program, many invokes"
// pattern.
const grouped = new Map();
for (const c of cases) {
const arr = grouped.get(c[0]) ?? [];
arr.push(c);
grouped.set(c[0], arr);
}
for (const [example, runs] of grouped) {
const dir = path.join(examplesDir, example);
let mod;
try {
mod = await loadModule(dir);
} catch (err) {
process.stderr.write(`FAIL ${example}: cannot load (${err.message})\n`);
failures += runs.length;
continue;
}
const context = vm.createContext({});
try {
vm.runInContext(mod.source, context, { filename: mod.modulePath });
} catch (err) {
process.stderr.write(`FAIL ${example}: bundle threw on load (${err.message})\n`);
failures += runs.length;
continue;
}
for (const [, fnName, payload, expected] of runs) {
let result;
try {
// Equivalent to `globalThis.<fn>(payload)` inside goja: marshal
// the payload across the V8/host boundary, call the function,
// stringify the result.
const fn = context[fnName];
if (typeof fn !== "function") {
throw new Error(`globalThis.${fnName} not set`);
}
const out = fn(payload);
result = JSON.stringify(out);
} catch (err) {
process.stderr.write(`FAIL ${example}/${fnName}(${JSON.stringify(payload)}): ${err.message}\n`);
failures++;
continue;
}
if (result !== expected) {
process.stderr.write(`FAIL ${example}/${fnName}(${JSON.stringify(payload)})\n want: ${expected}\n got: ${result}\n`);
failures++;
} else {
process.stdout.write(`ok ${example}/${fnName}(${JSON.stringify(payload)}) = ${result}\n`);
passes++;
}
}
}
process.stdout.write(`\n${passes} passed, ${failures} failed\n`);
process.exit(failures > 0 ? 1 : 0);