| 1 | #!/usr/bin/env node |
| 2 | /** Local TypeScript build plus deterministic embedded-runtime packaging. */ |
| 3 | import { readFile, writeFile, cp, mkdir } from 'node:fs/promises'; |
| 4 | import { fileURLToPath } from 'node:url'; |
| 5 | import { dirname } from 'node:path'; |
| 6 | import { spawnSync } from 'node:child_process'; |
| 7 | import ts from 'typescript'; |
| 8 | const root = fileURLToPath(new URL('../', import.meta.url)); |
| 9 | const result = spawnSync(process.execPath, [root + 'node_modules/typescript/bin/tsc', '-p', root + 'tsconfig.json'], { stdio: 'inherit' }); |
| 10 | if (result.status !== 0) process.exit(result.status || 1); |
| 11 | for (const name of ['pet.html', 'shared.html', 'pet.css', 'whale-points.tsv']) await cp(root + 'public/' + name, root + 'dist/' + name); |
| 12 | const modules = new Map(); |
| 13 | async function moduleFor(name) { |
| 14 | if (modules.has(name)) return; |
| 15 | if (!/^[a-z-]+$/.test(name)) throw new Error('Unexpected native module dependency.'); |
| 16 | const source = await readFile(root + `src/core/${name}.ts`, 'utf8'); |
| 17 | const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText; |
| 18 | modules.set(name, js); |
| 19 | for (const m of js.matchAll(/require\("\.\/([a-z-]+)\.js"\)/g)) await moduleFor(m[1]); |
| 20 | } |
| 21 | await moduleFor('pet-native'); |
| 22 | // JavaScriptCore has no structuredClone host API. Pet inputs are validated JSON. |
| 23 | const bundle = `/* Generated from pet/src/core. Run npm --prefix pet run sync. */\n(function(global){\n'use strict';\nif(!global.structuredClone)global.structuredClone=value=>JSON.parse(JSON.stringify(value));\nconst factories={},cache={};\n` |
| 24 | + [...modules].map(([id, js]) => `factories[${JSON.stringify(id)}]=function(exports,require){\n${js}\n};\n`).join('') |
| 25 | + `function load(id){id=id.replace(/^\\.\\//,'').replace(/\\.js$/,'');if(cache[id])return cache[id];if(!factories[id])throw Error('Missing core module');const e=cache[id]={};factories[id](e,load);return e;}\nglobal.PetNative=load('pet-native').PetNative;\n})(globalThis);\n`; |
| 26 | await writeFile(root + 'dist/pet-native.js', bundle); |
| 27 | // Committed native copies are generated from src/core and public/. `--check` |
| 28 | // regenerates them in memory and fails on any byte difference, so a green |
| 29 | // `npm run check` cannot sit beside a stale embedded TUI/Apple/Android core. |
| 30 | const check = process.argv.includes('--check'); |
| 31 | const generated = []; |
| 32 | if (check || process.argv.includes('--tui')) { |
| 33 | const destination = root + '../crates/tui/src/tui/pet_watch/'; |
| 34 | generated.push([destination + 'pet-native.js', Buffer.from(bundle)], |
| 35 | [destination + 'shared.html', await readFile(root + 'public/shared.html')]); |
| 36 | } |
| 37 | if (process.argv.includes('--study')) { |
| 38 | const study = root + 'world/'; |
| 39 | await mkdir(study, { recursive: true }); |
| 40 | for (const name of ['pet.html', 'pet.css', 'whale-points.tsv', 'core', 'ui/pet.js', 'ui/storage.js']) { |
| 41 | await mkdir(study + name.split('/').slice(0, -1).join('/'), { recursive: true }); |
| 42 | await cp(root + 'dist/' + name, study + name, { recursive: true }); |
| 43 | } |
| 44 | // The original conformance viewer must not retain a stale browser core. |
| 45 | await mkdir(study + '../dist', { recursive: true }); |
| 46 | await writeFile(study + '../dist/PetSim.js', "export * from '../world/core/pet-sim.js';\n"); |
| 47 | const renderer = await readFile(study + '../web.ts', 'utf8'); |
| 48 | await writeFile(study + '../dist/web.js', ts.transpileModule(renderer, { |
| 49 | compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 }, |
| 50 | }).outputText.replace("'./PetSim.ts'", "'./PetSim.js'")); |
| 51 | } |
| 52 | if (check || process.argv.includes('--apple')) { |
| 53 | const { petDemoEvents } = await import('../dist/core/pet-demo.js'); |
| 54 | const { compilePetTelemetry, encodePetJSONL } = await import('../dist/core/pet-telemetry.js'); |
| 55 | generated.push([root + 'ios/Resources/pet-native.js', Buffer.from(bundle)], |
| 56 | [root + 'ios/Resources/demo.jsonl', Buffer.from(encodePetJSONL(compilePetTelemetry(petDemoEvents(), 80_000)))]); |
| 57 | } |
| 58 | if (check) { |
| 59 | const stale = []; |
| 60 | for (const [path, bytes] of generated) { |
| 61 | const current = await readFile(path).catch(error => { if (error.code === 'ENOENT') return undefined; throw error; }); |
| 62 | if (!current || !bytes.equals(current)) stale.push(path.slice(root.length)); |
| 63 | } |
| 64 | if (stale.length) { |
| 65 | console.error(`Generated native artifacts are stale: ${stale.join(', ')}. Run npm --prefix pet run sync and commit them.`); |
| 66 | process.exit(1); |
| 67 | } |
| 68 | } else { |
| 69 | for (const [path, bytes] of generated) { await mkdir(dirname(path), { recursive: true }); await writeFile(path, bytes); } |
| 70 | } |
| 71 | console.log(`Built pet viewer and native core (${modules.size} shared modules), entirely local${check ? '; generated native artifacts match' : ''}.`); |
| 72 |