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>
204 lines
8.3 KiB
JavaScript
204 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// _pre-screen-worker-full.js — Test full HS pipeline: compile + activate
|
|
// This tests what actually happens in the browser: hs-activate on an element.
|
|
//
|
|
// Input: HS_BATCH_FILE env var points to a JSON array of source strings.
|
|
// Output: One line per source: RESULT:{"source":"...","status":"ok|error","detail":"..."}
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const PROJECT_ROOT = process.cwd();
|
|
const WASM_DIR = path.join(PROJECT_ROOT, 'shared/static/wasm');
|
|
|
|
// --- Minimal DOM stubs ---
|
|
function makeElement(tag) {
|
|
const el = {
|
|
tagName: tag, _attrs: {}, _children: [], _classes: new Set(),
|
|
style: {}, childNodes: [], children: [], textContent: '',
|
|
nodeType: 1,
|
|
classList: {
|
|
add(c) { el._classes.add(c); },
|
|
remove(c) { el._classes.delete(c); },
|
|
contains(c) { return el._classes.has(c); },
|
|
toggle(c) { if (el._classes.has(c)) el._classes.delete(c); else el._classes.add(c); },
|
|
},
|
|
setAttribute(k, v) { el._attrs[k] = String(v); },
|
|
getAttribute(k) { return el._attrs[k] || null; },
|
|
removeAttribute(k) { delete el._attrs[k]; },
|
|
appendChild(c) { el._children.push(c); return c; },
|
|
insertBefore(c) { el._children.push(c); return c; },
|
|
removeChild(c) { return c; },
|
|
replaceChild(n) { return n; },
|
|
cloneNode() { return makeElement(tag); },
|
|
get innerHTML() { return ''; },
|
|
set innerHTML(v) { el._children = []; },
|
|
get outerHTML() { return `<${tag}>`; },
|
|
dataset: new Proxy({}, {
|
|
get(_, k) { return el._attrs['data-' + k.replace(/[A-Z]/g, c => '-' + c.toLowerCase())]; },
|
|
set(_, k, v) { el._attrs['data-' + k.replace(/[A-Z]/g, c => '-' + c.toLowerCase())] = v; return true; }
|
|
}),
|
|
querySelectorAll() { return []; },
|
|
querySelector() { return null; },
|
|
addEventListener() {}, removeEventListener() {}, dispatchEvent() {},
|
|
};
|
|
return el;
|
|
}
|
|
|
|
global.window = global;
|
|
global.document = {
|
|
createElement: makeElement,
|
|
createDocumentFragment() { return makeElement('fragment'); },
|
|
head: makeElement('head'), body: makeElement('body'),
|
|
querySelector() { return null; }, querySelectorAll() { return []; },
|
|
createTextNode(s) { return { _isText: true, textContent: String(s), nodeType: 3 }; },
|
|
addEventListener() {},
|
|
createComment(s) { return { _isComment: true, textContent: s || '', nodeType: 8 }; },
|
|
getElementsByTagName() { return []; },
|
|
};
|
|
global.localStorage = { getItem() { return null; }, setItem() {}, removeItem() {} };
|
|
global.CustomEvent = class { constructor(n, o) { this.type = n; this.detail = (o || {}).detail || {}; } };
|
|
global.MutationObserver = class { observe() {} disconnect() {} };
|
|
global.requestIdleCallback = fn => setTimeout(fn, 0);
|
|
global.matchMedia = () => ({ matches: false });
|
|
global.navigator = { serviceWorker: { register() { return Promise.resolve(); } } };
|
|
global.location = { href: '', pathname: '/', hostname: 'localhost' };
|
|
global.history = { pushState() {}, replaceState() {} };
|
|
global.fetch = () => Promise.resolve({ ok: true, text() { return Promise.resolve(''); } });
|
|
global.XMLHttpRequest = class { open() {} send() {} };
|
|
|
|
// Load WASM kernel
|
|
require(path.join(WASM_DIR, 'sx_browser.bc.js'));
|
|
const K = globalThis.SxKernel;
|
|
if (!K) { console.error('FATAL: SxKernel not found'); process.exit(1); }
|
|
|
|
// Register FFI primitives
|
|
K.registerNative('host-global', args => {
|
|
const name = args[0];
|
|
return (name in globalThis) ? globalThis[name] : null;
|
|
});
|
|
K.registerNative('host-get', args => {
|
|
const [obj, prop] = args;
|
|
if (obj == null) return null;
|
|
const v = obj[prop];
|
|
return v === undefined ? null : v;
|
|
});
|
|
K.registerNative('host-set!', args => { if (args[0] != null) args[0][args[1]] = args[2]; return args[2]; });
|
|
K.registerNative('host-call', args => {
|
|
const [obj, method, ...rest] = args;
|
|
if (obj == null || typeof obj[method] !== 'function') return null;
|
|
const r = obj[method].apply(obj, rest);
|
|
return r === undefined ? null : r;
|
|
});
|
|
K.registerNative('host-new', args => new (Function.prototype.bind.apply(args[0], [null, ...args.slice(1)])));
|
|
K.registerNative('host-callback', args => {
|
|
const fn = args[0];
|
|
return function() { return K.callFn(fn, Array.from(arguments)); };
|
|
});
|
|
K.registerNative('host-typeof', args => typeof args[0]);
|
|
K.registerNative('host-await', args => args[0]);
|
|
|
|
K.eval('(define SX_VERSION "pre-screen-1.0")');
|
|
K.eval('(define SX_ENGINE "ocaml-vm-wasm-test")');
|
|
K.eval('(define parse sx-parse)');
|
|
K.eval('(define serialize sx-serialize)');
|
|
|
|
// Stub DOM primitives
|
|
K.eval('(define dom-add-class (fn (el cls) nil))');
|
|
K.eval('(define dom-remove-class (fn (el cls) nil))');
|
|
K.eval('(define dom-has-class? (fn (el cls) false))');
|
|
K.eval('(define dom-listen (fn (target event-name handler) nil))');
|
|
K.eval('(define dom-toggle-class (fn (el cls) nil))');
|
|
K.eval('(define dom-set-style! (fn (el prop val) nil))');
|
|
K.eval('(define dom-get-style (fn (el prop) ""))');
|
|
K.eval('(define dom-set-attr! (fn (el k v) nil))');
|
|
K.eval('(define dom-get-attr (fn (el k) nil))');
|
|
K.eval('(define dom-remove-attr! (fn (el k) nil))');
|
|
K.eval('(define dom-set-text! (fn (el t) nil))');
|
|
K.eval('(define dom-get-text (fn (el) ""))');
|
|
K.eval('(define dom-set-html! (fn (el h) nil))');
|
|
K.eval('(define dom-get-html (fn (el) ""))');
|
|
K.eval('(define dom-set-value! (fn (el v) nil))');
|
|
K.eval('(define dom-get-value (fn (el) ""))');
|
|
K.eval('(define dom-query-all (fn (sel) (list)))');
|
|
K.eval('(define dom-query (fn (sel) nil))');
|
|
K.eval('(define dom-query-in (fn (el sel) nil))');
|
|
K.eval('(define dom-query-all-in (fn (el sel) (list)))');
|
|
K.eval('(define dom-parent (fn (el) nil))');
|
|
K.eval('(define dom-children (fn (el) (list)))');
|
|
K.eval('(define dom-closest (fn (el sel) nil))');
|
|
K.eval('(define dom-matches? (fn (el sel) false))');
|
|
K.eval('(define dom-append! (fn (parent child) nil))');
|
|
K.eval('(define dom-remove! (fn (el) nil))');
|
|
K.eval('(define dom-create (fn (tag) {:tag tag :id "" :classes {} :style {} :_hs-activated false}))');
|
|
K.eval('(define io-sleep (fn (ms) nil))');
|
|
K.eval('(define io-dispatch (fn (el event-name detail) nil))');
|
|
K.eval('(define io-log (fn (&rest args) nil))');
|
|
|
|
// Load hyperscript modules (tokenizer, parser, compiler, runtime)
|
|
// Skip integration.sx — it needs load-library! which is browser-only.
|
|
// We define hs-handler inline below.
|
|
const hsFiles = [
|
|
'lib/hyperscript/tokenizer.sx',
|
|
'lib/hyperscript/parser.sx',
|
|
'lib/hyperscript/compiler.sx',
|
|
'lib/hyperscript/runtime.sx',
|
|
];
|
|
for (const f of hsFiles) {
|
|
const src = fs.readFileSync(path.join(PROJECT_ROOT, f), 'utf8');
|
|
const r = K.load(src);
|
|
if (typeof r === 'string' && r.startsWith('Error')) {
|
|
console.error(`Load failed: ${f}: ${r}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Define hs-handler (from integration.sx) — wraps compiled SX in (fn (me) (let ((it nil) (event nil)) <sx>))
|
|
K.eval(`(define hs-handler (fn (src)
|
|
(let ((sx (hs-to-sx-from-source src)))
|
|
(eval-expr
|
|
(list 'fn '(me)
|
|
(list 'let '((it nil) (event nil)) sx))))))`);
|
|
|
|
// Read batch input
|
|
const batchFile = process.env.HS_BATCH_FILE;
|
|
if (!batchFile) { console.error('No HS_BATCH_FILE env var'); process.exit(1); }
|
|
const sources = JSON.parse(fs.readFileSync(batchFile, 'utf8'));
|
|
|
|
// Test each source — full pipeline: compile + wrap in handler + activate
|
|
for (const src of sources) {
|
|
const escaped = src.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
|
|
let status = 'ok';
|
|
let detail = '';
|
|
|
|
try {
|
|
// Step 1: Compile
|
|
const compileExpr = `(hs-to-sx-from-source "${escaped}")`;
|
|
const compiled = K.eval(compileExpr);
|
|
if (compiled === null || compiled === undefined) {
|
|
status = 'error';
|
|
detail = 'compile returned nil';
|
|
} else if (typeof compiled === 'string' && compiled.startsWith('Error')) {
|
|
status = 'error';
|
|
detail = compiled.slice(0, 200);
|
|
} else {
|
|
// Step 2: Try to create a handler (hs-handler wraps in fn+let)
|
|
const handlerExpr = `(hs-handler "${escaped}")`;
|
|
const handler = K.eval(handlerExpr);
|
|
if (handler === null || handler === undefined) {
|
|
status = 'error';
|
|
detail = 'handler returned nil';
|
|
} else if (typeof handler === 'string' && handler.startsWith('Error')) {
|
|
status = 'error';
|
|
detail = handler.slice(0, 200);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
status = 'error';
|
|
detail = e.message ? e.message.slice(0, 200) : String(e).slice(0, 200);
|
|
}
|
|
|
|
console.log(`RESULT:${JSON.stringify({ source: src, status, detail })}`);
|
|
}
|