@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/CHANGELOG.md +38 -0
- package/README.md +106 -23
- package/dist/esm/env.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/load.js +2 -1
- package/dist/esm/prefetch.js +1 -1
- package/dist/esm/render.js +29 -27
- package/dist/esm/shell.js +9 -8
- package/env.js +56 -29
- package/index.js +5 -3
- package/load.js +111 -22
- package/package.json +8 -8
- package/prefetch.js +30 -62
- package/render.js +493 -193
- package/shell.js +74 -12
package/dist/esm/shell.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
const
|
|
2
|
-
${t}
|
|
3
|
-
${
|
|
4
|
-
`:"",
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,""")}"`}).join(""),s=o.replace(/"/g,""");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,"""),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
|
-
|
|
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
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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,
|
|
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))
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
50
|
-
"@symbo.ls/default-config": "^3.14.
|
|
51
|
-
"@symbo.ls/element": "^3.14.
|
|
52
|
-
"@symbo.ls/fetch": "^3.14.
|
|
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.
|
|
55
|
-
"@symbo.ls/utils": "^3.14.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
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
|
}
|