// Build the views an agent host draws (src/views/) into one self-contained // page: the view's script (the Studio's components, bundled), the Studio's // styles, and the engine inside the view (wai/wai-quantum-views) twice, as // WebAssembly (base64) and as the same build translated to JavaScript. The page // loads nothing and connects nowhere. The servers serve it for each of the six // views, naming the view in its . // // node build-views.mjs --wasm VIEWS.wasm --js VIEWS.mjs --out DIR // // writes DIR/wai-quantum-views.html and DIR/manifest.json (each part's size and // SHA-256, and every source the page is built from with its SHA-256). Run it // through wai/wai-quantum-views/build.sh, which builds the two engines first. // // node build-views.mjs --check // // says whether the committed page (wai/wai-quantum/views/) is built from the // sources as they are: every source it names unchanged, and no source added. import { build, transform } from 'esbuild'; import { createHash } from 'node:crypto'; import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const repo = join(here, '..', '..'); const arg = (k) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1] : null; }; const [wasmPath, jsPath, out] = [arg('--wasm'), arg('--js'), arg('--out')]; const checking = process.argv.includes('--check'); if (!checking && (!wasmPath || !jsPath || !out)) { console.error('usage: node build-views.mjs --wasm VIEWS.wasm --js VIEWS.mjs --out DIR'); process.exit(2); } // The most a view may be: the servers' tests hold every view to it. export const LIMIT = 1 << 20; // The name the servers replace with the view's. export const PLACEHOLDER = '__WAI_QUANTUM_VIEW__'; const sha = (b) => createHash('sha256').update(b).digest('hex'); const bytes = (s) => Buffer.byteLength(s, 'utf8'); // The views are in English: the Studio's components they share read their // words through src/i18n.js, and here its locales are English alone, so the // page carries no other language's words. const englishOnly = { name: 'english-only', setup(b) { b.onResolve({ filter: /\/locales\/index\.js$/ }, () => ({ path: 'english-only', namespace: 'wai-views-locales' })); b.onLoad({ filter: /.*/, namespace: 'wai-views-locales' }, () => { const dir = join(here, 'locales', 'en'); const areas = readdirSync(dir).filter((f) => f.endsWith('.json')).sort(); const names = areas.map((f, i) => `en${i}`); return { resolveDir: dir, loader: 'js', contents: `${areas.map((f, i) => `import ${names[i]} from './${f}';`).join('\n')}\nexport const AREAS = ${JSON.stringify(areas.map((f) => f.replace(/\.json$/, '')))};\nexport default { en: Object.assign({}, ${names.join(', ')}) };\n`, }; }); }, }; // The view's script, and every module it bundles. const app = await build({ absWorkingDir: here, entryPoints: [join(here, 'src/views/main.js')], bundle: true, format: 'esm', target: ['es2022'], minify: true, legalComments: 'none', write: false, metafile: true, logLevel: 'warning', plugins: [englishOnly] }); const script = app.outputFiles[0].text; const styleFiles = ['src/tokens.css', 'src/studio.css', 'src/views/views.css']; // Every source, relative to the repository. const sourcesOf = (inputs) => [ ...Object.keys(inputs).filter((p) => !p.startsWith('wai-views-locales:')).map((p) => join(here, p)), ...styleFiles.map((f) => join(here, f)), join(here, 'build-views.mjs'), ...['src/lib.rs', 'Cargo.toml', 'Cargo.lock', 'build.sh'].map((f) => join(repo, 'wai/wai-quantum-views', f)), ].map((p) => relative(repo, p).split('\\').join('/')).sort(); if (checking) { const committed = JSON.parse(readFileSync(join(repo, 'wai/wai-quantum/views/manifest.json'), 'utf8')); const now = sourcesOf(app.metafile.inputs); const stale = now.filter((p) => committed.sources[p] !== sha(readFileSync(join(repo, p)))); const gone = Object.keys(committed.sources).filter((p) => !now.includes(p)); if (stale.length || gone.length) { console.error(`the committed views page is not built from these sources: ${[...stale.map((p) => `${p} changed`), ...gone.map((p) => `${p} no longer used`)].join('; ')}. Rebuild it: bash wai/wai-quantum-views/build.sh`); process.exit(1); } console.log(`the committed views page is built from these ${now.length} sources`); process.exit(0); } // The engine as JavaScript, made a factory: it runs only when a host's policy refuses the WebAssembly. const engineJs = (await transform(readFileSync(jsPath, 'utf8'), { format: 'iife', globalName: '__wqEngine', minify: true, legalComments: 'none', target: 'es2022' })).code; const factory = `globalThis.wqEngineJs=function(){${engineJs};return __wqEngine};`; const wasm = readFileSync(wasmPath); const css = (await transform(styleFiles.map((f) => readFileSync(join(here, f), 'utf8')).join('\n'), { loader: 'css', minify: true, legalComments: 'none' })).code; // Text inside a `; if (html.split(PLACEHOLDER).length !== 2) throw new Error('the page must name its view in one place'); if (bytes(html) > LIMIT) throw new Error(`the page is ${bytes(html)} bytes, past the limit of ${LIMIT}`); const sources = sourcesOf(app.metafile.inputs); const manifest = { format: 'wai.quantum.views-bundle/1', about: 'The page every view of WAI Quantum is served from, the view named in its . Built by wai/wai-quantum-views/build.sh; do not edit it by hand.', placeholder: PLACEHOLDER, limit_bytes: LIMIT, bytes: { page: bytes(html), script: bytes(script), styles: bytes(css), engine_wasm: wasm.length, engine_wasm_base64: Math.ceil(wasm.length / 3) * 4, engine_js: bytes(factory) }, sha256: { page: sha(html), script: sha(script), styles: sha(css), engine_wasm: sha(wasm), engine_js: sha(factory) }, sources: Object.fromEntries(sources.map((p) => [p, sha(readFileSync(join(repo, p)))])), }; mkdirSync(out, { recursive: true }); writeFileSync(join(out, 'wai-quantum-views.html'), html); writeFileSync(join(out, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n'); console.log(`wrote ${join(out, 'wai-quantum-views.html')}: ${bytes(html)} bytes (script ${bytes(script)}, styles ${bytes(css)}, engine ${wasm.length} as WebAssembly and ${bytes(factory)} as JavaScript)`);