add DOM bridge, async/await, window export, and native_js to JS target

- el_runtime.js: add 19 dom_* builtins (browser-only, throw in Node),
  window_set/window_get for exposing El functions to the browser global
  scope, and native_js/native_js_call escape hatches for third-party libs
- codegen-js.el: destructure all new builtins in generated preamble; add
  @async decorator support that emits async function + await at call sites
  for known-async HTTP builtins and user-declared @async functions; pre-
  registration pass ensures forward calls to @async functions get await
- spec/codegen-js.md: mark Phase 3 (DOM bridge) implemented, document
  @async approach and its limitations, update builtin table and status
- examples/browser-counter.el: canonical example showing dom_get_element,
  dom_set_text, dom_is_null, window_set, and state_set/get
This commit is contained in:
Will Anderson
2026-05-04 10:29:43 -05:00
parent 1ed2dc3c11
commit a54b2bebf9
4 changed files with 335 additions and 17 deletions
+162
View File
@@ -522,6 +522,152 @@ function math_sin(f) { return Math.sin(f); }
function math_cos(f) { return Math.cos(f); }
function math_pi() { return Math.PI; }
// ── DOM bridge (browser-only) ──────────────────────────────────────────────
//
// These functions wrap the browser DOM API. Each throws a descriptive error
// when called from a Node environment, mirroring the pattern used by fs_*
// in browser mode.
function _ensureBrowser(name) {
if (IS_NODE) {
throw new Error(`${name}: not supported in Node runtime — DOM is browser-only`);
}
}
function dom_get_element(id) {
_ensureBrowser('dom_get_element');
return document.getElementById(String(id));
}
function dom_get_value(el) {
_ensureBrowser('dom_get_value');
return el == null ? '' : String(el.value ?? '');
}
function dom_set_value(el, v) {
_ensureBrowser('dom_set_value');
if (el != null) el.value = String(v);
}
function dom_get_text(el) {
_ensureBrowser('dom_get_text');
return el == null ? '' : String(el.textContent ?? '');
}
function dom_set_text(el, text) {
_ensureBrowser('dom_set_text');
if (el != null) el.textContent = String(text);
}
function dom_set_prop(el, prop, val) {
_ensureBrowser('dom_set_prop');
if (el != null) el[String(prop)] = val;
}
function dom_get_prop(el, prop) {
_ensureBrowser('dom_get_prop');
if (el == null) return null;
const v = el[String(prop)];
return v === undefined ? null : v;
}
function dom_set_style(el, prop, val) {
_ensureBrowser('dom_set_style');
if (el != null) el.style[String(prop)] = String(val);
}
function dom_add_class(el, cls) {
_ensureBrowser('dom_add_class');
if (el != null) el.classList.add(String(cls));
}
function dom_remove_class(el, cls) {
_ensureBrowser('dom_remove_class');
if (el != null) el.classList.remove(String(cls));
}
function dom_show(el) {
_ensureBrowser('dom_show');
if (el != null) el.style.display = '';
}
function dom_hide(el) {
_ensureBrowser('dom_hide');
if (el != null) el.style.display = 'none';
}
function dom_listen(el, event, handler) {
_ensureBrowser('dom_listen');
if (el != null) el.addEventListener(String(event), handler);
}
function dom_query(selector) {
_ensureBrowser('dom_query');
return document.querySelector(String(selector));
}
function dom_query_all(selector) {
_ensureBrowser('dom_query_all');
return Array.from(document.querySelectorAll(String(selector)));
}
function dom_create(tag) {
_ensureBrowser('dom_create');
return document.createElement(String(tag));
}
function dom_append(parent, child) {
_ensureBrowser('dom_append');
if (parent != null && child != null) parent.appendChild(child);
}
function dom_remove(el) {
_ensureBrowser('dom_remove');
if (el != null) el.remove();
}
function dom_is_null(el) {
return el === null || el === undefined;
}
// ── Window export helpers ──────────────────────────────────────────────────
//
// Expose El functions to the browser's global scope so they can be called
// from inline event handlers (onclick="increment()") or by external JS.
// In Node mode, writes to globalThis so the same pattern works in tests.
function window_set(name, val) {
if (typeof window !== 'undefined') {
window[String(name)] = val;
} else if (typeof globalThis !== 'undefined') {
globalThis[String(name)] = val;
}
}
function window_get(name) {
if (typeof window !== 'undefined') {
const v = window[String(name)];
return v === undefined ? null : v;
}
return null;
}
// ── native_js escape hatch ─────────────────────────────────────────────────
//
// Evaluate arbitrary JS from El source. Intended for calling third-party
// browser libraries (Supabase, Stripe, etc.) until proper El bindings exist.
// Use sparingly — this bypasses El's type system entirely.
function native_js(code) {
// eslint-disable-next-line no-eval
return eval(String(code));
}
function native_js_call(obj, method, args) {
if (obj == null) throw new Error('native_js_call: object is null');
return obj[String(method)](...(Array.isArray(args) ? args : []));
}
// ── Stubs for not-yet-supported features ───────────────────────────────────
//
// These compile but throw when called. See spec/codegen-js.md §7.
@@ -632,6 +778,15 @@ const __el = {
// Float / math
float_to_str, int_to_float, float_to_int, format_float, decimal_round,
str_to_float, math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi,
// DOM bridge (browser-only)
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
dom_append, dom_remove, dom_is_null,
// Window export helpers
window_set, window_get,
// native_js escape hatch
native_js, native_js_call,
// CGI / DHARMA / Engram / LLM (stubs)
el_cgi_init,
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
@@ -676,4 +831,11 @@ export {
dharma_connect, dharma_send, dharma_activate, dharma_emit, dharma_field,
engram_node, engram_search, engram_activate,
llm_call, llm_call_system,
// DOM bridge
dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,
dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,
dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,
dom_append, dom_remove, dom_is_null,
// Window / native_js
window_set, window_get, native_js, native_js_call,
};