@symbo.ls/brender 3.7.0 → 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 +11 -11
- package/dist/cjs/prefetch.js +66 -3
- package/dist/cjs/render.js +75 -9
- package/dist/esm/load.js +11 -11
- package/dist/esm/prefetch.js +66 -3
- package/dist/esm/render.js +77 -11
- package/load.js +13 -11
- package/package.json +2 -2
- package/prefetch.js +99 -1
- package/render.js +99 -12
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) => {
|
package/dist/cjs/prefetch.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
};
|
package/dist/cjs/render.js
CHANGED
|
@@ -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:
|
|
364
|
-
...stateOverrides ? { state: { ...
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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) => {
|
package/dist/esm/prefetch.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
};
|
package/dist/esm/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
|
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:
|
|
327
|
-
...stateOverrides ? { state: { ...
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
+
"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.
|
|
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
|
|
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:
|
|
369
|
-
...(stateOverrides ? { state: { ...
|
|
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
|
-
...
|
|
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
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
939
|
+
${resolvedHeadTags}
|
|
853
940
|
${fontLinks}
|
|
854
941
|
${globalCSS.fontFaceCSS ? `<style>${globalCSS.fontFaceCSS}</style>` : ''}
|
|
855
942
|
<style>
|