Lazy module loading (Step 5 piece 6 completion): - Add define-library wrappers + import declarations to 13 source .sx files - compile-modules.js generates module-manifest.json with dependency graph - compile-modules.js strips define-library/import before bytecode compilation (VM doesn't handle these as special forms) - sx-platform.js replaces hardcoded 24-file loadWebStack() with manifest-driven recursive loader — only downloads modules the page needs - Result: 12 modules loaded (was 24), zero errors, zero warnings - Fallback to full load if manifest missing VM transpilation prep (Step 6b): - Refactor lib/vm.sx: 20 accessor functions replace raw dict access - Factor out collect-n-from-stack, collect-n-pairs, pad-n-nils helpers - bootstrap_vm.py: transpiles 9 VM logic functions to OCaml - sx_vm_ref.ml: proof that vm.sx transpiles (preamble has stubs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
201 lines
7.6 KiB
JavaScript
201 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* wrap-modules.js — Add define-library wrappers and import declarations
|
|
* to browser .sx SOURCE files for lazy loading support.
|
|
*
|
|
* Targets the real source locations (spec/, web/, lib/), NOT dist/.
|
|
* Run bundle.sh after to copy to dist/, then compile-modules.js.
|
|
*
|
|
* - 8 unwrapped files get define-library + export + begin wrappers
|
|
* - 4 already-wrapped files get dependency import declarations
|
|
* - boot.sx gets imports (stays unwrapped — entry point)
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
|
|
// Source file → library name (null = entry point)
|
|
const MODULES = {
|
|
// Spec modules
|
|
'spec/render.sx': { lib: '(sx render)', deps: [] },
|
|
'spec/signals.sx': { lib: '(sx signals)', deps: [] },
|
|
'web/web-signals.sx': { lib: '(sx signals-web)', deps: ['(sx dom)', '(sx browser)'] },
|
|
'web/deps.sx': { lib: '(web deps)', deps: [] },
|
|
'web/router.sx': { lib: '(web router)', deps: [] },
|
|
'web/page-helpers.sx': { lib: '(web page-helpers)', deps: [] },
|
|
// Lib modules
|
|
'lib/freeze.sx': { lib: '(sx freeze)', deps: [] },
|
|
'lib/highlight.sx': { lib: '(sx highlight)', deps: [] },
|
|
'lib/bytecode.sx': { lib: '(sx bytecode)', deps: [] },
|
|
'lib/compiler.sx': { lib: '(sx compiler)', deps: [] },
|
|
'lib/vm.sx': { lib: '(sx vm)', deps: [] },
|
|
// Web FFI
|
|
'web/lib/dom.sx': { lib: '(sx dom)', deps: [] },
|
|
'web/lib/browser.sx': { lib: '(sx browser)', deps: [] },
|
|
// Web adapters
|
|
'web/adapter-html.sx': { lib: '(web adapter-html)', deps: ['(sx render)'] },
|
|
'web/adapter-sx.sx': { lib: '(web adapter-sx)', deps: ['(web boot-helpers)'] },
|
|
'web/adapter-dom.sx': { lib: '(web adapter-dom)', deps: ['(sx dom)', '(sx render)'] },
|
|
// Web framework
|
|
'web/lib/boot-helpers.sx': { lib: '(web boot-helpers)', deps: ['(sx dom)', '(sx browser)', '(web adapter-dom)'] },
|
|
'web/lib/hypersx.sx': { lib: '(sx hypersx)', deps: [] },
|
|
'web/engine.sx': { lib: '(web engine)', deps: ['(web boot-helpers)', '(sx dom)', '(sx browser)'] },
|
|
'web/orchestration.sx': { lib: '(web orchestration)', deps: ['(web boot-helpers)', '(sx dom)', '(sx browser)', '(web adapter-dom)', '(web engine)'] },
|
|
'web/boot.sx': { lib: null, deps: ['(sx dom)', '(sx browser)', '(web boot-helpers)', '(web adapter-dom)',
|
|
'(sx signals)', '(sx signals-web)', '(web router)', '(web page-helpers)',
|
|
'(web orchestration)', '(sx render)',
|
|
'(sx bytecode)', '(sx compiler)', '(sx vm)'] },
|
|
// Test harness
|
|
'spec/harness.sx': { lib: '(sx harness)', deps: [] },
|
|
'web/harness-reactive.sx': { lib: '(sx harness-reactive)', deps: [] },
|
|
'web/harness-web.sx': { lib: '(sx harness-web)', deps: [] },
|
|
};
|
|
|
|
// Extract top-level define names from source.
|
|
// Handles both `(define name ...)` and `(define\n name ...)` formats.
|
|
function extractDefineNames(source) {
|
|
const names = [];
|
|
const lines = source.split('\n');
|
|
let depth = 0;
|
|
let expectName = false;
|
|
for (const line of lines) {
|
|
if (depth === 0) {
|
|
const m = line.match(/^\(define\s+\(?(\S+)/);
|
|
if (m) {
|
|
names.push(m[1]);
|
|
expectName = false;
|
|
} else if (line.match(/^\(define\s*$/)) {
|
|
expectName = true;
|
|
}
|
|
} else if (depth === 1 && expectName) {
|
|
const m = line.match(/^\s+(\S+)/);
|
|
if (m) {
|
|
names.push(m[1]);
|
|
expectName = false;
|
|
}
|
|
}
|
|
for (const ch of line) {
|
|
if (ch === '(') depth++;
|
|
else if (ch === ')') depth--;
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
|
|
function processFile(relPath, info) {
|
|
const filePath = path.join(ROOT, relPath);
|
|
if (!fs.existsSync(filePath)) {
|
|
console.log(' SKIP', relPath, '(not found)');
|
|
return;
|
|
}
|
|
|
|
let source = fs.readFileSync(filePath, 'utf8');
|
|
const { lib, deps } = info;
|
|
const hasWrapper = source.includes('(define-library');
|
|
const hasDepImports = deps.length > 0 && source.match(/^\(import\s+\(/m) &&
|
|
!source.match(/^\(import\s+\(\w+ \w+\)\)\s*$/m); // more than just self-import
|
|
|
|
// Skip files with no deps and already wrapped (or no wrapper needed)
|
|
if (deps.length === 0 && (hasWrapper || !lib)) {
|
|
console.log(' ok', relPath, '(no changes needed)');
|
|
return;
|
|
}
|
|
|
|
// Build import lines for deps
|
|
const importLines = deps.map(d => `(import ${d})`).join('\n');
|
|
|
|
// CASE 1: Entry point (boot.sx) — just add imports at top
|
|
if (!lib) {
|
|
if (deps.length > 0 && !source.startsWith('(import')) {
|
|
source = importLines + '\n\n' + source;
|
|
fs.writeFileSync(filePath, source);
|
|
console.log(' +imports', relPath, `(${deps.length} deps, entry point)`);
|
|
} else {
|
|
console.log(' ok', relPath, '(entry point, already has imports)');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// CASE 2: Already wrapped — add imports before define-library
|
|
if (hasWrapper) {
|
|
if (deps.length > 0) {
|
|
// Check if imports already present
|
|
const firstImportCheck = deps[0].replace(/[()]/g, '\\$&');
|
|
if (source.match(new RegExp('\\(import ' + firstImportCheck))) {
|
|
console.log(' ok', relPath, '(already has dep imports)');
|
|
return;
|
|
}
|
|
const dlIdx = source.indexOf('(define-library');
|
|
source = source.slice(0, dlIdx) + importLines + '\n\n' + source.slice(dlIdx);
|
|
fs.writeFileSync(filePath, source);
|
|
console.log(' +imports', relPath, `(${deps.length} deps)`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// CASE 3: Needs full wrapping
|
|
if (deps.length === 0 && !hasWrapper) {
|
|
// Wrap with no deps
|
|
const names = extractDefineNames(source);
|
|
if (names.length === 0) {
|
|
console.log(' WARN', relPath, '— no defines found, skipping');
|
|
return;
|
|
}
|
|
const wrapped = buildWrapped(lib, names, source, '');
|
|
fs.writeFileSync(filePath, wrapped);
|
|
console.log(' wrapped', relPath, `as ${lib} (${names.length} exports)`);
|
|
return;
|
|
}
|
|
|
|
// Wrap with deps
|
|
const names = extractDefineNames(source);
|
|
if (names.length === 0) {
|
|
console.log(' WARN', relPath, '— no defines found, skipping');
|
|
return;
|
|
}
|
|
const wrapped = buildWrapped(lib, names, source, importLines);
|
|
fs.writeFileSync(filePath, wrapped);
|
|
console.log(' wrapped', relPath, `as ${lib} (${names.length} exports, ${deps.length} deps)`);
|
|
}
|
|
|
|
function buildWrapped(libName, exportNames, bodySource, importSection) {
|
|
const parts = [];
|
|
|
|
// Dependency imports (top-level, before define-library)
|
|
if (importSection) {
|
|
parts.push(importSection);
|
|
parts.push('');
|
|
}
|
|
|
|
// define-library header
|
|
parts.push(`(define-library ${libName}`);
|
|
parts.push(` (export ${exportNames.join(' ')})`);
|
|
parts.push(' (begin');
|
|
parts.push('');
|
|
|
|
// Body (original source, indented)
|
|
parts.push(bodySource);
|
|
parts.push('');
|
|
|
|
// Close begin + define-library
|
|
parts.push('))');
|
|
parts.push('');
|
|
|
|
// Self-import for backward compat
|
|
parts.push(`;; Re-export to global env`);
|
|
parts.push(`(import ${libName})`);
|
|
parts.push('');
|
|
|
|
return parts.join('\n');
|
|
}
|
|
|
|
console.log('Processing source .sx files...\n');
|
|
for (const [relPath, info] of Object.entries(MODULES)) {
|
|
processFile(relPath, info);
|
|
}
|
|
console.log('\nDone! Now run:');
|
|
console.log(' bash hosts/ocaml/browser/bundle.sh');
|
|
console.log(' node hosts/ocaml/browser/compile-modules.js');
|