@symbo.ls/brender 3.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # @symbo.ls/brender
2
+
3
+ Server-side renderer and client-side hydrator for DOMQL/Symbols apps. Converts DOMQL element trees into static HTML on the server, then reconnects that HTML to live DOMQL elements in the browser — without re-rendering from scratch.
4
+
5
+ ## How it works
6
+
7
+ Brender has two phases: **render** (server) and **liquidate** (browser).
8
+
9
+ ### Render (DOMQL -> HTML)
10
+
11
+ On the server (or at build time), brender:
12
+
13
+ 1. Creates a virtual DOM environment using [linkedom](https://github.com/WebReflection/linkedom)
14
+ 2. Runs DOMQL `create()` against that virtual DOM — the full component tree resolves, extends merge, props apply, and real DOM nodes are produced
15
+ 3. Walks every element node and stamps a `data-br="br-N"` attribute (sequential key)
16
+ 4. Walks the DOMQL element tree and records which `data-br` key belongs to which DOMQL element (`__ref.__brKey`)
17
+ 5. Returns the HTML string, a registry (`{br-key: domqlElement}`), and the element tree
18
+
19
+ ```
20
+ DOMQL Source Virtual DOM Output
21
+
22
+ { tag: 'div', <div> <div data-br="br-1">
23
+ Title: { <h1>Hello</h1> <h1 data-br="br-2">Hello</h1>
24
+ tag: 'h1', <p>World</p> <p data-br="br-3">World</p>
25
+ text: 'Hello' </div> </div>
26
+ },
27
+ Body: { + registry:
28
+ tag: 'p', br-1 -> root element
29
+ text: 'World' br-2 -> Title element
30
+ } br-3 -> Body element
31
+ }
32
+ ```
33
+
34
+ ### Liquidate (HTML -> DOMQL)
35
+
36
+ In the browser, when the app JS loads:
37
+
38
+ 1. The pre-rendered HTML is already in the DOM — the user sees the page instantly
39
+ 2. DOMQL re-creates the element tree from the same source definitions, but skips DOM creation (the nodes already exist)
40
+ 3. `hydrate()` walks the DOMQL tree and the real DOM simultaneously, matching `data-br` keys
41
+ 4. For each match: `element.node = domNode` and `domNode.ref = element`
42
+ 5. DOMQL now owns every node — reactive updates, event handlers, and state changes work as if the page was client-rendered
43
+
44
+ ```
45
+ Browser DOM (static) DOMQL Tree (no nodes) After hydrate()
46
+
47
+ <div data-br="br-1"> root { root.node = <div>
48
+ <h1 data-br="br-2">Hello</h1> __ref: {__brKey: 'br-1'} Title.node = <h1>
49
+ <p data-br="br-3">World</p> Title: { Body.node = <p>
50
+ </div> __ref: {__brKey: 'br-2'}
51
+ } domNode.ref = element
52
+ collectBrNodes() Body: { (bidirectional link)
53
+ {br-1: div, __ref: {__brKey: 'br-3'}
54
+ br-2: h1, }
55
+ br-3: p} }
56
+ ```
57
+
58
+ ## Files
59
+
60
+ | File | Purpose |
61
+ |------|---------|
62
+ | `render.js` | `render()` — full project render via smbls pipeline; `renderElement()` — single component via @domql/element |
63
+ | `hydrate.js` | `collectBrNodes()` — scans DOM for data-br nodes; `hydrate()` — reconnects DOMQL tree to DOM |
64
+ | `env.js` | `createEnv()` — linkedom virtual DOM with browser API stubs (requestAnimationFrame, history, location, etc.) |
65
+ | `keys.js` | `resetKeys()`, `assignKeys()` — stamps data-br on DOM nodes; `mapKeysToElements()` — builds registry |
66
+ | `metadata.js` | `extractMetadata()`, `generateHeadHtml()` — SEO meta tags from page definitions |
67
+ | `load.js` | `loadProject()` — imports a symbols/ directory; `loadAndRenderAll()` — renders every route |
68
+ | `index.js` | Re-exports everything |
69
+
70
+ ## API
71
+
72
+ ### `renderElement(elementDef, options?)`
73
+
74
+ Renders a single DOMQL element definition to HTML. Uses `@domql/element` create directly — no full smbls bootstrap needed.
75
+
76
+ ```js
77
+ import { renderElement } from '@symbo.ls/brender'
78
+
79
+ const result = await renderElement(
80
+ { tag: 'div', text: 'Hello', Child: { tag: 'span', text: 'World' } }
81
+ )
82
+
83
+ // result.html -> '<div data-br="br-1">Hello<span data-br="br-2">World</span></div>'
84
+ // result.registry -> { 'br-1': rootElement, 'br-2': childElement }
85
+ // result.element -> the DOMQL element tree
86
+ ```
87
+
88
+ With components and designSystem context:
89
+
90
+ ```js
91
+ const result = await renderElement(pageDef, {
92
+ context: {
93
+ components: { Nav, Footer, HeroSection },
94
+ designSystem: { color, font, spacing },
95
+ state: { user: null },
96
+ functions: { initApp },
97
+ methods: { navigate }
98
+ }
99
+ })
100
+ ```
101
+
102
+ ### `render(data, options?)`
103
+
104
+ Renders a full Symbols project. Requires the smbls pipeline (createDomqlElement) — handles routing, state, designSystem initialization, the full app context.
105
+
106
+ ```js
107
+ import { render, loadProject } from '@symbo.ls/brender'
108
+
109
+ const data = await loadProject('/path/to/project')
110
+ const result = await render(data, { route: '/about' })
111
+
112
+ // result.html -> full page HTML with data-br keys
113
+ // result.metadata -> { title, description, og:image, ... }
114
+ // result.registry -> { br-key: domqlElement }
115
+ // result.element -> root DOMQL element
116
+ ```
117
+
118
+ ### `hydrate(element, options?)`
119
+
120
+ Client-side. Reconnects a DOMQL element tree to existing DOM nodes via data-br keys.
121
+
122
+ ```js
123
+ import { collectBrNodes, hydrate } from '@symbo.ls/brender/hydrate'
124
+
125
+ // After DOMQL creates the element tree (without DOM nodes):
126
+ const hydrated = hydrate(elementTree, { root: document.body })
127
+
128
+ // Now every element.node points to the real DOM node
129
+ // and every domNode.ref points back to the DOMQL element
130
+ ```
131
+
132
+ ### `loadProject(path)`
133
+
134
+ Imports a standard `symbols/` directory structure:
135
+
136
+ ```
137
+ project/
138
+ symbols/
139
+ app.js -> data.app
140
+ state.js -> data.state
141
+ config.js -> data.config
142
+ dependencies.js -> data.dependencies
143
+ components/
144
+ index.js -> data.components
145
+ pages/
146
+ index.js -> data.pages
147
+ designSystem/
148
+ index.js -> data.designSystem
149
+ functions/
150
+ index.js -> data.functions
151
+ methods/
152
+ index.js -> data.methods
153
+ snippets/
154
+ index.js -> data.snippets
155
+ files/
156
+ index.js -> data.files
157
+ ```
158
+
159
+ ### `createEnv(html?)`
160
+
161
+ Creates a linkedom virtual DOM environment with stubs for browser APIs that DOMQL expects:
162
+
163
+ - `window.requestAnimationFrame` / `cancelAnimationFrame`
164
+ - `window.history` (pushState, replaceState)
165
+ - `window.location` (pathname, search, hash, origin)
166
+ - `window.URL`, `window.scrollTo`
167
+ - `globalThis.Node`, `globalThis.HTMLElement`, `globalThis.Window` (for instanceof checks in @domql/utils)
168
+
169
+ ### `generateHeadHtml(metadata)`
170
+
171
+ Converts a metadata object into HTML head tags:
172
+
173
+ ```js
174
+ generateHeadHtml({ title: 'My Page', description: 'About', 'og:image': '/img.png' })
175
+ // -> '<meta charset="UTF-8">\n<title>My Page</title>\n<meta name="description" content="About">\n<meta property="og:image" content="/img.png">'
176
+ ```
177
+
178
+ ## Examples
179
+
180
+ The `examples/` directory contains runnable experiments. Copy a project's source into `examples/` first (gitignored), then run:
181
+
182
+ ### Render to static HTML
183
+
184
+ ```bash
185
+ # Render all routes
186
+ node examples/render.js rita
187
+
188
+ # Render specific route
189
+ node examples/render.js rita /about
190
+
191
+ # Output goes to examples/rita_built/
192
+ # index.html - static HTML with data-br keys
193
+ # index-tree.json - DOMQL element tree (for inspection)
194
+ # index-registry.json - br-key -> element path mapping
195
+ ```
196
+
197
+ ### Liquidate (full round-trip)
198
+
199
+ ```bash
200
+ node examples/liquidate.js rita /
201
+
202
+ # Output:
203
+ # Step 1: Render DOMQL -> HTML (server side)
204
+ # HTML: 7338 chars
205
+ # data-br keys assigned: 129
206
+ #
207
+ # Step 2: Parse HTML into DOM (simulating browser)
208
+ # DOM nodes with data-br: 129
209
+ #
210
+ # Step 3: DOMQL element tree ready
211
+ # DOMQL elements: 201
212
+ #
213
+ # Step 4: Hydrate - remap DOMQL elements to DOM nodes
214
+ # Linked: 129 elements
215
+ # Unlinked: 0 elements
216
+ #
217
+ # Step 5: data-br -> DOMQL element mapping
218
+ # br-1 <main > root
219
+ # br-2 <nav > root.Nav
220
+ # br-3 <div > root.Nav.Inner
221
+ # br-4 <div > root.Nav.Inner.Logo "Rita Katona"
222
+ # br-11 <section > root.Hero
223
+ # br-15 <h1 > root.Hero.Content.Headline "Are you looking for..."
224
+ # ...
225
+ #
226
+ # Step 6: Mutate via DOMQL (proves elements own their nodes)
227
+ # Before: "Rita Katona..."
228
+ # After: "[LIQUIDATED] Rita Katona..."
229
+ # Same ref: true
230
+ ```
231
+
232
+ ### npm scripts
233
+
234
+ ```bash
235
+ npm run render:rita # render rita project
236
+ npm run render:survey # render survey project
237
+ npm run render:all # render both
238
+ npm run liquidate:rita # full liquidation round-trip for rita
239
+ npm run liquidate:survey # full liquidation round-trip for survey
240
+ ```
241
+
242
+ ## Experiment results
243
+
244
+ Tested against two real Symbols projects:
245
+
246
+ ### rita (portfolio site, 6 routes, 39 components)
247
+
248
+ | Route | HTML size | data-br keys | DOMQL elements | Link rate |
249
+ |-------|-----------|-------------|----------------|-----------|
250
+ | `/` | 7,338 | 129 | 201 | 100% |
251
+ | `/about` | 4,623 | 87 | 133 | 100% |
252
+ | `/from-workshops-to-1-on-1s` | 7,043 | 119 | - | 100% |
253
+ | `/hire-me-as-a-freelancer` | 5,195 | 82 | - | 100% |
254
+ | `/references-and-partners` | 6,102 | 91 | - | 100% |
255
+ | `/angel-investment` | 4,800 | 85 | - | 100% |
256
+
257
+ ### survey (benchmark app, 1 route, 7 components)
258
+
259
+ | Route | HTML size | data-br keys | Link rate |
260
+ |-------|-----------|-------------|-----------|
261
+ | `/` | 29,625 | 203 | 100% |
262
+
263
+ The difference between "data-br keys" and "DOMQL elements" is that some elements are virtual (text nodes, internal refs) and don't produce HTML element nodes.
264
+
265
+ ## The data-br contract
266
+
267
+ The `data-br` attribute is the bridge between server and client. The contract:
268
+
269
+ 1. **Sequential**: Keys are assigned in DOM tree order (`br-0`, `br-1`, `br-2`, ...) by depth-first traversal
270
+ 2. **Deterministic**: Same DOMQL input always produces the same key assignments — because DOMQL's `create()` is deterministic and `assignKeys()` walks in document order
271
+ 3. **Element nodes only**: Only `nodeType === 1` (elements) get keys, not text nodes or comments
272
+ 4. **Bidirectional**: After hydration, `element.node` and `node.ref` point to each other
273
+
274
+ This means the server and client don't need to exchange the registry — as long as both run the same DOMQL source, the keys will match. The registry JSON is exported for debugging and inspection only.
275
+
276
+ ## Architecture notes
277
+
278
+ - `renderElement()` uses `@domql/element` create directly — lightweight, no smbls bootstrap. Good for individual components
279
+ - `render()` uses the full `smbls/src/createDomql.js` pipeline — handles routing, designSystem initialization, uikit defaults, the works. Needed for complete apps
280
+ - `hydrate.js` is browser-only code (no linkedom dependency) — it's exported separately via `@symbo.ls/brender/hydrate`
281
+ - `createEnv()` sets `globalThis.window/document/Node/HTMLElement` because `@domql/utils` `isDOMNode` uses `instanceof` checks against global constructors
282
+ - `onRender` callbacks that do network requests or call `s.update()` will error during SSR — this is expected and harmless since the HTML is already produced before those callbacks fire
@@ -0,0 +1,57 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var env_exports = {};
19
+ __export(env_exports, {
20
+ createEnv: () => createEnv
21
+ });
22
+ module.exports = __toCommonJS(env_exports);
23
+ var import_linkedom = require("linkedom");
24
+ const createEnv = (html = "<!DOCTYPE html><html><head></head><body></body></html>") => {
25
+ const { window, document } = (0, import_linkedom.parseHTML)(html);
26
+ if (!window.requestAnimationFrame) {
27
+ window.requestAnimationFrame = (fn) => setTimeout(fn, 0);
28
+ }
29
+ if (!window.cancelAnimationFrame) {
30
+ window.cancelAnimationFrame = (id) => clearTimeout(id);
31
+ }
32
+ if (!window.history) {
33
+ window.history = {
34
+ pushState: () => {
35
+ },
36
+ replaceState: () => {
37
+ },
38
+ state: null
39
+ };
40
+ }
41
+ if (!window.location) {
42
+ window.location = { pathname: "/", search: "", hash: "", origin: "http://localhost" };
43
+ }
44
+ if (!window.URL) {
45
+ window.URL = URL;
46
+ }
47
+ if (!window.scrollTo) {
48
+ window.scrollTo = () => {
49
+ };
50
+ }
51
+ globalThis.window = window;
52
+ globalThis.document = document;
53
+ globalThis.Node = window.Node || globalThis.Node;
54
+ globalThis.HTMLElement = window.HTMLElement || globalThis.HTMLElement;
55
+ globalThis.Window = window.constructor;
56
+ return { window, document };
57
+ };