@symbo.ls/brender 3.6.6 → 3.6.8
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 +76 -6
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/prefetch.js +199 -0
- package/dist/cjs/render.js +335 -36
- package/dist/esm/index.js +9 -2
- package/dist/esm/prefetch.js +170 -0
- package/dist/esm/render.js +335 -36
- package/env.js +2 -2
- package/hydrate.js +8 -8
- package/index.js +10 -3
- package/keys.js +3 -3
- package/package.json +3 -3
- package/prefetch.js +256 -0
- package/render.js +379 -34
package/README.md
CHANGED
|
@@ -59,12 +59,14 @@ Browser DOM (static) DOMQL Tree (no nodes) After hydrate()
|
|
|
59
59
|
|
|
60
60
|
| File | Purpose |
|
|
61
61
|
|------|---------|
|
|
62
|
-
| `render.js` | `render()` — full project render via smbls pipeline; `renderElement()` — single component
|
|
62
|
+
| `render.js` | `render()` — full project render via smbls pipeline; `renderElement()` — single component; `renderPage()` — complete HTML page |
|
|
63
63
|
| `hydrate.js` | `collectBrNodes()` — scans DOM for data-br nodes; `hydrate()` — reconnects DOMQL tree to DOM |
|
|
64
64
|
| `env.js` | `createEnv()` — linkedom virtual DOM with browser API stubs (requestAnimationFrame, history, location, etc.) |
|
|
65
65
|
| `keys.js` | `resetKeys()`, `assignKeys()` — stamps data-br on DOM nodes; `mapKeysToElements()` — builds registry |
|
|
66
66
|
| `metadata.js` | Re-exports from [`@symbo.ls/helmet`](../helmet/) — `extractMetadata()`, `generateHeadHtml()`, `resolveMetadata()`, `applyMetadata()` |
|
|
67
|
+
| `prefetch.js` | `prefetchPageData()` — SSR data prefetching via DB adapter (Supabase); `injectPrefetchedState()` — injects fetched data into page definitions |
|
|
67
68
|
| `load.js` | `loadProject()` — imports a symbols/ directory; `loadAndRenderAll()` — renders every route |
|
|
69
|
+
| `sitemap.js` | `generateSitemap()` — generates sitemap.xml from routes |
|
|
68
70
|
| `index.js` | Re-exports everything |
|
|
69
71
|
|
|
70
72
|
## API
|
|
@@ -101,7 +103,7 @@ const result = await renderElement(pageDef, {
|
|
|
101
103
|
|
|
102
104
|
### `render(data, options?)`
|
|
103
105
|
|
|
104
|
-
Renders a full Symbols project. Requires the smbls pipeline (createDomqlElement) — handles routing, state, designSystem initialization, the full app context.
|
|
106
|
+
Renders a full Symbols project. Requires the smbls pipeline (createDomqlElement) — handles routing, state, designSystem initialization, the full app context. Uses esbuild to bundle the smbls source tree (cached after first call).
|
|
105
107
|
|
|
106
108
|
```js
|
|
107
109
|
import { render, loadProject } from '@symbo.ls/brender'
|
|
@@ -109,10 +111,40 @@ import { render, loadProject } from '@symbo.ls/brender'
|
|
|
109
111
|
const data = await loadProject('/path/to/project')
|
|
110
112
|
const result = await render(data, { route: '/about' })
|
|
111
113
|
|
|
112
|
-
// result.html
|
|
113
|
-
// result.metadata
|
|
114
|
-
// result.
|
|
115
|
-
// result.
|
|
114
|
+
// result.html -> full page HTML with data-br keys
|
|
115
|
+
// result.metadata -> { title, description, og:image, ... }
|
|
116
|
+
// result.emotionCSS -> array of CSS rule strings from emotion
|
|
117
|
+
// result.registry -> { br-key: domqlElement }
|
|
118
|
+
// result.element -> root DOMQL element
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `renderPage(data, route, options?)`
|
|
122
|
+
|
|
123
|
+
Renders a complete, ready-to-serve HTML page. Combines `render()` output with metadata, CSS (emotion + global), fonts, and optional ISR client bundle.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
import { renderPage, loadProject } from '@symbo.ls/brender'
|
|
127
|
+
|
|
128
|
+
const data = await loadProject('/path/to/project')
|
|
129
|
+
const result = await renderPage(data, '/about', {
|
|
130
|
+
lang: 'ka',
|
|
131
|
+
prefetch: true // enable SSR data prefetching
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// result.html -> complete <!DOCTYPE html> page
|
|
135
|
+
// result.route -> '/about'
|
|
136
|
+
// result.brKeyCount -> number of data-br keys
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `prefetchPageData(data, route, options?)`
|
|
140
|
+
|
|
141
|
+
Fetches data for a page's declarative `fetch` definitions during SSR, using the project's DB adapter (e.g. Supabase).
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { prefetchPageData } from '@symbo.ls/brender'
|
|
145
|
+
|
|
146
|
+
const stateUpdates = await prefetchPageData(data, '/blog')
|
|
147
|
+
// stateUpdates -> { articles: [...], events: [...] }
|
|
116
148
|
```
|
|
117
149
|
|
|
118
150
|
### `hydrate(element, options?)`
|
|
@@ -177,6 +209,40 @@ generateHeadHtml({ title: 'My Page', description: 'About', 'og:image': '/img.png
|
|
|
177
209
|
|
|
178
210
|
Metadata values can also be functions — see the [helmet plugin](../helmet/) for details.
|
|
179
211
|
|
|
212
|
+
## CLI
|
|
213
|
+
|
|
214
|
+
The `smbls brender` command renders all static routes for a project:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Basic usage — renders all static routes to dist-brender/
|
|
218
|
+
smbls brender
|
|
219
|
+
|
|
220
|
+
# Custom output directory
|
|
221
|
+
smbls brender --out-dir build
|
|
222
|
+
|
|
223
|
+
# Disable ISR client bundle
|
|
224
|
+
smbls brender --no-isr
|
|
225
|
+
|
|
226
|
+
# Disable SSR data prefetching
|
|
227
|
+
smbls brender --no-prefetch
|
|
228
|
+
|
|
229
|
+
# Watch mode — re-renders on file changes
|
|
230
|
+
smbls brender --watch
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Output directory defaults to `dist-brender` (or `brenderDistDir` from `symbols.json`) to avoid conflicting with the SPA's `dist/` folder.
|
|
234
|
+
|
|
235
|
+
Param routes (e.g. `/blog/:id`) are automatically skipped — they need runtime data.
|
|
236
|
+
|
|
237
|
+
### symbols.json
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"brender": true,
|
|
242
|
+
"brenderDistDir": "dist-brender"
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
180
246
|
## Examples
|
|
181
247
|
|
|
182
248
|
The `examples/` directory contains runnable experiments. Copy a project's source into `examples/` first (gitignored), then run:
|
|
@@ -279,9 +345,13 @@ This means the server and client don't need to exchange the registry — as long
|
|
|
279
345
|
|
|
280
346
|
- `renderElement()` uses `@domql/element` create directly — lightweight, no smbls bootstrap. Good for individual components
|
|
281
347
|
- `render()` uses the full `smbls/src/createDomql.js` pipeline — handles routing, designSystem initialization, uikit defaults, the works. Needed for complete apps
|
|
348
|
+
- The smbls source is bundled with esbuild (cached after first call) because the monorepo uses extensionless/directory imports that Node.js ESM can't resolve natively. The esbuild plugin patches SSR-incompatible code (window references, createRequire, circular imports) and stubs out browser-only packages (@symbo.ls/sync)
|
|
349
|
+
- `render()` sets `globalThis.document` and `globalThis.location` before each render to match the linkedom virtual env, then restores them after. This allows the bundled smbls code (which reads `window = globalThis`) to work in SSR
|
|
282
350
|
- `hydrate.js` is browser-only code (no linkedom dependency) — it's exported separately via `@symbo.ls/brender/hydrate`
|
|
283
351
|
- `createEnv()` sets `globalThis.window/document/Node/HTMLElement` because `@domql/utils` `isDOMNode` uses `instanceof` checks against global constructors
|
|
352
|
+
- Emotion CSS is extracted from the CSSOM sheet rules (emotion uses `insertRule()` which doesn't populate `textContent` in linkedom)
|
|
284
353
|
- `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
|
|
354
|
+
- Data prefetching (`prefetch.js`) walks page definitions to find `fetch` declarations, then executes them against the DB adapter before rendering. Fetched data is injected into page state so components render with real content
|
|
285
355
|
|
|
286
356
|
## Theme support
|
|
287
357
|
|
package/dist/cjs/index.js
CHANGED
|
@@ -25,13 +25,16 @@ __export(index_exports, {
|
|
|
25
25
|
generateHeadHtml: () => import_metadata.generateHeadHtml,
|
|
26
26
|
generateSitemap: () => import_sitemap.generateSitemap,
|
|
27
27
|
hydrate: () => import_hydrate.hydrate,
|
|
28
|
+
injectPrefetchedState: () => import_prefetch.injectPrefetchedState,
|
|
28
29
|
loadAndRenderAll: () => import_load.loadAndRenderAll,
|
|
29
30
|
loadProject: () => import_load.loadProject,
|
|
30
31
|
mapKeysToElements: () => import_keys.mapKeysToElements,
|
|
32
|
+
prefetchPageData: () => import_prefetch.prefetchPageData,
|
|
31
33
|
render: () => import_render.render,
|
|
32
34
|
renderElement: () => import_render.renderElement,
|
|
33
35
|
renderPage: () => import_render.renderPage,
|
|
34
36
|
renderRoute: () => import_render.renderRoute,
|
|
37
|
+
resetGlobalCSSCache: () => import_render.resetGlobalCSSCache,
|
|
35
38
|
resetKeys: () => import_keys.resetKeys
|
|
36
39
|
});
|
|
37
40
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -42,6 +45,7 @@ var import_render = require("./render.js");
|
|
|
42
45
|
var import_metadata = require("./metadata.js");
|
|
43
46
|
var import_hydrate = require("./hydrate.js");
|
|
44
47
|
var import_sitemap = require("./sitemap.js");
|
|
48
|
+
var import_prefetch = require("./prefetch.js");
|
|
45
49
|
var index_default = {
|
|
46
50
|
createEnv: import_env.createEnv,
|
|
47
51
|
resetKeys: import_keys.resetKeys,
|
|
@@ -53,9 +57,12 @@ var index_default = {
|
|
|
53
57
|
renderElement: import_render.renderElement,
|
|
54
58
|
renderRoute: import_render.renderRoute,
|
|
55
59
|
renderPage: import_render.renderPage,
|
|
60
|
+
resetGlobalCSSCache: import_render.resetGlobalCSSCache,
|
|
56
61
|
extractMetadata: import_metadata.extractMetadata,
|
|
57
62
|
generateHeadHtml: import_metadata.generateHeadHtml,
|
|
58
63
|
collectBrNodes: import_hydrate.collectBrNodes,
|
|
59
64
|
hydrate: import_hydrate.hydrate,
|
|
60
|
-
generateSitemap: import_sitemap.generateSitemap
|
|
65
|
+
generateSitemap: import_sitemap.generateSitemap,
|
|
66
|
+
prefetchPageData: import_prefetch.prefetchPageData,
|
|
67
|
+
injectPrefetchedState: import_prefetch.injectPrefetchedState
|
|
61
68
|
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var prefetch_exports = {};
|
|
29
|
+
__export(prefetch_exports, {
|
|
30
|
+
injectPrefetchedState: () => injectPrefetchedState,
|
|
31
|
+
prefetchPageData: () => prefetchPageData
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(prefetch_exports);
|
|
34
|
+
const isFunction = (v) => typeof v === "function";
|
|
35
|
+
const isArray = (v) => Array.isArray(v);
|
|
36
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
37
|
+
const resolveParams = (params, mockState) => {
|
|
38
|
+
if (!params) return void 0;
|
|
39
|
+
if (isFunction(params)) {
|
|
40
|
+
try {
|
|
41
|
+
const mockEl = {
|
|
42
|
+
state: mockState || {},
|
|
43
|
+
props: {},
|
|
44
|
+
call: () => void 0,
|
|
45
|
+
__ref: {}
|
|
46
|
+
};
|
|
47
|
+
return params(mockEl, mockState || {});
|
|
48
|
+
} catch {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return params;
|
|
53
|
+
};
|
|
54
|
+
const normalizeFetchConfig = (cfg, elementState) => {
|
|
55
|
+
if (!cfg) return null;
|
|
56
|
+
if (typeof cfg === "string") return { from: cfg, method: "select" };
|
|
57
|
+
const resolved = isFunction(cfg) ? null : { ...cfg };
|
|
58
|
+
if (!resolved) return null;
|
|
59
|
+
if (!resolved.method) resolved.method = "select";
|
|
60
|
+
if (isFunction(resolved.params)) {
|
|
61
|
+
resolved.params = resolveParams(resolved.params, elementState);
|
|
62
|
+
}
|
|
63
|
+
const isMutation = resolved.method === "insert" || resolved.method === "update" || resolved.method === "upsert" || resolved.method === "delete";
|
|
64
|
+
if (isMutation) return null;
|
|
65
|
+
if (resolved.on && resolved.on !== "create") return null;
|
|
66
|
+
return resolved;
|
|
67
|
+
};
|
|
68
|
+
const collectFetchDeclarations = (def, path = "") => {
|
|
69
|
+
if (!def || typeof def !== "object") return [];
|
|
70
|
+
if (isFunction(def)) return [];
|
|
71
|
+
const results = [];
|
|
72
|
+
const elementState = def.state || {};
|
|
73
|
+
if (def.fetch) {
|
|
74
|
+
const fetchDefs = isArray(def.fetch) ? def.fetch : [def.fetch];
|
|
75
|
+
for (const fd of fetchDefs) {
|
|
76
|
+
const config = normalizeFetchConfig(fd, elementState);
|
|
77
|
+
if (config) {
|
|
78
|
+
results.push({
|
|
79
|
+
config,
|
|
80
|
+
stateKey: config.as,
|
|
81
|
+
path,
|
|
82
|
+
elementState
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const key in def) {
|
|
88
|
+
if (key === "fetch" || key === "state" || key === "props" || key === "attr" || key === "on" || key === "define" || key === "childExtends" || key === "childProps" || key === "childrenAs") continue;
|
|
89
|
+
if (key.charAt(0) >= "A" && key.charAt(0) <= "Z" && isObject(def[key])) {
|
|
90
|
+
results.push(...collectFetchDeclarations(def[key], path ? `${path}.${key}` : key));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
};
|
|
95
|
+
const createSSRAdapter = async (dbConfig) => {
|
|
96
|
+
if (!dbConfig) return null;
|
|
97
|
+
const { adapter, createClient, url, key, projectId } = dbConfig;
|
|
98
|
+
if (adapter !== "supabase") return null;
|
|
99
|
+
const supabaseUrl = url || projectId && `https://${projectId}.supabase.co`;
|
|
100
|
+
if (!supabaseUrl || !key) return null;
|
|
101
|
+
let clientFactory = createClient;
|
|
102
|
+
if (!clientFactory) {
|
|
103
|
+
try {
|
|
104
|
+
const mod = await import("@supabase/supabase-js");
|
|
105
|
+
clientFactory = mod.createClient;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const client = clientFactory(supabaseUrl, key);
|
|
111
|
+
return {
|
|
112
|
+
rpc: ({ from, params }) => client.rpc(from, params),
|
|
113
|
+
select: async ({ from, select: sel, params, limit, offset, order, single }) => {
|
|
114
|
+
let q = client.from(from).select(sel || "*");
|
|
115
|
+
if (params) {
|
|
116
|
+
for (const k in params) {
|
|
117
|
+
const v = params[k];
|
|
118
|
+
if (v === null) q = q.is(k, null);
|
|
119
|
+
else if (Array.isArray(v)) q = q.in(k, v);
|
|
120
|
+
else q = q.eq(k, v);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (order) {
|
|
124
|
+
const orderBy = typeof order === "string" ? order : order.by;
|
|
125
|
+
q = q.order(orderBy, { ascending: order.asc !== false });
|
|
126
|
+
}
|
|
127
|
+
if (limit) q = q.limit(limit);
|
|
128
|
+
if (offset) q = q.range(offset, offset + (limit || 20) - 1);
|
|
129
|
+
if (single) q = q.single();
|
|
130
|
+
return q;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const executeSingle = async (adapter, config) => {
|
|
135
|
+
try {
|
|
136
|
+
const { method, from, params, transform, limit, offset, order, single } = config;
|
|
137
|
+
let result;
|
|
138
|
+
if (method === "rpc") {
|
|
139
|
+
result = await adapter.rpc({ from, params });
|
|
140
|
+
} else {
|
|
141
|
+
result = await adapter.select({ from, select: config.select, params, limit, offset, order, single });
|
|
142
|
+
}
|
|
143
|
+
let data = result?.data ?? null;
|
|
144
|
+
if (result?.error) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (data && transform && isFunction(transform)) {
|
|
148
|
+
try {
|
|
149
|
+
data = transform(data);
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return data;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const prefetchPageData = async (data, route = "/", options = {}) => {
|
|
159
|
+
const pages = data.pages || {};
|
|
160
|
+
const pageDef = pages[route];
|
|
161
|
+
if (!pageDef) return /* @__PURE__ */ new Map();
|
|
162
|
+
const dbConfig = data.config?.db || data.settings?.db || data.db;
|
|
163
|
+
if (!dbConfig) return /* @__PURE__ */ new Map();
|
|
164
|
+
const adapter = await createSSRAdapter(dbConfig);
|
|
165
|
+
if (!adapter) return /* @__PURE__ */ new Map();
|
|
166
|
+
const declarations = collectFetchDeclarations(pageDef);
|
|
167
|
+
if (!declarations.length) return /* @__PURE__ */ new Map();
|
|
168
|
+
const stateUpdates = /* @__PURE__ */ new Map();
|
|
169
|
+
const results = await Promise.allSettled(
|
|
170
|
+
declarations.map(async ({ config, stateKey, path }) => {
|
|
171
|
+
const fetchedData = await executeSingle(adapter, config);
|
|
172
|
+
if (fetchedData !== null && stateKey) {
|
|
173
|
+
const existing = stateUpdates.get(path) || {};
|
|
174
|
+
existing[stateKey] = fetchedData;
|
|
175
|
+
stateUpdates.set(path, existing);
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
return stateUpdates;
|
|
180
|
+
};
|
|
181
|
+
const injectPrefetchedState = (pageDef, stateUpdates) => {
|
|
182
|
+
if (!stateUpdates || !stateUpdates.size) return;
|
|
183
|
+
for (const [path, data] of stateUpdates) {
|
|
184
|
+
let target = pageDef;
|
|
185
|
+
if (path) {
|
|
186
|
+
const parts = path.split(".");
|
|
187
|
+
for (const part of parts) {
|
|
188
|
+
if (!target || typeof target !== "object") break;
|
|
189
|
+
target = target[part];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (target && typeof target === "object") {
|
|
193
|
+
if (!target.state || typeof target.state !== "object") {
|
|
194
|
+
target.state = {};
|
|
195
|
+
}
|
|
196
|
+
Object.assign(target.state, data);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|