#!/usr/bin/env node // Lay out the Create runtime from this repository's working tree, as the WAI site serves it // and as an exported project carries it under vendor/: // // /create/ wai-app's page files, each named wai-app- (the widgets, the // renderer, the session, the room frame, the styles, Create's own page // and its recordings), and create/pkg/ (wai-app's WebAssembly build, which // this script does not make) // /room/ joule-moq-web's room client, each file named wai-app-room-.js // (app.js as wai-app-room-client.js), its imports rewritten to those names // and to the WebAssembly packages room/moq/pkg/wai_app_room_moq.js, // room/moq-e2ee/pkg/wai_app_room_moq_e2ee.js and room/live/pkg/wai_app_room_live.js // (also not made here) // /create/wai-app-runtime.json every file an app needs, by its path under , // with its SHA-256: what an export copies under vendor/ // // Every served name is unique across the WAI site, whose deploy fingerprints assets keyed // by basename. // // node wai/wai-app/web/runtime.mjs // // The room client's files are those app.js reaches by its imports. A relative path string // in any of them that this script does not map fails the layout, so a new import is never // served unrenamed. Run it after the WebAssembly packages are in place: runtime.json lists // what is there. import fs from "node:fs"; import path from "node:path"; import crypto from "node:crypto"; import { fileURLToPath } from "node:url"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO = path.resolve(HERE, "../../.."); const WEB = path.join(REPO, "joule-code/joule-code-rs/crates/joule-moq-web/web"); const out = process.argv[2]; if (!out) { console.error("usage: node runtime.mjs "); process.exit(2); } const OUT = path.resolve(out); // wai-app's own files by their served names: unique across the site, which fingerprints its // assets keyed by basename, and the same in an export's vendor/. Which of them an app needs. const CREATE_FILES = { "app.js": "wai-app-app.js", "renderer.js": "wai-app-renderer.js", "components.js": "wai-app-components.js", "session.js": "wai-app-session.js", "ui.css": "wai-app.css", "room-frame.html": "wai-app-room-frame.html", "create.js": "wai-app-create.js", "model.js": "wai-app-model.js", "index.html": "index.html", "runtime.json": "wai-app-runtime.json", "pkg/wai_app.js": "pkg/wai_app.js", }; const APP_FILES = ["wai-app-app.js", "wai-app-renderer.js", "wai-app-components.js", "wai-app-session.js", "wai-app.css", "wai-app-room-frame.html", "pkg/wai_app.js", "pkg/wai_app_bg.wasm"]; const FRAME_CLIENT = "../../../joule-code/joule-code-rs/crates/joule-moq-web/web/room/app.js"; // The room client's served name for each file of joule-moq-web/web it is made of. function servedName(rel) { const fixed = { "room/app.js": "room/wai-app-room-client.js", "pkg/joule_moq_web.js": "room/moq/pkg/wai_app_room_moq.js", "pkg/joule_moq_web_e2ee.js": "room/moq-e2ee/pkg/wai_app_room_moq_e2ee.js", "pkg/wai_live_web.js": "room/live/pkg/wai_app_room_live.js", }; if (fixed[rel]) return fixed[rel]; const m = rel.match(/^(room|moq)\/([a-z0-9-]+)\.js$/); if (m) return `room/wai-app-room-${m[2]}.js`; return null; } const SPEC = /(["'`])(\.{1,2}\/[^"'`\s]*)\1/g; const sha256 = (b) => crypto.createHash("sha256").update(b).digest("hex"); function layRoomClient() { const todo = ["room/app.js"]; const done = new Set(); while (todo.length) { const rel = todo.pop(); if (done.has(rel)) continue; done.add(rel); const served = servedName(rel); const text = fs.readFileSync(path.join(WEB, rel), "utf8"); const rewritten = text.replace(SPEC, (all, q, spec) => { const target = path.posix.normalize(path.posix.join(path.posix.dirname(rel), spec)); const to = servedName(target); if (!to) throw new Error(`runtime: ${rel} names ${spec} (${target}), which the room client's layout does not map`); if (!target.startsWith("pkg/")) todo.push(target); let r = path.posix.relative(path.posix.dirname(served), to); if (!r.startsWith(".")) r = `./${r}`; return `${q}${r}${q}`; }); const header = `// joule-moq-web/web/${rel} (Apache-2.0), sha256 ${sha256(text)}; its imports renamed by wai/wai-app/web/runtime.mjs.\n`; fs.mkdirSync(path.join(OUT, path.dirname(served)), { recursive: true }); fs.writeFileSync(path.join(OUT, served), header + rewritten); } return [...done].sort(); } // wai-app's files under their served names, each relative path in them (an import, a // stylesheet, a frame, a data attribute) rewritten to those names; one the table does not // map fails the layout. The room frame loads the room client laid out beside it. function layCreate() { const dir = path.join(OUT, "create"); fs.mkdirSync(dir, { recursive: true }); for (const [src, served] of Object.entries(CREATE_FILES)) { if (src === "runtime.json" || src.startsWith("pkg/")) continue; let text = fs.readFileSync(path.join(HERE, src), "utf8"); if (src === "room-frame.html") { if (!text.includes(`src="${FRAME_CLIENT}"`)) throw new Error("runtime: room-frame.html does not load the room client by its source path"); text = text.replace(`src="${FRAME_CLIENT}"`, 'src="../room/wai-app-room-client.js"'); } text = text.replace(/(["'`])\.\/([a-z0-9_./-]+)\1/gi, (all, q, rel) => { if (rel === "recorded/") return all; const to = CREATE_FILES[rel]; if (!to) throw new Error(`runtime: ${src} names ./${rel}, which the layout does not map`); return `${q}./${to}${q}`; }); fs.writeFileSync(path.join(dir, served), text); } fs.mkdirSync(path.join(dir, "recorded"), { recursive: true }); for (const f of fs.readdirSync(path.join(HERE, "recorded"))) fs.copyFileSync(path.join(HERE, "recorded", f), path.join(dir, "recorded", f)); } function walk(d, base, acc) { if (!fs.existsSync(d)) return acc; for (const e of fs.readdirSync(d, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p, base, acc); else acc.push(path.relative(base, p).split(path.sep).join("/")); } return acc; } const room = layRoomClient(); layCreate(); const files = []; for (const f of APP_FILES) { const p = path.join(OUT, "create", f); if (!fs.existsSync(p)) throw new Error(`runtime: create/${f} is missing (build wai-app's WebAssembly package into ${path.join(OUT, "create/pkg")} first)`); files.push(`create/${f}`); } for (const f of walk(path.join(OUT, "room"), OUT, [])) files.push(f); const listed = files.map((f) => ({ path: f, sha256: sha256(fs.readFileSync(path.join(OUT, f))) })); fs.writeFileSync(path.join(OUT, "create/wai-app-runtime.json"), JSON.stringify({ note: "The runtime an app made with Create carries under vendor/, by path, with each file's SHA-256.", files: listed }, null, 1) + "\n"); console.log(`runtime: ${room.length} room client file(s), ${listed.length} runtime file(s) listed in create/wai-app-runtime.json`);