@symbo.ls/brender 3.14.7 → 3.14.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/env.js +76 -0
- package/hydrate.js +462 -0
- package/index.js +54 -0
- package/keys.js +54 -0
- package/load.js +141 -0
- package/metadata.js +5 -0
- package/package.json +4 -3
- package/prefetch.js +363 -0
- package/render.js +1894 -0
- package/sitemap.js +28 -0
package/load.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { resolve, join } from 'path'
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs'
|
|
3
|
+
import { tmpdir } from 'os'
|
|
4
|
+
import { randomBytes } from 'crypto'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bundles a module entry point with esbuild so that extensionless imports,
|
|
8
|
+
* bare specifiers, and other bundler conventions resolve correctly.
|
|
9
|
+
* Returns the default + named exports of the bundled module, or null on failure.
|
|
10
|
+
*/
|
|
11
|
+
const bundleAndImport = async (entryPath) => {
|
|
12
|
+
if (!existsSync(entryPath)) return null
|
|
13
|
+
|
|
14
|
+
let esbuild
|
|
15
|
+
try {
|
|
16
|
+
esbuild = await import('esbuild')
|
|
17
|
+
} catch {
|
|
18
|
+
// Fallback: try raw import if esbuild is not available
|
|
19
|
+
try { return await import(entryPath) } catch { return null }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const outFile = join(tmpdir(), `brender_${randomBytes(8).toString('hex')}.mjs`)
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await esbuild.build({
|
|
26
|
+
entryPoints: [entryPath],
|
|
27
|
+
bundle: true,
|
|
28
|
+
format: 'esm',
|
|
29
|
+
platform: 'node',
|
|
30
|
+
outfile: outFile,
|
|
31
|
+
write: true,
|
|
32
|
+
logLevel: 'silent',
|
|
33
|
+
// Mark node builtins as external
|
|
34
|
+
external: ['fs', 'path', 'os', 'crypto', 'url', 'http', 'https', 'stream', 'util', 'events', 'buffer', 'child_process', 'worker_threads', 'net', 'tls', 'dns', 'dgram', 'zlib', 'assert', 'querystring', 'string_decoder', 'readline', 'perf_hooks', 'async_hooks', 'v8', 'vm', 'cluster', 'inspector', 'module', 'process', 'tty'],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const mod = await import(`file://${outFile}`)
|
|
38
|
+
return mod
|
|
39
|
+
} catch {
|
|
40
|
+
// Fallback: try raw import
|
|
41
|
+
try { return await import(entryPath) } catch { return null } // fallback: module not found via this resolver, trying next
|
|
42
|
+
} finally {
|
|
43
|
+
try { unlinkSync(outFile) } catch {} // cleanup: ignore if temp file already removed
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Loads a Symbols project from a filesystem path.
|
|
49
|
+
* Expects the standard symbols/ directory structure.
|
|
50
|
+
*
|
|
51
|
+
* Uses esbuild to bundle each module so that extensionless imports
|
|
52
|
+
* and other bundler conventions work in Node.js.
|
|
53
|
+
*
|
|
54
|
+
* Used for prebuild scenarios where brender runs locally
|
|
55
|
+
* against a project directory (e.g. `smbls build --prerender`).
|
|
56
|
+
*
|
|
57
|
+
* For server runtime rendering, pass the project data
|
|
58
|
+
* directly to render() instead.
|
|
59
|
+
*/
|
|
60
|
+
export const loadProject = async (projectPath) => {
|
|
61
|
+
const symbolsDir = resolve(projectPath, 'symbols')
|
|
62
|
+
|
|
63
|
+
const [
|
|
64
|
+
appModule,
|
|
65
|
+
stateModule,
|
|
66
|
+
configModule,
|
|
67
|
+
depsModule,
|
|
68
|
+
componentsModule,
|
|
69
|
+
snippetsModule,
|
|
70
|
+
pagesModule,
|
|
71
|
+
functionsModule,
|
|
72
|
+
methodsModule,
|
|
73
|
+
designSystemModule,
|
|
74
|
+
filesModule,
|
|
75
|
+
assetsModule,
|
|
76
|
+
sharedLibsModule
|
|
77
|
+
] = await Promise.all([
|
|
78
|
+
bundleAndImport(join(symbolsDir, 'app.js')),
|
|
79
|
+
bundleAndImport(join(symbolsDir, 'state.js')),
|
|
80
|
+
bundleAndImport(join(symbolsDir, 'config.js')),
|
|
81
|
+
bundleAndImport(join(symbolsDir, 'dependencies.js')),
|
|
82
|
+
bundleAndImport(join(symbolsDir, 'components', 'index.js')),
|
|
83
|
+
bundleAndImport(join(symbolsDir, 'snippets', 'index.js')),
|
|
84
|
+
bundleAndImport(join(symbolsDir, 'pages', 'index.js')),
|
|
85
|
+
bundleAndImport(join(symbolsDir, 'functions', 'index.js')),
|
|
86
|
+
bundleAndImport(join(symbolsDir, 'methods', 'index.js')),
|
|
87
|
+
bundleAndImport(join(symbolsDir, 'designSystem', 'index.js')),
|
|
88
|
+
bundleAndImport(join(symbolsDir, 'files', 'index.js')),
|
|
89
|
+
bundleAndImport(join(symbolsDir, 'assets', 'index.js')).catch(() => null),
|
|
90
|
+
bundleAndImport(join(symbolsDir, 'sharedLibraries.js')).catch(() => null)
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
// Spread into plain objects — ESM module namespaces are non-extensible,
|
|
94
|
+
// which breaks downstream code that adds properties (e.g. polyglot functions).
|
|
95
|
+
const result = {
|
|
96
|
+
app: { ...(appModule?.default || {}) },
|
|
97
|
+
state: { ...(stateModule?.default || {}) },
|
|
98
|
+
dependencies: { ...(depsModule?.default || {}) },
|
|
99
|
+
components: { ...(componentsModule || {}) },
|
|
100
|
+
snippets: { ...(snippetsModule || {}) },
|
|
101
|
+
pages: { ...(pagesModule?.default || {}) },
|
|
102
|
+
functions: { ...(functionsModule || {}) },
|
|
103
|
+
methods: { ...(methodsModule || {}) },
|
|
104
|
+
designSystem: { ...(designSystemModule?.default || {}) },
|
|
105
|
+
files: { ...(filesModule?.default || {}) },
|
|
106
|
+
assets: { ...(assetsModule?.default || {}) },
|
|
107
|
+
config: { ...(configModule?.default || {}) },
|
|
108
|
+
sharedLibraries: sharedLibsModule?.default || []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pre-resolve string library references and merge into context
|
|
112
|
+
// so production builds have zero runtime overhead
|
|
113
|
+
if (result.sharedLibraries.length) {
|
|
114
|
+
const { resolveSharedLibraries, mergeSharedLibraries } = await import('@symbo.ls/utils')
|
|
115
|
+
const hasStrings = result.sharedLibraries.some(lib => typeof lib === 'string')
|
|
116
|
+
if (hasStrings) {
|
|
117
|
+
result.sharedLibraries = await resolveSharedLibraries(result.sharedLibraries)
|
|
118
|
+
}
|
|
119
|
+
mergeSharedLibraries(result, result.sharedLibraries)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Renders all routes from a project directory and returns
|
|
127
|
+
* a map of route -> { html, metadata }.
|
|
128
|
+
* Useful for static prebuilding.
|
|
129
|
+
*/
|
|
130
|
+
export const loadAndRenderAll = async (projectPath, renderFn) => {
|
|
131
|
+
const data = await loadProject(projectPath)
|
|
132
|
+
const pages = data.pages || {}
|
|
133
|
+
const routes = Object.keys(pages)
|
|
134
|
+
const results = {}
|
|
135
|
+
|
|
136
|
+
for (const route of routes) {
|
|
137
|
+
results[route] = await renderFn(data, { route })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return results
|
|
141
|
+
}
|
package/metadata.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/brender",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.8",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"source": "index.js",
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
|
+
"*.js",
|
|
28
29
|
"*.md",
|
|
29
30
|
"LICENSE"
|
|
30
31
|
],
|
|
@@ -40,8 +41,8 @@
|
|
|
40
41
|
"prepublishOnly": "npm run build"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@symbo.ls/css": "^3.14.
|
|
44
|
-
"@symbo.ls/helmet": "^3.14.
|
|
44
|
+
"@symbo.ls/css": "^3.14.10",
|
|
45
|
+
"@symbo.ls/helmet": "^3.14.8",
|
|
45
46
|
"linkedom": "^0.16.8"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
package/prefetch.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR data prefetching for brender.
|
|
3
|
+
*
|
|
4
|
+
* Walks a page definition tree, collects `fetch` declarations,
|
|
5
|
+
* executes them against the configured DB adapter (e.g. Supabase),
|
|
6
|
+
* and returns the fetched data keyed by element path + `as` field.
|
|
7
|
+
*
|
|
8
|
+
* This allows brender to inject fetched data into element state
|
|
9
|
+
* before rendering, so the SSR output matches the client-side SPA.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const isFunction = (v) => typeof v === 'function'
|
|
13
|
+
const isArray = (v) => Array.isArray(v)
|
|
14
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a fetch config's params — if it's a function, call it with
|
|
18
|
+
* a mock element and state to get static params for SSR.
|
|
19
|
+
*/
|
|
20
|
+
const resolveParams = (params, mockState) => {
|
|
21
|
+
if (!params) return undefined
|
|
22
|
+
if (isFunction(params)) {
|
|
23
|
+
try {
|
|
24
|
+
// Build a mock element with basic call() support
|
|
25
|
+
const mockEl = {
|
|
26
|
+
state: mockState || {},
|
|
27
|
+
call: () => undefined,
|
|
28
|
+
__ref: {}
|
|
29
|
+
}
|
|
30
|
+
return params(mockEl, mockState || {})
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return params
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Normalize a single fetch declaration to a standard config object.
|
|
40
|
+
*/
|
|
41
|
+
const normalizeFetchConfig = (cfg, elementState) => {
|
|
42
|
+
if (!cfg) return null
|
|
43
|
+
if (typeof cfg === 'string') return { from: cfg, method: 'select' }
|
|
44
|
+
|
|
45
|
+
const resolved = isFunction(cfg) ? null : { ...cfg }
|
|
46
|
+
if (!resolved) return null
|
|
47
|
+
|
|
48
|
+
// Default method
|
|
49
|
+
if (!resolved.method) resolved.method = 'select'
|
|
50
|
+
|
|
51
|
+
// Resolve function params
|
|
52
|
+
if (isFunction(resolved.params)) {
|
|
53
|
+
resolved.params = resolveParams(resolved.params, elementState)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Skip mutations and event-bound fetches
|
|
57
|
+
const isMutation = resolved.method === 'insert' || resolved.method === 'update' ||
|
|
58
|
+
resolved.method === 'upsert' || resolved.method === 'delete'
|
|
59
|
+
if (isMutation) return null
|
|
60
|
+
if (resolved.on && resolved.on !== 'create') return null
|
|
61
|
+
|
|
62
|
+
return resolved
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Walk a page definition tree and collect all fetch declarations
|
|
67
|
+
* along with the path to the element's state.
|
|
68
|
+
*
|
|
69
|
+
* Returns: [{ config, stateKey, path, elementState }]
|
|
70
|
+
*/
|
|
71
|
+
const collectFetchDeclarations = (def, path = '') => {
|
|
72
|
+
if (!def || typeof def !== 'object') return []
|
|
73
|
+
// Skip function values and arrays of primitives
|
|
74
|
+
if (isFunction(def)) return []
|
|
75
|
+
|
|
76
|
+
const results = []
|
|
77
|
+
const elementState = def.state || {}
|
|
78
|
+
|
|
79
|
+
if (def.fetch) {
|
|
80
|
+
const fetchDefs = isArray(def.fetch) ? def.fetch : [def.fetch]
|
|
81
|
+
for (const fd of fetchDefs) {
|
|
82
|
+
const config = normalizeFetchConfig(fd, elementState)
|
|
83
|
+
if (config) {
|
|
84
|
+
results.push({
|
|
85
|
+
config,
|
|
86
|
+
stateKey: config.as,
|
|
87
|
+
path,
|
|
88
|
+
elementState
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Recurse into child elements (capitalized keys = child elements)
|
|
95
|
+
for (const key in def) {
|
|
96
|
+
if (key === 'fetch' || key === 'state' || key === 'props' ||
|
|
97
|
+
key === 'attr' || key === 'on' || key === 'define' ||
|
|
98
|
+
key === 'childExtends' || key === 'childProps' || key === 'childrenAs') continue
|
|
99
|
+
// Child elements have capitalized keys
|
|
100
|
+
if (key.charAt(0) >= 'A' && key.charAt(0) <= 'Z' && isObject(def[key])) {
|
|
101
|
+
results.push(...collectFetchDeclarations(def[key], path ? `${path}.${key}` : key))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return results
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a Supabase adapter from project config for SSR use.
|
|
110
|
+
*/
|
|
111
|
+
const createSSRAdapter = async (dbConfig) => {
|
|
112
|
+
if (!dbConfig) return null
|
|
113
|
+
|
|
114
|
+
const { adapter, createClient, url, key, projectId } = dbConfig
|
|
115
|
+
if (adapter !== 'supabase') return null
|
|
116
|
+
|
|
117
|
+
const supabaseUrl = url || (projectId && `https://${projectId}.supabase.co`)
|
|
118
|
+
if (!supabaseUrl || !key) return null
|
|
119
|
+
|
|
120
|
+
// Always import @supabase/supabase-js for SSR — the serialized createClient
|
|
121
|
+
// from project data is a no-op placeholder that won't produce a real client.
|
|
122
|
+
let clientFactory
|
|
123
|
+
try {
|
|
124
|
+
const mod = await import('@supabase/supabase-js')
|
|
125
|
+
clientFactory = mod.createClient
|
|
126
|
+
} catch {
|
|
127
|
+
// Fall back to provided createClient only if import fails
|
|
128
|
+
clientFactory = createClient
|
|
129
|
+
}
|
|
130
|
+
if (!clientFactory) return null
|
|
131
|
+
|
|
132
|
+
const client = clientFactory(supabaseUrl, key)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
rpc: ({ from, params }) => client.rpc(from, params),
|
|
136
|
+
select: async ({ from, select: sel, params, limit, offset, order, single }) => {
|
|
137
|
+
let q = client.from(from).select(sel || '*')
|
|
138
|
+
if (params) {
|
|
139
|
+
for (const k in params) {
|
|
140
|
+
const v = params[k]
|
|
141
|
+
if (v === null) q = q.is(k, null)
|
|
142
|
+
else if (Array.isArray(v)) q = q.in(k, v)
|
|
143
|
+
else q = q.eq(k, v)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (order) {
|
|
147
|
+
const orderBy = typeof order === 'string' ? order : order.by
|
|
148
|
+
q = q.order(orderBy, { ascending: order.asc !== false })
|
|
149
|
+
}
|
|
150
|
+
if (limit) q = q.limit(limit)
|
|
151
|
+
if (offset) q = q.range(offset, offset + (limit || 20) - 1)
|
|
152
|
+
if (single) q = q.single()
|
|
153
|
+
return q
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Execute a single fetch config against the adapter.
|
|
160
|
+
* Returns the fetched data, or null on error.
|
|
161
|
+
*/
|
|
162
|
+
const executeSingle = async (adapter, config) => {
|
|
163
|
+
try {
|
|
164
|
+
const { method, from, params, transform, limit, offset, order, single } = config
|
|
165
|
+
let result
|
|
166
|
+
|
|
167
|
+
if (method === 'rpc') {
|
|
168
|
+
result = await adapter.rpc({ from, params })
|
|
169
|
+
} else {
|
|
170
|
+
result = await adapter.select({ from, select: config.select, params, limit, offset, order, single })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let data = result?.data ?? null
|
|
174
|
+
if (result?.error) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Apply transform
|
|
179
|
+
if (data && transform && isFunction(transform)) {
|
|
180
|
+
try { data = transform(data) } catch { /* skip transform errors */ }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return data
|
|
184
|
+
} catch {
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Prefetch all data for a page route.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} data - Full project data (from loadProject)
|
|
193
|
+
* @param {string} route - Route to prefetch for (e.g. '/', '/blog')
|
|
194
|
+
* @param {object} [options]
|
|
195
|
+
* @returns {Promise<Map<string, object>>} Map of element path → { [stateKey]: data }
|
|
196
|
+
*/
|
|
197
|
+
export const prefetchPageData = async (data, route = '/', options = {}) => {
|
|
198
|
+
const pages = data.pages || {}
|
|
199
|
+
const pageDef = pages[route]
|
|
200
|
+
if (!pageDef) return new Map()
|
|
201
|
+
|
|
202
|
+
const config = data.config || data.settings || {}
|
|
203
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db
|
|
204
|
+
if (!dbConfig) return new Map()
|
|
205
|
+
|
|
206
|
+
const adapter = await createSSRAdapter(dbConfig)
|
|
207
|
+
if (!adapter) return new Map()
|
|
208
|
+
|
|
209
|
+
const declarations = collectFetchDeclarations(pageDef)
|
|
210
|
+
if (!declarations.length) return new Map()
|
|
211
|
+
|
|
212
|
+
const stateUpdates = new Map()
|
|
213
|
+
|
|
214
|
+
// Execute all fetches in parallel
|
|
215
|
+
const results = await Promise.allSettled(
|
|
216
|
+
declarations.map(async ({ config, stateKey, path }) => {
|
|
217
|
+
const fetchedData = await executeSingle(adapter, config)
|
|
218
|
+
if (fetchedData !== null) {
|
|
219
|
+
const existing = stateUpdates.get(path) || {}
|
|
220
|
+
if (stateKey) {
|
|
221
|
+
// Named: store under the `as` key
|
|
222
|
+
existing[stateKey] = fetchedData
|
|
223
|
+
} else if (isObject(fetchedData)) {
|
|
224
|
+
// No `as` key + transform returned an object: spread into state
|
|
225
|
+
Object.assign(existing, fetchedData)
|
|
226
|
+
}
|
|
227
|
+
stateUpdates.set(path, existing)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return stateUpdates
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Inject prefetched data into a page definition's state objects.
|
|
237
|
+
* Mutates the definition in place (caller should deep-clone first).
|
|
238
|
+
*
|
|
239
|
+
* @param {object} pageDef - Page definition (will be mutated)
|
|
240
|
+
* @param {Map<string, object>} stateUpdates - Map from prefetchPageData
|
|
241
|
+
*/
|
|
242
|
+
/**
|
|
243
|
+
* Fetch polyglot translations from the DB for SSR use.
|
|
244
|
+
* Returns a map of { [lang]: { key: text, ... } } for all configured languages.
|
|
245
|
+
*
|
|
246
|
+
* @param {object} data - Full project data (from loadProject)
|
|
247
|
+
* @returns {Promise<object|null>} Translation map keyed by language, or null on failure
|
|
248
|
+
*/
|
|
249
|
+
export const fetchSSRTranslations = async (data) => {
|
|
250
|
+
// Config fields may be nested under data.config or spread at the top level
|
|
251
|
+
const config = data.config || {}
|
|
252
|
+
const polyglot = config.polyglot || data.polyglot
|
|
253
|
+
if (!polyglot?.fetch) return null
|
|
254
|
+
|
|
255
|
+
const dbConfig = config.fetch || data.fetch || config.db || data.db
|
|
256
|
+
if (!dbConfig) return null
|
|
257
|
+
|
|
258
|
+
const adapter = await createSSRAdapter(dbConfig)
|
|
259
|
+
if (!adapter) return null
|
|
260
|
+
|
|
261
|
+
const fetchConfig = polyglot.fetch
|
|
262
|
+
const rpcName = fetchConfig.rpc || fetchConfig.from || 'get_translations_if_changed'
|
|
263
|
+
const languages = polyglot.languages || [polyglot.defaultLang || 'en']
|
|
264
|
+
|
|
265
|
+
const translations = {}
|
|
266
|
+
|
|
267
|
+
// Fetch translations for all languages in parallel
|
|
268
|
+
const results = await Promise.allSettled(
|
|
269
|
+
languages.map(async (lang) => {
|
|
270
|
+
try {
|
|
271
|
+
const res = await adapter.rpc({
|
|
272
|
+
from: rpcName,
|
|
273
|
+
params: { p_lang: lang, p_cached_version: 0 }
|
|
274
|
+
})
|
|
275
|
+
if (res.error || !res.data) return
|
|
276
|
+
const result = res.data
|
|
277
|
+
if (result.translations) {
|
|
278
|
+
translations[lang] = result.translations
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.warn('[brender] SSR translation fetch failed:', e.message)
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return Object.keys(translations).length ? translations : null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Pre-evaluate children functions and replace them with static results.
|
|
291
|
+
* During SSR, DOMQL's runtime state cascading and async re-render cycle
|
|
292
|
+
* may not work correctly (trackSourcemapDeep stack overflows, etc.).
|
|
293
|
+
* By pre-evaluating the children functions, we produce static element
|
|
294
|
+
* definitions that DOMQL can render directly.
|
|
295
|
+
*/
|
|
296
|
+
const preEvaluateChildren = (def, inheritedState) => {
|
|
297
|
+
if (!def || typeof def !== 'object') return
|
|
298
|
+
for (const key in def) {
|
|
299
|
+
if (key === 'state' || key === 'fetch' || key === 'props' ||
|
|
300
|
+
key === 'attr' || key === 'on' || key === 'define' ||
|
|
301
|
+
key === 'childExtends' || key === 'childProps' || key === 'childrenAs') continue
|
|
302
|
+
if (key.charAt(0) >= 'A' && key.charAt(0) <= 'Z' && isObject(def[key])) {
|
|
303
|
+
const child = def[key]
|
|
304
|
+
// Determine effective state for this element (own state or inherited)
|
|
305
|
+
const effectiveState = child.state && typeof child.state === 'object'
|
|
306
|
+
? { ...inheritedState, ...child.state }
|
|
307
|
+
: inheritedState
|
|
308
|
+
|
|
309
|
+
// Pre-evaluate children function
|
|
310
|
+
if (isFunction(child.children)) {
|
|
311
|
+
try {
|
|
312
|
+
const mockEl = {
|
|
313
|
+
state: effectiveState,
|
|
314
|
+
call: (fn) => {
|
|
315
|
+
if (fn === 'getActiveLang' || fn === 'getLang') return effectiveState?.lang || 'ka'
|
|
316
|
+
if (fn === 'polyglot') return arguments[1] || ''
|
|
317
|
+
return undefined
|
|
318
|
+
},
|
|
319
|
+
__ref: {}
|
|
320
|
+
}
|
|
321
|
+
const result = child.children(mockEl, effectiveState)
|
|
322
|
+
if (isArray(result) && result.length > 0) {
|
|
323
|
+
// Replace children function with static array
|
|
324
|
+
child.children = result
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// If evaluation fails, leave the function as-is
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Recurse deeper
|
|
332
|
+
preEvaluateChildren(child, effectiveState)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export const injectPrefetchedState = (pageDef, stateUpdates) => {
|
|
338
|
+
if (!stateUpdates || !stateUpdates.size) return
|
|
339
|
+
|
|
340
|
+
for (const [path, data] of stateUpdates) {
|
|
341
|
+
// Navigate to the element at the path
|
|
342
|
+
let target = pageDef
|
|
343
|
+
if (path) {
|
|
344
|
+
const parts = path.split('.')
|
|
345
|
+
for (const part of parts) {
|
|
346
|
+
if (!target || typeof target !== 'object') break
|
|
347
|
+
target = target[part]
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (target && typeof target === 'object') {
|
|
352
|
+
// Merge fetched data into the element's state
|
|
353
|
+
if (!target.state || typeof target.state !== 'object') {
|
|
354
|
+
target.state = {}
|
|
355
|
+
}
|
|
356
|
+
Object.assign(target.state, data)
|
|
357
|
+
|
|
358
|
+
// Pre-evaluate children functions with the injected state
|
|
359
|
+
// so DOMQL gets static element definitions instead of functions
|
|
360
|
+
preEvaluateChildren(target, target.state)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|