@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 +282 -0
- package/dist/cjs/env.js +57 -0
- package/dist/cjs/hydrate.js +472 -0
- package/dist/cjs/index.js +58 -0
- package/dist/cjs/keys.js +62 -0
- package/dist/cjs/load.js +82 -0
- package/dist/cjs/metadata.js +102 -0
- package/dist/cjs/render.js +341 -0
- package/dist/esm/env.js +38 -0
- package/dist/esm/hydrate.js +453 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/keys.js +43 -0
- package/dist/esm/load.js +63 -0
- package/dist/esm/metadata.js +83 -0
- package/dist/esm/render.js +311 -0
- package/env.js +43 -0
- package/hydrate.js +388 -0
- package/index.js +40 -0
- package/keys.js +54 -0
- package/load.js +81 -0
- package/metadata.js +117 -0
- package/package.json +48 -0
- package/render.js +386 -0
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
|
package/dist/cjs/env.js
ADDED
|
@@ -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
|
+
};
|