@symbo.ls/brender 3.7.3 → 3.7.4

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/dist/cjs/load.js CHANGED
@@ -103,17 +103,17 @@ const loadProject = async (projectPath) => {
103
103
  bundleAndImport((0, import_path.join)(symbolsDir, "files", "index.js"))
104
104
  ]);
105
105
  return {
106
- app: appModule?.default || {},
107
- state: stateModule?.default || {},
108
- dependencies: depsModule?.default || {},
109
- components: componentsModule || {},
110
- snippets: snippetsModule || {},
111
- pages: pagesModule?.default || {},
112
- functions: functionsModule || {},
113
- methods: methodsModule || {},
114
- designSystem: designSystemModule?.default || {},
115
- files: filesModule?.default || {},
116
- config: configModule?.default || {}
106
+ app: { ...appModule?.default || {} },
107
+ state: { ...stateModule?.default || {} },
108
+ dependencies: { ...depsModule?.default || {} },
109
+ components: { ...componentsModule || {} },
110
+ snippets: { ...snippetsModule || {} },
111
+ pages: { ...pagesModule?.default || {} },
112
+ functions: { ...functionsModule || {} },
113
+ methods: { ...methodsModule || {} },
114
+ designSystem: { ...designSystemModule?.default || {} },
115
+ files: { ...filesModule?.default || {} },
116
+ config: { ...configModule?.default || {} }
117
117
  };
118
118
  };
