node --check passed the day the page came up empty. This runs the script's synchronous start with every element stubbed, so a handler wired above the helper it calls fails typecheck.
- commit
- 98b870a662afd2b14e8d640e3cd368aa5a99afcf
- parent
- 8771bf30cbdb0e2f7f95956dc2428cd46582702c
- author
- Dami <dami.etoile@gmail.com>
| M | package.json | 2 | +- |
| A | scripts/ | 46 | ++++++++++++++++++++++++++++++ |
@@ -8,7 +8,7 @@
88 "deploy": "wrangler deploy",99 "test": "vitest run",1010 "test:conformance": "vitest run --config vitest.conformance.config.ts",11- "typecheck": "tsc --noEmit && node scripts/build-signer.mjs --check",11+ "typecheck": "tsc --noEmit && node scripts/build-signer.mjs --check && node scripts/check-console.mjs",1212 "build:signer": "node scripts/build-signer.mjs"1313 },1414 "dependencies": {@@ -0,0 +1,46 @@
1+// Runs the console script's synchronous start against a stub document, so a2+// handler wired above the helper it calls, or any other error before the3+// first await, fails typecheck instead of a live page. Syntax alone is not4+// enough: `node --check` passed the day the page came up empty.5+import { readFileSync } from "node:fs";6+7+const src = readFileSync(new URL("../src/dashboard.ts", import.meta.url), "utf8");8+const js = JSON.parse(src.match(/^const JS = (".*");$/m)[1]);9+10+// Every element lookup answers with an object that accepts any property and11+// any method call, so the script gets through its wiring.12+const element = () => new Proxy(function () {}, {13+ get: (t, k) => (k === Symbol.toPrimitive ? () => "" : k === "classList" ? { toggle() {}, add() {}, remove() {}, contains: () => false } : k === "style" ? { setProperty() {} } : element()),14+ set: () => true,15+ apply: () => element(),16+});17+const doc = element();18+const g = globalThis;19+g.document = doc;20+g.window = g;21+g.location = { host: "check.localhost", hostname: "check.localhost", origin: "http://check.localhost", protocol: "http:", hash: "", pathname: "/", search: "" };22+g.localStorage = { getItem: () => null, setItem() {}, removeItem() {} };23+g.history = { replaceState() {} };24+g.navigator = { clipboard: { writeText: async () => {} } };25+g.confirm = () => false;26+g.prompt = () => null;27+g.alert = () => {};28+g.fetch = async () => ({ ok: true, json: async () => ({}), text: async () => "" });29+g.WebSocket = class { close() {} send() {} };30+g.SIGNER_URL = "/signer.js";31+g.addEventListener = () => {};32+33+let failed = null;34+process.on("unhandledRejection", (e) => { failed = e; });35+try {36+ new Function(js)();37+ // The script is an async IIFE; its synchronous part runs before this resolves.38+ await new Promise((r) => setTimeout(r, 50));39+} catch (e) {40+ failed = e;41+}42+if (failed && !/fetch|json|network/i.test(String(failed))) {43+ console.error("console script fails before it boots:", failed);44+ process.exit(1);45+}46+console.log("console script starts");