Restore hyperscript work on stable site base (908f4f80)
Reset to last known-good state (908f4f80) where links, stepper, and
islands all work, then recovered all hyperscript implementation,
conformance tests, behavioral tests, Playwright specs, site sandbox,
IO-aware server loading, and upstream test suite from f271c88a.
Excludes runtime changes (VM resolve hook, VmSuspended browser handler,
sx_ref.ml guard recovery) that need careful re-integration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1296,6 +1296,129 @@ async function modeEvalAt(browser, url, phase, expr) {
|
||||
return { url, phase, expr, result: evalResult, bootLog: bootLogs };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mode: sandbox stack=site — full website via local OCaml HTTP server
|
||||
//
|
||||
// Starts sx_server --http as a subprocess, navigates Playwright to it.
|
||||
// Full SSR, island hydration, HS activation, SPA navigation — no Docker.
|
||||
//
|
||||
// Usage:
|
||||
// sx_playwright mode=sandbox stack=site url=/sx/(applications.(hyperscript))
|
||||
// sx_playwright mode=sandbox stack=site url=/ expr="document.title"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function startSiteServer(projectRoot) {
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const port = 49152 + Math.floor(Math.random() * 16000);
|
||||
const serverBin = path.join(projectRoot, 'hosts/ocaml/_build/default/bin/sx_server.exe');
|
||||
|
||||
const proc = spawn(serverBin, ['--http', String(port)], {
|
||||
cwd: projectRoot,
|
||||
env: { ...process.env, SX_PROJECT_DIR: projectRoot, OCAMLRUNPARAM: 'b' },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stderrBuf = '';
|
||||
let stdoutBuf = '';
|
||||
proc.stderr.on('data', chunk => { stderrBuf += chunk.toString(); });
|
||||
proc.stdout.on('data', chunk => { stdoutBuf += chunk.toString(); });
|
||||
|
||||
const ready = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Server did not start within 30s\n' + stderrBuf.slice(-1000))), 30000);
|
||||
proc.stderr.on('data', () => {
|
||||
if (stderrBuf.includes('Listening on port')) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
proc.on('error', err => { clearTimeout(timeout); reject(err); });
|
||||
proc.on('exit', code => { clearTimeout(timeout); reject(new Error('Server exited with code ' + code + '\n' + stderrBuf.slice(-1000))); });
|
||||
});
|
||||
|
||||
return { proc, port, ready, getLog: () => stderrBuf + stdoutBuf };
|
||||
}
|
||||
|
||||
function stopSiteServer(server) {
|
||||
if (server && server.proc && !server.proc.killed) {
|
||||
server.proc.kill('SIGTERM');
|
||||
setTimeout(() => { if (!server.proc.killed) server.proc.kill('SIGKILL'); }, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function modeSandboxSite(page, expr, url, setup) {
|
||||
const path = require('path');
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
||||
const consoleLogs = [];
|
||||
page.on('console', msg => {
|
||||
consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500) });
|
||||
});
|
||||
|
||||
const server = startSiteServer(PROJECT_ROOT);
|
||||
process.on('exit', () => stopSiteServer(server));
|
||||
|
||||
try {
|
||||
await server.ready;
|
||||
} catch (err) {
|
||||
stopSiteServer(server);
|
||||
return { mode: 'sandbox', stack: 'site', error: err.message };
|
||||
}
|
||||
|
||||
const baseUrl = 'http://localhost:' + server.port;
|
||||
const targetUrl = url || '/';
|
||||
|
||||
try {
|
||||
await page.goto(baseUrl + targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
|
||||
// Wait for SX boot to complete
|
||||
await page.waitForFunction(
|
||||
() => document.documentElement.getAttribute('data-sx-ready') === 'true',
|
||||
{ timeout: 15000 }
|
||||
).catch(() => {}); // boot might not set this on all pages
|
||||
|
||||
// Run setup SX expression if provided
|
||||
if (setup) {
|
||||
await page.evaluate(s => {
|
||||
try { window.SxKernel.eval(s); } catch(e) { console.error('[site-setup]', e.message); }
|
||||
}, setup);
|
||||
}
|
||||
|
||||
// Evaluate expr — JS if it starts with "document." or "window.", SX otherwise
|
||||
const result = await page.evaluate(expr => {
|
||||
if (!expr) return { result: 'nil' };
|
||||
const isJs = /^(document\.|window\.|globalThis\.|[\(\[]|function|typeof|!|true|false|\d)/.test(expr.trim());
|
||||
if (isJs) {
|
||||
try { return { result: String(eval(expr)) }; }
|
||||
catch(e) { return { result: 'JS Error: ' + e.message }; }
|
||||
}
|
||||
const K = window.SxKernel;
|
||||
if (!K) return { result: 'Error: SxKernel not available' };
|
||||
try {
|
||||
const r = K.eval(expr);
|
||||
if (r === null || r === undefined) return { result: 'nil' };
|
||||
if (typeof r === 'string') return { result: r };
|
||||
return { result: JSON.stringify(r) };
|
||||
} catch(e) { return { result: 'Error: ' + e.message }; }
|
||||
}, expr);
|
||||
|
||||
const logs = consoleLogs.filter(l =>
|
||||
l.text.includes('[sx]') || l.text.includes('[sx-platform]') ||
|
||||
l.type === 'error' || l.type === 'warning'
|
||||
);
|
||||
|
||||
return {
|
||||
mode: 'sandbox',
|
||||
stack: 'site',
|
||||
url: targetUrl,
|
||||
port: server.port,
|
||||
result: result.result,
|
||||
log: logs.length > 0 ? logs : undefined,
|
||||
};
|
||||
} finally {
|
||||
stopSiteServer(server);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mode: sandbox — offline WASM kernel in a blank page, no server needed
|
||||
//
|
||||
@@ -1331,20 +1454,34 @@ const SANDBOX_STACKS = {
|
||||
],
|
||||
};
|
||||
|
||||
async function modeSandbox(page, expr, files, setup, stack, bytecode) {
|
||||
async function modeSandbox(page, expr, files, setup, stack, bytecode, url) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
||||
const WASM_DIR = path.join(PROJECT_ROOT, 'shared/static/wasm');
|
||||
const SX_DIR = path.join(WASM_DIR, 'sx');
|
||||
|
||||
// Full website sandbox — start real OCaml server, navigate directly
|
||||
if (stack === 'site') {
|
||||
return await modeSandboxSite(page, expr, url || setup, setup);
|
||||
}
|
||||
|
||||
const usePlatform = stack === 'platform' || stack === 'platform-hs';
|
||||
|
||||
const consoleLogs = [];
|
||||
page.on('console', msg => {
|
||||
consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500) });
|
||||
});
|
||||
|
||||
// 1. Navigate to blank page
|
||||
await page.goto('about:blank');
|
||||
// 1. Navigate to blank page (use a real URL when platform mode needs XHR)
|
||||
if (usePlatform) {
|
||||
await page.route('**/sandbox.html', route => {
|
||||
route.fulfill({ body: '<!doctype html><html><head></head><body></body></html>', contentType: 'text/html' });
|
||||
});
|
||||
await page.goto('http://localhost/wasm/sandbox.html');
|
||||
} else {
|
||||
await page.goto('about:blank');
|
||||
}
|
||||
|
||||
// 2. Inject WASM kernel
|
||||
const kernelSrc = fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8');
|
||||
@@ -1352,6 +1489,31 @@ async function modeSandbox(page, expr, files, setup, stack, bytecode) {
|
||||
await page.waitForFunction('!!window.SxKernel', { timeout: 10000 });
|
||||
|
||||
// 3. Register FFI primitives + IO suspension driver
|
||||
if (usePlatform) {
|
||||
// Inject the real sx-platform.js — gives us manifest loader, __sxLoadLibrary,
|
||||
// __resolve-symbol, beginModuleLoad/endModuleLoad cycle, the works.
|
||||
// Serve the WASM dir as a file:// base so the platform can fetch manifests & .sxbc
|
||||
const platformSrc = fs.readFileSync(path.join(WASM_DIR, 'sx-platform.js'), 'utf8');
|
||||
// Set up a base URL the platform can fetch from — we'll intercept requests
|
||||
await page.route('**/wasm/**', route => {
|
||||
const url = new URL(route.request().url());
|
||||
const filePath = path.join(WASM_DIR, url.pathname.replace(/.*\/wasm\//, ''));
|
||||
if (fs.existsSync(filePath)) {
|
||||
route.fulfill({ body: fs.readFileSync(filePath), contentType: 'application/octet-stream' });
|
||||
} else {
|
||||
route.fulfill({ status: 404, body: 'Not found: ' + filePath });
|
||||
}
|
||||
});
|
||||
// Inject platform JS as inline script with a fake src for base URL detection
|
||||
await page.addScriptTag({ content: platformSrc });
|
||||
// Wait for platform to boot — it runs synchronously since readyState != "loading"
|
||||
await page.waitForFunction(() => {
|
||||
return document.documentElement.getAttribute('data-sx-ready') === 'true'
|
||||
|| !!window.__sxLoadLibrary;
|
||||
}, { timeout: 30000 });
|
||||
// Give setTimeout-based JIT enable time to run
|
||||
await page.waitForTimeout(200);
|
||||
} else {
|
||||
await page.evaluate(() => {
|
||||
const K = window.SxKernel;
|
||||
|
||||
@@ -1466,6 +1628,16 @@ async function modeSandbox(page, expr, files, setup, stack, bytecode) {
|
||||
K.eval('(define parse sx-parse)');
|
||||
K.eval('(define serialize sx-serialize)');
|
||||
});
|
||||
} // end if/else usePlatform
|
||||
|
||||
// When using platform stack, modules are already loaded by sx-platform.js.
|
||||
// Skip the manual module loading below.
|
||||
if (usePlatform) {
|
||||
// Platform already loaded everything. Just load extra HS modules if platform-hs.
|
||||
if (stack === 'platform-hs') {
|
||||
await page.evaluate(() => window.__sxLoadLibrary('hs-integration'));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Load stack modules
|
||||
const loadErrors = [];
|
||||
@@ -1481,7 +1653,7 @@ async function modeSandbox(page, expr, files, setup, stack, bytecode) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (stack && stack !== 'core') {
|
||||
if (stack && stack !== 'core' && !usePlatform) {
|
||||
const modules = resolveStack(stack);
|
||||
if (modules.length > 0) {
|
||||
// beginModuleLoad if available
|
||||
@@ -1721,7 +1893,7 @@ async function main() {
|
||||
result = await modeEvalAt(browser, url, args.phase || 'before-hydrate', args.expr || '(type-of ~cssx/tw)');
|
||||
break;
|
||||
case 'sandbox':
|
||||
result = await modeSandbox(page, args.expr || '"hello"', args.files || [], args.setup || '', args.stack || '', !!args.bytecode);
|
||||
result = await modeSandbox(page, args.expr || '"hello"', args.files || [], args.setup || '', args.stack || '', !!args.bytecode, args.url || '');
|
||||
break;
|
||||
default:
|
||||
result = { error: `Unknown mode: ${mode}` };
|
||||
|
||||
Reference in New Issue
Block a user