@symbo.ls/brender 3.14.664 → 3.14.666

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/esm/shell.js CHANGED
@@ -1,8 +1,9 @@
1
- const A=/<head(\s[^>]*)?>/i,C=/<body(\s[^>]*)?>/i,d=(e,t)=>{const s=new RegExp(`\\s${t}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`,"i").exec(e);return s?s[2]??s[3]??s[4]??"":null},y=e=>{const t=e.slice(0,12).toLowerCase();if(t.startsWith("<title"))return"title";if(t.startsWith("<meta")){if(d(e,"charset")!==null)return"charset";for(const s of["name","property","http-equiv","itemprop"]){const r=d(e,s);if(r!==null)return`${s}:${r.toLowerCase()}`}return null}if(t.startsWith("<link")){const s=(d(e,"rel")||"").toLowerCase();return s==="canonical"?"link:canonical":s==="icon"||s==="shortcut icon"?`link:${s}:${d(e,"href")||""}`:s==="alternate"&&d(e,"hreflang")!==null?`link:alternate:${(d(e,"hreflang")||"").toLowerCase()}`:null}return null},k=/<title[^>]*>[\s\S]*?<\/title\s*>|<meta\b[^>]*>|<link\b[^>]*>/gi,L=/<(script|style|noscript|template|textarea)\b[\s\S]*?<\/\1\s*>/gi,E=e=>[...e.matchAll(L)].map(t=>[t.index,t.index+t[0].length,t[0]]),R=(e,t,s=r=>r)=>{let r="",i=0;for(const[c,n,l]of E(e))r+=t(e.slice(i,c))+s(l),i=n;return r+t(e.slice(i))},f=(e,t,s=0)=>{const r=E(e),i=new RegExp(t.source,t.flags.includes("g")?t.flags:t.flags+"g");i.lastIndex=s;let c;for(;c=i.exec(e);){const n=c.index,l=r.find(([u,h])=>n>u&&n<h);if(!l)return c;i.lastIndex=l[1]}return null},B=e=>!!e&&!/^(?:[a-z][a-z0-9+.-]*:|\/|#|\?)/i.test(e),_=(e,t)=>e.replace(/(\s(?:src|href)\s*=\s*)(?:(["'])([^"']*)\2|([^\s"'>]+))/gi,(s,r,i,c,n)=>{const l=i?c:n;if(!B(l))return s;const u=i||"";return`${r}${u}${t}${l.replace(/^\.\//,"")}${u}`}),D=(e,t)=>{if(!t)return e;const s="../".repeat(t);return R(e,r=>r.replace(/<link\b[^>]*>/gi,i=>_(i,s)),r=>/^<script\b/i.test(r)?r.replace(/^<script\b[^>]*>/i,i=>_(i,s)):r)},I=(e,{headTags:t="",headEnd:s="",body:r="",depth:i=0,bootScripts:c=""}={})=>{if(typeof e!="string"||!e.trim())throw new Error("brender: the shell document is empty");let n=D(e,i);const l=new Set;for(const o of t.matchAll(k)){const a=y(o[0]);a&&l.add(a)}const u=o=>R(o,a=>a.replace(k,O=>l.has(y(O))?"":O)),h=t?`
2
- ${t}`:"",g=s?`
3
- ${s}
4
- `:"",p=f(n,A),w=p&&f(n,/<\/head\s*>/i,p.index);if(p&&w){const o=p.index+p[0].length,a=w.index;n=n.slice(0,o)+h+u(n.slice(o,a))+g+n.slice(a)}else{n=u(n);const o=f(n,/<html(\s[^>]*)?>/i)||f(n,/<!doctype[^>]*>/i),a=o?o.index+o[0].length:0;n=n.slice(0,a)+h+g+n.slice(a)}const $=`
5
- <script>window.__BRENDER__=true<\/script>
6
- ${c?c+`
7
- `:""}${r}
8
- `,x=f(n,C);if(x){const o=x.index+x[0].length;return n.slice(0,o)+$+n.slice(o)}const m=f(n,/<\/(?:body|html)\s*>/i),b=m?m.index:n.length;return n.slice(0,b)+$+n.slice(b)};export{I as composeIntoShell,y as headTagKey};
1
+ const I=/<head(\s[^>]*)?>/i,P=/<body(\s[^>]*)?>/i,f=(e,n)=>{const t=new RegExp(`\\s${n}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`,"i").exec(e);return t?t[2]??t[3]??t[4]??"":null},S=e=>{const n=e.slice(0,12).toLowerCase();if(n.startsWith("<title"))return"title";if(n.startsWith("<meta")){if(f(e,"charset")!==null)return"charset";for(const t of["name","property","http-equiv","itemprop"]){const r=f(e,t);if(r!==null)return`${t}:${r.toLowerCase()}`}return null}if(n.startsWith("<link")){const t=(f(e,"rel")||"").toLowerCase();return t==="canonical"?"link:canonical":t==="icon"||t==="shortcut icon"?`link:${t}:${f(e,"href")||""}`:t==="alternate"&&f(e,"hreflang")!==null?`link:alternate:${(f(e,"hreflang")||"").toLowerCase()}`:null}return null},v=/<title[^>]*>[\s\S]*?<\/title\s*>|<meta\b[^>]*>|<link\b[^>]*>/gi,_=/<(script|style|noscript|template|textarea)\b[\s\S]*?<\/\1\s*>/gi,x=e=>[...e.matchAll(_)].map(n=>[n.index,n.index+n[0].length,n[0]]),L=(e,n,t=r=>r)=>{let r="",i=0;for(const[o,l,a]of x(e))r+=n(e.slice(i,o))+t(a),i=l;return r+n(e.slice(i))},d=(e,n,t=0)=>{const r=x(e),i=new RegExp(n.source,n.flags.includes("g")?n.flags:n.flags+"g");i.lastIndex=t;let o;for(;o=i.exec(e);){const l=o.index,a=r.find(([s,u])=>l>s&&l<u);if(!a)return o;i.lastIndex=a[1]}return null},j=e=>!!e&&!/^(?:[a-z][a-z0-9+.-]*:|\/|#|\?)/i.test(e),E=(e,n)=>e.replace(/(\s(?:src|href)\s*=\s*)(?:(["'])([^"']*)\2|([^\s"'>]+))/gi,(t,r,i,o,l)=>{const a=i?o:l;if(!j(a))return t;const s=i||"";return`${r}${s}${n}${a.replace(/^\.\//,"")}${s}`}),q=(e,n)=>{if(!n)return e;const t="../".repeat(n);return L(e,r=>r.replace(/<link\b[^>]*>/gi,i=>E(i,t)),r=>/^<script\b/i.test(r)?r.replace(/^<script\b[^>]*>/i,i=>E(i,t)):r)},W=new Set(["","module","text/javascript","application/javascript"]),m=(e,n)=>new RegExp(`\\s${n}(?:\\s*=|\\s|>|/)`,"i").test(e),B=e=>{const n=[],t=new Set;for(const[,,r]of x(e)){if(!/^<script\b/i.test(r))continue;const i=r.slice(0,r.indexOf(">")+1),o=f(i,"src");if(!o||t.has(o))continue;const l=(f(i,"type")||"").trim().toLowerCase();if(!W.has(l)||m(i,"async")||m(i,"nomodule"))continue;t.add(o);const a=["crossorigin","integrity","referrerpolicy"].map(u=>{const h=f(i,u);return h===null?m(i,u)?` ${u}`:"":` ${u}="${h.replace(/"/g,"&quot;")}"`}).join(""),s=o.replace(/"/g,"&quot;");n.push(l==="module"?`<link rel="modulepreload" href="${s}" fetchpriority="high"${a}>`:`<link rel="preload" as="script" href="${s}" fetchpriority="high"${a}>`)}return n.join(`
2
+ `)},D=(e,n)=>{const t=d(e,/<html(\s[^>]*)?>/i);if(!t)return e;const r=t[0],i=String(n).replace(/"/g,"&quot;"),o=f(r,"lang")!==null?r.replace(/(\slang\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/i,`$1"${i}"`):r.replace(/^<html/i,`<html lang="${i}"`);return e.slice(0,t.index)+o+e.slice(t.index+r.length)},H=(e,{headTags:n="",headEnd:t="",body:r="",depth:i=0,bootScripts:o="",lang:l,preloadScripts:a=!1}={})=>{if(typeof e!="string"||!e.trim())throw new Error("brender: the shell document is empty");let s=q(e,i);l&&(s=D(s,l));const u=a?B(s):"";u&&(t=t?`${u}
3
+ ${t}`:u);const h=new Set;for(const c of n.matchAll(v)){const p=S(c[0]);p&&h.add(p)}const w=c=>L(c,p=>p.replace(v,R=>h.has(S(R))?"":R)),y=n?`
4
+ ${n}`:"",O=t?`
5
+ ${t}
6
+ `:"",g=d(s,I),b=g&&d(s,/<\/head\s*>/i,g.index);if(g&&b){const c=g.index+g[0].length,p=b.index;s=s.slice(0,c)+y+w(s.slice(c,p))+O+s.slice(p)}else{s=w(s);const c=d(s,/<html(\s[^>]*)?>/i)||d(s,/<!doctype[^>]*>/i),p=c?c.index+c[0].length:0;s=s.slice(0,p)+y+O+s.slice(p)}const k=`
7
+ ${r}
8
+ ${o?o+`
9
+ `:""}`,$=d(s,P);if($){const c=$.index+$[0].length;return s.slice(0,c)+k+s.slice(c)}const C=d(s,/<\/(?:body|html)\s*>/i),A=C?C.index:s.length;return s.slice(0,A)+k+s.slice(A)};export{H as composeIntoShell,S as headTagKey,B as scriptPreloads};
package/env.js CHANGED
@@ -33,12 +33,58 @@ const patchSelectValue = (document) => {
33
33
  })
34
34
  }
35
35
 
36
+ // Web Storage for one environment.
37
+ const createStorage = () => {
38
+ const store = {}
39
+ return {
40
+ getItem: (k) => store[k] ?? null,
41
+ setItem: (k, v) => { store[k] = String(v) },
42
+ removeItem: (k) => { delete store[k] },
43
+ clear: () => { for (const k in store) delete store[k] },
44
+ get length () { return Object.keys(store).length },
45
+ key: (i) => Object.keys(store)[i] ?? null
46
+ }
47
+ }
48
+
36
49
  /**
37
50
  * Creates a virtual DOM environment for server-side rendering.
38
51
  * Returns window and document that DOMQL can use as context.
39
52
  */
40
53
  export const createEnv = (html = '<!DOCTYPE html><html><head></head><body></body></html>') => {
41
- const { window, document } = parseHTML(html)
54
+ // Every environment owns its location, Web Storage and history.
55
+ // linkedom's window is a Proxy over globalThis itself: a property set on it
56
+ // is set on globalThis, and a property it does not know is read from
57
+ // globalThis — after the per-document globals parseHTML takes, which is
58
+ // where an environment's own objects have to live (`window.X` and
59
+ // `document.defaultView.X` read them first).
60
+ // - location was set on the window: that set globalThis.location, render()
61
+ // put it back after the render, and every later environment found it
62
+ // there and kept it — every render in a process shared one location and
63
+ // rewrote only its pathname. A render inherited the search and hash of
64
+ // the one before it, and renders that overlapped read each other's
65
+ // pathname: three of five overlapping renders of different routes
66
+ // rendered the home page.
67
+ // - Storage was set on the window too, so every environment's
68
+ // `window.localStorage` was the NEWEST environment's store: renders that
69
+ // overlapped read each other's persisted language and labels. (A new
70
+ // store per environment keeps renders one after another apart — polyglot
71
+ // keeps the language a page switched to and the labels it loaded there,
72
+ // and the next page of a build booted with them: xma.info report,
73
+ // brender build item 7.)
74
+ // - The history stub was set once on globalThis and shared by all.
75
+ // A complete Location shape (host/hostname/protocol/href too): framework
76
+ // modules read these at module load, before any route is set.
77
+ const own = {
78
+ location: { pathname: '/', search: '', hash: '', origin: 'http://localhost', protocol: 'http:', host: 'localhost', hostname: 'localhost', port: '', href: 'http://localhost/' },
79
+ localStorage: createStorage(),
80
+ sessionStorage: createStorage(),
81
+ history: {
82
+ pushState: () => {},
83
+ replaceState: () => {},
84
+ state: null
85
+ }
86
+ }
87
+ const { window, document } = parseHTML(html, own)
42
88
 
43
89
  // Stub APIs that DOMQL/smbls may call during rendering
44
90
  if (!window.requestAnimationFrame) {
@@ -51,18 +97,6 @@ export const createEnv = (html = '<!DOCTYPE html><html><head></head><body></body
51
97
  if (!window.cancelAnimationFrame) {
52
98
  window.cancelAnimationFrame = (id) => clearTimeout(id)
53
99
  }
54
- if (!window.history) {
55
- window.history = {
56
- pushState: () => {},
57
- replaceState: () => {},
58
- state: null
59
- }
60
- }
61
- if (!window.location) {
62
- // A complete Location shape (host/hostname/protocol/href too): framework
63
- // modules read these at module load, before any route is set.
64
- window.location = { pathname: '/', search: '', hash: '', origin: 'http://localhost', protocol: 'http:', host: 'localhost', hostname: 'localhost', port: '', href: 'http://localhost/' }
65
- }
66
100
  if (!window.URL) {
67
101
  window.URL = URL
68
102
  }
@@ -70,22 +104,15 @@ export const createEnv = (html = '<!DOCTYPE html><html><head></head><body></body
70
104
  window.scrollTo = () => {}
71
105
  }
72
106
 
73
- // Storage stubs
74
- const createStorage = () => {
75
- const store = {}
76
- return {
77
- getItem: (k) => store[k] ?? null,
78
- setItem: (k, v) => { store[k] = String(v) },
79
- removeItem: (k) => { delete store[k] },
80
- clear: () => { for (const k in store) delete store[k] },
81
- get length () { return Object.keys(store).length },
82
- key: (i) => Object.keys(store)[i] ?? null
83
- }
84
- }
85
- if (!window.localStorage) window.localStorage = createStorage()
86
- if (!window.sessionStorage) window.sessionStorage = createStorage()
87
- if (!globalThis.localStorage) globalThis.localStorage = window.localStorage
88
- if (!globalThis.sessionStorage) globalThis.sessionStorage = window.sessionStorage
107
+ // The BARE globals stay what they were (the window's setter used to write
108
+ // them): `localStorage` / `sessionStorage` are the newest environment's
109
+ // stores, and `history` is the first environment's stub when the host has
110
+ // none. render() swaps the bare storage and location to the rendering
111
+ // environment's own for the length of a render; overlapping renders still
112
+ // share them (a render queue or per-render globals — an open decision).
113
+ globalThis.localStorage = own.localStorage
114
+ globalThis.sessionStorage = own.sessionStorage
115
+ if (!globalThis.history) globalThis.history = own.history
89
116
 
90
117
  // linkedom's HTMLSelectElement has a `value` getter and NO setter: a
91
118
  // `value` prop on a <select> threw "Cannot set property value … which has
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, getAccumulatedEmotionCSS, replaceEmotionCSS, adoptionScripts } from './render.js'
4
+ import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache, getAccumulatedEmotionCSS, replaceEmotionCSS, adoptionScripts, prerenderParts } 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'
@@ -30,7 +30,8 @@ export {
30
30
  prefetchPageData,
31
31
  injectPrefetchedState,
32
32
  composeIntoShell,
33
- adoptionScripts
33
+ adoptionScripts,
34
+ prerenderParts
34
35
  }
35
36
 
36
37
  export default {
@@ -55,5 +56,6 @@ export default {
55
56
  prefetchPageData,
56
57
  injectPrefetchedState,
57
58
  composeIntoShell,
58
- adoptionScripts
59
+ adoptionScripts,
60
+ prerenderParts
59
61
  }
package/load.js CHANGED
@@ -1,15 +1,51 @@
1
- import { resolve, join } from 'path'
2
- import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs'
1
+ import { resolve, join, extname, sep } from 'path'
2
+ import { existsSync, readFileSync, unlinkSync } from 'fs'
3
3
  import { tmpdir } from 'os'
4
4
  import { randomBytes } from 'crypto'
5
+ import { pathToFileURL } from 'url'
6
+
7
+ // ── import.meta.url in project code ─────────────────────────────────────────
8
+ // `new URL('../../assets/fonts/x.woff2', import.meta.url)` is how a project
9
+ // names a file next to its source; the bundler turns it into the built
10
+ // asset's address. Bundled into ONE temp module here, every project file's
11
+ // `import.meta.url` became that temp file's, so the URL pointed into the
12
+ // build machine's temp folder — every pre-rendered @font-face read
13
+ // `file:///var/folders/…`, the page drew a fallback font, then shifted (CLS
14
+ // 0.40; xma.info report, brender build item 6). Each project file (anything
15
+ // outside node_modules) gets its OWN source address instead: the URL then
16
+ // names the real source file, which a build maps to its output (the CLI's
17
+ // brenderAssets) and refuses to ship as a file: URL.
18
+ const IMPORT_META_URL = /\bimport\.meta\.url\b/g
19
+ const HAS_IMPORT_META_URL = /\bimport\.meta\.url\b/
20
+ const LOADERS = { '.js': 'js', '.mjs': 'js', '.cjs': 'js', '.jsx': 'jsx', '.ts': 'ts', '.mts': 'ts', '.tsx': 'tsx' }
21
+
22
+ export const sourceUrlPlugin = {
23
+ name: 'brender-source-import-meta-url',
24
+ setup (build) {
25
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
26
+ if (args.path.includes(`${sep}node_modules${sep}`)) return
27
+ const contents = readFileSync(args.path, 'utf8')
28
+ if (!HAS_IMPORT_META_URL.test(contents)) return
29
+ const url = JSON.stringify(pathToFileURL(args.path).href)
30
+ return {
31
+ contents: `const __brenderImportMetaUrl = ${url};\n` + contents.replace(IMPORT_META_URL, '__brenderImportMetaUrl'),
32
+ loader: LOADERS[extname(args.path)] || 'js'
33
+ }
34
+ })
35
+ }
36
+ }
5
37
 
6
38
  /**
7
39
  * Bundles a module entry point with esbuild so that extensionless imports,
8
40
  * bare specifiers, and other bundler conventions resolve correctly.
9
- * Returns the default + named exports of the bundled module, or null on failure.
41
+ * Returns the default + named exports of the bundled module, or null on failure
42
+ * (or, with `strict`, throws what went wrong).
10
43
  */
11
- const bundleAndImport = async (entryPath) => {
12
- if (!existsSync(entryPath)) return null
44
+ const bundleAndImport = async (entryPath, { strict = false } = {}) => {
45
+ if (!existsSync(entryPath)) {
46
+ if (strict) throw new Error(`${entryPath} does not exist`)
47
+ return null
48
+ }
13
49
 
14
50
  let esbuild
15
51
  try {
@@ -30,20 +66,69 @@ const bundleAndImport = async (entryPath) => {
30
66
  outfile: outFile,
31
67
  write: true,
32
68
  logLevel: 'silent',
69
+ plugins: [sourceUrlPlugin],
33
70
  // Mark node builtins as external
34
71
  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
72
  })
36
73
 
37
74
  const mod = await import(`file://${outFile}`)
38
75
  return mod
39
- } catch {
76
+ } catch (err) {
40
77
  // Fallback: try raw import
41
- try { return await import(entryPath) } catch { return null } // fallback: module not found via this resolver, trying next
78
+ try { return await import(entryPath) } catch (rawErr) { // fallback: module not found via this resolver, trying next
79
+ if (strict) throw err
80
+ return null
81
+ }
42
82
  } finally {
43
83
  try { unlinkSync(outFile) } catch {} // cleanup: ignore if temp file already removed
44
84
  }
45
85
  }
46
86
 
87
+ // A project's own context module (`context`): the object its entry hands to
88
+ // `create(app, context)` — `{ state, components, pages, functions,
89
+ // designSystem, files, sharedLibraries, ...config }`. The build's entry is
90
+ // what decides which pages a site has; a project whose public entry boots a
91
+ // smaller context than symbols/pages/index.js (xma.info: `siteContext.js`,
92
+ // no admin pages) got every page pre-rendered, admin ones included (xma.info
93
+ // report, brender build item 10). Its keys are spread into plain objects (a
94
+ // module namespace is not extensible); config keys stay at the top level,
95
+ // where render() reads them, as in a frank payload.
96
+ const CONTEXT_OBJECT_KEYS = ['state', 'dependencies', 'components', 'snippets', 'pages', 'functions', 'methods', 'designSystem', 'files', 'assets']
97
+
98
+ const loadContextModule = async (projectPath, contextPath) => {
99
+ const entry = resolve(projectPath, contextPath)
100
+ let mod
101
+ try {
102
+ mod = await bundleAndImport(entry, { strict: true })
103
+ } catch (err) {
104
+ throw new Error(`brender: cannot load the context module ${contextPath} (${err.message})`)
105
+ }
106
+ const context = mod && (mod.default || mod)
107
+ if (!context || typeof context !== 'object') {
108
+ throw new Error(`brender: the context module ${contextPath} has no default export object`)
109
+ }
110
+ const out = { ...context }
111
+ for (const key of CONTEXT_OBJECT_KEYS) {
112
+ if (context[key] && typeof context[key] === 'object') out[key] = { ...context[key] }
113
+ }
114
+ out.sharedLibraries = Array.isArray(context.sharedLibraries) ? context.sharedLibraries : []
115
+ return out
116
+ }
117
+
118
+ const mergeLibraries = async (result) => {
119
+ // Pre-resolve string library references and merge into context
120
+ // so production builds have zero runtime overhead
121
+ if (result.sharedLibraries.length) {
122
+ const { resolveSharedLibraries, mergeSharedLibraries } = await import('@symbo.ls/utils')
123
+ const hasStrings = result.sharedLibraries.some(lib => typeof lib === 'string')
124
+ if (hasStrings) {
125
+ result.sharedLibraries = await resolveSharedLibraries(result.sharedLibraries)
126
+ }
127
+ mergeSharedLibraries(result, result.sharedLibraries)
128
+ }
129
+ return result
130
+ }
131
+
47
132
  /**
48
133
  * Loads a Symbols project from a filesystem path.
49
134
  * Expects the standard symbols/ directory structure.
@@ -52,13 +137,28 @@ const bundleAndImport = async (entryPath) => {
52
137
  * and other bundler conventions work in Node.js.
53
138
  *
54
139
  * Used for prebuild scenarios where brender runs locally
55
- * against a project directory (e.g. `smbls build --prerender`).
140
+ * against a project directory (e.g. `smbls build`).
56
141
  *
57
142
  * For server runtime rendering, pass the project data
58
143
  * directly to render() instead.
144
+ *
145
+ * @param {string} projectPath
146
+ * @param {object} [options]
147
+ * @param {string} [options.dir='symbols'] - the symbols directory (symbols.json `dir`)
148
+ * @param {string} [options.context] - a module, relative to `projectPath`,
149
+ * whose default export is the context the build's entry boots with
150
+ * (symbols.json `brender.context`). Everything but `app` comes from it.
59
151
  */
60
- export const loadProject = async (projectPath) => {
61
- const symbolsDir = resolve(projectPath, 'symbols')
152
+ export const loadProject = async (projectPath, options = {}) => {
153
+ const symbolsDir = resolve(projectPath, options.dir || 'symbols')
154
+
155
+ if (options.context) {
156
+ const [appModule, context] = await Promise.all([
157
+ bundleAndImport(join(symbolsDir, 'app.js')),
158
+ loadContextModule(projectPath, options.context)
159
+ ])
160
+ return mergeLibraries({ app: { ...(appModule?.default || {}) }, ...context })
161
+ }
62
162
 
63
163
  const [
64
164
  appModule,
@@ -108,18 +208,7 @@ export const loadProject = async (projectPath) => {
108
208
  sharedLibraries: sharedLibsModule?.default || []
109
209
  }
110
210
 
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
211
+ return mergeLibraries(result)
123
212
  }
124
213
 
125
214
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/brender",
3
- "version": "3.14.664",
3
+ "version": "3.14.666",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "module": "./dist/esm/index.js",
@@ -46,18 +46,18 @@
46
46
  "prepublishOnly": "npm run build"
47
47
  },
48
48
  "dependencies": {
49
- "@symbo.ls/css": "^3.14.777",
50
- "@symbo.ls/default-config": "^3.14.785",
51
- "@symbo.ls/element": "^3.14.803",
52
- "@symbo.ls/fetch": "^3.14.664",
49
+ "@symbo.ls/css": "^3.14.779",
50
+ "@symbo.ls/default-config": "^3.14.787",
51
+ "@symbo.ls/element": "^3.14.805",
52
+ "@symbo.ls/fetch": "^3.14.666",
53
53
  "@symbo.ls/helmet": "^3.14.670",
54
- "@symbo.ls/scratch": "^3.14.788",
55
- "@symbo.ls/utils": "^3.14.786",
54
+ "@symbo.ls/scratch": "^3.14.789",
55
+ "@symbo.ls/utils": "^3.14.787",
56
56
  "esbuild": "^0.28.1",
57
57
  "linkedom": "^0.16.8"
58
58
  },
59
59
  "peerDependencies": {
60
- "smbls": "^3.14.833"
60
+ "smbls": "^3.14.839"
61
61
  },
62
62
  "optionalDependencies": {
63
63
  "@symbo.ls/funcql": "^3.14.0"
package/prefetch.js CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * SSR data prefetching for brender.
3
3
  *
4
- * Walks a page definition tree, collects `fetch` declarations,
5
- * executes them against the configured DB adapter,
6
- * and returns the fetched data keyed by element path + `as` field.
4
+ * `fetchSSRTranslations` loads a project's table/RPC labels before a render.
7
5
  *
8
- * This allows brender to inject fetched data into element state
9
- * before rendering, so the SSR output matches the client-side SPA.
6
+ * `prefetchPageData` walks a page definition tree, collects `fetch`
7
+ * declarations, executes them against the configured DB adapter, and returns
8
+ * the fetched data keyed by element path + `as` field. render() does NOT use
9
+ * it: the fetch plugin loads a page's data during the render, with the real
10
+ * element (a `transform` here gets no element). Kept for direct callers.
10
11
  */
11
12
 
12
13
  const isFunction = (v) => typeof v === 'function'
@@ -192,6 +193,10 @@ const executeSingle = async (adapter, config) => {
192
193
  /**
193
194
  * Prefetch all data for a page route.
194
195
  *
196
+ * @deprecated render() and renderPage() no longer use it: the fetch plugin
197
+ * loads a page's data during the render, with the real element (a `transform`
198
+ * here gets none). The `prefetch` render option does not call it. Kept for
199
+ * direct callers; to be removed.
195
200
  * @param {object} data - Full project data (from loadProject)
196
201
  * @param {string} route - Route to prefetch for (e.g. '/', '/blog')
197
202
  * @param {object} [options]
@@ -235,13 +240,6 @@ export const prefetchPageData = async (data, route = '/', options = {}) => {
235
240
  return stateUpdates
236
241
  }
237
242
 
238
- /**
239
- * Inject prefetched data into a page definition's state objects.
240
- * Mutates the definition in place (caller should deep-clone first).
241
- *
242
- * @param {object} pageDef - Page definition (will be mutated)
243
- * @param {Map<string, object>} stateUpdates - Map from prefetchPageData
244
- */
245
243
  /**
246
244
  * Fetch polyglot translations from the DB for SSR use.
247
245
  * Returns a map of { [lang]: { key: text, ... } } for all configured languages.
@@ -254,6 +252,14 @@ export const fetchSSRTranslations = async (data) => {
254
252
  const config = data.config || {}
255
253
  const polyglot = config.polyglot || data.polyglot
256
254
  if (!polyglot?.fetch) return null
255
+ // A FUNCTION is the project's own loader — `(lang, version, el) => …`,
256
+ // called by the polyglot plugin. It runs inside the render, with the real
257
+ // root element (its el.call / el.getDB), and render() reads its labels back
258
+ // from the root state. This used to fall through to the RPC branch below
259
+ // with brender's default name, `get_translations_if_changed`: one request
260
+ // per language per route that the project never serves — 44 answered 404
261
+ // in one build (xma.info report, brender build item 11).
262
+ if (typeof polyglot.fetch === 'function') return null
257
263
 
258
264
  const dbConfig = config.fetch || data.fetch || config.db || data.db
259
265
  if (!dbConfig) return null
@@ -291,53 +297,19 @@ export const fetchSSRTranslations = async (data) => {
291
297
  }
292
298
 
293
299
  /**
294
- * Pre-evaluate children functions and replace them with static results.
295
- * During SSR, DOMQL's runtime state cascading and async re-render cycle
296
- * may not work correctly (trackSourcemapDeep stack overflows, etc.).
297
- * By pre-evaluating the children functions, we produce static element
298
- * definitions that DOMQL can render directly.
300
+ * Merge prefetched data into a page definition's element states. Mutates the
301
+ * definition in place (the caller deep-clones first).
302
+ *
303
+ * @deprecated render() no longer uses it (the fetch plugin loads a page's data
304
+ * during the render); kept for callers of prefetchPageData, to be removed.
305
+ *
306
+ * It used to go on and evaluate every `children` FUNCTION under the target
307
+ * with a stand-in element — whose call() answered 'ka' for any language and
308
+ * returned the enclosing arrow function's `arguments[1]` for a label — and
309
+ * freeze the result into a static array the real data could never replace
310
+ * (xma.info report, brender build items 5 and 13). A children function stays
311
+ * a function now: the render runs it with the real element.
299
312
  */
300
- const preEvaluateChildren = (def, inheritedState) => {
301
- if (!def || typeof def !== 'object') return
302
- for (const key in def) {
303
- if (key === 'state' || key === 'fetch' || key === 'props' ||
304
- key === 'attr' || key === 'on' || key === 'define' ||
305
- key === 'childExtends' || key === 'childProps' || key === 'childrenAs') continue
306
- if (key.charAt(0) >= 'A' && key.charAt(0) <= 'Z' && isObject(def[key])) {
307
- const child = def[key]
308
- // Determine effective state for this element (own state or inherited)
309
- const effectiveState = child.state && typeof child.state === 'object'
310
- ? { ...inheritedState, ...child.state }
311
- : inheritedState
312
-
313
- // Pre-evaluate children function
314
- if (isFunction(child.children)) {
315
- try {
316
- const mockEl = {
317
- state: effectiveState,
318
- call: (fn) => {
319
- if (fn === 'getActiveLang' || fn === 'getLang') return effectiveState?.lang || 'ka'
320
- if (fn === 'polyglot') return arguments[1] || ''
321
- return undefined
322
- },
323
- __ref: {}
324
- }
325
- const result = child.children(mockEl, effectiveState)
326
- if (isArray(result) && result.length > 0) {
327
- // Replace children function with static array
328
- child.children = result
329
- }
330
- } catch {
331
- // If evaluation fails, leave the function as-is
332
- }
333
- }
334
-
335
- // Recurse deeper
336
- preEvaluateChildren(child, effectiveState)
337
- }
338
- }
339
- }
340
-
341
313
  export const injectPrefetchedState = (pageDef, stateUpdates) => {
342
314
  if (!stateUpdates || !stateUpdates.size) return
343
315
 
@@ -358,10 +330,6 @@ export const injectPrefetchedState = (pageDef, stateUpdates) => {
358
330
  target.state = {}
359
331
  }
360
332
  Object.assign(target.state, data)
361
-
362
- // Pre-evaluate children functions with the injected state
363
- // so DOMQL gets static element definitions instead of functions
364
- preEvaluateChildren(target, target.state)
365
333
  }
366
334
  }
367
335
  }