| 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 = () => ({ |
| 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 | } |