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.

95 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 = () => ({
20 style: {},
21 setAttribute: noop,
22 appendChild: noop,
23 addEventListener: noop,
24 classList: { add: noop, remove: noop, contains: () => false },
25 });
26 globalThis.window ??= globalThis;
27 globalThis.self ??= globalThis;
28 globalThis.document ??= {
29 documentElement: element(),
30 head: element(),
31 body: element(),
32 createElement: element,
33 querySelector: () => null,
34 querySelectorAll: () => [],
35 addEventListener: noop,
36 removeEventListener: noop,
37 cookie: "",
38 visibilityState: "visible",
39 };
40 globalThis.location ??= new URL("https://g1t.sh/");
41 globalThis.history ??= { state: null, pushState: noop, replaceState: noop };
42 globalThis.matchMedia ??= () => ({ matches: false, addEventListener: noop, removeEventListener: noop, addListener: noop, removeListener: noop });
43 globalThis.localStorage ??= { getItem: () => null, setItem: noop, removeItem: noop };
44 globalThis.sessionStorage ??= globalThis.localStorage;
45 globalThis.addEventListener ??= noop;
46 globalThis.removeEventListener ??= noop;
47 globalThis.requestAnimationFrame ??= (fn) => setTimeout(fn, 0);
48 const { root, modules } = workerData;
49 let error = null;
50 try {
51 for (const module of modules) await import(pathToFileURL(join(root, module)).href);
52 } catch (caught) {
53 error = String(caught?.stack ?? caught)
54 .split("\n")
55 .slice(0, 2)
56 .join(" | ");
57 }
58 parentPort.postMessage(error);
59} else {
60 const root = resolve(process.argv[2] ?? "build/client");
61 const assets = join(root, "assets");
62 const manifestFile = readdirSync(assets).find((name) => /^manifest-.*\.js$/.test(name));
63 if (!manifestFile) throw new Error(`No route manifest in ${assets}: build the site first.`);
64 const text = readFileSync(join(assets, manifestFile), "utf8");
65 const manifest = JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1));
66 const rootModule = manifest.routes.root.module;
67 const routes = Object.values(manifest.routes).filter((route) => route.module && route.id !== "root");
68
69 const check = (modules) =>
70 new Promise((done) => {
71 const worker = new Worker(new URL(import.meta.url), { workerData: { root, modules } });
72 worker.once("message", (error) => {
73 done(error);
74 void worker.terminate();
75 });
76 worker.once("error", (error) => done(String(error?.stack ?? error).split("\n")[0]));
77 });
78
79 const failed = [];
80 const queue = [{ id: "root", modules: [rootModule] }, ...routes.map((route) => ({ id: route.id, modules: [rootModule, route.module] }))];
81 await Promise.all(
82 Array.from({ length: Math.min(8, availableParallelism()) }, async () => {
83 for (let next = queue.shift(); next; next = queue.shift()) {
84 const error = await check(next.modules);
85 if (error) failed.push(`${next.id}: ${error}`);
86 }
87 }),
88 );
89 if (failed.length) {
90 console.error(`${failed.length} of ${routes.length + 1} routes throw as they load:`);
91 for (const line of failed.sort().slice(0, 20)) console.error(` ${line}`);
92 process.exit(1);
93 }
94 console.log(`Every route loads (${routes.length + 1}).`);
95}