open-standards/wai/wai-app/web/runtime.mjs
Transaction Science 608cd843e9 wai-app: Create's crate, a library with no binary, whose export wai create writes
wai/wai-ui becomes wai/wai-app, the library `wai_app`, with no binary and no `cli`
feature: `wai`, built with its feature `app`, is its command line and its MCP
server. Its served files are named wai-app-* (the WebAssembly package wai_app),
the recordings wai-app-*.json.

- export.rs gives the files of a starter project for a program and the facts its
  project file holds (title, summary, `wai dev`'s mode for the app's room, a watch
  room's file, a show's audience); `wai create --from app.oui` writes them, the
  project file and the licence. The export no longer writes app.json, room.toml,
  relay.toml, package.json, check.mjs or serve.mjs: wai.json names the room, `wai
  dev` runs it and `wai app check` checks the project. Its README and AGENTS.md
  say so, and the README its licence.
- runtime.rs reads a Create runtime by its listing (create/wai-app-runtime.json),
  checking each file's SHA-256.
- bridge.rs keeps the bridge on 127.0.0.1 a coding agent streams a program into
  the page through, with a stream helper; the stdio server, its tools and the
  binary's test go (`wai mcp --toolset app` serves them).
- The page's Export hands over app.oui, the program in its canonical form, and the
  command that makes it a project; the ZIP writer and the page's export in
  WebAssembly go. The page names `wai mcp --toolset app` for the coding agent.
- joule-ui/docs/create.md §4 and §5: the export as `wai create --from app.oui`, and
  the agent's tools app_guide, app_check, app_stream and app_export.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 05:13:27 -04:00

147 lines
7.1 KiB
JavaScript

#!/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/:
//
// <out>/create/ wai-app's page files, each named wai-app-<name> (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)
// <out>/room/ joule-moq-web's room client, each file named wai-app-room-<name>.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)
// <out>/create/wai-app-runtime.json every file an app needs, by its path under <out>,
// 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 <out>
//
// 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 <out>");
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`);