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
75 linesCodeBlameRaw
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]
10import { readFileSync, readdirSync } from "node:fs";
11import { availableParallelism } from "node:os";
12import { join, resolve } from "node:path";
13import { pathToFileURL } from "node:url";
14import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
15
16if (!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}