#!/usr/bin/env node /** * compile-modules.js — Pre-compile .sx files to bytecode JSON. * * Uses the js_of_ocaml kernel in Node.js to compile each .sx module, * then serializes the bytecode as JSON for fast browser loading. * * Usage: node compile-modules.js [dist-dir] */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const distDir = process.argv[2] || path.join(__dirname, 'dist'); const sxDir = path.join(distDir, 'sx'); if (!fs.existsSync(sxDir)) { console.error('sx dir not found:', sxDir); process.exit(1); } // Load the js_of_ocaml kernel const kernelPath = path.join(__dirname, '..', '_build', 'default', 'browser', 'sx_browser.bc.js'); if (!fs.existsSync(kernelPath)) { console.error('Kernel not found:', kernelPath); process.exit(1); } require(kernelPath); const K = globalThis.SxKernel; if (!K) { console.error('SxKernel not initialized'); process.exit(1); } const FILES = [ 'render.sx', 'core-signals.sx', 'signals.sx', 'deps.sx', 'router.sx', 'page-helpers.sx', 'freeze.sx', 'bytecode.sx', 'compiler.sx', 'vm.sx', 'dom.sx', 'browser.sx', 'adapter-html.sx', 'adapter-sx.sx', 'adapter-dom.sx', 'boot-helpers.sx', 'hypersx.sx', 'harness.sx', 'harness-reactive.sx', 'harness-web.sx', 'engine.sx', 'orchestration.sx', 'boot.sx', ]; // Load all files to build up the env (need compiler loaded) console.log('Loading SX environment...'); for (const file of FILES) { const r = K.load(fs.readFileSync(path.join(sxDir, file), 'utf8')); if (typeof r === 'string' && r.startsWith('Error')) { console.error(' FAIL', file, r); process.exit(1); } } console.log(' ' + FILES.length + ' files loaded'); // Compile each file to bytecode console.log('Compiling bytecode modules...'); let compiled = 0, skipped = 0; for (const file of FILES) { const srcPath = path.join(sxDir, file); const src = fs.readFileSync(srcPath, 'utf8'); const hash = crypto.createHash('sha256').update(src).digest('hex').slice(0, 16); try { // Parse source to get expression list, then compile const code = K.eval('(compile-module (sx-parse ' + JSON.stringify(src) + '))'); if (typeof code === 'string' && code.startsWith('Error')) { console.error(' SKIP', file, '—', code); skipped++; continue; } const json = { magic: 'SXBC', version: 1, hash: hash, module: serializeModule(code), }; const outPath = srcPath.replace(/\.sx$/, '.sxbc.json'); fs.writeFileSync(outPath, JSON.stringify(json)); const size = fs.statSync(outPath).size; console.log(' ok', file, '→', Math.round(size / 1024) + 'K'); compiled++; } catch (e) { console.error(' SKIP', file, '—', e.message || e); skipped++; } } console.log('Done:', compiled, 'compiled,', skipped, 'skipped'); // --- Serialization --- function serializeModule(code) { return { bytecode: extractList(code.bytecode), constants: extractList(code.constants).map(serializeConstant), }; } function extractList(v) { if (!v) return []; if (Array.isArray(v)) return v; if (v._type === 'list' && v.items) return v.items; if (v.items) return v.items; return []; } function serializeConstant(c) { if (c === null || c === undefined) return { t: 'nil' }; if (typeof c === 'number') return { t: 'n', v: c }; if (typeof c === 'string') return { t: 's', v: c }; if (typeof c === 'boolean') return { t: 'b', v: c }; if (c._type === 'symbol') return { t: 'sym', v: c.name }; if (c._type === 'keyword') return { t: 'kw', v: c.name }; if (c._type === 'list') return { t: 'list', v: extractList(c).map(serializeConstant) }; // Code object (nested lambda bytecode) if (c.bytecode) return { t: 'code', v: serializeModule(c) }; if (c._type === 'dict') { // Check if it's a code object stored as dict const bc = c.get ? c.get('bytecode') : c.bytecode; if (bc) return { t: 'code', v: serializeModule(c) }; // Regular dict const entries = {}; if (c.forEach) c.forEach((v, k) => { entries[k] = serializeConstant(v); }); return { t: 'dict', v: entries }; } return { t: 'nil' }; }