119
119
  const loadAndRenderAll = async (projectPath, renderFn) => {
@@ -27,6 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
28
  var prefetch_exports = {};
29
29
  __export(prefetch_exports, {
30
+ fetchSSRTranslations: () => fetchSSRTranslations,
30
31
  injectPrefetchedState: () => injectPrefetchedState,
31
32
  prefetchPageData: () => prefetchPageData
32
33
  });
@@ -159,7 +160,8 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
159
160
  const pages = data.pages || {};
160
161
  const pageDef = pages[route];
161
162
  if (!pageDef) return /* @__PURE__ */ new Map();
162
- const dbConfig = data.config?.db || data.settings?.db || data.db;
163
+ const config = data.config || data.settings || {};
164
+ const dbConfig = config.fetch || config.db || data.db;
163
165
  if (!dbConfig) return /* @__PURE__ */ new Map();
164
166
  const adapter = await createSSRAdapter(dbConfig);
165
167
  if (!adapter) return /* @__PURE__ */ new Map();
@@ -167,8 +169,8 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
167
169
  if (!declarations.length) return /* @__PURE__ */ new Map();
168
170
  const stateUpdates = /* @__PURE__ */ new Map();
169
171
  const results = await Promise.allSettled(
170
- declarations.map(async ({ config, stateKey, path }) => {
171
- const fetchedData = await executeSingle(adapter, config);
172
+ declarations.map(async ({ config: config2, stateKey, path }) => {
173
+ const fetchedData = await executeSingle(adapter, config2);
172
174
  if (fetchedData !== null && stateKey) {
173
175
  const existing = stateUpdates.get(path) || {};
174
176
  existing[stateKey] = fetchedData;
@@ -178,6 +180,66 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
178
180
  );
179
181
  return stateUpdates;
180
182
  };
183
+ const fetchSSRTranslations = async (data) => {
184
+ const config = data.config || data.settings || {};
185
+ const polyglot = config.polyglot;
186
+ if (!polyglot?.fetch) return null;
187
+ const dbConfig = config.fetch || config.db || data.db;
188
+ if (!dbConfig) return null;
189
+ const adapter = await createSSRAdapter(dbConfig);
190
+ if (!adapter) return null;
191
+ const fetchConfig = polyglot.fetch;
192
+ const rpcName = fetchConfig.rpc || fetchConfig.from || "get_translations_if_changed";
193
+ const languages = polyglot.languages || [polyglot.defaultLang || "en"];
194
+ const translations = {};
195
+ const results = await Promise.allSettled(
196
+ languages.map(async (lang) => {
197
+ try {
198
+ const res = await adapter.rpc({
199
+ from: rpcName,
200
+ params: { p_lang: lang, p_cached_version: 0 }
201
+ });
202
+ if (res.error || !res.data) return;
203
+ const result = res.data;
204
+ if (result.translations) {
205
+ translations[lang] = result.translations;
206
+ }
207
+ } catch {
208
+ }
209
+ })
210
+ );
211
+ return Object.keys(translations).length ? translations : null;
212
+ };
213
+ const preEvaluateChildren = (def, inheritedState) => {
214
+ if (!def || typeof def !== "object") return;
215
+ for (const key in def) {
216
+ if (key === "state" || key === "fetch" || key === "props" || key === "attr" || key === "on" || key === "define" || key === "childExtends" || key === "childProps" || key === "childrenAs") continue;
217
+ if (key.charAt(0) >= "A" && key.charAt(0) <= "Z" && isObject(def[key])) {
218
+ const child = def[key];
219
+ const effectiveState = child.state && typeof child.state === "object" ? { ...inheritedState, ...child.state } : inheritedState;
220
+ if (isFunction(child.children)) {
221
+ try {
222
+ const mockEl = {
223
+ state: effectiveState,
224
+ props: {},
225
+ call: (fn) => {
226
+ if (fn === "getActiveLang" || fn === "getLang") return effectiveState?.lang || "ka";
227
+ if (fn === "polyglot") return arguments[1] || "";
228
+ return void 0;
229
+ },
230
+ __ref: {}
231
+ };
232
+ const result = child.children(mockEl, effectiveState);
233
+ if (isArray(result) && result.length > 0) {
234
+ child.children = result;
235
+ }
236
+ } catch {
237
+ }
238
+ }
239
+ preEvaluateChildren(child, effectiveState);
240
+ }
241
+ }
242
+ };
181
243
  const injectPrefetchedState = (pageDef, stateUpdates) => {
182
244
  if (!stateUpdates || !stateUpdates.size) return;
183
245
  for (const [path, data] of stateUpdates) {
@@ -194,6 +256,7 @@ const injectPrefetchedState = (pageDef, stateUpdates) => {
194
256
  target.state = {};
195
257
  }
196
258
  Object.assign(target.state, data);
259
+ preEvaluateChildren(target, target.state);
197
260
  }
198
261
  }
199
262
  };
@@ -44,6 +44,7 @@ var import_metadata = require("./metadata.js");
44
44
  var import_hydrate = require("./hydrate.js");
45
45
  var import_prefetch = require("./prefetch.js");
46
46
  var import_linkedom = require("linkedom");
47
+ var import_create_instance = __toESM(require("@emotion/css/create-instance"), 1);
47
48
  const import_meta = {};
48
49
  const structuredCloneDeep = (obj, seen = /* @__PURE__ */ new WeakMap()) => {
49
50
  if (obj === null || typeof obj !== "object") return obj;
@@ -76,7 +77,7 @@ const safeJsonReplacer = () => {
76
77
  let _cachedCreateDomql = null;
77
78
  const bundleCreateDomql = async () => {
78
79
  if (_cachedCreateDomql) return _cachedCreateDomql;
79
- const brenderDir = new URL(".", import_meta.url).pathname;
80
+ const brenderDir = (0, import_fs.realpathSync)(new URL(".", import_meta.url).pathname);
80
81
  const monorepoRoot = (0, import_path.resolve)(brenderDir, "../..");
81
82
  const entry = (0, import_path.resolve)(monorepoRoot, "packages", "smbls", "src", "createDomql.js");
82
83
  const esbuild = await import("esbuild");
@@ -346,10 +347,18 @@ const render = async (data, options = {}) => {
346
347
  (0, import_prefetch.injectPrefetchedState)(pageDef, stateUpdates);
347
348
  prefetchedPages[route] = pageDef;
348
349
  }
349
- } catch {
350
+ } catch (prefetchErr) {
351
+ console.error("[brender] Prefetch error:", prefetchErr);
350
352
  prefetchedPages = data.pages;
351
353
  }
352
354
  }
355
+ let ssrTranslations;
356
+ if (prefetch) {
357
+ try {
358
+ ssrTranslations = await (0, import_prefetch.fetchSSRTranslations)(data);
359
+ } catch {
360
+ }
361
+ }
353
362
  const { window, document } = (0, import_env.createEnv)();
354
363
  const body = document.body;
355
364
  window.location.pathname = route;
@@ -359,9 +368,35 @@ const render = async (data, options = {}) => {
359
368
  globalThis.location = window.location;
360
369
  const { createDomqlElement } = await bundleCreateDomql();
361
370
  const app = structuredCloneDeep(data.app || {});
371
+ const config = data.config || data.settings || {};
372
+ const polyglotConfig = config.polyglot ? { ...config.polyglot } : void 0;
373
+ if (ssrTranslations && polyglotConfig) {
374
+ polyglotConfig.translations = {
375
+ ...polyglotConfig.translations || {},
376
+ ...ssrTranslations
377
+ };
378
+ }
379
+ const baseState = structuredCloneDeep(data.state || {});
380
+ if (ssrTranslations || polyglotConfig) {
381
+ if (!baseState.root) baseState.root = {};
382
+ if (polyglotConfig) {
383
+ baseState.root.lang = baseState.root.lang || polyglotConfig.defaultLang || "en";
384
+ }
385
+ if (ssrTranslations) {
386
+ baseState.root.translations = {
387
+ ...baseState.root.translations || {},
388
+ ...ssrTranslations
389
+ };
390
+ }
391
+ }
392
+ const ssrEmotion = (0, import_create_instance.default)({
393
+ key: "smbls",
394
+ container: document.head,
395
+ speedy: false
396
+ });
362
397
  const ctx = {
363
- state: structuredCloneDeep(data.state || {}),
364
- ...stateOverrides ? { state: { ...structuredCloneDeep(data.state || {}), ...stateOverrides } } : {},
398
+ state: baseState,
399
+ ...stateOverrides ? { state: { ...baseState, ...stateOverrides } } : {},
365
400
  dependencies: structuredCloneDeep(data.dependencies || {}),
366
401
  components: structuredCloneDeep(data.components || {}),
367
402
  snippets: structuredCloneDeep(data.snippets || {}),
@@ -370,17 +405,25 @@ const render = async (data, options = {}) => {
370
405
  methods: data.methods || {},
371
406
  designSystem: structuredCloneDeep(data.designSystem || {}),
372
407
  files: data.files || {},
373
- ...data.config || data.settings || {},
408
+ ...config,
409
+ // Override polyglot with SSR-enriched version
410
+ ...polyglotConfig ? { polyglot: polyglotConfig } : {},
374
411
  // Virtual DOM environment
375
412
  document,
376
413
  window,
377
414
  parent: { node: body },
415
+ // Use SSR emotion instance (non-speedy) for proper @media rule extraction
416
+ initOptions: { emotion: ssrEmotion },
417
+ // Disable sourcemap tracking in SSR — it causes stack overflows
418
+ // when state contains large data arrays (articles, events, etc.)
419
+ domqlOptions: { sourcemap: false },
378
420
  // Caller overrides
379
421
  ...contextOverrides || {}
380
422
  };
381
423
  (0, import_keys.resetKeys)();
382
424
  const element = await createDomqlElement(app, ctx);
383
- await new Promise((r) => setTimeout(r, 50));
425
+ const flushDelay = prefetch ? 2e3 : 50;
426
+ await new Promise((r) => setTimeout(r, flushDelay));
384
427
  (0, import_keys.assignKeys)(body);
385
428
  const registry = (0, import_keys.mapKeysToElements)(element);
386
429
  const metadata = (0, import_metadata.extractMetadata)(data, route);
@@ -420,12 +463,20 @@ const render = async (data, options = {}) => {
420
463
  }
421
464
  }
422
465
  }
423
- const html = fixSvgContent(body.innerHTML);
466
+ let html = fixSvgContent(body.innerHTML);
467
+ if (ssrTranslations) {
468
+ const defaultLang = polyglotConfig?.defaultLang || "en";
469
+ const langMap = ssrTranslations[defaultLang] || Object.values(ssrTranslations)[0] || {};
470
+ html = html.replace(/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g, (match, key) => {
471
+ const trimmed = key.trim();
472
+ return langMap[trimmed] ?? match;
473
+ });
474
+ }
424
475
  if (_prevDoc !== void 0) globalThis.document = _prevDoc;
425
476
  else delete globalThis.document;
426
477
  if (_prevLoc !== void 0) globalThis.location = _prevLoc;
427
478
  else delete globalThis.location;
428
- return { html, metadata, registry, element, emotionCSS, document, window };
479
+ return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations };
429
480
  };
430
481
  const renderElement = async (elementDef, options = {}) => {
431
482
  const { context = {} } = options;
@@ -693,10 +744,25 @@ const renderPage = async (data, route = "/", options = {}) => {
693
744
  <\/script>
694
745
  <script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
695
746
  }
747
+ const config = data.config || data.settings || {};
748
+ const polyglotCfg = config.polyglot;
749
+ let resolvedHeadTags = headTags;
750
+ if (polyglotCfg) {
751
+ const defaultLang = polyglotCfg.defaultLang || "en";
752
+ const translations = {
753
+ ...polyglotCfg.translations || {},
754
+ ...result.ssrTranslations || {}
755
+ };
756
+ const langMap = translations[defaultLang] || {};
757
+ resolvedHeadTags = headTags.replace(/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g, (match, key) => {
758
+ const trimmed = key.trim();
759
+ return langMap[trimmed] ?? match;
760
+ });
761
+ }
696
762
  const html = `<!DOCTYPE html>
697
763
  <html lang="${htmlLang}">
698
764
  <head>
699
- ${headTags}
765
+ ${resolvedHeadTags}
700
766
  ${fontLinks}
701
767
  ${globalCSS.fontFaceCSS ? `<style>${globalCSS.fontFaceCSS}</style>` : ""}
702
768
  <style>
package/dist/esm/load.js CHANGED
@@ -70,17 +70,17 @@ const loadProject = async (projectPath) => {
70
70
  bundleAndImport(join(symbolsDir, "files", "index.js"))
71
71
  ]);
72
72
  return {
73
- app: appModule?.default || {},
74
- state: stateModule?.default || {},
75
- dependencies: depsModule?.default || {},
76
- components: componentsModule || {},
77
- snippets: snippetsModule || {},
78
- pages: pagesModule?.default || {},
79
- functions: functionsModule || {},
80
- methods: methodsModule || {},
81
- designSystem: designSystemModule?.default || {},
82
- files: filesModule?.default || {},
83
- config: configModule?.default || {}
73
+ app: { ...appModule?.default || {} },
74
+ state: { ...stateModule?.default || {} },
75
+ dependencies: { ...depsModule?.default || {} },
76
+ components: { ...componentsModule || {} },
77
+ snippets: { ...snippetsModule || {} },
78
+ pages: { ...pagesModule?.default || {} },
79
+ functions: { ...functionsModule || {} },
80
+ methods: { ...methodsModule || {} },
81
+ designSystem: { ...designSystemModule?.default || {} },
82
+ files: { ...filesModule?.default || {} },
83
+ config: { ...configModule?.default || {} }
84
84
  };
85
85
  };
86
86
  const loadAndRenderAll = async (projectPath, renderFn) => {
@@ -126,7 +126,8 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
126
126
  const pages = data.pages || {};
127
127
  const pageDef = pages[route];
128
128
  if (!pageDef) return /* @__PURE__ */ new Map();
129
- const dbConfig = data.config?.db || data.settings?.db || data.db;
129
+ const config = data.config || data.settings || {};
130
+ const dbConfig = config.fetch || config.db || data.db;
130
131
  if (!dbConfig) return /* @__PURE__ */ new Map();
131
132
  const adapter = await createSSRAdapter(dbConfig);
132
133
  if (!adapter) return /* @__PURE__ */ new Map();
@@ -134,8 +135,8 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
134
135
  if (!declarations.length) return /* @__PURE__ */ new Map();
135
136
  const stateUpdates = /* @__PURE__ */ new Map();
136
137
  const results = await Promise.allSettled(
137
- declarations.map(async ({ config, stateKey, path }) => {
138
- const fetchedData = await executeSingle(adapter, config);
138
+ declarations.map(async ({ config: config2, stateKey, path }) => {
139
+ const fetchedData = await executeSingle(adapter, config2);
139
140
  if (fetchedData !== null && stateKey) {
140
141
  const existing = stateUpdates.get(path) || {};
141
142
  existing[stateKey] = fetchedData;
@@ -145,6 +146,66 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
145
146
  );
146
147
  return stateUpdates;
147
148
  };
149
+ const fetchSSRTranslations = async (data) => {
150
+ const config = data.config || data.settings || {};
151
+ const polyglot = config.polyglot;
152
+ if (!polyglot?.fetch) return null;
153
+ const dbConfig = config.fetch || config.db || data.db;
154
+ if (!dbConfig) return null;
155
+ const adapter = await createSSRAdapter(dbConfig);
156
+ if (!adapter) return null;
157
+ const fetchConfig = polyglot.fetch;
158
+ const rpcName = fetchConfig.rpc || fetchConfig.from || "get_translations_if_changed";
159
+ const languages = polyglot.languages || [polyglot.defaultLang || "en"];
160
+ const translations = {};
161
+ const results = await Promise.allSettled(
162
+ languages.map(async (lang) => {
163
+ try {
164
+ const res = await adapter.rpc({
165
+ from: rpcName,
166
+ params: { p_lang: lang, p_cached_version: 0 }
167
+ });
168
+ if (res.error || !res.data) return;
169
+ const result = res.data;
170
+ if (result.translations) {
171
+ translations[lang] = result.translations;
172
+ }
173
+ } catch {
174
+ }
175
+ })
176
+ );
177
+ return Object.keys(translations).length ? translations : null;
178
+ };
179
+ const preEvaluateChildren = (def, inheritedState) => {
180
+ if (!def || typeof def !== "object") return;
181
+ for (const key in def) {
182
+ if (key === "state" || key === "fetch" || key === "props" || key === "attr" || key === "on" || key === "define" || key === "childExtends" || key === "childProps" || key === "childrenAs") continue;
183
+ if (key.charAt(0) >= "A" && key.charAt(0) <= "Z" && isObject(def[key])) {
184
+ const child = def[key];
185
+ const effectiveState = child.state && typeof child.state === "object" ? { ...inheritedState, ...child.state } : inheritedState;
186
+ if (isFunction(child.children)) {
187
+ try {
188
+ const mockEl = {
189
+ state: effectiveState,
190
+ props: {},
191
+ call: (fn) => {
192
+ if (fn === "getActiveLang" || fn === "getLang") return effectiveState?.lang || "ka";
193
+ if (fn === "polyglot") return arguments[1] || "";
194
+ return void 0;
195
+ },
196
+ __ref: {}
197
+ };
198
+ const result = child.children(mockEl, effectiveState);
199
+ if (isArray(result) && result.length > 0) {
200
+ child.children = result;
201
+ }
202
+ } catch {
203
+ }
204
+ }
205
+ preEvaluateChildren(child, effectiveState);
206
+ }
207
+ }
208
+ };
148
209
  const injectPrefetchedState = (pageDef, stateUpdates) => {
149
210
  if (!stateUpdates || !stateUpdates.size) return;
150
211
  for (const [path, data] of stateUpdates) {
@@ -161,10 +222,12 @@ const injectPrefetchedState = (pageDef, stateUpdates) => {
161
222
  target.state = {};
162
223
  }
163
224
  Object.assign(target.state, data);
225
+ preEvaluateChildren(target, target.state);
164
226
  }
165
227
  }
166
228
  };
167
229
  export {
230
+ fetchSSRTranslations,
168
231
  injectPrefetchedState,
169
232
  prefetchPageData
170
233
  };
@@ -1,13 +1,14 @@
1
1
  import { resolve, join } from "path";
2
- import { existsSync, writeFileSync, unlinkSync, readFileSync } from "fs";
2
+ import { existsSync, writeFileSync, unlinkSync, readFileSync, realpathSync } from "fs";
3
3
  import { tmpdir } from "os";
4
4
  import { randomBytes } from "crypto";
5
5
  import { createEnv } from "./env.js";
6
6
  import { resetKeys, assignKeys, mapKeysToElements } from "./keys.js";
7
7
  import { extractMetadata, generateHeadHtml } from "./metadata.js";
8
8
  import { hydrate } from "./hydrate.js";
9
- import { prefetchPageData, injectPrefetchedState } from "./prefetch.js";
9
+ import { prefetchPageData, injectPrefetchedState, fetchSSRTranslations } from "./prefetch.js";
10
10
  import { parseHTML } from "linkedom";
11
+ import createEmotionInstance from "@emotion/css/create-instance";
11
12
  const structuredCloneDeep = (obj, seen = /* @__PURE__ */ new WeakMap()) => {
12
13
  if (obj === null || typeof obj !== "object") return obj;
13
14
  if (seen.has(obj)) return seen.get(obj);
@@ -39,7 +40,7 @@ const safeJsonReplacer = () => {
39
40
  let _cachedCreateDomql = null;
40
41
  const bundleCreateDomql = async () => {
41
42
  if (_cachedCreateDomql) return _cachedCreateDomql;
42
- const brenderDir = new URL(".", import.meta.url).pathname;
43
+ const brenderDir = realpathSync(new URL(".", import.meta.url).pathname);
43
44
  const monorepoRoot = resolve(brenderDir, "../..");
44
45
  const entry = resolve(monorepoRoot, "packages", "smbls", "src", "createDomql.js");
45
46
  const esbuild = await import("esbuild");
@@ -309,10 +310,18 @@ const render = async (data, options = {}) => {
309
310
  injectPrefetchedState(pageDef, stateUpdates);
310
311
  prefetchedPages[route] = pageDef;
311
312
  }
312
- } catch {
313
+ } catch (prefetchErr) {
314
+ console.error("[brender] Prefetch error:", prefetchErr);
313
315
  prefetchedPages = data.pages;
314
316
  }
315
317
  }
318
+ let ssrTranslations;
319
+ if (prefetch) {
320
+ try {
321
+ ssrTranslations = await fetchSSRTranslations(data);
322
+ } catch {
323
+ }
324
+ }
316
325
  const { window, document } = createEnv();
317
326
  const body = document.body;
318
327
  window.location.pathname = route;
@@ -322,9 +331,35 @@ const render = async (data, options = {}) => {
322
331
  globalThis.location = window.location;
323
332
  const { createDomqlElement } = await bundleCreateDomql();
324
333
  const app = structuredCloneDeep(data.app || {});
334
+ const config = data.config || data.settings || {};
335
+ const polyglotConfig = config.polyglot ? { ...config.polyglot } : void 0;
336
+ if (ssrTranslations && polyglotConfig) {
337
+ polyglotConfig.translations = {
338
+ ...polyglotConfig.translations || {},
339
+ ...ssrTranslations
340
+ };
341
+ }
342
+ const baseState = structuredCloneDeep(data.state || {});
343
+ if (ssrTranslations || polyglotConfig) {
344
+ if (!baseState.root) baseState.root = {};
345
+ if (polyglotConfig) {
346
+ baseState.root.lang = baseState.root.lang || polyglotConfig.defaultLang || "en";
347
+ }
348
+ if (ssrTranslations) {
349
+ baseState.root.translations = {
350
+ ...baseState.root.translations || {},
351
+ ...ssrTranslations
352
+ };
353
+ }
354
+ }
355
+ const ssrEmotion = createEmotionInstance({
356
+ key: "smbls",
357
+ container: document.head,
358
+ speedy: false
359
+ });
325
360
  const ctx = {
326
- state: structuredCloneDeep(data.state || {}),
327
- ...stateOverrides ? { state: { ...structuredCloneDeep(data.state || {}), ...stateOverrides } } : {},
361
+ state: baseState,
362
+ ...stateOverrides ? { state: { ...baseState, ...stateOverrides } } : {},
328
363
  dependencies: structuredCloneDeep(data.dependencies || {}),
329
364
  components: structuredCloneDeep(data.components || {}),
330
365
  snippets: structuredCloneDeep(data.snippets || {}),
@@ -333,17 +368,25 @@ const render = async (data, options = {}) => {
333
368
  methods: data.methods || {},
334
369
  designSystem: structuredCloneDeep(data.designSystem || {}),
335
370
  files: data.files || {},
336
- ...data.config || data.settings || {},
371
+ ...config,
372
+ // Override polyglot with SSR-enriched version
373
+ ...polyglotConfig ? { polyglot: polyglotConfig } : {},
337
374
  // Virtual DOM environment
338
375
  document,
339
376
  window,
340
377
  parent: { node: body },
378
+ // Use SSR emotion instance (non-speedy) for proper @media rule extraction
379
+ initOptions: { emotion: ssrEmotion },
380
+ // Disable sourcemap tracking in SSR — it causes stack overflows
381
+ // when state contains large data arrays (articles, events, etc.)
382
+ domqlOptions: { sourcemap: false },
341
383
  // Caller overrides
342
384
  ...contextOverrides || {}
343
385
  };
344
386
  resetKeys();
345
387
  const element = await createDomqlElement(app, ctx);
346
- await new Promise((r) => setTimeout(r, 50));
388
+ const flushDelay = prefetch ? 2e3 : 50;
389
+ await new Promise((r) => setTimeout(r, flushDelay));
347
390
  assignKeys(body);
348
391
  const registry = mapKeysToElements(element);
349
392
  const metadata = extractMetadata(data, route);
@@ -383,12 +426,20 @@ const render = async (data, options = {}) => {
383
426
  }
384
427
  }
385
428
  }
386
- const html = fixSvgContent(body.innerHTML);
429
+ let html = fixSvgContent(body.innerHTML);
430
+ if (ssrTranslations) {
431
+ const defaultLang = polyglotConfig?.defaultLang || "en";
432
+ const langMap = ssrTranslations[defaultLang] || Object.values(ssrTranslations)[0] || {};
433
+ html = html.replace(/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g, (match, key) => {
434
+ const trimmed = key.trim();
435
+ return langMap[trimmed] ?? match;
436
+ });
437
+ }
387
438
  if (_prevDoc !== void 0) globalThis.document = _prevDoc;
388
439
  else delete globalThis.document;
389
440
  if (_prevLoc !== void 0) globalThis.location = _prevLoc;
390
441
  else delete globalThis.location;
391
- return { html, metadata, registry, element, emotionCSS, document, window };
442
+ return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations };
392
443
  };
393
444
  const renderElement = async (elementDef, options = {}) => {
394
445
  const { context = {} } = options;
@@ -656,10 +707,25 @@ const renderPage = async (data, route = "/", options = {}) => {
656
707
  <\/script>
657
708
  <script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
658
709
  }
710
+ const config = data.config || data.settings || {};
711
+ const polyglotCfg = config.polyglot;
712
+ let resolvedHeadTags = headTags;
713
+ if (polyglotCfg) {
714
+ const defaultLang = polyglotCfg.defaultLang || "en";
715
+ const translations = {
716
+ ...polyglotCfg.translations || {},
717
+ ...result.ssrTranslations || {}
718
+ };
719
+ const langMap = translations[defaultLang] || {};
720
+ resolvedHeadTags = headTags.replace(/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g, (match, key) => {
721
+ const trimmed = key.trim();
722
+ return langMap[trimmed] ?? match;
723
+ });
724
+ }
659
725
  const html = `<!DOCTYPE html>
660
726
  <html lang="${htmlLang}">
661
727
  <head>
662
- ${headTags}
728
+ ${resolvedHeadTags}
663
729
  ${fontLinks}
664
730
  ${globalCSS.fontFaceCSS ? `<style>${globalCSS.fontFaceCSS}</style>` : ""}
665
731
  <style>
package/load.js CHANGED
@@ -86,18 +86,20 @@ export const loadProject = async (projectPath) => {
86
86
  bundleAndImport(join(symbolsDir, 'files', 'index.js'))
87
87
  ])
88
88
 
89
+ // Spread into plain objects — ESM module namespaces are non-extensible,
90
+ // which breaks downstream code that adds properties (e.g. polyglot functions).
89
91
  return {
90
- app: appModule?.default || {},
91
- state: stateModule?.default || {},
92
- dependencies: depsModule?.default || {},
93
- components: componentsModule || {},
94
- snippets: snippetsModule || {},
95
- pages: pagesModule?.default || {},
96
- functions: functionsModule || {},
97
- methods: methodsModule || {},
98
- designSystem: designSystemModule?.default || {},
99
- files: filesModule?.default || {},
100
- config: configModule?.default || {}
92
+ app: { ...(appModule?.default || {}) },
93
+ state: { ...(stateModule?.default || {}) },
94
+ dependencies: { ...(depsModule?.default || {}) },
95
+ components: { ...(componentsModule || {}) },
96
+ snippets: { ...(snippetsModule || {}) },
97
+ pages: { ...(pagesModule?.default || {}) },
98
+ functions: { ...(functionsModule || {}) },
99
+ methods: { ...(methodsModule || {}) },
100
+ designSystem: { ...(designSystemModule?.default || {}) },
101
+ files: { ...(filesModule?.default || {}) },
102
+ config: { ...(configModule?.default || {}) }
101
103
  }
102
104
  }
103
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/brender",
3
- "version": "3.7.3",
3
+ "version": "3.7.4",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "module": "./dist/esm/index.js",
@@ -36,7 +36,7 @@
36
36
  "dev:rita": "node examples/serve-rita.js"
37
37
  },
38
38
  "dependencies": {
39
- "@symbo.ls/helmet": "^3.7.3",
39
+ "@symbo.ls/helmet": "^3.7.4",
40
40
  "linkedom": "^0.16.8"
41
41
  },
42
42
  "devDependencies": {
package/prefetch.js CHANGED
@@ -198,7 +198,8 @@ export const prefetchPageData = async (data, route = '/', options = {}) => {
198
198
  const pageDef = pages[route]
199
199
  if (!pageDef) return new Map()
200
200
 
201
- const dbConfig = data.config?.db || data.settings?.db || data.db
201
+ const config = data.config || data.settings || {}
202
+ const dbConfig = config.fetch || config.db || data.db
202
203
  if (!dbConfig) return new Map()
203
204
 
204
205
  const adapter = await createSSRAdapter(dbConfig)
@@ -231,6 +232,99 @@ export const prefetchPageData = async (data, route = '/', options = {}) => {
231
232
  * @param {object} pageDef - Page definition (will be mutated)
232
233
  * @param {Map<string, object>} stateUpdates - Map from prefetchPageData
233
234
  */
235
+ /**
236
+ * Fetch polyglot translations from the DB for SSR use.
237
+ * Returns a map of { [lang]: { key: text, ... } } for all configured languages.
238
+ *
239
+ * @param {object} data - Full project data (from loadProject)
240
+ * @returns {Promise<object|null>} Translation map keyed by language, or null on failure
241
+ */
242
+ export const fetchSSRTranslations = async (data) => {
243
+ const config = data.config || data.settings || {}
244
+ const polyglot = config.polyglot
245
+ if (!polyglot?.fetch) return null
246
+
247
+ const dbConfig = config.fetch || config.db || data.db
248
+ if (!dbConfig) return null
249
+
250
+ const adapter = await createSSRAdapter(dbConfig)
251
+ if (!adapter) return null
252
+
253
+ const fetchConfig = polyglot.fetch
254
+ const rpcName = fetchConfig.rpc || fetchConfig.from || 'get_translations_if_changed'
255
+ const languages = polyglot.languages || [polyglot.defaultLang || 'en']
256
+
257
+ const translations = {}
258
+
259
+ // Fetch translations for all languages in parallel
260
+ const results = await Promise.allSettled(
261
+ languages.map(async (lang) => {
262
+ try {
263
+ const res = await adapter.rpc({
264
+ from: rpcName,
265
+ params: { p_lang: lang, p_cached_version: 0 }
266
+ })
267
+ if (res.error || !res.data) return
268
+ const result = res.data
269
+ if (result.translations) {
270
+ translations[lang] = result.translations
271
+ }
272
+ } catch {}
273
+ })
274
+ )
275
+
276
+ return Object.keys(translations).length ? translations : null
277
+ }
278
+
279
+ /**
280
+ * Pre-evaluate children functions and replace them with static results.
281
+ * During SSR, DOMQL's runtime state cascading and async re-render cycle
282
+ * may not work correctly (trackSourcemapDeep stack overflows, etc.).
283
+ * By pre-evaluating the children functions, we produce static element
284
+ * definitions that DOMQL can render directly.
285
+ */
286
+ const preEvaluateChildren = (def, inheritedState) => {
287
+ if (!def || typeof def !== 'object') return
288
+ for (const key in def) {
289
+ if (key === 'state' || key === 'fetch' || key === 'props' ||
290
+ key === 'attr' || key === 'on' || key === 'define' ||
291
+ key === 'childExtends' || key === 'childProps' || key === 'childrenAs') continue
292
+ if (key.charAt(0) >= 'A' && key.charAt(0) <= 'Z' && isObject(def[key])) {
293
+ const child = def[key]
294
+ // Determine effective state for this element (own state or inherited)
295
+ const effectiveState = child.state && typeof child.state === 'object'
296
+ ? { ...inheritedState, ...child.state }
297
+ : inheritedState
298
+
299
+ // Pre-evaluate children function
300
+ if (isFunction(child.children)) {
301
+ try {
302
+ const mockEl = {
303
+ state: effectiveState,
304
+ props: {},
305
+ call: (fn) => {
306
+ if (fn === 'getActiveLang' || fn === 'getLang') return effectiveState?.lang || 'ka'
307
+ if (fn === 'polyglot') return arguments[1] || ''
308
+ return undefined
309
+ },
310
+ __ref: {}
311
+ }
312
+ const result = child.children(mockEl, effectiveState)
313
+ if (isArray(result) && result.length > 0) {
314
+ // Replace children function with static array
315
+ child.children = result
316
+ }
317
+ } catch {
318
+ // If evaluation fails, leave the function as-is
319
+ }
320
+ }
321
+
322
+ // Recurse deeper
323
+ preEvaluateChildren(child, effectiveState)
324
+ }
325
+ }
326
+ }
327
+
234
328
  export const injectPrefetchedState = (pageDef, stateUpdates) => {
235
329
  if (!stateUpdates || !stateUpdates.size) return
236
330
 
@@ -251,6 +345,10 @@ export const injectPrefetchedState = (pageDef, stateUpdates) => {
251
345
  target.state = {}
252
346
  }
253
347
  Object.assign(target.state, data)
348
+
349
+ // Pre-evaluate children functions with the injected state
350
+ // so DOMQL gets static element definitions instead of functions
351
+ preEvaluateChildren(target, target.state)
254
352
  }
255
353
  }
256
354
  }
package/render.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { resolve, join } from 'path'
2
- import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs'
2
+ import { existsSync, writeFileSync, unlinkSync, readFileSync, realpathSync } from 'fs'
3
3
  import { tmpdir } from 'os'
4
4
  import { randomBytes } from 'crypto'
5
5
  import { createEnv } from './env.js'
6
6
  import { resetKeys, assignKeys, mapKeysToElements } from './keys.js'
7
7
  import { extractMetadata, generateHeadHtml } from './metadata.js'
8
8
  import { hydrate } from './hydrate.js'
9
- import { prefetchPageData, injectPrefetchedState } from './prefetch.js'
9
+ import { prefetchPageData, injectPrefetchedState, fetchSSRTranslations } from './prefetch.js'
10
10
  import { parseHTML } from 'linkedom'
11
+ import createEmotionInstance from '@emotion/css/create-instance'
11
12
 
12
13
  // Deep clone that preserves functions and avoids circular refs
13
14
  const structuredCloneDeep = (obj, seen = new WeakMap()) => {
@@ -50,7 +51,7 @@ let _cachedCreateDomql = null
50
51
  const bundleCreateDomql = async () => {
51
52
  if (_cachedCreateDomql) return _cachedCreateDomql
52
53
 
53
- const brenderDir = new URL('.', import.meta.url).pathname
54
+ const brenderDir = realpathSync(new URL('.', import.meta.url).pathname)
54
55
  const monorepoRoot = resolve(brenderDir, '../..')
55
56
  const entry = resolve(monorepoRoot, 'packages', 'smbls', 'src', 'createDomql.js')
56
57
 
@@ -339,11 +340,21 @@ export const render = async (data, options = {}) => {
339
340
  injectPrefetchedState(pageDef, stateUpdates)
340
341
  prefetchedPages[route] = pageDef
341
342
  }
342
- } catch {
343
+ } catch (prefetchErr) {
344
+ console.error('[brender] Prefetch error:', prefetchErr)
343
345
  prefetchedPages = data.pages
344
346
  }
345
347
  }
346
348
 
349
+ // ── SSR polyglot translations ──
350
+ // Fetch translations from the DB so polyglot resolves during render
351
+ let ssrTranslations
352
+ if (prefetch) {
353
+ try {
354
+ ssrTranslations = await fetchSSRTranslations(data)
355
+ } catch {}
356
+ }
357
+
347
358
  const { window, document } = createEnv()
348
359
  const body = document.body
349
360
 
@@ -364,9 +375,45 @@ export const render = async (data, options = {}) => {
364
375
 
365
376
  const app = structuredCloneDeep(data.app || {})
366
377
 
378
+ const config = data.config || data.settings || {}
379
+
380
+ // Inject SSR translations into polyglot config and root state
381
+ const polyglotConfig = config.polyglot ? { ...config.polyglot } : undefined
382
+ if (ssrTranslations && polyglotConfig) {
383
+ polyglotConfig.translations = {
384
+ ...(polyglotConfig.translations || {}),
385
+ ...ssrTranslations
386
+ }
387
+ }
388
+
389
+ const baseState = structuredCloneDeep(data.state || {})
390
+ // Ensure root state has lang and translations for polyglot resolution
391
+ if (ssrTranslations || polyglotConfig) {
392
+ if (!baseState.root) baseState.root = {}
393
+ if (polyglotConfig) {
394
+ baseState.root.lang = baseState.root.lang || polyglotConfig.defaultLang || 'en'
395
+ }
396
+ if (ssrTranslations) {
397
+ baseState.root.translations = {
398
+ ...(baseState.root.translations || {}),
399
+ ...ssrTranslations
400
+ }
401
+ }
402
+ }
403
+
404
+ // Create SSR emotion instance with speedy: false.
405
+ // In linkedom, emotion's insertRule() doesn't handle @media rules properly,
406
+ // so responsive CSS is lost. Non-speedy mode uses text nodes instead,
407
+ // which preserves @media rules in cache.inserted as strings.
408
+ const ssrEmotion = createEmotionInstance({
409
+ key: 'smbls',
410
+ container: document.head,
411
+ speedy: false
412
+ })
413
+
367
414
  const ctx = {
368
- state: structuredCloneDeep(data.state || {}),
369
- ...(stateOverrides ? { state: { ...structuredCloneDeep(data.state || {}), ...stateOverrides } } : {}),
415
+ state: baseState,
416
+ ...(stateOverrides ? { state: { ...baseState, ...stateOverrides } } : {}),
370
417
  dependencies: structuredCloneDeep(data.dependencies || {}),
371
418
  components: structuredCloneDeep(data.components || {}),
372
419
  snippets: structuredCloneDeep(data.snippets || {}),
@@ -375,11 +422,18 @@ export const render = async (data, options = {}) => {
375
422
  methods: data.methods || {},
376
423
  designSystem: structuredCloneDeep(data.designSystem || {}),
377
424
  files: data.files || {},
378
- ...(data.config || data.settings || {}),
425
+ ...config,
426
+ // Override polyglot with SSR-enriched version
427
+ ...(polyglotConfig ? { polyglot: polyglotConfig } : {}),
379
428
  // Virtual DOM environment
380
429
  document,
381
430
  window,
382
431
  parent: { node: body },
432
+ // Use SSR emotion instance (non-speedy) for proper @media rule extraction
433
+ initOptions: { emotion: ssrEmotion },
434
+ // Disable sourcemap tracking in SSR — it causes stack overflows
435
+ // when state contains large data arrays (articles, events, etc.)
436
+ domqlOptions: { sourcemap: false },
383
437
  // Caller overrides
384
438
  ...(contextOverrides || {})
385
439
  }
@@ -388,8 +442,12 @@ export const render = async (data, options = {}) => {
388
442
 
389
443
  const element = await createDomqlElement(app, ctx)
390
444
 
391
- // Allow async microtasks (fetch callbacks, state updates) to flush
392
- await new Promise(r => setTimeout(r, 50))
445
+ // Allow async operations (fetch callbacks, state updates, re-renders) to flush.
446
+ // DOMQL's fetch plugin fires on element creation and updates state asynchronously.
447
+ // With prefetch enabled, data is pre-injected but DOMQL's fetch may also fire
448
+ // and trigger state updates. Give enough time for these to complete.
449
+ const flushDelay = prefetch ? 2000 : 50
450
+ await new Promise(r => setTimeout(r, flushDelay))
393
451
 
394
452
  // Assign data-br keys for hydration
395
453
  assignKeys(body)
@@ -444,7 +502,18 @@ export const render = async (data, options = {}) => {
444
502
  }
445
503
  }
446
504
 
447
- const html = fixSvgContent(body.innerHTML)
505
+ let html = fixSvgContent(body.innerHTML)
506
+
507
+ // Post-process: resolve any remaining {{ key | polyglot }} templates
508
+ // that weren't resolved during DOMQL rendering (e.g. due to timing)
509
+ if (ssrTranslations) {
510
+ const defaultLang = polyglotConfig?.defaultLang || 'en'
511
+ const langMap = ssrTranslations[defaultLang] || Object.values(ssrTranslations)[0] || {}
512
+ html = html.replace(/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g, (match, key) => {
513
+ const trimmed = key.trim()
514
+ return langMap[trimmed] ?? match
515
+ })
516
+ }
448
517
 
449
518
  // Restore globalThis after render
450
519
  if (_prevDoc !== undefined) globalThis.document = _prevDoc
@@ -452,7 +521,7 @@ export const render = async (data, options = {}) => {
452
521
  if (_prevLoc !== undefined) globalThis.location = _prevLoc
453
522
  else delete globalThis.location
454
523
 
455
- return { html, metadata, registry, element, emotionCSS, document, window }
524
+ return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations }
456
525
  }
457
526
 
458
527
  /**
@@ -846,10 +915,28 @@ export const renderPage = async (data, route = '/', options = {}) => {
846
915
  <script type="module" src="${prefix}${isr.clientScript}"></script>`
847
916
  }
848
917
 
918
+ // Resolve any {{ key | polyglot }} templates in head tags (title, meta, etc.)
919
+ const config = data.config || data.settings || {}
920
+ const polyglotCfg = config.polyglot
921
+ let resolvedHeadTags = headTags
922
+ if (polyglotCfg) {
923
+ const defaultLang = polyglotCfg.defaultLang || 'en'
924
+ // Use SSR-fetched translations (from render result) merged with static translations
925
+ const translations = {
926
+ ...(polyglotCfg.translations || {}),
927
+ ...(result.ssrTranslations || {})
928
+ }
929
+ const langMap = translations[defaultLang] || {}
930
+ resolvedHeadTags = headTags.replace(/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g, (match, key) => {
931
+ const trimmed = key.trim()
932
+ return langMap[trimmed] ?? match
933
+ })
934
+ }
935
+
849
936
  const html = `<!DOCTYPE html>
850
937
  <html lang="${htmlLang}">
851
938
  <head>
852
- ${headTags}
939
+ ${resolvedHeadTags}
853
940
  ${fontLinks}
854
941
  ${globalCSS.fontFaceCSS ? `<style>${globalCSS.fontFaceCSS}</style>` : ''}
855
942
  <style>