@symbo.ls/brender 3.6.4 → 3.6.7

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 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 via @domql/element |
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 -> 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
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
+ };