clone

tiny dependency-free nostr libraries for plain html, each under 5kb and named for an element. public domain

Fix the bundle and give it one canonical order

Dami committed 961372d
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>
4 files changed, +107 -5
Mscripts/bundle.ts7++-
Ascripts/libs.ts36+++++++++++++++++
Mscripts/size.ts5+-
Atest/bundle.test.js64++++++++++++++++++++++++++++++
M scripts/bundle.ts +4 -3
A scripts/libs.ts +36 -0
@@ -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()}`;
M scripts/size.ts +3 -2
A test/bundle.test.js +64 -0
@@ -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+});