From 84230b177044462a287c9f944128d0fc88210acd Mon Sep 17 00:00:00 2001
From: Bendik Aagaard Lynghaug
Date: Tue, 8 Sep 2026 16:56:53 +0200
Subject: [PATCH] =?UTF-8?q?feat(html):=20browser=20renderer=20=E2=80=94=20?=
=?UTF-8?q?runtime-cached=20tagged=20templates?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Each template literal parses once, keyed by its strings array in a
WeakMap: static parts become a with hole markers, renders
clone and fill. Interpolated values never pass through innerHTML (child
values become text nodes or adopted DOM; attributes go through
setAttribute). Event handlers set as properties so nanomorph copies them.
Document-level roots (//) parse via DOMParser because
drops them. Replaces the v7 browserify transform entirely —
production speed is now a runtime property.
Tests run in happy-dom (new devDependency), including a check that server
and browser renderers serialize identical markup.
Co-Authored-By: Claude Fable 5
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
---
package.json | 7 +-
packages/html/browser.js | 270 +++++++++++++++++++++++++++--
packages/html/package.json | 2 +
packages/html/test/browser.test.js | 148 ++++++++++++++++
4 files changed, 415 insertions(+), 12 deletions(-)
create mode 100644 packages/html/test/browser.test.js
diff --git a/package.json b/package.json
index 399dba4..522d36d 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,11 @@
"node": ">=24"
},
"scripts": {
- "test": "node --test packages/core/test/ packages/html/test/"
+ "test": "node --test packages/core/test/ packages/html/test/ packages/component/test/"
},
"repository": "choojs/choo",
- "license": "MIT"
+ "license": "MIT",
+ "devDependencies": {
+ "happy-dom": "^20.14.0"
+ }
}
diff --git a/packages/html/browser.js b/packages/html/browser.js
index 65c97db..0aef7f0 100644
--- a/packages/html/browser.js
+++ b/packages/html/browser.js
@@ -1,15 +1,265 @@
-// Phase 2 (rendering core) lands here: a runtime-only tagged template that
-// parses each template once (keyed by its strings array in a WeakMap) and
-// instantiates DOM by cloning. No compile step, ever.
+// The v8 browser renderer: a runtime-only tagged template.
//
-// Until then, importing @choojs/html in a browser build fails loudly rather
-// than silently rendering nothing.
+// Each unique template literal is parsed once, keyed by its (frozen,
+// per-call-site) strings array in a WeakMap: the static parts become a
+// element with markers where the holes are, plus a list of
+// instructions (node paths + hole indices). Every render clones the
+// template and fills the holes. No compile step, no HTML re-parsing on
+// re-render — production speed is a property of the runtime, not of a
+// build tool (this replaces nanohtml's browserify/babel transform).
+//
+// Interpolated values never pass through innerHTML: child values become
+// text nodes or adopted DOM nodes, attribute values go through
+// setAttribute. Only the author-written static strings are parsed as HTML.
+//
+// Known limits (documented, matching or narrowing nanohtml's):
+// - dynamic tag names (html`<${tag}>`) are not supported
+// - holes inside '}
`
+ assert.strictEqual(el.querySelector('script'), null)
+ assert.strictEqual(el.textContent, '')
+})
+
+test('the same call site reuses its parsed template', () => {
+ const view = (x) => html`${x}`
+ const a = view('a')
+ const b = view('b')
+ assert.notStrictEqual(a, b, 'each render is a fresh node')
+ assert.strictEqual(a.textContent, 'a')
+ assert.strictEqual(b.textContent, 'b')
+})
+
+test('nested templates embed as nodes', () => {
+ const inner = html`hi`
+ const el = html`