| 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 | + | } |