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]>
147 lines
7.1 KiB
JavaScript
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`);
|