dist/nobel.js joined files with newlines, so each (()=>{...})() called the
previous one's result and the bundle failed at runtime. Files are now
joined with semicolons, as jsDelivr combine does.
scripts/libs.ts is the one list of libraries: groups, symbols,
dependencies and the load order for single files. A new test loads that
file as a plain tag, async, and injected after the page has loaded.- commit
- 961372d6e3af2ba7a51de91123092505e3c7a66a
- parent
- f73872f96040f6a677cc3190f73d38458748d923
- author
- Dami <dami.etoile@gmail.com>
| M | scripts/ | 7 | ++- |
| A | scripts/ | 36 | +++++++++++++++++ |
| M | scripts/ | 5 | +- |
| A | test/ | 64 | ++++++++++++++++++++++++++++++ |
@@ -1,11 +1,12 @@
1-// Concatenates the libraries into dist/nobel.js and minifies it into dist/nobel.min.js.1+// Concatenates the libraries, in the canonical order from libs.ts, into dist/nobel.js and minifies it into dist/nobel.min.js.22 import { transform } from "npm:esbuild@0.25";33 import { brotliCompressSync } from "node:zlib";4+import { libs } from "./libs.ts";45 5-const order = ["neon", "krypton", "fluorine", "chlorine", "argon", "hydrogen", "helium", "gold", "mercury", "xenon", "radon", "iodine", "oganesson", "europium", "praseodymium", "cerium"];6+const order = libs.map((l) => l.name);67 const root = new URL("../", import.meta.url);78 const parts = await Promise.all(order.map((n) => Deno.readTextFile(new URL(`${n}/${n}.js`, root))));8-const src = order.map((n, i) => `// ${n}.js\n${parts[i].trim()}\n`).join("");9+const src = order.map((n, i) => `// ${n}.js\n${parts[i].trim()};\n`).join("");910 const { code } = await transform(src, { minify: true, target: "es2022" });1011 1112 await Deno.mkdir(new URL("dist/", root), { recursive: true });@@ -0,0 +1,36 @@
1+// The one list of nobel libraries. Its order is the canonical load order for single files:2+// the base first, then peers that only listen, then libraries that start work, and xenon last,3+// so any subset concatenated in this order works however the file is loaded.4+export type Lib = { name: string; sym: string; z: number; group: string; needs: string[] };5+6+export const groups: Record<string, string> = {7+ base: "period 1, the base: relays and keys",8+ "alkali metal": "things that charge and flow",9+ "noble gas": "the core: data and transport",10+ halogen: "secrets and trust",11+ "transition metal": "signed exchanges",12+ lanthanide: "visual presets",13+};14+15+export const libs: Lib[] = [16+ { name: "hydrogen", sym: "H", z: 1, group: "base", needs: [] },17+ { name: "helium", sym: "He", z: 2, group: "base", needs: [] },18+ { name: "neon", sym: "Ne", z: 10, group: "noble gas", needs: [] },19+ { name: "fluorine", sym: "F", z: 9, group: "halogen", needs: ["helium"] },20+ { name: "chlorine", sym: "Cl", z: 17, group: "halogen", needs: ["helium", "fluorine"] },21+ { name: "argon", sym: "Ar", z: 18, group: "noble gas", needs: [] },22+ { name: "lithium", sym: "Li", z: 3, group: "alkali metal", needs: ["hydrogen", "argon"] },23+ { name: "gold", sym: "Au", z: 79, group: "transition metal", needs: ["hydrogen"] },24+ { name: "mercury", sym: "Hg", z: 80, group: "transition metal", needs: [] },25+ { name: "radon", sym: "Rn", z: 86, group: "noble gas", needs: ["hydrogen"] },26+ { name: "iodine", sym: "I", z: 53, group: "halogen", needs: ["hydrogen", "helium", "fluorine", "xenon"] },27+ { name: "oganesson", sym: "Og", z: 118, group: "noble gas", needs: ["hydrogen"] },28+ { name: "praseodymium", sym: "Pr", z: 59, group: "lanthanide", needs: ["hydrogen", "radon"] },29+ { name: "europium", sym: "Eu", z: 63, group: "lanthanide", needs: ["hydrogen", "xenon"] },30+ { name: "cerium", sym: "Ce", z: 58, group: "lanthanide", needs: [] },31+ { name: "xenon", sym: "Xe", z: 54, group: "noble gas", needs: ["hydrogen"] },32+];33+34+// n + symbol; one-letter symbols add the next letter of the name that is not another element's symbol.35+const prefixes: Record<string, string> = { hydrogen: "nhy", fluorine: "nfl", iodine: "nio" };36+export const prefix = (l: Lib) => prefixes[l.name] ?? `n${l.sym.toLowerCase()}`;@@ -1,10 +1,11 @@
11 // Prints raw / gzip / brotli sizes and fails if a library exceeds the 5kb raw budget.22 import { brotliCompressSync, gzipSync } from "node:zlib";3+import { libs } from "./libs.ts";34 45 const BUDGET = 5120;5-const libs = ["hydrogen", "xenon", "krypton", "fluorine", "chlorine", "argon", "helium", "neon", "radon", "iodine", "oganesson", "gold", "mercury", "europium", "praseodymium", "cerium"];6+const names = libs.map((l) => l.name);67 let over = false;7-for (const name of libs) {8+for (const name of names) {89 const src = await Deno.readFile(new URL(`../${name}/${name}.js`, import.meta.url));910 const raw = src.length, gz = gzipSync(src).length, br = brotliCompressSync(src).length;1011 over ||= raw > BUDGET;@@ -0,0 +1,64 @@
1+// One file in the canonical order from scripts/libs.ts must work however it is loaded:2+// a plain tag, async, or injected after the page has loaded.3+import { assert, assertEquals } from "jsr:@std/assert@1";4+import { chromium } from "npm:playwright@1.58";5+import { serve } from "../server.ts";6+import { libs } from "../scripts/libs.ts";7+8+async function chromiumPath() {9+ if (Deno.env.get("CHROMIUM")) return Deno.env.get("CHROMIUM");10+ const cache = `${Deno.env.get("HOME")}/Library/Caches/ms-playwright`;11+ try {12+ const dirs = [...Deno.readDirSync(cache)].map((d) => d.name).filter((n) => n.startsWith("chromium_headless_shell-")).sort();13+ const dir = `${cache}/${dirs.at(-1)}`;14+ return `${dir}/${[...Deno.readDirSync(dir)][0].name}/chrome-headless-shell`;15+ } catch { return undefined; }16+}17+18+const root = new URL("../", import.meta.url);19+const bundle = (await Promise.all(libs.map((l) => Deno.readTextFile(new URL(`${l.name}/${l.name}.js`, root))))).join(";\n"); // jsDelivr combine joins with ";" too20+const demo = await Deno.readTextFile(new URL("demo/index.html", root));21+// The demo's own inline script uses helium at once; defer it until the bundle has run.22+const body = demo.replace(/<script src="\.\.\/(?!demo)[a-z]+\/[a-z]+\.js"><\/script>\n/g, "")23+ .replace(/<script>\n\t\/\/ Profiles are/, `<script>\nlet go = ()=>{\n\t// Profiles are`)24+ .replace(/\t}\)\n<\/script>\n<\/body>/, "\t})\n}\nwindow.helium ? go() : addEventListener(\"nobel-ready\", go)\n</script>\n</body>");25+const ready = `document.dispatchEvent(new Event("nobel-ready"));dispatchEvent(new Event("nobel-ready"))`;26+const loads = {27+ plain: `<script src="/bundle.js"></script>`,28+ async: `<script async src="/bundle.js"></script>`,29+ late: `<script>addEventListener("load", ()=>setTimeout(()=>document.head.append(Object.assign(document.createElement("script"), {src:"/bundle.js"})), 200))</script>`,30+};31+32+Deno.test({ name: "one file in canonical order works however it loads", sanitizeResources: false, sanitizeOps: false }, async (t) => {33+ const server = serve(0), base = `http://localhost:${server.addr.port}`;34+ const browser = await chromium.launch({ executablePath: await chromiumPath(), args: ["--disable-features=LocalNetworkAccessChecks"] });35+ try {36+ for (const [how, tag] of Object.entries(loads)) {37+ await t.step(how, async () => {38+ const ctx = await browser.newContext(), page = await ctx.newPage(), errors = [];39+ page.on("pageerror", (e) => errors.push(e.message));40+ await page.route(`${base}/bundle.js`, (r) => r.fulfill({ contentType: "text/javascript", body: bundle + "\n" + ready }));41+ await page.route(`${base}/demo/${how}.html`, (r) => r.fulfill({ contentType: "text/html", body: body.replace('<script src="relay.js"></script>', `<script src="relay.js"></script>\n${tag}`) }));42+ await page.goto(`${base}/demo/${how}.html`);43+ await page.waitForFunction(() => window.xenon && window.helium);44+ await page.fill("#compose textarea", "hello https://example.com");45+ await page.click("#compose button");46+ await page.waitForSelector("#feed li");47+ assertEquals(await page.locator("#feed li p a").first().getAttribute("href"), "https://example.com", "neon rendered the link");48+ await page.fill("#profile input", "Ada");49+ await page.click("#profile button");50+ await page.waitForFunction(() => document.querySelector("#feed li b")?.textContent === "Ada");51+ await page.reload();52+ await page.waitForSelector("#feed li");53+ await page.waitForTimeout(300);54+ // argon is wired in when it stored what was posted; which copy wins the race to render varies.55+ assert(await page.evaluate(async () => (await argon.query([{ kinds: [1] }])).length > 0), "argon stored the posts");56+ assertEquals(errors, []);57+ await ctx.close();58+ });59+ }60+ } finally {61+ await browser.close();62+ await server.shutdown();63+ }64+});