#!/usr/bin/env node // _pre-screen-worker-timed.js — Test each source with per-source timing // Reports exact time for each source so we can identify slow ones. 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 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 inline 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 with timing for (const src of sources) { const escaped = src.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); let status = 'ok'; let detail = ''; const t0 = Date.now(); try { // Test compilation const compileResult = K.eval(`(inspect (hs-to-sx-from-source "${escaped}"))`); if (typeof compileResult === 'string' && compileResult.startsWith('Error')) { status = 'error'; detail = compileResult.slice(0, 200); } else { // Test handler creation const handlerResult = K.eval(`(hs-handler "${escaped}")`); if (typeof handlerResult === 'string' && handlerResult.startsWith('Error')) { status = 'error'; detail = handlerResult.slice(0, 200); } } } catch (e) { status = 'error'; detail = e.message ? e.message.slice(0, 200) : String(e).slice(0, 200); } const elapsed = Date.now() - t0; console.log(`RESULT:${JSON.stringify({ source: src, status, detail, ms: elapsed })}`); }