Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

The site's build stops a bundle whose pages throw as they load (#51): after react-router build, apps/web/scripts/load-check.mjs loads each route's modules from the client build, root then the route, each in a fresh worker as a browser would, and fails the build on any that throws. Deploy and CI build the site with npm run build, so the bundle Deploy #356 shipped (every page's script dying on "Cannot read properties of undefined (reading 'filter')") would have stopped there. Where you're needed's rows say Open, not Merge, for a pull request ready to merge, since it opens the pull request. Checked: the check fails all 258 routes of that bundle with production's error and passes all 258 of today's (5 s, no browser); the site's typecheck and tests (955).

G1t-Session:
ses_01m4ygex20ezftekecszgw2gv5
syntaqxcommitted Parent4c4f9a5Browse files
3 files+78−20/3 viewed
+2−1
192192 )}
193193 </div>
194194 <ButtonLink to={row.action.to} variant="outline" size="xs" className="border-line-strong px-2.5 text-fg hover:border-accent/50 hover:bg-raised">
195− {row.action.label}
195+ {/* It opens the pull request, where it merges: "Merge" here would read as merging on the spot. */}
196+ {row.kind === "ready" ? "Open" : row.action.label}
196197 </ButtonLink>
197198 </li>
198199 );
+1−1
33 "private": true,
44 "type": "module",
55 "scripts": {
6− "build": "react-router build",
6+ "build": "react-router build && node scripts/load-check.mjs",
77 "test": "node --test app/**/*.test.ts",
88 "dev": "react-router dev",
99 "typecheck": "wrangler types --include-env=false && react-router typegen && tsc -b --force",
+75−0
1+// After the site builds: loads each route's modules from the client build,
2+// root first and then the route, the way a browser does, each in a fresh
3+// worker so each has its own module graph. A module that throws as it
4+// loads stops the build: the bundle can put code in chunks that import each
5+// other, and a module that reads another as it loads may read it before it
6+// is there, which kills every page's script (#51: "Cannot read properties of
7+// undefined (reading 'filter')"). Nothing renders; only loading is checked.
8+//
9+// node scripts/load-check.mjs [build/client]
10+import { readFileSync, readdirSync } from "node:fs";
11+import { availableParallelism } from "node:os";
12+import { join, resolve } from "node:path";
13+import { pathToFileURL } from "node:url";
14+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
15+
16+if (!isMainThread) {
17+ // Enough of a browser for modules that look at it as they load; nothing here runs a page.
18+ const noop = () => {};
19+ const element = () => ({ style: {}, setAttribute: noop, appendChild: noop, addEventListener: noop, classList: { add: noop, remove: noop, contains: () => false } });
20+ globalThis.window ??= globalThis;
21+ globalThis.self ??= globalThis;
22+ globalThis.document ??= { documentElement: element(), head: element(), body: element(), createElement: element, querySelector: () => null, querySelectorAll: () => [], addEventListener: noop, removeEventListener: noop, cookie: "", visibilityState: "visible" };
23+ globalThis.location ??= new URL("https://g1t.sh/");
24+ globalThis.history ??= { state: null, pushState: noop, replaceState: noop };
25+ globalThis.matchMedia ??= () => ({ matches: false, addEventListener: noop, removeEventListener: noop, addListener: noop, removeListener: noop });
26+ globalThis.localStorage ??= { getItem: () => null, setItem: noop, removeItem: noop };
27+ globalThis.sessionStorage ??= globalThis.localStorage;
28+ globalThis.addEventListener ??= noop;
29+ globalThis.removeEventListener ??= noop;
30+ globalThis.requestAnimationFrame ??= (fn) => setTimeout(fn, 0);
31+ const { root, modules } = workerData;
32+ let error = null;
33+ try {
34+ for (const module of modules) await import(pathToFileURL(join(root, module)).href);
35+ } catch (caught) {
36+ error = String(caught?.stack ?? caught).split("\n").slice(0, 2).join(" | ");
37+ }
38+ parentPort.postMessage(error);
39+} else {
40+ const root = resolve(process.argv[2] ?? "build/client");
41+ const assets = join(root, "assets");
42+ const manifestFile = readdirSync(assets).find((name) => /^manifest-.*\.js$/.test(name));
43+ if (!manifestFile) throw new Error(`No route manifest in ${assets}: build the site first.`);
44+ const text = readFileSync(join(assets, manifestFile), "utf8");
45+ const manifest = JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1));
46+ const rootModule = manifest.routes.root.module;
47+ const routes = Object.values(manifest.routes).filter((route) => route.module && route.id !== "root");
48+
49+ const check = (modules) =>
50+ new Promise((done) => {
51+ const worker = new Worker(new URL(import.meta.url), { workerData: { root, modules } });
52+ worker.once("message", (error) => {
53+ done(error);
54+ void worker.terminate();
55+ });
56+ worker.once("error", (error) => done(String(error?.stack ?? error).split("\n")[0]));
57+ });
58+
59+ const failed = [];
60+ const queue = [{ id: "root", modules: [rootModule] }, ...routes.map((route) => ({ id: route.id, modules: [rootModule, route.module] }))];
61+ await Promise.all(
62+ Array.from({ length: Math.min(8, availableParallelism()) }, async () => {
63+ for (let next = queue.shift(); next; next = queue.shift()) {
64+ const error = await check(next.modules);
65+ if (error) failed.push(`${next.id}: ${error}`);
66+ }
67+ }),
68+ );
69+ if (failed.length) {
70+ console.error(`${failed.length} of ${routes.length + 1} routes throw as they load:`);
71+ for (const line of failed.sort().slice(0, 20)) console.error(` ${line}`);
72+ process.exit(1);
73+ }
74+ console.log(`Every route loads (${routes.length + 1}).`);
75+}

Comments

Sign in to comment.