@symbo.ls/brender 3.7.4 → 3.7.6
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/index.js +4 -0
- package/dist/cjs/prefetch.js +17 -14
- package/dist/cjs/render.js +163 -45
- package/dist/esm/index.js +5 -1
- package/dist/esm/prefetch.js +17 -14
- package/dist/esm/render.js +163 -45
- package/index.js +5 -1
- package/package.json +2 -2
- package/prefetch.js +23 -14
- package/render.js +218 -59
package/dist/cjs/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
extractMetadata: () => import_metadata.extractMetadata,
|
|
25
25
|
generateHeadHtml: () => import_metadata.generateHeadHtml,
|
|
26
26
|
generateSitemap: () => import_sitemap.generateSitemap,
|
|
27
|
+
getAccumulatedEmotionCSS: () => import_render.getAccumulatedEmotionCSS,
|
|
27
28
|
hydrate: () => import_hydrate.hydrate,
|
|
28
29
|
injectPrefetchedState: () => import_prefetch.injectPrefetchedState,
|
|
29
30
|
loadAndRenderAll: () => import_load.loadAndRenderAll,
|
|
@@ -34,6 +35,7 @@ __export(index_exports, {
|
|
|
34
35
|
renderElement: () => import_render.renderElement,
|
|
35
36
|
renderPage: () => import_render.renderPage,
|
|
36
37
|
renderRoute: () => import_render.renderRoute,
|
|
38
|
+
replaceEmotionCSS: () => import_render.replaceEmotionCSS,
|
|
37
39
|
resetGlobalCSSCache: () => import_render.resetGlobalCSSCache,
|
|
38
40
|
resetKeys: () => import_keys.resetKeys
|
|
39
41
|
});
|
|
@@ -58,6 +60,8 @@ var index_default = {
|
|
|
58
60
|
renderRoute: import_render.renderRoute,
|
|
59
61
|
renderPage: import_render.renderPage,
|
|
60
62
|
resetGlobalCSSCache: import_render.resetGlobalCSSCache,
|
|
63
|
+
getAccumulatedEmotionCSS: import_render.getAccumulatedEmotionCSS,
|
|
64
|
+
replaceEmotionCSS: import_render.replaceEmotionCSS,
|
|
61
65
|
extractMetadata: import_metadata.extractMetadata,
|
|
62
66
|
generateHeadHtml: import_metadata.generateHeadHtml,
|
|
63
67
|
collectBrNodes: import_hydrate.collectBrNodes,
|
package/dist/cjs/prefetch.js
CHANGED
|
@@ -99,15 +99,14 @@ const createSSRAdapter = async (dbConfig) => {
|
|
|
99
99
|
if (adapter !== "supabase") return null;
|
|
100
100
|
const supabaseUrl = url || projectId && `https://${projectId}.supabase.co`;
|
|
101
101
|
if (!supabaseUrl || !key) return null;
|
|
102
|
-
let clientFactory
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
102
|
+
let clientFactory;
|
|
103
|
+
try {
|
|
104
|
+
const mod = await import("@supabase/supabase-js");
|
|
105
|
+
clientFactory = mod.createClient;
|
|
106
|
+
} catch {
|
|
107
|
+
clientFactory = createClient;
|
|
110
108
|
}
|
|
109
|
+
if (!clientFactory) return null;
|
|
111
110
|
const client = clientFactory(supabaseUrl, key);
|
|
112
111
|
return {
|
|
113
112
|
rpc: ({ from, params }) => client.rpc(from, params),
|
|
@@ -161,7 +160,7 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
|
|
|
161
160
|
const pageDef = pages[route];
|
|
162
161
|
if (!pageDef) return /* @__PURE__ */ new Map();
|
|
163
162
|
const config = data.config || data.settings || {};
|
|
164
|
-
const dbConfig = config.fetch || config.db || data.db;
|
|
163
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db;
|
|
165
164
|
if (!dbConfig) return /* @__PURE__ */ new Map();
|
|
166
165
|
const adapter = await createSSRAdapter(dbConfig);
|
|
167
166
|
if (!adapter) return /* @__PURE__ */ new Map();
|
|
@@ -171,9 +170,13 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
|
|
|
171
170
|
const results = await Promise.allSettled(
|
|
172
171
|
declarations.map(async ({ config: config2, stateKey, path }) => {
|
|
173
172
|
const fetchedData = await executeSingle(adapter, config2);
|
|
174
|
-
if (fetchedData !== null
|
|
173
|
+
if (fetchedData !== null) {
|
|
175
174
|
const existing = stateUpdates.get(path) || {};
|
|
176
|
-
|
|
175
|
+
if (stateKey) {
|
|
176
|
+
existing[stateKey] = fetchedData;
|
|
177
|
+
} else if (isObject(fetchedData)) {
|
|
178
|
+
Object.assign(existing, fetchedData);
|
|
179
|
+
}
|
|
177
180
|
stateUpdates.set(path, existing);
|
|
178
181
|
}
|
|
179
182
|
})
|
|
@@ -181,10 +184,10 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
|
|
|
181
184
|
return stateUpdates;
|
|
182
185
|
};
|
|
183
186
|
const fetchSSRTranslations = async (data) => {
|
|
184
|
-
const config = data.config ||
|
|
185
|
-
const polyglot = config.polyglot;
|
|
187
|
+
const config = data.config || {};
|
|
188
|
+
const polyglot = config.polyglot || data.polyglot;
|
|
186
189
|
if (!polyglot?.fetch) return null;
|
|
187
|
-
const dbConfig = config.fetch || config.db || data.db;
|
|
190
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db;
|
|
188
191
|
if (!dbConfig) return null;
|
|
189
192
|
const adapter = await createSSRAdapter(dbConfig);
|
|
190
193
|
if (!adapter) return null;
|
package/dist/cjs/render.js
CHANGED
|
@@ -27,10 +27,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
28
|
var render_exports = {};
|
|
29
29
|
__export(render_exports, {
|
|
30
|
+
getAccumulatedEmotionCSS: () => getAccumulatedEmotionCSS,
|
|
30
31
|
render: () => render,
|
|
31
32
|
renderElement: () => renderElement,
|
|
32
33
|
renderPage: () => renderPage,
|
|
33
34
|
renderRoute: () => renderRoute,
|
|
35
|
+
replaceEmotionCSS: () => replaceEmotionCSS,
|
|
34
36
|
resetGlobalCSSCache: () => resetGlobalCSSCache
|
|
35
37
|
});
|
|
36
38
|
module.exports = __toCommonJS(render_exports);
|
|
@@ -46,6 +48,51 @@ var import_prefetch = require("./prefetch.js");
|
|
|
46
48
|
var import_linkedom = require("linkedom");
|
|
47
49
|
var import_create_instance = __toESM(require("@emotion/css/create-instance"), 1);
|
|
48
50
|
const import_meta = {};
|
|
51
|
+
const ssrResolve = (map, key) => {
|
|
52
|
+
if (!map || !key) return void 0;
|
|
53
|
+
if (map[key] !== void 0) return map[key];
|
|
54
|
+
const parts = key.split(".");
|
|
55
|
+
let v = map;
|
|
56
|
+
for (const p of parts) {
|
|
57
|
+
if (v == null || typeof v !== "object") return void 0;
|
|
58
|
+
v = v[p];
|
|
59
|
+
}
|
|
60
|
+
return v;
|
|
61
|
+
};
|
|
62
|
+
const ssrTranslate = function(key, lang) {
|
|
63
|
+
if (!key) return "";
|
|
64
|
+
const ctx = this?.context;
|
|
65
|
+
const poly = ctx?.polyglot;
|
|
66
|
+
const activeLang = lang || poly?.defaultLang || "ka";
|
|
67
|
+
if (poly?.translations) {
|
|
68
|
+
const langMap = poly.translations[activeLang];
|
|
69
|
+
if (langMap) {
|
|
70
|
+
const val = ssrResolve(langMap, key);
|
|
71
|
+
if (val !== void 0) return val;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const root = this?.state?.root || ctx?.state?.root;
|
|
75
|
+
if (root?.translations) {
|
|
76
|
+
const langMap = root.translations[activeLang];
|
|
77
|
+
if (langMap) {
|
|
78
|
+
const val = ssrResolve(langMap, key);
|
|
79
|
+
if (val !== void 0) return val;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const defaultLang = poly?.defaultLang || "en";
|
|
83
|
+
if (defaultLang !== activeLang && poly?.translations) {
|
|
84
|
+
const fallback = poly.translations[defaultLang];
|
|
85
|
+
if (fallback) {
|
|
86
|
+
const val = ssrResolve(fallback, key);
|
|
87
|
+
if (val !== void 0) return val;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return key;
|
|
91
|
+
};
|
|
92
|
+
const ssrGetActiveLang = function() {
|
|
93
|
+
const ctx = this?.context;
|
|
94
|
+
return this?.state?.root?.lang || ctx?.polyglot?.defaultLang || "ka";
|
|
95
|
+
};
|
|
49
96
|
const structuredCloneDeep = (obj, seen = /* @__PURE__ */ new WeakMap()) => {
|
|
50
97
|
if (obj === null || typeof obj !== "object") return obj;
|
|
51
98
|
if (seen.has(obj)) return seen.get(obj);
|
|
@@ -321,7 +368,16 @@ const UIKIT_STUBS = {
|
|
|
321
368
|
Text: { tag: "span" }
|
|
322
369
|
};
|
|
323
370
|
const render = async (data, options = {}) => {
|
|
324
|
-
const { route = "/", state: stateOverrides, context: contextOverrides, prefetch = false } = options;
|
|
371
|
+
const { route = "/", pathname, state: stateOverrides, context: contextOverrides, prefetch = false } = options;
|
|
372
|
+
const locationPath = pathname || route;
|
|
373
|
+
const _prevLocPrefetch = globalThis.location;
|
|
374
|
+
const _prevWinPrefetch = globalThis.window;
|
|
375
|
+
if (!globalThis.location || globalThis.location.pathname !== locationPath) {
|
|
376
|
+
globalThis.location = { pathname: locationPath, href: locationPath, search: "", hash: "", origin: "" };
|
|
377
|
+
}
|
|
378
|
+
if (!globalThis.window) {
|
|
379
|
+
globalThis.window = { location: globalThis.location };
|
|
380
|
+
}
|
|
325
381
|
let prefetchedPages;
|
|
326
382
|
if (prefetch) {
|
|
327
383
|
try {
|
|
@@ -359,16 +415,26 @@ const render = async (data, options = {}) => {
|
|
|
359
415
|
} catch {
|
|
360
416
|
}
|
|
361
417
|
}
|
|
418
|
+
if (_prevLocPrefetch !== void 0) globalThis.location = _prevLocPrefetch;
|
|
419
|
+
else delete globalThis.location;
|
|
420
|
+
if (_prevWinPrefetch !== void 0) globalThis.window = _prevWinPrefetch;
|
|
421
|
+
else delete globalThis.window;
|
|
362
422
|
const { window, document } = (0, import_env.createEnv)();
|
|
363
423
|
const body = document.body;
|
|
364
|
-
window.location.pathname =
|
|
424
|
+
window.location.pathname = locationPath;
|
|
365
425
|
const _prevDoc = globalThis.document;
|
|
366
426
|
const _prevLoc = globalThis.location;
|
|
367
427
|
globalThis.document = document;
|
|
368
428
|
globalThis.location = window.location;
|
|
369
429
|
const { createDomqlElement } = await bundleCreateDomql();
|
|
370
430
|
const app = structuredCloneDeep(data.app || {});
|
|
371
|
-
const config = data.config ||
|
|
431
|
+
const config = { ...data.config || {} };
|
|
432
|
+
if (data.polyglot && !config.polyglot) config.polyglot = data.polyglot;
|
|
433
|
+
if (data.fetch && !config.fetch) config.fetch = data.fetch;
|
|
434
|
+
if (data.router && !config.router) config.router = data.router;
|
|
435
|
+
for (const k of ["useReset", "useVariable", "useFontImport", "useIconSprite", "useSvgSprite", "useDefaultConfig", "useDocumentTheme"]) {
|
|
436
|
+
if (data[k] != null && config[k] == null) config[k] = data[k];
|
|
437
|
+
}
|
|
372
438
|
const polyglotConfig = config.polyglot ? { ...config.polyglot } : void 0;
|
|
373
439
|
if (ssrTranslations && polyglotConfig) {
|
|
374
440
|
polyglotConfig.translations = {
|
|
@@ -401,7 +467,13 @@ const render = async (data, options = {}) => {
|
|
|
401
467
|
components: structuredCloneDeep(data.components || {}),
|
|
402
468
|
snippets: structuredCloneDeep(data.snippets || {}),
|
|
403
469
|
pages: structuredCloneDeep(prefetchedPages || data.pages || {}),
|
|
404
|
-
functions:
|
|
470
|
+
functions: {
|
|
471
|
+
...data.functions || {},
|
|
472
|
+
// SSR polyglot functions — enable {{ key | polyglot }} resolution during render
|
|
473
|
+
polyglot: ssrTranslate,
|
|
474
|
+
getActiveLang: ssrGetActiveLang,
|
|
475
|
+
getLang: ssrGetActiveLang
|
|
476
|
+
},
|
|
405
477
|
methods: data.methods || {},
|
|
406
478
|
designSystem: structuredCloneDeep(data.designSystem || {}),
|
|
407
479
|
files: data.files || {},
|
|
@@ -426,7 +498,7 @@ const render = async (data, options = {}) => {
|
|
|
426
498
|
await new Promise((r) => setTimeout(r, flushDelay));
|
|
427
499
|
(0, import_keys.assignKeys)(body);
|
|
428
500
|
const registry = (0, import_keys.mapKeysToElements)(element);
|
|
429
|
-
const metadata = (0, import_metadata.extractMetadata)(data, route);
|
|
501
|
+
const metadata = (0, import_metadata.extractMetadata)(data, route, element, element?.state);
|
|
430
502
|
const emotionCSS = [];
|
|
431
503
|
const emotionInstance = ctx.emotion || element && element.context && element.context.emotion;
|
|
432
504
|
if (emotionInstance && emotionInstance.cache) {
|
|
@@ -476,7 +548,7 @@ const render = async (data, options = {}) => {
|
|
|
476
548
|
else delete globalThis.document;
|
|
477
549
|
if (_prevLoc !== void 0) globalThis.location = _prevLoc;
|
|
478
550
|
else delete globalThis.location;
|
|
479
|
-
return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations };
|
|
551
|
+
return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations, prefetchedPages };
|
|
480
552
|
};
|
|
481
553
|
const renderElement = async (elementDef, options = {}) => {
|
|
482
554
|
const { context = {} } = options;
|
|
@@ -660,63 +732,82 @@ ${frameRules}
|
|
|
660
732
|
return _cachedGlobalCSS;
|
|
661
733
|
}
|
|
662
734
|
};
|
|
663
|
-
let
|
|
735
|
+
let _accumulatedEmotionCSS = /* @__PURE__ */ new Set();
|
|
664
736
|
const resetGlobalCSSCache = () => {
|
|
665
737
|
_cachedGlobalCSS = null;
|
|
666
|
-
|
|
738
|
+
_accumulatedEmotionCSS = /* @__PURE__ */ new Set();
|
|
739
|
+
};
|
|
740
|
+
const getAccumulatedEmotionCSS = () => Array.from(_accumulatedEmotionCSS).join("\n");
|
|
741
|
+
const replaceEmotionCSS = (html, newCSS) => {
|
|
742
|
+
return html.replace(
|
|
743
|
+
/<style data-emotion="smbls">[\s\S]*?<\/style>/,
|
|
744
|
+
newCSS ? `<style data-emotion="smbls">
|
|
745
|
+
${newCSS}
|
|
746
|
+
</style>` : ""
|
|
747
|
+
);
|
|
667
748
|
};
|
|
668
749
|
const renderRoute = async (data, options = {}) => {
|
|
669
|
-
const { route = "/" } = options;
|
|
750
|
+
const { route = "/", pathname } = options;
|
|
751
|
+
const result = await render(data, { route, pathname, prefetch: true });
|
|
752
|
+
if (!result) return null;
|
|
670
753
|
const ds = data.designSystem || {};
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
context: {
|
|
675
|
-
components: data.components || {},
|
|
676
|
-
snippets: data.snippets || {},
|
|
677
|
-
designSystem: ds,
|
|
678
|
-
state: data.state || {},
|
|
679
|
-
functions: data.functions || {},
|
|
680
|
-
methods: data.methods || {}
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
const { document: cssDoc } = (0, import_linkedom.parseHTML)(`<html><head></head><body>${result.html}</body></html>`);
|
|
684
|
-
let emotionInstance;
|
|
754
|
+
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
|
|
755
|
+
let prefetchedState = null;
|
|
756
|
+
let activeLang = null;
|
|
685
757
|
try {
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
758
|
+
const el = result.element;
|
|
759
|
+
const polyglot = el?.context?.polyglot || data.polyglot || data.config?.polyglot;
|
|
760
|
+
activeLang = el?.state?.root?.lang || polyglot?.defaultLang || "en";
|
|
761
|
+
if (result.prefetchedPages && result.prefetchedPages[route]) {
|
|
762
|
+
const pageDef = result.prefetchedPages[route];
|
|
763
|
+
const collectStates = (def, result2 = {}) => {
|
|
764
|
+
if (!def || typeof def !== "object") return result2;
|
|
765
|
+
if (def.state && typeof def.state === "object") {
|
|
766
|
+
for (const [k, v] of Object.entries(def.state)) {
|
|
767
|
+
if (v !== void 0 && v !== null && typeof v !== "function") {
|
|
768
|
+
result2[k] = v;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (const [key, child] of Object.entries(def)) {
|
|
773
|
+
if (key === "state" || key === "props" || key === "attr" || key === "on" || key === "define" || key === "__ref" || key.startsWith("__")) continue;
|
|
774
|
+
if (child && typeof child === "object" && !Array.isArray(child)) {
|
|
775
|
+
collectStates(child, result2);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return result2;
|
|
779
|
+
};
|
|
780
|
+
prefetchedState = collectStates(pageDef);
|
|
781
|
+
}
|
|
782
|
+
} catch (e) {
|
|
689
783
|
}
|
|
690
|
-
(0, import_hydrate.hydrate)(result.element, {
|
|
691
|
-
root: cssDoc.body,
|
|
692
|
-
renderEvents: false,
|
|
693
|
-
events: false,
|
|
694
|
-
emotion: emotionInstance,
|
|
695
|
-
designSystem: ds
|
|
696
|
-
});
|
|
697
|
-
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
|
|
698
784
|
return {
|
|
699
|
-
html:
|
|
700
|
-
css:
|
|
785
|
+
html: result.html,
|
|
786
|
+
css: result.emotionCSS ? result.emotionCSS.join("\n") : "",
|
|
701
787
|
globalCSS,
|
|
702
788
|
resetCss: globalCSS.resetRules || generateResetCSS(ds.reset),
|
|
703
789
|
fontLinks: generateFontLinks(ds),
|
|
704
|
-
metadata: (0, import_metadata.extractMetadata)(data, route),
|
|
705
|
-
brKeyCount: Object.keys(result.registry).length
|
|
790
|
+
metadata: result.metadata || (0, import_metadata.extractMetadata)(data, route),
|
|
791
|
+
brKeyCount: result.registry ? Object.keys(result.registry).length : 0,
|
|
792
|
+
ssrTranslations: result.ssrTranslations,
|
|
793
|
+
prefetchedState,
|
|
794
|
+
activeLang
|
|
706
795
|
};
|
|
707
796
|
};
|
|
708
797
|
const renderPage = async (data, route = "/", options = {}) => {
|
|
709
|
-
const { lang, themeColor, isr, prefetch = true } = options;
|
|
798
|
+
const { lang, themeColor, isr, hydrate: hydrate2 = true, prefetch = true } = options;
|
|
710
799
|
const htmlLang = lang || data.state?.lang || data.app?.metadata?.lang || "en";
|
|
711
800
|
const result = await render(data, { route, prefetch });
|
|
712
801
|
if (!result) return null;
|
|
713
802
|
const metadata = { ...result.metadata };
|
|
714
803
|
if (themeColor) metadata["theme-color"] = themeColor;
|
|
715
804
|
const headTags = (0, import_metadata.generateHeadHtml)(metadata);
|
|
716
|
-
if (
|
|
717
|
-
|
|
805
|
+
if (result.emotionCSS && result.emotionCSS.length) {
|
|
806
|
+
for (const rule of result.emotionCSS) {
|
|
807
|
+
if (rule) _accumulatedEmotionCSS.add(rule);
|
|
808
|
+
}
|
|
718
809
|
}
|
|
719
|
-
const emotionCSS =
|
|
810
|
+
const emotionCSS = Array.from(_accumulatedEmotionCSS).join("\n");
|
|
720
811
|
const ds = data.designSystem || {};
|
|
721
812
|
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
|
|
722
813
|
const fontLinks = generateFontLinks(ds);
|
|
@@ -725,7 +816,32 @@ const renderPage = async (data, route = "/", options = {}) => {
|
|
|
725
816
|
if (isr && isr.clientScript) {
|
|
726
817
|
const depth = route === "/" ? 0 : route.replace(/^\/|\/$/g, "").split("/").length;
|
|
727
818
|
const prefix = depth > 0 ? "../".repeat(depth) : "./";
|
|
728
|
-
|
|
819
|
+
if (hydrate2) {
|
|
820
|
+
let translationSeed = "";
|
|
821
|
+
if (result.ssrTranslations) {
|
|
822
|
+
const polyglotCfg2 = data.polyglot || data.config?.polyglot;
|
|
823
|
+
const storagePrefix = polyglotCfg2?.storagePrefix || "";
|
|
824
|
+
const storageLangKey = polyglotCfg2?.storageLangKey || "";
|
|
825
|
+
const seedEntries = [];
|
|
826
|
+
for (const lang2 in result.ssrTranslations) {
|
|
827
|
+
const map = result.ssrTranslations[lang2];
|
|
828
|
+
if (map && typeof map === "object") {
|
|
829
|
+
seedEntries.push(`localStorage.setItem(${JSON.stringify(storagePrefix + lang2)},${JSON.stringify(JSON.stringify(map))})`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (storageLangKey) {
|
|
833
|
+
const defaultLang = polyglotCfg2?.defaultLang || "en";
|
|
834
|
+
seedEntries.push(`if(!localStorage.getItem(${JSON.stringify(storageLangKey)}))localStorage.setItem(${JSON.stringify(storageLangKey)},${JSON.stringify(defaultLang)})`);
|
|
835
|
+
}
|
|
836
|
+
if (seedEntries.length) {
|
|
837
|
+
translationSeed = `<script>try{${seedEntries.join(";")}}catch(e){}<\/script>
|
|
838
|
+
`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
isrBody = `${translationSeed}<script>window.__BRENDER__ = true<\/script>
|
|
842
|
+
<script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
|
|
843
|
+
} else {
|
|
844
|
+
isrBody = `<script type="module">
|
|
729
845
|
{
|
|
730
846
|
const brEls = document.querySelectorAll('body > :not(script):not(style)')
|
|
731
847
|
const observer = new MutationObserver((mutations) => {
|
|
@@ -743,9 +859,11 @@ const renderPage = async (data, route = "/", options = {}) => {
|
|
|
743
859
|
}
|
|
744
860
|
<\/script>
|
|
745
861
|
<script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
|
|
862
|
+
}
|
|
746
863
|
}
|
|
747
|
-
const
|
|
748
|
-
|
|
864
|
+
const headConfig = { ...data.config || {} };
|
|
865
|
+
if (data.polyglot && !headConfig.polyglot) headConfig.polyglot = data.polyglot;
|
|
866
|
+
const polyglotCfg = headConfig.polyglot;
|
|
749
867
|
let resolvedHeadTags = headTags;
|
|
750
868
|
if (polyglotCfg) {
|
|
751
869
|
const defaultLang = polyglotCfg.defaultLang || "en";
|
package/dist/esm/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEnv } from "./env.js";
|
|
2
2
|
import { resetKeys, assignKeys, mapKeysToElements } from "./keys.js";
|
|
3
3
|
import { loadProject, loadAndRenderAll } from "./load.js";
|
|
4
|
-
import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache } from "./render.js";
|
|
4
|
+
import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache, getAccumulatedEmotionCSS, replaceEmotionCSS } from "./render.js";
|
|
5
5
|
import { extractMetadata, generateHeadHtml } from "./metadata.js";
|
|
6
6
|
import { collectBrNodes, hydrate } from "./hydrate.js";
|
|
7
7
|
import { generateSitemap } from "./sitemap.js";
|
|
@@ -18,6 +18,8 @@ var index_default = {
|
|
|
18
18
|
renderRoute,
|
|
19
19
|
renderPage,
|
|
20
20
|
resetGlobalCSSCache,
|
|
21
|
+
getAccumulatedEmotionCSS,
|
|
22
|
+
replaceEmotionCSS,
|
|
21
23
|
extractMetadata,
|
|
22
24
|
generateHeadHtml,
|
|
23
25
|
collectBrNodes,
|
|
@@ -34,6 +36,7 @@ export {
|
|
|
34
36
|
extractMetadata,
|
|
35
37
|
generateHeadHtml,
|
|
36
38
|
generateSitemap,
|
|
39
|
+
getAccumulatedEmotionCSS,
|
|
37
40
|
hydrate,
|
|
38
41
|
injectPrefetchedState,
|
|
39
42
|
loadAndRenderAll,
|
|
@@ -44,6 +47,7 @@ export {
|
|
|
44
47
|
renderElement,
|
|
45
48
|
renderPage,
|
|
46
49
|
renderRoute,
|
|
50
|
+
replaceEmotionCSS,
|
|
47
51
|
resetGlobalCSSCache,
|
|
48
52
|
resetKeys
|
|
49
53
|
};
|
package/dist/esm/prefetch.js
CHANGED
|
@@ -65,15 +65,14 @@ const createSSRAdapter = async (dbConfig) => {
|
|
|
65
65
|
if (adapter !== "supabase") return null;
|
|
66
66
|
const supabaseUrl = url || projectId && `https://${projectId}.supabase.co`;
|
|
67
67
|
if (!supabaseUrl || !key) return null;
|
|
68
|
-
let clientFactory
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
68
|
+
let clientFactory;
|
|
69
|
+
try {
|
|
70
|
+
const mod = await import("@supabase/supabase-js");
|
|
71
|
+
clientFactory = mod.createClient;
|
|
72
|
+
} catch {
|
|
73
|
+
clientFactory = createClient;
|
|
76
74
|
}
|
|
75
|
+
if (!clientFactory) return null;
|
|
77
76
|
const client = clientFactory(supabaseUrl, key);
|
|
78
77
|
return {
|
|
79
78
|
rpc: ({ from, params }) => client.rpc(from, params),
|
|
@@ -127,7 +126,7 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
|
|
|
127
126
|
const pageDef = pages[route];
|
|
128
127
|
if (!pageDef) return /* @__PURE__ */ new Map();
|
|
129
128
|
const config = data.config || data.settings || {};
|
|
130
|
-
const dbConfig = config.fetch || config.db || data.db;
|
|
129
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db;
|
|
131
130
|
if (!dbConfig) return /* @__PURE__ */ new Map();
|
|
132
131
|
const adapter = await createSSRAdapter(dbConfig);
|
|
133
132
|
if (!adapter) return /* @__PURE__ */ new Map();
|
|
@@ -137,9 +136,13 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
|
|
|
137
136
|
const results = await Promise.allSettled(
|
|
138
137
|
declarations.map(async ({ config: config2, stateKey, path }) => {
|
|
139
138
|
const fetchedData = await executeSingle(adapter, config2);
|
|
140
|
-
if (fetchedData !== null
|
|
139
|
+
if (fetchedData !== null) {
|
|
141
140
|
const existing = stateUpdates.get(path) || {};
|
|
142
|
-
|
|
141
|
+
if (stateKey) {
|
|
142
|
+
existing[stateKey] = fetchedData;
|
|
143
|
+
} else if (isObject(fetchedData)) {
|
|
144
|
+
Object.assign(existing, fetchedData);
|
|
145
|
+
}
|
|
143
146
|
stateUpdates.set(path, existing);
|
|
144
147
|
}
|
|
145
148
|
})
|
|
@@ -147,10 +150,10 @@ const prefetchPageData = async (data, route = "/", options = {}) => {
|
|
|
147
150
|
return stateUpdates;
|
|
148
151
|
};
|
|
149
152
|
const fetchSSRTranslations = async (data) => {
|
|
150
|
-
const config = data.config ||
|
|
151
|
-
const polyglot = config.polyglot;
|
|
153
|
+
const config = data.config || {};
|
|
154
|
+
const polyglot = config.polyglot || data.polyglot;
|
|
152
155
|
if (!polyglot?.fetch) return null;
|
|
153
|
-
const dbConfig = config.fetch || config.db || data.db;
|
|
156
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db;
|
|
154
157
|
if (!dbConfig) return null;
|
|
155
158
|
const adapter = await createSSRAdapter(dbConfig);
|
|
156
159
|
if (!adapter) return null;
|
package/dist/esm/render.js
CHANGED
|
@@ -9,6 +9,51 @@ import { hydrate } from "./hydrate.js";
|
|
|
9
9
|
import { prefetchPageData, injectPrefetchedState, fetchSSRTranslations } from "./prefetch.js";
|
|
10
10
|
import { parseHTML } from "linkedom";
|
|
11
11
|
import createEmotionInstance from "@emotion/css/create-instance";
|
|
12
|
+
const ssrResolve = (map, key) => {
|
|
13
|
+
if (!map || !key) return void 0;
|
|
14
|
+
if (map[key] !== void 0) return map[key];
|
|
15
|
+
const parts = key.split(".");
|
|
16
|
+
let v = map;
|
|
17
|
+
for (const p of parts) {
|
|
18
|
+
if (v == null || typeof v !== "object") return void 0;
|
|
19
|
+
v = v[p];
|
|
20
|
+
}
|
|
21
|
+
return v;
|
|
22
|
+
};
|
|
23
|
+
const ssrTranslate = function(key, lang) {
|
|
24
|
+
if (!key) return "";
|
|
25
|
+
const ctx = this?.context;
|
|
26
|
+
const poly = ctx?.polyglot;
|
|
27
|
+
const activeLang = lang || poly?.defaultLang || "ka";
|
|
28
|
+
if (poly?.translations) {
|
|
29
|
+
const langMap = poly.translations[activeLang];
|
|
30
|
+
if (langMap) {
|
|
31
|
+
const val = ssrResolve(langMap, key);
|
|
32
|
+
if (val !== void 0) return val;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const root = this?.state?.root || ctx?.state?.root;
|
|
36
|
+
if (root?.translations) {
|
|
37
|
+
const langMap = root.translations[activeLang];
|
|
38
|
+
if (langMap) {
|
|
39
|
+
const val = ssrResolve(langMap, key);
|
|
40
|
+
if (val !== void 0) return val;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const defaultLang = poly?.defaultLang || "en";
|
|
44
|
+
if (defaultLang !== activeLang && poly?.translations) {
|
|
45
|
+
const fallback = poly.translations[defaultLang];
|
|
46
|
+
if (fallback) {
|
|
47
|
+
const val = ssrResolve(fallback, key);
|
|
48
|
+
if (val !== void 0) return val;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return key;
|
|
52
|
+
};
|
|
53
|
+
const ssrGetActiveLang = function() {
|
|
54
|
+
const ctx = this?.context;
|
|
55
|
+
return this?.state?.root?.lang || ctx?.polyglot?.defaultLang || "ka";
|
|
56
|
+
};
|
|
12
57
|
const structuredCloneDeep = (obj, seen = /* @__PURE__ */ new WeakMap()) => {
|
|
13
58
|
if (obj === null || typeof obj !== "object") return obj;
|
|
14
59
|
if (seen.has(obj)) return seen.get(obj);
|
|
@@ -284,7 +329,16 @@ const UIKIT_STUBS = {
|
|
|
284
329
|
Text: { tag: "span" }
|
|
285
330
|
};
|
|
286
331
|
const render = async (data, options = {}) => {
|
|
287
|
-
const { route = "/", state: stateOverrides, context: contextOverrides, prefetch = false } = options;
|
|
332
|
+
const { route = "/", pathname, state: stateOverrides, context: contextOverrides, prefetch = false } = options;
|
|
333
|
+
const locationPath = pathname || route;
|
|
334
|
+
const _prevLocPrefetch = globalThis.location;
|
|
335
|
+
const _prevWinPrefetch = globalThis.window;
|
|
336
|
+
if (!globalThis.location || globalThis.location.pathname !== locationPath) {
|
|
337
|
+
globalThis.location = { pathname: locationPath, href: locationPath, search: "", hash: "", origin: "" };
|
|
338
|
+
}
|
|
339
|
+
if (!globalThis.window) {
|
|
340
|
+
globalThis.window = { location: globalThis.location };
|
|
341
|
+
}
|
|
288
342
|
let prefetchedPages;
|
|
289
343
|
if (prefetch) {
|
|
290
344
|
try {
|
|
@@ -322,16 +376,26 @@ const render = async (data, options = {}) => {
|
|
|
322
376
|
} catch {
|
|
323
377
|
}
|
|
324
378
|
}
|
|
379
|
+
if (_prevLocPrefetch !== void 0) globalThis.location = _prevLocPrefetch;
|
|
380
|
+
else delete globalThis.location;
|
|
381
|
+
if (_prevWinPrefetch !== void 0) globalThis.window = _prevWinPrefetch;
|
|
382
|
+
else delete globalThis.window;
|
|
325
383
|
const { window, document } = createEnv();
|
|
326
384
|
const body = document.body;
|
|
327
|
-
window.location.pathname =
|
|
385
|
+
window.location.pathname = locationPath;
|
|
328
386
|
const _prevDoc = globalThis.document;
|
|
329
387
|
const _prevLoc = globalThis.location;
|
|
330
388
|
globalThis.document = document;
|
|
331
389
|
globalThis.location = window.location;
|
|
332
390
|
const { createDomqlElement } = await bundleCreateDomql();
|
|
333
391
|
const app = structuredCloneDeep(data.app || {});
|
|
334
|
-
const config = data.config ||
|
|
392
|
+
const config = { ...data.config || {} };
|
|
393
|
+
if (data.polyglot && !config.polyglot) config.polyglot = data.polyglot;
|
|
394
|
+
if (data.fetch && !config.fetch) config.fetch = data.fetch;
|
|
395
|
+
if (data.router && !config.router) config.router = data.router;
|
|
396
|
+
for (const k of ["useReset", "useVariable", "useFontImport", "useIconSprite", "useSvgSprite", "useDefaultConfig", "useDocumentTheme"]) {
|
|
397
|
+
if (data[k] != null && config[k] == null) config[k] = data[k];
|
|
398
|
+
}
|
|
335
399
|
const polyglotConfig = config.polyglot ? { ...config.polyglot } : void 0;
|
|
336
400
|
if (ssrTranslations && polyglotConfig) {
|
|
337
401
|
polyglotConfig.translations = {
|
|
@@ -364,7 +428,13 @@ const render = async (data, options = {}) => {
|
|
|
364
428
|
components: structuredCloneDeep(data.components || {}),
|
|
365
429
|
snippets: structuredCloneDeep(data.snippets || {}),
|
|
366
430
|
pages: structuredCloneDeep(prefetchedPages || data.pages || {}),
|
|
367
|
-
functions:
|
|
431
|
+
functions: {
|
|
432
|
+
...data.functions || {},
|
|
433
|
+
// SSR polyglot functions — enable {{ key | polyglot }} resolution during render
|
|
434
|
+
polyglot: ssrTranslate,
|
|
435
|
+
getActiveLang: ssrGetActiveLang,
|
|
436
|
+
getLang: ssrGetActiveLang
|
|
437
|
+
},
|
|
368
438
|
methods: data.methods || {},
|
|
369
439
|
designSystem: structuredCloneDeep(data.designSystem || {}),
|
|
370
440
|
files: data.files || {},
|
|
@@ -389,7 +459,7 @@ const render = async (data, options = {}) => {
|
|
|
389
459
|
await new Promise((r) => setTimeout(r, flushDelay));
|
|
390
460
|
assignKeys(body);
|
|
391
461
|
const registry = mapKeysToElements(element);
|
|
392
|
-
const metadata = extractMetadata(data, route);
|
|
462
|
+
const metadata = extractMetadata(data, route, element, element?.state);
|
|
393
463
|
const emotionCSS = [];
|
|
394
464
|
const emotionInstance = ctx.emotion || element && element.context && element.context.emotion;
|
|
395
465
|
if (emotionInstance && emotionInstance.cache) {
|
|
@@ -439,7 +509,7 @@ const render = async (data, options = {}) => {
|
|
|
439
509
|
else delete globalThis.document;
|
|
440
510
|
if (_prevLoc !== void 0) globalThis.location = _prevLoc;
|
|
441
511
|
else delete globalThis.location;
|
|
442
|
-
return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations };
|
|
512
|
+
return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations, prefetchedPages };
|
|
443
513
|
};
|
|
444
514
|
const renderElement = async (elementDef, options = {}) => {
|
|
445
515
|
const { context = {} } = options;
|
|
@@ -623,63 +693,82 @@ ${frameRules}
|
|
|
623
693
|
return _cachedGlobalCSS;
|
|
624
694
|
}
|
|
625
695
|
};
|
|
626
|
-
let
|
|
696
|
+
let _accumulatedEmotionCSS = /* @__PURE__ */ new Set();
|
|
627
697
|
const resetGlobalCSSCache = () => {
|
|
628
698
|
_cachedGlobalCSS = null;
|
|
629
|
-
|
|
699
|
+
_accumulatedEmotionCSS = /* @__PURE__ */ new Set();
|
|
700
|
+
};
|
|
701
|
+
const getAccumulatedEmotionCSS = () => Array.from(_accumulatedEmotionCSS).join("\n");
|
|
702
|
+
const replaceEmotionCSS = (html, newCSS) => {
|
|
703
|
+
return html.replace(
|
|
704
|
+
/<style data-emotion="smbls">[\s\S]*?<\/style>/,
|
|
705
|
+
newCSS ? `<style data-emotion="smbls">
|
|
706
|
+
${newCSS}
|
|
707
|
+
</style>` : ""
|
|
708
|
+
);
|
|
630
709
|
};
|
|
631
710
|
const renderRoute = async (data, options = {}) => {
|
|
632
|
-
const { route = "/" } = options;
|
|
711
|
+
const { route = "/", pathname } = options;
|
|
712
|
+
const result = await render(data, { route, pathname, prefetch: true });
|
|
713
|
+
if (!result) return null;
|
|
633
714
|
const ds = data.designSystem || {};
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
context: {
|
|
638
|
-
components: data.components || {},
|
|
639
|
-
snippets: data.snippets || {},
|
|
640
|
-
designSystem: ds,
|
|
641
|
-
state: data.state || {},
|
|
642
|
-
functions: data.functions || {},
|
|
643
|
-
methods: data.methods || {}
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
const { document: cssDoc } = parseHTML(`<html><head></head><body>${result.html}</body></html>`);
|
|
647
|
-
let emotionInstance;
|
|
715
|
+
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
|
|
716
|
+
let prefetchedState = null;
|
|
717
|
+
let activeLang = null;
|
|
648
718
|
try {
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
719
|
+
const el = result.element;
|
|
720
|
+
const polyglot = el?.context?.polyglot || data.polyglot || data.config?.polyglot;
|
|
721
|
+
activeLang = el?.state?.root?.lang || polyglot?.defaultLang || "en";
|
|
722
|
+
if (result.prefetchedPages && result.prefetchedPages[route]) {
|
|
723
|
+
const pageDef = result.prefetchedPages[route];
|
|
724
|
+
const collectStates = (def, result2 = {}) => {
|
|
725
|
+
if (!def || typeof def !== "object") return result2;
|
|
726
|
+
if (def.state && typeof def.state === "object") {
|
|
727
|
+
for (const [k, v] of Object.entries(def.state)) {
|
|
728
|
+
if (v !== void 0 && v !== null && typeof v !== "function") {
|
|
729
|
+
result2[k] = v;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const [key, child] of Object.entries(def)) {
|
|
734
|
+
if (key === "state" || key === "props" || key === "attr" || key === "on" || key === "define" || key === "__ref" || key.startsWith("__")) continue;
|
|
735
|
+
if (child && typeof child === "object" && !Array.isArray(child)) {
|
|
736
|
+
collectStates(child, result2);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return result2;
|
|
740
|
+
};
|
|
741
|
+
prefetchedState = collectStates(pageDef);
|
|
742
|
+
}
|
|
743
|
+
} catch (e) {
|
|
652
744
|
}
|
|
653
|
-
hydrate(result.element, {
|
|
654
|
-
root: cssDoc.body,
|
|
655
|
-
renderEvents: false,
|
|
656
|
-
events: false,
|
|
657
|
-
emotion: emotionInstance,
|
|
658
|
-
designSystem: ds
|
|
659
|
-
});
|
|
660
|
-
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
|
|
661
745
|
return {
|
|
662
|
-
html:
|
|
663
|
-
css:
|
|
746
|
+
html: result.html,
|
|
747
|
+
css: result.emotionCSS ? result.emotionCSS.join("\n") : "",
|
|
664
748
|
globalCSS,
|
|
665
749
|
resetCss: globalCSS.resetRules || generateResetCSS(ds.reset),
|
|
666
750
|
fontLinks: generateFontLinks(ds),
|
|
667
|
-
metadata: extractMetadata(data, route),
|
|
668
|
-
brKeyCount: Object.keys(result.registry).length
|
|
751
|
+
metadata: result.metadata || extractMetadata(data, route),
|
|
752
|
+
brKeyCount: result.registry ? Object.keys(result.registry).length : 0,
|
|
753
|
+
ssrTranslations: result.ssrTranslations,
|
|
754
|
+
prefetchedState,
|
|
755
|
+
activeLang
|
|
669
756
|
};
|
|
670
757
|
};
|
|
671
758
|
const renderPage = async (data, route = "/", options = {}) => {
|
|
672
|
-
const { lang, themeColor, isr, prefetch = true } = options;
|
|
759
|
+
const { lang, themeColor, isr, hydrate: hydrate2 = true, prefetch = true } = options;
|
|
673
760
|
const htmlLang = lang || data.state?.lang || data.app?.metadata?.lang || "en";
|
|
674
761
|
const result = await render(data, { route, prefetch });
|
|
675
762
|
if (!result) return null;
|
|
676
763
|
const metadata = { ...result.metadata };
|
|
677
764
|
if (themeColor) metadata["theme-color"] = themeColor;
|
|
678
765
|
const headTags = generateHeadHtml(metadata);
|
|
679
|
-
if (
|
|
680
|
-
|
|
766
|
+
if (result.emotionCSS && result.emotionCSS.length) {
|
|
767
|
+
for (const rule of result.emotionCSS) {
|
|
768
|
+
if (rule) _accumulatedEmotionCSS.add(rule);
|
|
769
|
+
}
|
|
681
770
|
}
|
|
682
|
-
const emotionCSS =
|
|
771
|
+
const emotionCSS = Array.from(_accumulatedEmotionCSS).join("\n");
|
|
683
772
|
const ds = data.designSystem || {};
|
|
684
773
|
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
|
|
685
774
|
const fontLinks = generateFontLinks(ds);
|
|
@@ -688,7 +777,32 @@ const renderPage = async (data, route = "/", options = {}) => {
|
|
|
688
777
|
if (isr && isr.clientScript) {
|
|
689
778
|
const depth = route === "/" ? 0 : route.replace(/^\/|\/$/g, "").split("/").length;
|
|
690
779
|
const prefix = depth > 0 ? "../".repeat(depth) : "./";
|
|
691
|
-
|
|
780
|
+
if (hydrate2) {
|
|
781
|
+
let translationSeed = "";
|
|
782
|
+
if (result.ssrTranslations) {
|
|
783
|
+
const polyglotCfg2 = data.polyglot || data.config?.polyglot;
|
|
784
|
+
const storagePrefix = polyglotCfg2?.storagePrefix || "";
|
|
785
|
+
const storageLangKey = polyglotCfg2?.storageLangKey || "";
|
|
786
|
+
const seedEntries = [];
|
|
787
|
+
for (const lang2 in result.ssrTranslations) {
|
|
788
|
+
const map = result.ssrTranslations[lang2];
|
|
789
|
+
if (map && typeof map === "object") {
|
|
790
|
+
seedEntries.push(`localStorage.setItem(${JSON.stringify(storagePrefix + lang2)},${JSON.stringify(JSON.stringify(map))})`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (storageLangKey) {
|
|
794
|
+
const defaultLang = polyglotCfg2?.defaultLang || "en";
|
|
795
|
+
seedEntries.push(`if(!localStorage.getItem(${JSON.stringify(storageLangKey)}))localStorage.setItem(${JSON.stringify(storageLangKey)},${JSON.stringify(defaultLang)})`);
|
|
796
|
+
}
|
|
797
|
+
if (seedEntries.length) {
|
|
798
|
+
translationSeed = `<script>try{${seedEntries.join(";")}}catch(e){}<\/script>
|
|
799
|
+
`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
isrBody = `${translationSeed}<script>window.__BRENDER__ = true<\/script>
|
|
803
|
+
<script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
|
|
804
|
+
} else {
|
|
805
|
+
isrBody = `<script type="module">
|
|
692
806
|
{
|
|
693
807
|
const brEls = document.querySelectorAll('body > :not(script):not(style)')
|
|
694
808
|
const observer = new MutationObserver((mutations) => {
|
|
@@ -706,9 +820,11 @@ const renderPage = async (data, route = "/", options = {}) => {
|
|
|
706
820
|
}
|
|
707
821
|
<\/script>
|
|
708
822
|
<script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
|
|
823
|
+
}
|
|
709
824
|
}
|
|
710
|
-
const
|
|
711
|
-
|
|
825
|
+
const headConfig = { ...data.config || {} };
|
|
826
|
+
if (data.polyglot && !headConfig.polyglot) headConfig.polyglot = data.polyglot;
|
|
827
|
+
const polyglotCfg = headConfig.polyglot;
|
|
712
828
|
let resolvedHeadTags = headTags;
|
|
713
829
|
if (polyglotCfg) {
|
|
714
830
|
const defaultLang = polyglotCfg.defaultLang || "en";
|
|
@@ -1189,9 +1305,11 @@ const generateFontLinks = (ds) => {
|
|
|
1189
1305
|
].join("\n");
|
|
1190
1306
|
};
|
|
1191
1307
|
export {
|
|
1308
|
+
getAccumulatedEmotionCSS,
|
|
1192
1309
|
render,
|
|
1193
1310
|
renderElement,
|
|
1194
1311
|
renderPage,
|
|
1195
1312
|
renderRoute,
|
|
1313
|
+
replaceEmotionCSS,
|
|
1196
1314
|
resetGlobalCSSCache
|
|
1197
1315
|
};
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEnv } from './env.js'
|
|
2
2
|
import { resetKeys, assignKeys, mapKeysToElements } from './keys.js'
|
|
3
3
|
import { loadProject, loadAndRenderAll } from './load.js'
|
|
4
|
-
import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache } from './render.js'
|
|
4
|
+
import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache, getAccumulatedEmotionCSS, replaceEmotionCSS } from './render.js'
|
|
5
5
|
import { extractMetadata, generateHeadHtml } from './metadata.js'
|
|
6
6
|
import { collectBrNodes, hydrate } from './hydrate.js'
|
|
7
7
|
import { generateSitemap } from './sitemap.js'
|
|
@@ -19,6 +19,8 @@ export {
|
|
|
19
19
|
renderRoute,
|
|
20
20
|
renderPage,
|
|
21
21
|
resetGlobalCSSCache,
|
|
22
|
+
getAccumulatedEmotionCSS,
|
|
23
|
+
replaceEmotionCSS,
|
|
22
24
|
extractMetadata,
|
|
23
25
|
generateHeadHtml,
|
|
24
26
|
collectBrNodes,
|
|
@@ -40,6 +42,8 @@ export default {
|
|
|
40
42
|
renderRoute,
|
|
41
43
|
renderPage,
|
|
42
44
|
resetGlobalCSSCache,
|
|
45
|
+
getAccumulatedEmotionCSS,
|
|
46
|
+
replaceEmotionCSS,
|
|
43
47
|
extractMetadata,
|
|
44
48
|
generateHeadHtml,
|
|
45
49
|
collectBrNodes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/brender",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.6",
|
|
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.6",
|
|
40
40
|
"linkedom": "^0.16.8"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
package/prefetch.js
CHANGED
|
@@ -118,15 +118,17 @@ const createSSRAdapter = async (dbConfig) => {
|
|
|
118
118
|
const supabaseUrl = url || (projectId && `https://${projectId}.supabase.co`)
|
|
119
119
|
if (!supabaseUrl || !key) return null
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
// Always import @supabase/supabase-js for SSR — the serialized createClient
|
|
122
|
+
// from project data is a no-op placeholder that won't produce a real client.
|
|
123
|
+
let clientFactory
|
|
124
|
+
try {
|
|
125
|
+
const mod = await import('@supabase/supabase-js')
|
|
126
|
+
clientFactory = mod.createClient
|
|
127
|
+
} catch {
|
|
128
|
+
// Fall back to provided createClient only if import fails
|
|
129
|
+
clientFactory = createClient
|
|
129
130
|
}
|
|
131
|
+
if (!clientFactory) return null
|
|
130
132
|
|
|
131
133
|
const client = clientFactory(supabaseUrl, key)
|
|
132
134
|
|
|
@@ -199,7 +201,7 @@ export const prefetchPageData = async (data, route = '/', options = {}) => {
|
|
|
199
201
|
if (!pageDef) return new Map()
|
|
200
202
|
|
|
201
203
|
const config = data.config || data.settings || {}
|
|
202
|
-
const dbConfig = config.fetch || config.db || data.db
|
|
204
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db
|
|
203
205
|
if (!dbConfig) return new Map()
|
|
204
206
|
|
|
205
207
|
const adapter = await createSSRAdapter(dbConfig)
|
|
@@ -214,9 +216,15 @@ export const prefetchPageData = async (data, route = '/', options = {}) => {
|
|
|
214
216
|
const results = await Promise.allSettled(
|
|
215
217
|
declarations.map(async ({ config, stateKey, path }) => {
|
|
216
218
|
const fetchedData = await executeSingle(adapter, config)
|
|
217
|
-
if (fetchedData !== null
|
|
219
|
+
if (fetchedData !== null) {
|
|
218
220
|
const existing = stateUpdates.get(path) || {}
|
|
219
|
-
|
|
221
|
+
if (stateKey) {
|
|
222
|
+
// Named: store under the `as` key
|
|
223
|
+
existing[stateKey] = fetchedData
|
|
224
|
+
} else if (isObject(fetchedData)) {
|
|
225
|
+
// No `as` key + transform returned an object: spread into state
|
|
226
|
+
Object.assign(existing, fetchedData)
|
|
227
|
+
}
|
|
220
228
|
stateUpdates.set(path, existing)
|
|
221
229
|
}
|
|
222
230
|
})
|
|
@@ -240,11 +248,12 @@ export const prefetchPageData = async (data, route = '/', options = {}) => {
|
|
|
240
248
|
* @returns {Promise<object|null>} Translation map keyed by language, or null on failure
|
|
241
249
|
*/
|
|
242
250
|
export const fetchSSRTranslations = async (data) => {
|
|
243
|
-
|
|
244
|
-
const
|
|
251
|
+
// Config fields may be nested under data.config or spread at the top level
|
|
252
|
+
const config = data.config || {}
|
|
253
|
+
const polyglot = config.polyglot || data.polyglot
|
|
245
254
|
if (!polyglot?.fetch) return null
|
|
246
255
|
|
|
247
|
-
const dbConfig = config.fetch || config.db || data.db
|
|
256
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db
|
|
248
257
|
if (!dbConfig) return null
|
|
249
258
|
|
|
250
259
|
const adapter = await createSSRAdapter(dbConfig)
|
package/render.js
CHANGED
|
@@ -10,6 +10,64 @@ import { prefetchPageData, injectPrefetchedState, fetchSSRTranslations } from '.
|
|
|
10
10
|
import { parseHTML } from 'linkedom'
|
|
11
11
|
import createEmotionInstance from '@emotion/css/create-instance'
|
|
12
12
|
|
|
13
|
+
// Lightweight SSR polyglot functions — resolve translations from context
|
|
14
|
+
// without needing the full polyglot plugin runtime
|
|
15
|
+
const ssrResolve = (map, key) => {
|
|
16
|
+
if (!map || !key) return undefined
|
|
17
|
+
if (map[key] !== undefined) return map[key]
|
|
18
|
+
// Try nested dot-path (e.g. "ui.main.latest")
|
|
19
|
+
const parts = key.split('.')
|
|
20
|
+
let v = map
|
|
21
|
+
for (const p of parts) {
|
|
22
|
+
if (v == null || typeof v !== 'object') return undefined
|
|
23
|
+
v = v[p]
|
|
24
|
+
}
|
|
25
|
+
return v
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ssrTranslate = function (key, lang) {
|
|
29
|
+
if (!key) return ''
|
|
30
|
+
const ctx = this?.context
|
|
31
|
+
const poly = ctx?.polyglot
|
|
32
|
+
const activeLang = lang || poly?.defaultLang || 'ka'
|
|
33
|
+
|
|
34
|
+
// Static translations from context.polyglot.translations
|
|
35
|
+
if (poly?.translations) {
|
|
36
|
+
const langMap = poly.translations[activeLang]
|
|
37
|
+
if (langMap) {
|
|
38
|
+
const val = ssrResolve(langMap, key)
|
|
39
|
+
if (val !== undefined) return val
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Server-loaded translations in root state
|
|
44
|
+
const root = this?.state?.root || ctx?.state?.root
|
|
45
|
+
if (root?.translations) {
|
|
46
|
+
const langMap = root.translations[activeLang]
|
|
47
|
+
if (langMap) {
|
|
48
|
+
const val = ssrResolve(langMap, key)
|
|
49
|
+
if (val !== undefined) return val
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback to default lang
|
|
54
|
+
const defaultLang = poly?.defaultLang || 'en'
|
|
55
|
+
if (defaultLang !== activeLang && poly?.translations) {
|
|
56
|
+
const fallback = poly.translations[defaultLang]
|
|
57
|
+
if (fallback) {
|
|
58
|
+
const val = ssrResolve(fallback, key)
|
|
59
|
+
if (val !== undefined) return val
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return key
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ssrGetActiveLang = function () {
|
|
67
|
+
const ctx = this?.context
|
|
68
|
+
return this?.state?.root?.lang || ctx?.polyglot?.defaultLang || 'ka'
|
|
69
|
+
}
|
|
70
|
+
|
|
13
71
|
// Deep clone that preserves functions and avoids circular refs
|
|
14
72
|
const structuredCloneDeep = (obj, seen = new WeakMap()) => {
|
|
15
73
|
if (obj === null || typeof obj !== 'object') return obj
|
|
@@ -307,12 +365,24 @@ const UIKIT_STUBS = {
|
|
|
307
365
|
* @returns {Promise<{ html: string, metadata: object, registry: object, element: object }>}
|
|
308
366
|
*/
|
|
309
367
|
export const render = async (data, options = {}) => {
|
|
310
|
-
const { route = '/', state: stateOverrides, context: contextOverrides, prefetch = false } = options
|
|
368
|
+
const { route = '/', pathname, state: stateOverrides, context: contextOverrides, prefetch = false } = options
|
|
369
|
+
// pathname is the actual URL path (e.g. /podcast/abc-123), route is the page pattern (e.g. /podcast/:id)
|
|
370
|
+
const locationPath = pathname || route
|
|
311
371
|
|
|
312
372
|
// ── SSR data prefetching ──
|
|
313
373
|
// When prefetch is enabled, walk the page definition to find fetch
|
|
314
374
|
// declarations, execute them against the DB adapter, and inject
|
|
315
375
|
// the results into element state before rendering.
|
|
376
|
+
// Set up globalThis.location early so fetch params that reference
|
|
377
|
+
// window.location.pathname (e.g. to extract :id from URL) work during prefetch.
|
|
378
|
+
const _prevLocPrefetch = globalThis.location
|
|
379
|
+
const _prevWinPrefetch = globalThis.window
|
|
380
|
+
if (!globalThis.location || globalThis.location.pathname !== locationPath) {
|
|
381
|
+
globalThis.location = { pathname: locationPath, href: locationPath, search: '', hash: '', origin: '' }
|
|
382
|
+
}
|
|
383
|
+
if (!globalThis.window) {
|
|
384
|
+
globalThis.window = { location: globalThis.location }
|
|
385
|
+
}
|
|
316
386
|
let prefetchedPages
|
|
317
387
|
if (prefetch) {
|
|
318
388
|
try {
|
|
@@ -355,11 +425,17 @@ export const render = async (data, options = {}) => {
|
|
|
355
425
|
} catch {}
|
|
356
426
|
}
|
|
357
427
|
|
|
428
|
+
// Restore location/window before createEnv sets them properly
|
|
429
|
+
if (_prevLocPrefetch !== undefined) globalThis.location = _prevLocPrefetch
|
|
430
|
+
else delete globalThis.location
|
|
431
|
+
if (_prevWinPrefetch !== undefined) globalThis.window = _prevWinPrefetch
|
|
432
|
+
else delete globalThis.window
|
|
433
|
+
|
|
358
434
|
const { window, document } = createEnv()
|
|
359
435
|
const body = document.body
|
|
360
436
|
|
|
361
437
|
// Set route on location so the router picks it up
|
|
362
|
-
window.location.pathname =
|
|
438
|
+
window.location.pathname = locationPath
|
|
363
439
|
|
|
364
440
|
// Set globalThis.document/location so the bundled smbls code
|
|
365
441
|
// (which uses `window = globalThis`) can access them during SSR.
|
|
@@ -375,7 +451,15 @@ export const render = async (data, options = {}) => {
|
|
|
375
451
|
|
|
376
452
|
const app = structuredCloneDeep(data.app || {})
|
|
377
453
|
|
|
378
|
-
|
|
454
|
+
// Config fields may be nested under data.config or spread at the top level
|
|
455
|
+
// (frank spreads config.js exports at the top level of the JSON)
|
|
456
|
+
const config = { ...(data.config || {}) }
|
|
457
|
+
if (data.polyglot && !config.polyglot) config.polyglot = data.polyglot
|
|
458
|
+
if (data.fetch && !config.fetch) config.fetch = data.fetch
|
|
459
|
+
if (data.router && !config.router) config.router = data.router
|
|
460
|
+
for (const k of ['useReset', 'useVariable', 'useFontImport', 'useIconSprite', 'useSvgSprite', 'useDefaultConfig', 'useDocumentTheme']) {
|
|
461
|
+
if (data[k] != null && config[k] == null) config[k] = data[k]
|
|
462
|
+
}
|
|
379
463
|
|
|
380
464
|
// Inject SSR translations into polyglot config and root state
|
|
381
465
|
const polyglotConfig = config.polyglot ? { ...config.polyglot } : undefined
|
|
@@ -418,7 +502,13 @@ export const render = async (data, options = {}) => {
|
|
|
418
502
|
components: structuredCloneDeep(data.components || {}),
|
|
419
503
|
snippets: structuredCloneDeep(data.snippets || {}),
|
|
420
504
|
pages: structuredCloneDeep(prefetchedPages || data.pages || {}),
|
|
421
|
-
functions:
|
|
505
|
+
functions: {
|
|
506
|
+
...(data.functions || {}),
|
|
507
|
+
// SSR polyglot functions — enable {{ key | polyglot }} resolution during render
|
|
508
|
+
polyglot: ssrTranslate,
|
|
509
|
+
getActiveLang: ssrGetActiveLang,
|
|
510
|
+
getLang: ssrGetActiveLang
|
|
511
|
+
},
|
|
422
512
|
methods: data.methods || {},
|
|
423
513
|
designSystem: structuredCloneDeep(data.designSystem || {}),
|
|
424
514
|
files: data.files || {},
|
|
@@ -455,7 +545,9 @@ export const render = async (data, options = {}) => {
|
|
|
455
545
|
const registry = mapKeysToElements(element)
|
|
456
546
|
|
|
457
547
|
// Extract metadata for the rendered route
|
|
458
|
-
|
|
548
|
+
// Pass the rendered element and its state so function-valued metadata
|
|
549
|
+
// (e.g. detail page titles from fetched data) can be resolved
|
|
550
|
+
const metadata = extractMetadata(data, route, element, element?.state)
|
|
459
551
|
|
|
460
552
|
// Extract emotion-generated CSS
|
|
461
553
|
// Emotion uses insertRule() (CSSOM) which doesn't populate textContent in linkedom.
|
|
@@ -521,7 +613,7 @@ export const render = async (data, options = {}) => {
|
|
|
521
613
|
if (_prevLoc !== undefined) globalThis.location = _prevLoc
|
|
522
614
|
else delete globalThis.location
|
|
523
615
|
|
|
524
|
-
return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations }
|
|
616
|
+
return { html, metadata, registry, element, emotionCSS, document, window, ssrTranslations, prefetchedPages }
|
|
525
617
|
}
|
|
526
618
|
|
|
527
619
|
/**
|
|
@@ -768,16 +860,34 @@ const generateGlobalCSS = async (ds, config) => {
|
|
|
768
860
|
}
|
|
769
861
|
}
|
|
770
862
|
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
|
|
863
|
+
// Accumulate emotion CSS across all page renders.
|
|
864
|
+
// Each page may generate unique CSS classes (e.g. page-specific components/styles).
|
|
865
|
+
// Emotion's singleton cache marks classes as "inserted" after the first render,
|
|
866
|
+
// so subsequent renders only produce NEW classes not seen before.
|
|
867
|
+
// We merge all CSS rules across renders to ensure every page has complete styles.
|
|
868
|
+
let _accumulatedEmotionCSS = new Set()
|
|
776
869
|
|
|
777
870
|
/**
|
|
778
871
|
* Reset the cached global CSS and emotion CSS (useful when rendering multiple projects).
|
|
779
872
|
*/
|
|
780
|
-
export const resetGlobalCSSCache = () => { _cachedGlobalCSS = null;
|
|
873
|
+
export const resetGlobalCSSCache = () => { _cachedGlobalCSS = null; _accumulatedEmotionCSS = new Set() }
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Returns the complete accumulated emotion CSS from all renders so far.
|
|
877
|
+
* Call this after rendering ALL pages to get the full CSS needed by every page.
|
|
878
|
+
*/
|
|
879
|
+
export const getAccumulatedEmotionCSS = () => Array.from(_accumulatedEmotionCSS).join('\n')
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Replace the emotion CSS in a rendered HTML page with updated CSS.
|
|
883
|
+
* Used in two-pass rendering: render all pages first, then inject complete CSS.
|
|
884
|
+
*/
|
|
885
|
+
export const replaceEmotionCSS = (html, newCSS) => {
|
|
886
|
+
return html.replace(
|
|
887
|
+
/<style data-emotion="smbls">[\s\S]*?<\/style>/,
|
|
888
|
+
newCSS ? `<style data-emotion="smbls">\n${newCSS}\n</style>` : ''
|
|
889
|
+
)
|
|
890
|
+
}
|
|
781
891
|
|
|
782
892
|
// ── Route-level SSR ───────────────────────────────────────────────────────────
|
|
783
893
|
|
|
@@ -792,49 +902,61 @@ export const resetGlobalCSSCache = () => { _cachedGlobalCSS = null; _cachedEmoti
|
|
|
792
902
|
* @returns {Promise<{ html: string, css: string, resetCss: string, fontLinks: string, metadata: object, brKeyCount: number }>}
|
|
793
903
|
*/
|
|
794
904
|
export const renderRoute = async (data, options = {}) => {
|
|
795
|
-
const { route = '/' } = options
|
|
796
|
-
const ds = data.designSystem || {}
|
|
797
|
-
const pageDef = (data.pages || {})[route]
|
|
798
|
-
if (!pageDef) return null
|
|
799
|
-
|
|
800
|
-
const result = await renderElement(pageDef, {
|
|
801
|
-
context: {
|
|
802
|
-
components: data.components || {},
|
|
803
|
-
snippets: data.snippets || {},
|
|
804
|
-
designSystem: ds,
|
|
805
|
-
state: data.state || {},
|
|
806
|
-
functions: data.functions || {},
|
|
807
|
-
methods: data.methods || {}
|
|
808
|
-
}
|
|
809
|
-
})
|
|
905
|
+
const { route = '/', pathname } = options
|
|
810
906
|
|
|
811
|
-
//
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const { default: createInstance } = await import('@emotion/css/create-instance')
|
|
816
|
-
emotionInstance = createInstance({ key: 'smbls', container: cssDoc.head })
|
|
817
|
-
} catch {}
|
|
818
|
-
|
|
819
|
-
hydrate(result.element, {
|
|
820
|
-
root: cssDoc.body,
|
|
821
|
-
renderEvents: false,
|
|
822
|
-
events: false,
|
|
823
|
-
emotion: emotionInstance,
|
|
824
|
-
designSystem: ds
|
|
825
|
-
})
|
|
907
|
+
// Use the full render pipeline which handles polyglot, prefetch, emotion, etc.
|
|
908
|
+
// Pass pathname so the virtual DOM location reflects the actual URL (for dynamic routes)
|
|
909
|
+
const result = await render(data, { route, pathname, prefetch: true })
|
|
910
|
+
if (!result) return null
|
|
826
911
|
|
|
827
|
-
|
|
912
|
+
const ds = data.designSystem || {}
|
|
828
913
|
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings)
|
|
829
914
|
|
|
915
|
+
// Extract prefetched state and language for metadata resolution
|
|
916
|
+
let prefetchedState = null
|
|
917
|
+
let activeLang = null
|
|
918
|
+
try {
|
|
919
|
+
const el = result.element
|
|
920
|
+
const polyglot = el?.context?.polyglot || data.polyglot || data.config?.polyglot
|
|
921
|
+
activeLang = el?.state?.root?.lang || polyglot?.defaultLang || 'en'
|
|
922
|
+
|
|
923
|
+
// Get prefetched data from the injected page definitions (pre-DOMQL proxy)
|
|
924
|
+
if (result.prefetchedPages && result.prefetchedPages[route]) {
|
|
925
|
+
const pageDef = result.prefetchedPages[route]
|
|
926
|
+
// Collect all state entries from the page definition tree
|
|
927
|
+
const collectStates = (def, result = {}) => {
|
|
928
|
+
if (!def || typeof def !== 'object') return result
|
|
929
|
+
if (def.state && typeof def.state === 'object') {
|
|
930
|
+
for (const [k, v] of Object.entries(def.state)) {
|
|
931
|
+
if (v !== undefined && v !== null && typeof v !== 'function') {
|
|
932
|
+
result[k] = v
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// Recurse into all child definitions (not just those with state)
|
|
937
|
+
for (const [key, child] of Object.entries(def)) {
|
|
938
|
+
if (key === 'state' || key === 'props' || key === 'attr' || key === 'on' || key === 'define' || key === '__ref' || key.startsWith('__')) continue
|
|
939
|
+
if (child && typeof child === 'object' && !Array.isArray(child)) {
|
|
940
|
+
collectStates(child, result)
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return result
|
|
944
|
+
}
|
|
945
|
+
prefetchedState = collectStates(pageDef)
|
|
946
|
+
}
|
|
947
|
+
} catch (e) { /* ignore */ }
|
|
948
|
+
|
|
830
949
|
return {
|
|
831
|
-
html:
|
|
832
|
-
css:
|
|
950
|
+
html: result.html,
|
|
951
|
+
css: result.emotionCSS ? result.emotionCSS.join('\n') : '',
|
|
833
952
|
globalCSS,
|
|
834
953
|
resetCss: globalCSS.resetRules || generateResetCSS(ds.reset),
|
|
835
954
|
fontLinks: generateFontLinks(ds),
|
|
836
|
-
metadata: extractMetadata(data, route),
|
|
837
|
-
brKeyCount: Object.keys(result.registry).length
|
|
955
|
+
metadata: result.metadata || extractMetadata(data, route),
|
|
956
|
+
brKeyCount: result.registry ? Object.keys(result.registry).length : 0,
|
|
957
|
+
ssrTranslations: result.ssrTranslations,
|
|
958
|
+
prefetchedState,
|
|
959
|
+
activeLang
|
|
838
960
|
}
|
|
839
961
|
}
|
|
840
962
|
|
|
@@ -853,11 +975,12 @@ export const renderRoute = async (data, options = {}) => {
|
|
|
853
975
|
* @param {string} [options.lang='en'] - HTML lang attribute
|
|
854
976
|
* @param {string} [options.themeColor] - theme-color meta
|
|
855
977
|
* @param {object} [options.isr] - ISR options with clientScript path
|
|
978
|
+
* @param {boolean} [options.hydrate=true] - Use true hydration (attach to existing DOM) instead of full SPA re-render
|
|
856
979
|
* @param {boolean} [options.prefetch=true] - Whether to prefetch data via DB adapter
|
|
857
980
|
* @returns {Promise<{ html: string, route: string, brKeyCount: number }>}
|
|
858
981
|
*/
|
|
859
982
|
export const renderPage = async (data, route = '/', options = {}) => {
|
|
860
|
-
const { lang, themeColor, isr, prefetch = true } = options
|
|
983
|
+
const { lang, themeColor, isr, hydrate = true, prefetch = true } = options
|
|
861
984
|
|
|
862
985
|
// Detect lang from project config, app metadata, or default
|
|
863
986
|
const htmlLang = lang || data.state?.lang || data.app?.metadata?.lang || 'en'
|
|
@@ -870,13 +993,16 @@ export const renderPage = async (data, route = '/', options = {}) => {
|
|
|
870
993
|
if (themeColor) metadata['theme-color'] = themeColor
|
|
871
994
|
const headTags = generateHeadHtml(metadata)
|
|
872
995
|
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
|
|
877
|
-
|
|
996
|
+
// Accumulate emotion CSS from each page render.
|
|
997
|
+
// Each page may introduce unique CSS classes not seen on previous pages.
|
|
998
|
+
// Emotion's singleton cache only emits NEW classes per render, so we
|
|
999
|
+
// collect all rules across renders to build the complete stylesheet.
|
|
1000
|
+
if (result.emotionCSS && result.emotionCSS.length) {
|
|
1001
|
+
for (const rule of result.emotionCSS) {
|
|
1002
|
+
if (rule) _accumulatedEmotionCSS.add(rule)
|
|
1003
|
+
}
|
|
878
1004
|
}
|
|
879
|
-
const emotionCSS =
|
|
1005
|
+
const emotionCSS = Array.from(_accumulatedEmotionCSS).join('\n')
|
|
880
1006
|
|
|
881
1007
|
// Generate global CSS (variables, reset, keyframes) via scratch pipeline
|
|
882
1008
|
const ds = data.designSystem || {}
|
|
@@ -893,9 +1019,40 @@ export const renderPage = async (data, route = '/', options = {}) => {
|
|
|
893
1019
|
// Calculate relative path from route directory to root
|
|
894
1020
|
const depth = route === '/' ? 0 : route.replace(/^\/|\/$/g, '').split('/').length
|
|
895
1021
|
const prefix = depth > 0 ? '../'.repeat(depth) : './'
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1022
|
+
|
|
1023
|
+
if (hydrate) {
|
|
1024
|
+
// True hydration: signal the SPA to adopt existing DOM nodes
|
|
1025
|
+
// instead of creating new ones. The SPA detects __BRENDER__ flag
|
|
1026
|
+
// and uses onlyResolveExtends + node adoption.
|
|
1027
|
+
//
|
|
1028
|
+
// Seed client-side polyglot with SSR translations so that
|
|
1029
|
+
// el.call('polyglot', key) resolves immediately during hydration
|
|
1030
|
+
// instead of showing raw keys until the async fetch completes.
|
|
1031
|
+
let translationSeed = ''
|
|
1032
|
+
if (result.ssrTranslations) {
|
|
1033
|
+
const polyglotCfg = data.polyglot || data.config?.polyglot
|
|
1034
|
+
const storagePrefix = polyglotCfg?.storagePrefix || ''
|
|
1035
|
+
const storageLangKey = polyglotCfg?.storageLangKey || ''
|
|
1036
|
+
const seedEntries = []
|
|
1037
|
+
for (const lang in result.ssrTranslations) {
|
|
1038
|
+
const map = result.ssrTranslations[lang]
|
|
1039
|
+
if (map && typeof map === 'object') {
|
|
1040
|
+
seedEntries.push(`localStorage.setItem(${JSON.stringify(storagePrefix + lang)},${JSON.stringify(JSON.stringify(map))})`)
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (storageLangKey) {
|
|
1044
|
+
const defaultLang = polyglotCfg?.defaultLang || 'en'
|
|
1045
|
+
seedEntries.push(`if(!localStorage.getItem(${JSON.stringify(storageLangKey)}))localStorage.setItem(${JSON.stringify(storageLangKey)},${JSON.stringify(defaultLang)})`)
|
|
1046
|
+
}
|
|
1047
|
+
if (seedEntries.length) {
|
|
1048
|
+
translationSeed = `<script>try{${seedEntries.join(';')}}catch(e){}</script>\n`
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
isrBody = `${translationSeed}<script>window.__BRENDER__ = true</script>
|
|
1052
|
+
<script type="module" src="${prefix}${isr.clientScript}"></script>`
|
|
1053
|
+
} else {
|
|
1054
|
+
// Legacy swap mode: SPA creates new DOM, MutationObserver removes brender nodes
|
|
1055
|
+
isrBody = `<script type="module">
|
|
899
1056
|
{
|
|
900
1057
|
const brEls = document.querySelectorAll('body > :not(script):not(style)')
|
|
901
1058
|
const observer = new MutationObserver((mutations) => {
|
|
@@ -913,11 +1070,13 @@ export const renderPage = async (data, route = '/', options = {}) => {
|
|
|
913
1070
|
}
|
|
914
1071
|
</script>
|
|
915
1072
|
<script type="module" src="${prefix}${isr.clientScript}"></script>`
|
|
1073
|
+
}
|
|
916
1074
|
}
|
|
917
1075
|
|
|
918
1076
|
// Resolve any {{ key | polyglot }} templates in head tags (title, meta, etc.)
|
|
919
|
-
const
|
|
920
|
-
|
|
1077
|
+
const headConfig = { ...(data.config || {}) }
|
|
1078
|
+
if (data.polyglot && !headConfig.polyglot) headConfig.polyglot = data.polyglot
|
|
1079
|
+
const polyglotCfg = headConfig.polyglot
|
|
921
1080
|
let resolvedHeadTags = headTags
|
|
922
1081
|
if (polyglotCfg) {
|
|
923
1082
|
const defaultLang = polyglotCfg.defaultLang || 'en'
|