@symbo.ls/brender 3.14.665 → 3.14.667
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 +27 -0
- package/README.md +14 -5
- package/dist/esm/env.js +1 -1
- package/dist/esm/render.js +28 -28
- package/env.js +57 -35
- package/package.json +4 -4
- package/render.js +229 -53
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @symbo.ls/brender
|
|
2
2
|
|
|
3
|
+
## 3.14.667
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Server renders run one at a time per process, and a page at /<lang>/… starts in that language.
|
|
8
|
+
|
|
9
|
+
- brender: `render()` (and `renderRoute` / `renderPage`) run one at a time per process; a per-render `timeout` (default 30 s, `0`/`false` turns it off) rejects with `BRENDER_RENDER_TIMEOUT`, puts the render's globals back and lets the next render start.
|
|
10
|
+
- brender + mermaid: before the app starts, the render stores the address's language (the first path segment, when it is one of the project's languages) under polyglot's key, so the fetch seed is recorded in the language the browser asks in; the served page's start-up script stores the page's language. One helper in utils (`polyglotAddressLang`, `polyglotStartupScript`).
|
|
11
|
+
- cli: the publish pre-render passes its route limit to brender's `timeout`; `symbols.json` `brender.timeout` sets each page's render limit for `smbls build` and `smbls brender`.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies
|
|
14
|
+
- @symbo.ls/utils@3.14.788
|
|
15
|
+
|
|
16
|
+
## 3.14.666
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Cross-tenant isolation for server renders, and the wider CLI production gate.
|
|
21
|
+
|
|
22
|
+
- fetch: a server render context (`isolateFetchCache`, set by brender) has its own query store — cache, requests in flight, invalidations and mounted queries — so two renders of different projects in one process never share rows, in sequence or at the same time; a server render binds no browser-only refetch (no focus / online listeners, no `refetchInterval` timer), so no request of one page lands in another page's seed and the process exits when the work is done.
|
|
23
|
+
- brender: renders that start together share one framework bundle; every render owns its `location`, Web Storage and `history`.
|
|
24
|
+
- cli: destructive platform writes (project delete, transfer, members, invites, library changes, version writes) print the target and need a typed "production" or `--yes` for production; `project create` is not gated.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies
|
|
27
|
+
- smbls@3.14.839
|
|
28
|
+
- @symbo.ls/fetch@3.14.666
|
|
29
|
+
|
|
3
30
|
## 3.14.665
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -119,6 +119,10 @@ const result = await render(data, { route: '/about' })
|
|
|
119
119
|
// result.element -> root DOMQL element
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
**One render at a time.** A render puts its environment on the process's bare globals (`document`, `location`, `localStorage`, `sessionStorage`, `window`) because the framework reads them bare, so `render()` — and `renderRoute()` / `renderPage()`, which call it — runs one render at a time per process: a call made while another render runs waits until that one has ended (returned, thrown or timed out), then starts. Calls made one after another (`smbls build`, `smbls brender`) do not wait. N renders started together take as long as N renders one after another.
|
|
123
|
+
|
|
124
|
+
**`timeout`** (default `30000` ms; `0` or `false`: none — then a render that never ends holds every later one) is how long a render may run once its turn starts — the wait for earlier renders does not count, and neither does building the process's framework bundle on its first render. A render past it is abandoned: the call rejects with an error whose `code` is `BRENDER_RENDER_TIMEOUT` and whose message, `route` and `pathname` name the page; the bare globals it put in place go back; the next render starts. When the wait it hung on ends later, its own remaining steps stop: it never puts its globals back over a later render's, and never makes an environment of its own after its time. What it cannot stop: framework or page code of the abandoned render that resumes with that wait reads and writes the bare globals of whatever render runs then, and process-wide framework state (the active root state behind `getRootState()` without `this`). Until it has unwound, a render that runs meanwhile carries no fetch seed and `diagnostics: null`: its recording could hold the abandoned render's requests.
|
|
125
|
+
|
|
122
126
|
### `renderPage(data, route, options?)`
|
|
123
127
|
|
|
124
128
|
Renders a complete, ready-to-serve HTML page. Combines `render()` output with metadata, CSS (atomic + global), fonts, and optional ISR client bundle.
|
|
@@ -148,6 +152,7 @@ Options:
|
|
|
148
152
|
| `eagerImages` | `2` | Images past the first `eagerImages` in the markup get `loading="lazy"`, unless they name their own `loading`. `false` turns it off. |
|
|
149
153
|
| `lang` | the page's | `<html lang>`. With `shell` and a project with languages, the page's language (the root state's `lang`). |
|
|
150
154
|
| `prefetch` | `true` | Fetch table/RPC labels before the render and wait for the page's requests (up to 2 s). `false` fetches no labels first and waits only a 50 ms quiet period. Either way the page's data comes from its `fetch` declarations during the render. |
|
|
155
|
+
| `timeout` | `30000` | Milliseconds the render may run once its turn starts; past it the render is abandoned and the call rejects (`BRENDER_RENDER_TIMEOUT`). `0` or `false`: no limit. See `render()`. |
|
|
151
156
|
| `depth` | `0` | Directory depth of the output file: rebases the shell's relative script/link URLs |
|
|
152
157
|
| `adoptDom` | `false` | Ship the element-path registry, so the client adopts the markup instead of replacing it (`smbls build` passes it unless `brender: { hydrate: false }`) |
|
|
153
158
|
| `isr` | — | `{ clientScript }`: a standalone document that boots this client bundle |
|
|
@@ -157,9 +162,11 @@ Options:
|
|
|
157
162
|
|
|
158
163
|
**Data and labels.** A page's declarative `fetch` runs during the render, through the fetch plugin, with the real element: its `transform(data, el, state)`, and `children` re-run on the answer. The render waits for the requests it starts. Labels: a table/RPC `polyglot.fetch` (`{ rpc }`) is fetched before the render, for every language; a FUNCTION `polyglot.fetch` is the project's own loader and runs inside the render, called by the polyglot plugin with the root element. Labels resolve in the page's language — the root state's `lang` (a router guard that reads `/en/…` sets it), else `polyglot.defaultLang`.
|
|
159
164
|
|
|
160
|
-
**
|
|
165
|
+
**The address's language.** A page at an address whose first segment is one of the project's languages (`/en/about` with `polyglot.languages: ['ka', 'en']`) starts in that language: before the app starts, the render stores it in its own environment's storage under polyglot's key (`storageLangKey`, default `smbls_lang`) — where a site's pre-boot script puts it in the browser — so polyglot starts in it and the shell's first requests (a header's menu, created before any router guard runs) are made, and recorded in the fetch seed, in the page's language. The seed is keyed by address and language: the browser adopts those answers only if it starts in the same language, so a site with language-prefixed addresses stores the address's language before its app starts (a pre-boot script that runs before the app's own). An address with no language segment starts as before: the stored choice, else the root state's `lang`, else `polyglot.defaultLang`. A standalone page with an ISR client (`isr`) starts its client itself: for a visitor with no language stored yet, its start-up script stores the page's language (it stored `polyglot.defaultLang`), so that client starts where the server started.
|
|
166
|
+
|
|
167
|
+
**Diagnostics.** `result.diagnostics` names what a page was made without: `failedRequests` (`{ url, reason }`, origin and path only — a query can carry a key) and `rawLabelKeys` (label keys the page shows raw). `null` when another render overlapped this one in the same process (since renders run one at a time, only a render abandoned at its timeout and still unwinding can). `smbls build` prints them; `smbls build --strict` fails on them.
|
|
161
168
|
|
|
162
|
-
**Every render is the same render.** Each render processes the design system on a config of its own, the way the page's single boot does (`context.isolateDesignSystem`), with its own `localStorage`/`sessionStorage`, so the tenth page of a build has the theme variables, language and labels of the first. Each render also
|
|
169
|
+
**Every render is the same render.** Each render processes the design system on a config of its own, the way the page's single boot does (`context.isolateDesignSystem`), with its own `localStorage`/`sessionStorage`, so the tenth page of a build has the theme variables, language and labels of the first. Each render also has a `@symbo.ls/fetch` query cache of its own (`context.isolateFetchCache`): a page makes every request its markup depends on itself, so its fetch seed carries every answer it shows, even when an earlier page of the build read the same key; and renders in one process — one after another or at once, of one project or of several — never share rows, requests in flight or invalidations. Its queries bind no window focus, reconnect or `refetchInterval` refetch: once its markup is taken, a render's queries leave no listener, no timer and no request behind. A process builds the framework bundle once, also when its first renders start together.
|
|
163
170
|
|
|
164
171
|
### The page the browser gets (the client's contract)
|
|
165
172
|
|
|
@@ -281,7 +288,7 @@ Output directory defaults to `dist-brender` (or `brenderDistDir` from `symbols.j
|
|
|
281
288
|
|
|
282
289
|
Param routes (e.g. `/blog/:id`) are skipped unless `brender.paths` lists addresses for them.
|
|
283
290
|
|
|
284
|
-
`smbls brender` and `smbls build` share one route loop: the same `brender` settings from `symbols.json` (`context`, `paths`, `exclude`, `strict`, `eagerImages`), `file:` URLs resolved into the output (a `file:` URL that names no project file fails its route), and the same diagnostics. `smbls brender` writes standalone pages in a directory layout (`/about` → `about/index.html`), each with the CSS of every page it rendered.
|
|
291
|
+
`smbls brender` and `smbls build` share one route loop: the same `brender` settings from `symbols.json` (`context`, `paths`, `exclude`, `strict`, `eagerImages`, `timeout`), `file:` URLs resolved into the output (a `file:` URL that names no project file fails its route), and the same diagnostics. `smbls brender` writes standalone pages in a directory layout (`/about` → `about/index.html`), each with the CSS of every page it rendered.
|
|
285
292
|
|
|
286
293
|
### `smbls build`
|
|
287
294
|
|
|
@@ -312,7 +319,8 @@ With brender on (the default), `smbls build` pre-renders every static route INTO
|
|
|
312
319
|
"exclude": ["/admin*"],
|
|
313
320
|
"paths": { "/:lang": ["/en"], "/:lang/about": ["/en/about"] },
|
|
314
321
|
"strict": true,
|
|
315
|
-
"eagerImages": 2
|
|
322
|
+
"eagerImages": 2,
|
|
323
|
+
"timeout": 30000
|
|
316
324
|
}
|
|
317
325
|
}
|
|
318
326
|
```
|
|
@@ -325,6 +333,7 @@ With brender on (the default), `smbls build` pre-renders every static route INTO
|
|
|
325
333
|
| `strict` | As `--strict`. |
|
|
326
334
|
| `eagerImages` | As renderPage's. |
|
|
327
335
|
| `hydrate` | `true` (default): every page ships its element-path registry and the client adopts it; `false`: replace pages (no registry; the client renders fresh). |
|
|
336
|
+
| `timeout` | Each page's render time limit in ms, as render()'s `timeout` (default `30000`; `0`: none). A page past it fails with `BRENDER_RENDER_TIMEOUT` and the next page renders. `smbls build` and `smbls brender` pass it to every render. |
|
|
328
337
|
|
|
329
338
|
## Examples
|
|
330
339
|
|
|
@@ -429,7 +438,7 @@ The client finds an element's node through the registry the page carries (elemen
|
|
|
429
438
|
- `renderElement()` uses `@domql/element` create directly — lightweight, no smbls bootstrap. Good for individual components
|
|
430
439
|
- `render()` uses the full `smbls/src/createDomql.js` pipeline — handles routing, designSystem initialization, uikit defaults, the works. Needed for complete apps
|
|
431
440
|
- The smbls source is bundled with esbuild (cached after first call) because the monorepo uses extensionless/directory imports that Node.js ESM can't resolve natively. The esbuild plugin patches SSR-incompatible code (window references, createRequire, circular imports) and stubs out browser-only packages
|
|
432
|
-
- `render()` sets `globalThis.document` and `globalThis.
|
|
441
|
+
- `render()` sets `globalThis.document`, `globalThis.location`, `globalThis.localStorage` and `globalThis.sessionStorage` before each render to match the linkedom virtual env, then restores them after (or at its timeout). This allows the bundled smbls code (which reads `window = globalThis`) to work in SSR. Renders run one at a time per process, so no two hold these globals at once
|
|
433
442
|
- `hydrate.js` is browser-only code (no linkedom dependency) — it's exported separately via `@symbo.ls/brender/hydrate`
|
|
434
443
|
- `createEnv()` sets `globalThis.window/document/Node/HTMLElement` because `@domql/utils` `isDOMNode` uses `instanceof` checks against global constructors
|
|
435
444
|
- CSS is extracted from the atomic CSS engine's generated rules
|
package/dist/esm/env.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseHTML as
|
|
1
|
+
import{parseHTML as h}from"linkedom";const a=new WeakSet;class u{static[Symbol.hasInstance](t){return a.has(t)}}const b=o=>{let t=Object.getPrototypeOf(o.createElement("select")),e;for(;t&&!(e=Object.getOwnPropertyDescriptor(t,"value"));)t=Object.getPrototypeOf(t);!t||!e||!e.get||e.set||Object.defineProperty(t,"value",{configurable:!0,enumerable:e.enumerable,get:e.get,set(l){const r=l==null?"":String(l);let s=!1;for(const n of this.querySelectorAll("option")){const c=n.hasAttribute("value")?n.getAttribute("value"):n.textContent;!s&&c===r?(n.setAttribute("selected",""),s=!0):n.removeAttribute("selected")}}})},i=()=>{const o={};return{getItem:t=>o[t]??null,setItem:(t,e)=>{o[t]=String(e)},removeItem:t=>{delete o[t]},clear:()=>{for(const t in o)delete o[t]},get length(){return Object.keys(o).length},key:t=>Object.keys(o)[t]??null}},m=(o="<!DOCTYPE html><html><head></head><body></body></html>")=>{const t={location:{pathname:"/",search:"",hash:"",origin:"http://localhost",protocol:"http:",host:"localhost",hostname:"localhost",port:"",href:"http://localhost/"},localStorage:i(),sessionStorage:i(),history:{pushState:()=>{},replaceState:()=>{},state:null}},{window:e,document:l}=h(o,t);return e.requestAnimationFrame||(e.requestAnimationFrame=r=>{const s=setTimeout(r,0);return s?.unref&&s.unref(),s}),e.cancelAnimationFrame||(e.cancelAnimationFrame=r=>clearTimeout(r)),e.URL||(e.URL=URL),e.scrollTo||(e.scrollTo=()=>{}),globalThis.localStorage=t.localStorage,globalThis.sessionStorage=t.sessionStorage,globalThis.history||(globalThis.history=t.history),b(l),e.MutationObserver||(e.MutationObserver=class{constructor(){}observe(){}disconnect(){}takeRecords(){return[]}}),globalThis.window=e,globalThis.document=l,globalThis.Node=e.Node||globalThis.Node,globalThis.Element=e.Element||globalThis.Element,globalThis.HTMLElement=e.HTMLElement||globalThis.HTMLElement,globalThis.MutationObserver=e.MutationObserver,a.add(e),e.constructor&&e.constructor!==Object?globalThis.Window=e.constructor:(typeof globalThis.Window!="function"||globalThis.Window===Object)&&(globalThis.Window=u),{window:e,document:l}};export{m as createEnv};
|
package/dist/esm/render.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import{resolve as m,join as Ce,dirname as x}from"path";import{existsSync as b,unlinkSync as De,readFileSync as W,realpathSync as Ie}from"fs";import{resolvePackageEntry as Oe}from"./packageEntry.js";import{tmpdir as Me}from"os";import{randomBytes as Pe}from"crypto";import{createRequire as oe}from"module";import{pathToFileURL as Fe}from"url";import{createEnv as ue}from"./env.js";import{resetKeys as de,assignKeys as me,mapKeysToElements as ge}from"./keys.js";import{extractMetadata as se,generateHeadHtml as Be}from"./metadata.js";import"./hydrate.js";import{composeIntoShell as Ke}from"./shell.js";import{fetchSSRTranslations as Ne}from"./prefetch.js";import{createEarlySeedScript as Ge}from"@symbo.ls/fetch";import{headerEntries as He}from"@symbo.ls/fetch/rest";import{brenderRegistryBlock as Je,escapeRegExp as We,jsonForScript as Ue,polyglotFallbackLang as U}from"@symbo.ls/utils";let Z=null;const ze=async()=>{if(Z)return Z;try{return Z=(await import("@symbo.ls/funcql")).funcqlPlugin,Z}catch{return null}};import"linkedom";import{createEarlySeedBlock as Ye}from"@symbo.ls/fetch";import{reset as Ve}from"@symbo.ls/css";const Q=(e,t)=>{if(!e||!t)return;if(e[t]!==void 0)return e[t];const o=t.split(".");let s=e;for(const r of o){if(s==null||typeof s!="object")return;s=s[r]}return s},ne=(e,t)=>U(e?.lang,t),he=(e,t,o,s)=>{if(o?.translations){const n=o.translations[t];if(n){const a=Q(n,e);if(a!==void 0)return a}}if(s?.translations){const n=s.translations[t];if(n){const a=Q(n,e);if(a!==void 0)return a}}const r=U(void 0,o);if(r!==t&&o?.translations){const n=o.translations[r];if(n){const a=Q(n,e);if(a!==void 0)return a}}},Xe=function(e,t){if(!e)return"";const o=this?.context,s=o?.polyglot,r=this?.state?.root||o?.state?.root,n=he(e,t||ne(r,s),s,r);if(n!==void 0)return n;const a=ie(ce);return s&&a&&a.labelKeys.add(e),e},ye=function(){return ne(this?.state?.root,this?.context?.polyglot)},_=(e,t=new WeakMap)=>{if(e===null||typeof e!="object")return e;if(t.has(e))return t.get(e);if(Array.isArray(e)){const s=[];t.set(e,s);for(const r of e)s.push(typeof r=="object"&&r!==null?_(r,t):r);return s}const o={};t.set(e,o);for(const s of Object.keys(e)){const r=e[s];o[s]=typeof r=="object"&&r!==null?_(r,t):r}return o};let $=null;try{import.meta.url&&($=oe(import.meta.url))}catch{}const Ze=()=>{if(!import.meta.url||!$)return{isMonorepo:!1,monorepoRoot:null,resolvePackage:l=>l};const e=Ie(new URL(".",import.meta.url).pathname),t=m(e,"../..");if(b(m(t,"packages","smbls","src","createDomql.js")))return{isMonorepo:!0,monorepoRoot:t};let s;try{const l=$.resolve("smbls/package.json");s=x(l)}catch{let l=e;for(;l!==x(l);){const y=m(l,"node_modules","smbls");if(b(m(y,"package.json"))){s=y;break}l=x(l)}}let r=$;if(s)try{r=oe(m(s,"package.json"))}catch{}let n,a=process.cwd();for(;a!==x(a);){if(b(m(a,"package.json"))){n=a;break}a=x(a)}let u=r;if(n)try{u=oe(m(n,"package.json"))}catch{}return{isMonorepo:!1,smblsRoot:s,brenderDir:e,projectRoot:n,_smblsRequire:r,_projectRequire:u}},P=(e,t)=>{const o=e.isMonorepo?[$]:[e._smblsRequire,e._projectRequire,$].filter(Boolean);for(const s of o)try{return s.resolve(t)}catch{}return null},Zt=(e,t,...o)=>{if(e.isMonorepo)return m(e.monorepoRoot,"packages",t,...o);const s=P(e,`${t}/package.json`);return s?m(x(s),...o):null},Qt=(e,t,...o)=>{if(e.isMonorepo)return m(e.monorepoRoot,"plugins",t,...o);const s=P(e,`@symbo.ls/${t}/package.json`);return s?m(x(s),...o):null},re=(e,t,...o)=>{if(e.isMonorepo){for(const r of["packages","plugins"]){const n=m(e.monorepoRoot,r,t,...o);if(b(n))return n}return null}const s=P(e,`@symbo.ls/${t}/package.json`);return s?m(x(s),...o):null},be=(e,t,...o)=>{if(e.isMonorepo)return m(e.monorepoRoot,"packages","domql","packages",t,...o);const s=P(e,`@symbo.ls/${t}/package.json`);return s?m(x(s),...o):null};let z=null;const Se="smbls/ssr",Qe=async()=>{const e=await import("smbls/ssr");if(typeof e?.createDomqlElement!="function")throw new Error("smbls/ssr does not export createDomqlElement");return e},et=async()=>{if(z)return z;const e=Ze();let t;if($&&e.isMonorepo)t=m(e.monorepoRoot,"packages","smbls","src","createDomql.js");else{let f;try{return z=await Qe(),z}catch(c){f=c}if(!$)throw new Error(`brender: ${Se} is not available in this bundled runtime (${f.message}). Bundle smbls \u2265 the release that adds the smbls/ssr export alongside @symbo.ls/brender.`);const i=e.smblsRoot&&m(e.smblsRoot,"src","createDomql.js");if(i&&b(i))t=i;else throw new Error(`brender: cannot load ${Se} (${f.message}). Server rendering needs smbls installed in this project, at a release that exports smbls/ssr`+(e.smblsRoot?` (found smbls at ${e.smblsRoot}).`:" (no smbls package found)."))}const o=await import("esbuild"),s=Ce(Me(),`br_createDomql_${Pe(6).toString("hex")}.mjs`),r=Oe,n=e.isMonorepo?await import(Fe(m(e.monorepoRoot,"scripts","vite-workspace-source.mjs")).href):null,a=n?new Set(n.workspacePackageRoots().map(f=>n.workspacePackageAt(f).name)):null,u=/^(?:smbls(?:\/.*)?|domql|css-in-props(?:\/.*)?|@symbo\.ls\/[^/]+)$/,l={name:"workspace-resolve",setup(f){n&&(f.onResolve({filter:/^[^./]/},i=>{if(u.test(i.path))return;const c=i.path.split("/"),d=i.path.startsWith("@")?c.slice(0,2).join("/"):c[0];if(!a.has(d))return;const p=n.resolveWorkspaceSource(i.path,i.importer||t);return p?{path:p}:{errors:[{text:`"${i.path}" names no source file in the workspace`}]}}),f.onLoad({filter:/[\\/]dist[\\/]/},i=>n.isWorkspaceDist(i.path)?{errors:[{text:`${i.path} is a BUILD of a workspace package \u2014 the render bundles source`}]}:void 0)),f.onResolve({filter:/^smbls/},i=>{const c=i.path.replace(/^smbls\/?/,""),d=e.isMonorepo?m(e.monorepoRoot,"packages","smbls"):e.smblsRoot;if(!d)return;if(!c){const g=r(d);return g?{path:g}:void 0}const p=m(d,c);if(b(p))return{path:p};if(b(p+".js"))return{path:p+".js"};const h=m(p,"index.js");if(b(h))return{path:h}}),f.onResolve({filter:/^domql$/},i=>{if(e.isMonorepo){const c=m(e.monorepoRoot,"packages","domql","src","index.js");if(b(c))return{path:c};const d=m(e.monorepoRoot,"packages","domql","index.js");if(b(d))return{path:d}}else try{const c=_require.resolve("domql/package.json"),d=r(x(c));if(d)return{path:d}}catch{}}),f.onResolve({filter:/^@symbo\.ls\//},i=>{const c=i.path.replace("@symbo.ls/","");if(c==="sync")return{path:"sync-stub",namespace:"brender-stub"};if(e.isMonorepo){for(const p of["packages","plugins"]){const h=m(e.monorepoRoot,p,c,"src","index.js");if(b(h))return{path:h};const g=m(e.monorepoRoot,p,c,"index.js");if(b(g))return{path:g}}const d=m(e.monorepoRoot,"packages","default-config","blank","index.js");if(c==="default-config"&&b(d))return{path:d}}else{const d=re(e,c,"src","index.js");if(d&&b(d))return{path:d};const p=re(e,c,"index.js");if(p&&b(p))return{path:p};if(c==="default-config"){const h=re(e,"default-config","blank","index.js");if(h&&b(h))return{path:h}}}}),f.onResolve({filter:/^@domql\//},i=>{const c=i.path.replace("@symbo.ls/","");if(e.isMonorepo){const d=m(e.monorepoRoot,"packages","domql","packages",c,"src","index.js");if(b(d))return{path:d};const p=m(e.monorepoRoot,"packages","domql","packages",c,"index.js");if(b(p))return{path:p}}else{const d=be(e,c,"src","index.js");if(d&&b(d))return{path:d};const p=be(e,c,"index.js");if(p&&b(p))return{path:p}}}),f.onResolve({filter:/^css-in-props/},i=>{let c;if(e.isMonorepo)c=m(e.monorepoRoot,"packages","css-in-props");else{const h=P(e,"css-in-props/package.json");h&&(c=x(h))}if(!c)return;const d=i.path.replace(/^css-in-props\/?/,"");if(d){const h=m(c,d),g=m(h,"index.js");if(b(g))return{path:g};if(b(h+".js"))return{path:h+".js"};if(b(h))return{path:h}}const p=r(c);if(p)return{path:p}}),f.onResolve({filter:/^@emotion\//},i=>{let c;if(e.isMonorepo)c=m(e.monorepoRoot,"node_modules",i.path);else{const d=P(e,`${i.path}/package.json`);if(!d)return;c=x(d)}if(b(c)){const d=m(c,"package.json");if(b(d))try{const p=JSON.parse(W(d,"utf8")),h=p.module||p.main||"dist/emotion-css.esm.js";return{path:m(c,h)}}catch{}return{path:c}}}),f.onResolve({filter:/\.json$/},i=>{if(i.resolveDir){const c=m(i.resolveDir,i.path);if(b(c))return{path:c}}}),f.onLoad({filter:/smbls\/.*options\.js$/},async i=>{if(!i.path.includes("smbls/")||!i.path.endsWith("options.js"))return;let c=W(i.path,"utf8");return c=c.replace(/import\s*\{[^}]*version[^}]*\}\s*from\s*['"][^'"]*package\.json['"][^;\n]*/,"const version = '0.0.0'"),c=c.replace(/import\s*\{[^}]*createRequire[^}]*\}\s*from\s*['"]module['"][^;\n]*/g,"// createRequire removed for brender build"),{contents:c,loader:"js"}}),f.onLoad({filter:/smbls\/.*init\.js$/},async i=>{if(!i.path.includes("smbls/")||!i.path.endsWith("init.js"))return;let c=W(i.path,"utf8");return c=c.replace(/import\s*\{[^}]*createRequire[^}]*\}\s*from\s*['"]module['"][^;\n]*/g,"// createRequire removed for brender build"),{contents:c,loader:"js"}}),f.onLoad({filter:/.*/,namespace:"brender-stub"},()=>({contents:"export const SyncComponent = {}; export const Inspect = {}; export const Notifications = {}; export const syncPlugin = () => ({}); export const notificationsPlugin = () => ({}); export const seedRawDesignSystem = () => {}; export const inspectPlugin = () => ({}); export default {}",loader:"js"})),f.onLoad({filter:/smbls\/src\/router\.js$/},async i=>{let c=W(i.path,"utf8");return c=c.replace(/import\s*\{\s*Link\s*\}\s*from\s*['"]smbls['"]/,"const Link = { tag: 'a', attr: { href: (el) => el.href } }"),{contents:c,loader:"js"}}),f.onLoad({filter:/fetchOnCreate\.js$/},async i=>{if(!i.path.includes("smbls/"))return;let c=W(i.path,"utf8");return c=c.replace(/window\s*&&\s*window\.location\s*\?\s*window\.location\.host\.includes/g,"window && window.location && window.location.host ? window.location.host.includes"),{contents:c,loader:"js"}})}};await o.build({entryPoints:[t],bundle:!0,format:"esm",platform:"node",outfile:s,write:!0,logLevel:"warning",plugins:[l],nodePaths:e.isMonorepo?[m(e.monorepoRoot,"node_modules")]:[...e.smblsRoot?[m(e.smblsRoot,"node_modules")]:[],...e.projectRoot?[m(e.projectRoot,"node_modules")]:[],...e.smblsRoot?[m(e.smblsRoot,"..","..","node_modules")]:[]].filter(f=>b(f)),supported:{"import-attributes":!1},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","color-contrast-checker","linkedom"]});const y=await import(`file://${s}`);if(process.env.BRENDER_DEBUG)console.log("[brender] Bundle saved:",s);else try{De(s)}catch{}return z=y,y},tt={Box:{},Focusable:{},Block:{display:"block"},Inline:{display:"inline"},Flex:{display:"flex"},InlineFlex:{display:"inline-flex"},Grid:{display:"grid"},InlineGrid:{display:"inline-grid"},Link:{tag:"a",attr:{href:e=>e.href,target:e=>e.target,rel:e=>e.rel}},A:{extends:"Link"},RouteLink:{extends:"Link"},Img:{tag:"img",attr:{src:e=>{let t=e.src;return typeof t=="string"&&t.includes("{{")&&(t=e.call("replaceLiteralsWithObjectFields",t,e.state)),t},alt:e=>e.alt,loading:e=>e.loading}},Image:{extends:"Img"},Button:{tag:"button"},FocusableComponent:{tag:"button"},Form:{tag:"form"},Input:{tag:"input"},TextArea:{tag:"textarea"},Textarea:{tag:"textarea"},Select:{tag:"select"},Label:{tag:"label"},Iframe:{tag:"iframe"},Video:{tag:"video"},Audio:{tag:"audio"},Canvas:{tag:"canvas"},Span:{tag:"span"},P:{tag:"p"},H1:{tag:"h1"},H2:{tag:"h2"},H3:{tag:"h3"},H4:{tag:"h4"},H5:{tag:"h5"},H6:{tag:"h6"},Svg:{tag:"svg",attr:{xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink"}},Text:{tag:"span"}},ot=(e,t)=>{const o={};return ke(e,(s,r)=>{if(!r.__brKey)return;const n=t(s);o[n===""?"__root":n]=r.__brKey}),o},ke=(e,t)=>{const o=s=>{const r=s&&s.__ref;if(r&&(t(s,r),r.__children))for(const n of r.__children)o(s[n])};o(e)},st=new Set(["app","config","pages","components","state","snippets","designSystem","dependencies","functions","methods","files","sharedLibraries"]);let F=0;const Re=Symbol.for("brender.fetchTracked"),we=()=>F,nt=256*1024,rt=1024*1024,ct=new Set(["accept-language","accept"]),xe={renders:0,record:null},it=50,ce={renders:0,record:null},je=(e,t)=>{e.renders++,e.renders===1?e.record=t():e.record&&(e.record.tainted=!0);const o=e.record;return{record:o,close:()=>{e.renders--,e.record===o&&(e.record=null)}}},ie=e=>e.record&&!e.record.tainted?e.record:null,at=e=>{try{const t=new URL(e);return t.origin+t.pathname}catch{return String(e).split("?")[0]}},ae=(e,t,o)=>{!e||e.tainted||e.failures.length>=it||e.failures.push({url:at(t),reason:o})},ve=e=>e&&typeof e=="object"&&typeof e.url=="string"?e.url:String(e),lt=e=>{const[t,o]=e,s=t&&typeof t=="object"&&typeof t.url=="string",r=ve(t),n=String(o&&o.method||s&&t.method||"GET").toUpperCase(),a={},u=l=>{if(!l)return;const y=!Array.isArray(l)&&typeof l.entries!="function";for(const[f,i]of He(l))y&&i==null||(a[String(f).toLowerCase()]=String(i))};return s&&u(t.headers),o&&u(o.headers),{url:r,method:n,headers:a}},ft=(e,t,o,s)=>{if(!o||!o.ok||typeof o.clone!="function")return;const r=o.headers&&o.headers.get&&o.headers.get("content-type")||"";if(!/json|^text\//i.test(r))return;let n;try{n=o.clone()}catch{return}s(n.text().then(a=>{e.tainted||a.length>nt||e.bytes+a.length>rt||(e.bytes+=a.length,e.entries.push({url:t.url,lang:t.headers["accept-language"]||"",status:o.status,type:r,body:a}))},()=>{}))},pt=()=>{const e=globalThis.fetch;if(typeof e!="function"||e[Re])return;const t=function(...o){F++;let s=!1;const r=()=>{s||(s=!0,F--)},n=i=>{F++,i.then(()=>{F--},()=>{F--})},a=ie(xe),u=ie(ce);let l=null;if(a)try{l=lt(o),(l.method!=="GET"||!/^https?:/i.test(l.url)||Object.keys(l.headers).some(i=>!ct.has(i)))&&(l=null)}catch{l=null}const y=()=>ve(o[0]);let f;try{f=e.apply(this,o)}catch(i){throw ae(u,y(),i&&i.message||"failed"),r(),i}return f&&typeof f.then=="function"?f.then(i=>{l&&ft(a,l,i,n),u&&i&&i.ok===!1&&ae(u,y(),"HTTP "+i.status),r()},i=>{ae(u,y(),i&&i.message||"network error"),r()}):r(),f};t[Re]=!0,globalThis.fetch=t},eo=({brRegistry:e,fetchSeed:t,hydrateFlag:o=!0}={})=>{if(!e||!Object.keys(e).length)return"";const s=Ue(e);return o?`<script>window.__BR_REGISTRY__=${s};window.__BRENDER_HYDRATE__=true<\/script>`+(t&&t.length?`
|
|
2
|
-
`+
|
|
1
|
+
import{resolve as u,join as Fe,dirname as j}from"path";import{existsSync as b,unlinkSync as Be,readFileSync as J,realpathSync as Ge}from"fs";import{resolvePackageEntry as Ke}from"./packageEntry.js";import{tmpdir as Ne}from"os";import{randomBytes as He}from"crypto";import{createRequire as oe}from"module";import{pathToFileURL as Ue}from"url";import{createEnv as ue}from"./env.js";import{resetKeys as de,assignKeys as me,mapKeysToElements as ge}from"./keys.js";import{extractMetadata as ne,generateHeadHtml as We}from"./metadata.js";import"./hydrate.js";import{composeIntoShell as Je}from"./shell.js";import{fetchSSRTranslations as ze}from"./prefetch.js";import{createEarlySeedScript as Ye}from"@symbo.ls/fetch";import{headerEntries as Xe}from"@symbo.ls/fetch/rest";import{brenderRegistryBlock as Ve,escapeRegExp as Qe,jsonForScript as Ze,polyglotFallbackLang as z,polyglotLangStorageKey as et,polyglotAddressLang as tt,polyglotStartupScript as ot}from"@symbo.ls/utils";let Y=null;const nt=async()=>{if(Y)return Y;try{return Y=(await import("@symbo.ls/funcql")).funcqlPlugin,Y}catch{return null}};import"linkedom";import{createEarlySeedBlock as st}from"@symbo.ls/fetch";import{reset as rt}from"@symbo.ls/css";const X=(e,t)=>{if(!e||!t)return;if(e[t]!==void 0)return e[t];const o=t.split(".");let n=e;for(const s of o){if(n==null||typeof n!="object")return;n=n[s]}return n},se=(e,t)=>z(e?.lang,t),he=(e,t,o,n)=>{if(o?.translations){const r=o.translations[t];if(r){const i=X(r,e);if(i!==void 0)return i}}if(n?.translations){const r=n.translations[t];if(r){const i=X(r,e);if(i!==void 0)return i}}const s=z(void 0,o);if(s!==t&&o?.translations){const r=o.translations[s];if(r){const i=X(r,e);if(i!==void 0)return i}}},ct=function(e,t){if(!e)return"";const o=this?.context,n=o?.polyglot,s=this?.state?.root||o?.state?.root,r=he(e,t||se(s,n),n,s);if(r!==void 0)return r;const i=ie(ce);return n&&i&&i.labelKeys.add(e),e},ye=function(){return se(this?.state?.root,this?.context?.polyglot)},L=(e,t=new WeakMap)=>{if(e===null||typeof e!="object")return e;if(t.has(e))return t.get(e);if(Array.isArray(e)){const n=[];t.set(e,n);for(const s of e)n.push(typeof s=="object"&&s!==null?L(s,t):s);return n}const o={};t.set(e,o);for(const n of Object.keys(e)){const s=e[n];o[n]=typeof s=="object"&&s!==null?L(s,t):s}return o};let A=null;try{import.meta.url&&(A=oe(import.meta.url))}catch{}const it=()=>{if(!import.meta.url||!A)return{isMonorepo:!1,monorepoRoot:null,resolvePackage:l=>l};const e=Ge(new URL(".",import.meta.url).pathname),t=u(e,"../..");if(b(u(t,"packages","smbls","src","createDomql.js")))return{isMonorepo:!0,monorepoRoot:t};let n;try{const l=A.resolve("smbls/package.json");n=j(l)}catch{let l=e;for(;l!==j(l);){const g=u(l,"node_modules","smbls");if(b(u(g,"package.json"))){n=g;break}l=j(l)}}let s=A;if(n)try{s=oe(u(n,"package.json"))}catch{}let r,i=process.cwd();for(;i!==j(i);){if(b(u(i,"package.json"))){r=i;break}i=j(i)}let d=s;if(r)try{d=oe(u(r,"package.json"))}catch{}return{isMonorepo:!1,smblsRoot:n,brenderDir:e,projectRoot:r,_smblsRequire:s,_projectRequire:d}},O=(e,t)=>{const o=e.isMonorepo?[A]:[e._smblsRequire,e._projectRequire,A].filter(Boolean);for(const n of o)try{return n.resolve(t)}catch{}return null},yo=(e,t,...o)=>{if(e.isMonorepo)return u(e.monorepoRoot,"packages",t,...o);const n=O(e,`${t}/package.json`);return n?u(j(n),...o):null},bo=(e,t,...o)=>{if(e.isMonorepo)return u(e.monorepoRoot,"plugins",t,...o);const n=O(e,`@symbo.ls/${t}/package.json`);return n?u(j(n),...o):null},re=(e,t,...o)=>{if(e.isMonorepo){for(const s of["packages","plugins"]){const r=u(e.monorepoRoot,s,t,...o);if(b(r))return r}return null}const n=O(e,`@symbo.ls/${t}/package.json`);return n?u(j(n),...o):null},be=(e,t,...o)=>{if(e.isMonorepo)return u(e.monorepoRoot,"packages","domql","packages",t,...o);const n=O(e,`@symbo.ls/${t}/package.json`);return n?u(j(n),...o):null};let V=null;const Se="smbls/ssr",at=async()=>{const e=await import("smbls/ssr");if(typeof e?.createDomqlElement!="function")throw new Error("smbls/ssr does not export createDomqlElement");return e},ke=()=>(V||(V=lt().catch(e=>{throw V=null,e})),V),lt=async()=>{const e=it();let t;if(A&&e.isMonorepo)t=u(e.monorepoRoot,"packages","smbls","src","createDomql.js");else{let f;try{return await at()}catch(a){f=a}if(!A)throw new Error(`brender: ${Se} is not available in this bundled runtime (${f.message}). Bundle smbls \u2265 the release that adds the smbls/ssr export alongside @symbo.ls/brender.`);const c=e.smblsRoot&&u(e.smblsRoot,"src","createDomql.js");if(c&&b(c))t=c;else throw new Error(`brender: cannot load ${Se} (${f.message}). Server rendering needs smbls installed in this project, at a release that exports smbls/ssr`+(e.smblsRoot?` (found smbls at ${e.smblsRoot}).`:" (no smbls package found)."))}const o=await import("esbuild"),n=Fe(Ne(),`br_createDomql_${He(6).toString("hex")}.mjs`),s=Ke,r=e.isMonorepo?await import(Ue(u(e.monorepoRoot,"scripts","vite-workspace-source.mjs")).href):null,i=r?new Set(r.workspacePackageRoots().map(f=>r.workspacePackageAt(f).name)):null,d=/^(?:smbls(?:\/.*)?|domql|css-in-props(?:\/.*)?|@symbo\.ls\/[^/]+)$/,l={name:"workspace-resolve",setup(f){r&&(f.onResolve({filter:/^[^./]/},c=>{if(d.test(c.path))return;const a=c.path.split("/"),p=c.path.startsWith("@")?a.slice(0,2).join("/"):a[0];if(!i.has(p))return;const m=r.resolveWorkspaceSource(c.path,c.importer||t);return m?{path:m}:{errors:[{text:`"${c.path}" names no source file in the workspace`}]}}),f.onLoad({filter:/[\\/]dist[\\/]/},c=>r.isWorkspaceDist(c.path)?{errors:[{text:`${c.path} is a BUILD of a workspace package \u2014 the render bundles source`}]}:void 0)),f.onResolve({filter:/^smbls/},c=>{const a=c.path.replace(/^smbls\/?/,""),p=e.isMonorepo?u(e.monorepoRoot,"packages","smbls"):e.smblsRoot;if(!p)return;if(!a){const S=s(p);return S?{path:S}:void 0}const m=u(p,a);if(b(m))return{path:m};if(b(m+".js"))return{path:m+".js"};const y=u(m,"index.js");if(b(y))return{path:y}}),f.onResolve({filter:/^domql$/},c=>{if(e.isMonorepo){const a=u(e.monorepoRoot,"packages","domql","src","index.js");if(b(a))return{path:a};const p=u(e.monorepoRoot,"packages","domql","index.js");if(b(p))return{path:p}}else try{const a=_require.resolve("domql/package.json"),p=s(j(a));if(p)return{path:p}}catch{}}),f.onResolve({filter:/^@symbo\.ls\//},c=>{const a=c.path.replace("@symbo.ls/","");if(a==="sync")return{path:"sync-stub",namespace:"brender-stub"};if(e.isMonorepo){for(const m of["packages","plugins"]){const y=u(e.monorepoRoot,m,a,"src","index.js");if(b(y))return{path:y};const S=u(e.monorepoRoot,m,a,"index.js");if(b(S))return{path:S}}const p=u(e.monorepoRoot,"packages","default-config","blank","index.js");if(a==="default-config"&&b(p))return{path:p}}else{const p=re(e,a,"src","index.js");if(p&&b(p))return{path:p};const m=re(e,a,"index.js");if(m&&b(m))return{path:m};if(a==="default-config"){const y=re(e,"default-config","blank","index.js");if(y&&b(y))return{path:y}}}}),f.onResolve({filter:/^@domql\//},c=>{const a=c.path.replace("@symbo.ls/","");if(e.isMonorepo){const p=u(e.monorepoRoot,"packages","domql","packages",a,"src","index.js");if(b(p))return{path:p};const m=u(e.monorepoRoot,"packages","domql","packages",a,"index.js");if(b(m))return{path:m}}else{const p=be(e,a,"src","index.js");if(p&&b(p))return{path:p};const m=be(e,a,"index.js");if(m&&b(m))return{path:m}}}),f.onResolve({filter:/^css-in-props/},c=>{let a;if(e.isMonorepo)a=u(e.monorepoRoot,"packages","css-in-props");else{const y=O(e,"css-in-props/package.json");y&&(a=j(y))}if(!a)return;const p=c.path.replace(/^css-in-props\/?/,"");if(p){const y=u(a,p),S=u(y,"index.js");if(b(S))return{path:S};if(b(y+".js"))return{path:y+".js"};if(b(y))return{path:y}}const m=s(a);if(m)return{path:m}}),f.onResolve({filter:/^@emotion\//},c=>{let a;if(e.isMonorepo)a=u(e.monorepoRoot,"node_modules",c.path);else{const p=O(e,`${c.path}/package.json`);if(!p)return;a=j(p)}if(b(a)){const p=u(a,"package.json");if(b(p))try{const m=JSON.parse(J(p,"utf8")),y=m.module||m.main||"dist/emotion-css.esm.js";return{path:u(a,y)}}catch{}return{path:a}}}),f.onResolve({filter:/\.json$/},c=>{if(c.resolveDir){const a=u(c.resolveDir,c.path);if(b(a))return{path:a}}}),f.onLoad({filter:/smbls\/.*options\.js$/},async c=>{if(!c.path.includes("smbls/")||!c.path.endsWith("options.js"))return;let a=J(c.path,"utf8");return a=a.replace(/import\s*\{[^}]*version[^}]*\}\s*from\s*['"][^'"]*package\.json['"][^;\n]*/,"const version = '0.0.0'"),a=a.replace(/import\s*\{[^}]*createRequire[^}]*\}\s*from\s*['"]module['"][^;\n]*/g,"// createRequire removed for brender build"),{contents:a,loader:"js"}}),f.onLoad({filter:/smbls\/.*init\.js$/},async c=>{if(!c.path.includes("smbls/")||!c.path.endsWith("init.js"))return;let a=J(c.path,"utf8");return a=a.replace(/import\s*\{[^}]*createRequire[^}]*\}\s*from\s*['"]module['"][^;\n]*/g,"// createRequire removed for brender build"),{contents:a,loader:"js"}}),f.onLoad({filter:/.*/,namespace:"brender-stub"},()=>({contents:"export const SyncComponent = {}; export const Inspect = {}; export const Notifications = {}; export const syncPlugin = () => ({}); export const notificationsPlugin = () => ({}); export const seedRawDesignSystem = () => {}; export const inspectPlugin = () => ({}); export default {}",loader:"js"})),f.onLoad({filter:/smbls\/src\/router\.js$/},async c=>{let a=J(c.path,"utf8");return a=a.replace(/import\s*\{\s*Link\s*\}\s*from\s*['"]smbls['"]/,"const Link = { tag: 'a', attr: { href: (el) => el.href } }"),{contents:a,loader:"js"}}),f.onLoad({filter:/fetchOnCreate\.js$/},async c=>{if(!c.path.includes("smbls/"))return;let a=J(c.path,"utf8");return a=a.replace(/window\s*&&\s*window\.location\s*\?\s*window\.location\.host\.includes/g,"window && window.location && window.location.host ? window.location.host.includes"),{contents:a,loader:"js"}})}};await o.build({entryPoints:[t],bundle:!0,format:"esm",platform:"node",outfile:n,write:!0,logLevel:"warning",plugins:[l],nodePaths:e.isMonorepo?[u(e.monorepoRoot,"node_modules")]:[...e.smblsRoot?[u(e.smblsRoot,"node_modules")]:[],...e.projectRoot?[u(e.projectRoot,"node_modules")]:[],...e.smblsRoot?[u(e.smblsRoot,"..","..","node_modules")]:[]].filter(f=>b(f)),supported:{"import-attributes":!1},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","color-contrast-checker","linkedom"]});const g=await import(`file://${n}`);if(process.env.BRENDER_DEBUG)console.log("[brender] Bundle saved:",n);else try{Be(n)}catch{}return g},ft={Box:{},Focusable:{},Block:{display:"block"},Inline:{display:"inline"},Flex:{display:"flex"},InlineFlex:{display:"inline-flex"},Grid:{display:"grid"},InlineGrid:{display:"inline-grid"},Link:{tag:"a",attr:{href:e=>e.href,target:e=>e.target,rel:e=>e.rel}},A:{extends:"Link"},RouteLink:{extends:"Link"},Img:{tag:"img",attr:{src:e=>{let t=e.src;return typeof t=="string"&&t.includes("{{")&&(t=e.call("replaceLiteralsWithObjectFields",t,e.state)),t},alt:e=>e.alt,loading:e=>e.loading}},Image:{extends:"Img"},Button:{tag:"button"},FocusableComponent:{tag:"button"},Form:{tag:"form"},Input:{tag:"input"},TextArea:{tag:"textarea"},Textarea:{tag:"textarea"},Select:{tag:"select"},Label:{tag:"label"},Iframe:{tag:"iframe"},Video:{tag:"video"},Audio:{tag:"audio"},Canvas:{tag:"canvas"},Span:{tag:"span"},P:{tag:"p"},H1:{tag:"h1"},H2:{tag:"h2"},H3:{tag:"h3"},H4:{tag:"h4"},H5:{tag:"h5"},H6:{tag:"h6"},Svg:{tag:"svg",attr:{xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink"}},Text:{tag:"span"}},pt=(e,t)=>{const o={};return Re(e,(n,s)=>{if(!s.__brKey)return;const r=t(n);o[r===""?"__root":r]=s.__brKey}),o},Re=(e,t)=>{const o=n=>{const s=n&&n.__ref;if(s&&(t(n,s),s.__children))for(const r of s.__children)o(n[r])};o(e)},ut=new Set(["app","config","pages","components","state","snippets","designSystem","dependencies","functions","methods","files","sharedLibraries"]);let P=0;const we=Symbol.for("brender.fetchTracked"),xe=()=>P,dt=256*1024,mt=1024*1024,gt=new Set(["accept-language","accept"]),je={renders:0,record:null},ht=50,ce={renders:0,record:null},_e=(e,t)=>{e.renders++,e.renders===1?e.record=t():e.record&&(e.record.tainted=!0);const o=e.record;return{record:o,close:()=>{e.renders--,e.record===o&&(e.record=null)}}},ie=e=>e.record&&!e.record.tainted?e.record:null,yt=e=>{try{const t=new URL(e);return t.origin+t.pathname}catch{return String(e).split("?")[0]}},ae=(e,t,o)=>{!e||e.tainted||e.failures.length>=ht||e.failures.push({url:yt(t),reason:o})},Ee=e=>e&&typeof e=="object"&&typeof e.url=="string"?e.url:String(e),bt=e=>{const[t,o]=e,n=t&&typeof t=="object"&&typeof t.url=="string",s=Ee(t),r=String(o&&o.method||n&&t.method||"GET").toUpperCase(),i={},d=l=>{if(!l)return;const g=!Array.isArray(l)&&typeof l.entries!="function";for(const[f,c]of Xe(l))g&&c==null||(i[String(f).toLowerCase()]=String(c))};return n&&d(t.headers),o&&d(o.headers),{url:s,method:r,headers:i}},St=(e,t,o,n)=>{if(!o||!o.ok||typeof o.clone!="function")return;const s=o.headers&&o.headers.get&&o.headers.get("content-type")||"";if(!/json|^text\//i.test(s))return;let r;try{r=o.clone()}catch{return}n(r.text().then(i=>{e.tainted||i.length>dt||e.bytes+i.length>mt||(e.bytes+=i.length,e.entries.push({url:t.url,lang:t.headers["accept-language"]||"",status:o.status,type:s,body:i}))},()=>{}))},kt=()=>{const e=globalThis.fetch;if(typeof e!="function"||e[we])return;const t=function(...o){P++;let n=!1;const s=()=>{n||(n=!0,P--)},r=c=>{P++,c.then(()=>{P--},()=>{P--})},i=ie(je),d=ie(ce);let l=null;if(i)try{l=bt(o),(l.method!=="GET"||!/^https?:/i.test(l.url)||Object.keys(l.headers).some(c=>!gt.has(c)))&&(l=null)}catch{l=null}const g=()=>Ee(o[0]);let f;try{f=e.apply(this,o)}catch(c){throw ae(d,g(),c&&c.message||"failed"),s(),c}return f&&typeof f.then=="function"?f.then(c=>{l&&St(i,l,c,r),d&&c&&c.ok===!1&&ae(d,g(),"HTTP "+c.status),s()},c=>{ae(d,g(),c&&c.message||"network error"),s()}):s(),f};t[we]=!0,globalThis.fetch=t},So=({brRegistry:e,fetchSeed:t,hydrateFlag:o=!0}={})=>{if(!e||!Object.keys(e).length)return"";const n=Ze(e);return o?`<script>window.__BR_REGISTRY__=${n};window.__BRENDER_HYDRATE__=true<\/script>`+(t&&t.length?`
|
|
2
|
+
`+Ye(t):""):`<script>window.__BR_REGISTRY__=${n}<\/script>`},Rt=3e4,wt=2147483647;let ve=Promise.resolve();const Te=()=>{},xt=e=>{const t=ve.then(e);return ve=t.then(Te,Te),t},jt=e=>{if(e===void 0)return Rt;if(e===0||e===!1||e===1/0)return 0;if(typeof e!="number"||!(e>0))throw new TypeError(`brender: options.timeout is a number of milliseconds, or 0 / false for no limit \u2014 not ${JSON.stringify(e)}`);return Math.min(e,wt)},_t=(e,t)=>{const o=e.route||"/",n=e.pathname||o,s=n===o?o:`${n} (route ${o})`,r=new Error(`brender: the render of ${s} did not finish within ${t} ms and was abandoned, so the renders after it can run (options.timeout)`);return r.code="BRENDER_RENDER_TIMEOUT",r.route=o,r.pathname=n,r.timeout=t,r},Et=e=>{e.closed=!0;for(const t of e.undo.splice(0))t()},vt=e=>{let t=!1;return()=>{t||(t=!0,e())}},Le=(e,t={})=>{let o;try{o=jt(t.timeout)}catch(n){return Promise.reject(n)}return xt(()=>Tt(e,t,o))},Tt=async(e,t,o)=>{await ke();const n={closed:!1,undo:[]},s=Lt(e,t,n);if(!o)return s;let r;const i=new Promise((d,l)=>{r=setTimeout(()=>{Et(n),l(_t(t,o))},o)});try{return await Promise.race([s,i])}finally{clearTimeout(r)}},Lt=async(e,t,o)=>{const{route:n="/",pathname:s,state:r,context:i,prefetch:d=!1}=t,l=s||n;kt();const g=_e(ce,()=>({failures:[],labelKeys:new Set,tainted:!1}));try{return await $t(e,{route:n,locationPath:l,stateOverrides:r,contextOverrides:i,prefetch:d},g.record,o)}finally{g.close()}},$t=async(e,{route:t,locationPath:o,stateOverrides:n,contextOverrides:s,prefetch:r},i,d)=>{const l=()=>{if(d.closed)throw new Error("brender: this render was abandoned at its timeout")};let g;if(r){try{g=await ze(e)}catch(m){d.closed||console.warn("[brender] SSR translation fetch failed:",m.message)}l()}const{window:f,document:c}=ue(),a=c.body;f.location.pathname=o;const p=[Q("document",c),Q("location",f.location),Q("localStorage",f.localStorage),Q("sessionStorage",f.sessionStorage)].map(vt);d.undo.push(...p);try{const{createDomqlElement:m}=await ke();l();const y=L(e.app||{}),S={...e.config||{}};e.polyglot&&!S.polyglot&&(S.polyglot=e.polyglot),e.fetch&&!S.fetch&&(S.fetch=e.fetch),e.router&&!S.router&&(S.router=e.router);for(const k of Mt)e[k]!=null&&S[k]==null&&(S[k]=e[k]);const h=S.polyglot?{...S.polyglot}:void 0,q=tt(o,h);q&&f.localStorage.setItem(et(h),q),g&&h&&(h.translations={...h.translations||{},...g});const _=L(e.state||{});(g||h)&&(_.root||(_.root={}),h&&(_.root.lang=z(_.root.lang,h)),g&&(_.root.translations={..._.root.translations||{},...g})),rt();const F={};for(const k in e){if(ut.has(k))continue;const w=e[k];F[k]=w&&typeof w=="object"?L(w):w}const $={...F,state:_,...n?{state:{..._,...n}}:{},dependencies:L(e.dependencies||{}),components:L(e.components||{}),snippets:L(e.snippets||{}),pages:L(e.pages||{}),functions:{...e.functions||{},polyglot:ct,getActiveLang:ye,getLang:ye},methods:e.methods||{},designSystem:L(e.designSystem||{}),files:e.files||{},sharedLibraries:e.sharedLibraries||[],...S,...h?{polyglot:h}:{},document:c,window:f,parent:{node:a},initOptions:{},domqlOptions:{sourcemap:!1},firstRender:!1,sync:!1,inspect:!1,notifications:!1,isolateDesignSystem:!0,isolateFetchCache:!0,...s||{}},B=await nt();l(),B&&($.plugins=$.plugins||[],$.plugins.some(k=>k.name==="funcql")||$.plugins.push(B)),de();const G=_e(je,()=>({entries:[],bytes:0,tainted:!1})),D=G.record;let v;try{v=await m(y,$),l();const k=Date.now(),w=r?2e3:50,te=Oe=>new Promise(Pe=>setTimeout(Pe,Oe));for(await te(Math.min(50,w));r&&!d.closed&&xe()>0&&Date.now()-k<w;)await te(10),xe()===0&&await te(Math.max(0,Math.min(50,w-(Date.now()-k))));l()}finally{G.close()}const C=D&&!D.tainted?D.entries.slice():[];me(a);const K=ge(v),{elementPath:N}=await import("@symbo.ls/element");l();const M=pt(v,N),I=ne(e,t,v,v?.state),E=[],H=c.head||c.querySelector("head");if(H)for(const k of H.querySelectorAll("style")){if(k.sheet&&k.sheet.cssRules)for(const w of k.sheet.cssRules)w.cssText&&E.push(w);if(!E.length){const w=k.textContent||"";w&&E.push({cssText:w})}}const pe=E.map(k=>k.cssText),{globalCSS:R,classRules:x}=Ft(E);let T=qe(a.innerHTML);const U=v?.state?.root,W=se(U,h),ee=At(g,U);ee&&(T=Ae(T,$e(ee,W,h)));const Ie=i&&!i.tainted?{failedRequests:i.failures.slice(),rawLabelKeys:Dt(i.labelKeys,T,W,h,U)}:null;return{html:T,metadata:I,registry:K,brRegistry:M,element:v,emotionCSS:pe,globalCSS:R,classRules:x,fetchSeed:C,document:c,window:f,ssrTranslations:ee||void 0,activeLang:W,diagnostics:Ie}}finally{for(const m of p)m()}},Q=(e,t)=>{const o=Object.getOwnPropertyDescriptor(globalThis,e);return o&&!o.configurable?!o.writable&&!o.set?()=>{}:(globalThis[e]=t,()=>{}):(Object.defineProperty(globalThis,e,{value:t,writable:!0,configurable:!0,enumerable:!0}),()=>{o?Object.defineProperty(globalThis,e,o):delete globalThis[e]})},le=/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g,fe=e=>e&&typeof e.parse=="function"?e.parse():e,At=(e,t)=>{const o=fe(t?.translations),n={};for(const s of[e,o])if(!(!s||typeof s!="object"))for(const r in s){const i=fe(s[r]);i&&typeof i=="object"&&(n[r]={...n[r]||{},...i})}return Object.keys(n).length?n:null},$e=(e,t,o)=>e[t]||e[z(void 0,o)]||Object.values(e)[0]||{},Ae=(e,t)=>e.replace(le,(o,n)=>X(t,n.trim())??o),qt=/<script\b[^>]*\btype\s*=\s*["']?application\/json["']?[^>]*>[\s\S]*?<\/script\s*>/gi,Ct=(e,t)=>new RegExp(`(?:^|[^\\w.-])${Qe(t)}(?![\\w-]|\\.\\w)`).test(e),Dt=(e,t,o,n,s)=>{const r=new Set,i=t.replace(qt,"");for(const d of e)/\s/.test(d)||he(d,o,n,s)===void 0&&Ct(i,d)&&r.add(d);for(const d of i.matchAll(le))r.add(d[1].trim());return[...r]},ko=async(e,t={})=>{const{context:o={}}=t,{window:n,document:s}=ue(),r=s.body,{create:i}=await import("@symbo.ls/element"),d=await import("@symbo.ls/utils"),l={...ft,...o.components||{}},g={...d,...o.utils||{},...o.functions||{}};de();let f;try{f=i(e,{node:r},"root",{context:{document:s,window:n,...o,components:l,utils:g}})}catch{}me(r);const c=f?ge(f):{};return{html:qe(r.innerHTML),registry:c,element:f}},qe=e=>e.replace(/(<svg\b[^>]*>)([\s\S]*?)(<\/svg>)/gi,(t,o,n,s)=>{if(n.includes("<")){const r=n.replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&").replace(/"/g,'"').replace(/'/g,"'");return o+r+s}return t}),Mt=["globalTheme","themeStorageKey","themeRoot","useReset","useVariable","useFontImport","useIconSprite","useSvgSprite","useDocumentTheme","useDefaultConfig","useDefaultIcons","useThemeSuffixedVars","verbose","semanticIcons"],It=/\.[A-Za-z_\\-]/,Ot=e=>{if(typeof e!="string"||!e)return!1;const t=e.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,'""').replace(/\[[^\]]*\]/g,"[]");return!It.test(t)},Pt=/^\s*(:root|\[data-theme|\[data-skin)/,Ce=e=>{const t=e&&e.cssText;if(typeof t!="string")return null;if(/^@(-webkit-)?keyframes\b/.test(t))return"keyframes";if(t.startsWith("@font-face"))return null;if(typeof e.selectorText=="string")return Ot(e.selectorText)?Pt.test(e.selectorText)?"root":"reset":null;const o=e.cssRules&&Array.from(e.cssRules);if(o&&o.length&&/^@(media|supports|container)\b/.test(t)){const n=new Set(o.map(Ce));return n.has(null)||n.size!==1?n.has(null)?null:"reset":[...n][0]}return null},Ft=e=>{const t={root:[],reset:[],keyframes:[]},o=[];for(const n of e||[]){const s=Ce(n);s?t[s].push(n.cssText):n&&n.cssText&&o.push(n.cssText)}return{globalCSS:{rootRule:t.root.join(`
|
|
3
3
|
`),resetRules:t.reset.join(`
|
|
4
4
|
`),keyframeRules:t.keyframes.join(`
|
|
5
|
-
`),fontFaceCSS:""},classRules:o}};let
|
|
6
|
-
`),
|
|
5
|
+
`),fontFaceCSS:""},classRules:o}};let Z=new Set;const Ro=()=>{Z=new Set},wo=()=>Array.from(Z).join(`
|
|
6
|
+
`),xo=(e,t)=>e.replace(/<style data-emotion="smbls">[\s\S]*?<\/style>/,t?`<style data-emotion="smbls">
|
|
7
7
|
${t}
|
|
8
|
-
</style>`:""),
|
|
9
|
-
`),classRules:
|
|
10
|
-
`)
|
|
11
|
-
${
|
|
12
|
-
${
|
|
13
|
-
${
|
|
14
|
-
</style>`,
|
|
15
|
-
${
|
|
16
|
-
</style>`:"",
|
|
17
|
-
|
|
18
|
-
`:"";K=`${
|
|
8
|
+
</style>`:""),jo=async(e,t={})=>{const{route:o="/",pathname:n,prefetch:s=!0,timeout:r}=t,i=await Le(e,{route:o,pathname:n,prefetch:s,timeout:r});if(!i)return null;const d=e.designSystem||{},l=i.globalCSS;let g=null;try{const c=Bt(i.element);Object.keys(c).length&&(g=c)}catch{}const f=i.activeLang;return{html:i.html,css:i.classRules.join(`
|
|
9
|
+
`),classRules:i.classRules.slice(),globalCSS:l,resetCss:l.resetRules,fontLinks:Me(d),metadata:i.metadata||ne(e,o),brKeyCount:i.registry?Object.keys(i.registry).length:0,brRegistry:i.brRegistry||{},fetchSeed:i.fetchSeed||[],ssrTranslations:i.ssrTranslations,prefetchedState:g,activeLang:f,diagnostics:i.diagnostics}},Bt=e=>{const t={};return Re(e,o=>{const n=o.fetch;if(!(!n||typeof n!="object"||!o.state))for(const s of Array.isArray(n)?n:[n]){const r=s&&typeof s=="object"&&typeof s.as=="string"?s.as:null;if(!r)continue;const i=r.startsWith("~/"),d=i?r.slice(2):r,l=i?o.state.root:o.state,g=fe(l&&l[d]);d&&g!=null&&typeof g!="function"&&(t[d]=g)}}),t},_o=async(e,t="/",o={})=>{const{lang:n,themeColor:s,isr:r,hydrate:i=!0,prefetch:d=!0,shell:l,adoptDom:g=!1,pathname:f,eagerImages:c=De,preloadScripts:a=!0,nonce:p,timeout:m}=o,y=Ut(p),S=n||e.state?.lang||e.app?.metadata?.lang||"en",h=await Le(e,{route:t,pathname:f,prefetch:d,timeout:m});if(!h)return null;const q={...h.metadata};if(s&&(q["theme-color"]=s),typeof l=="string"&&!e.name){const R="\0brender-undeclared-title\0";if(ne({...e,name:R},t,h.element,h.element?.state).title===R)for(const T of["title","og:title","twitter:title"])q[T]===h.metadata.title&&delete q[T]}const _=We(q);for(const R of h.classRules)R&&Z.add(R);const F=Array.from(Z).join(`
|
|
10
|
+
`),$=h.globalCSS,B=`<style data-brender>
|
|
11
|
+
${$.rootRule||""}
|
|
12
|
+
${$.resetRules||""}
|
|
13
|
+
${$.keyframeRules||""}
|
|
14
|
+
</style>`,G=F?`<style data-emotion="smbls">
|
|
15
|
+
${F}
|
|
16
|
+
</style>`:"",D=Me(e.designSystem||{}),v=Object.keys(h.registry).length,C=Ht({brRegistry:h.brRegistry,fetchSeed:h.fetchSeed,adoptDom:g});let K="";if(r&&r.clientScript){const R=Number.isInteger(o.depth)?o.depth:t==="/"?0:t.replace(/^\/|\/$/g,"").split("/").length,x=R>0?"../".repeat(R):"./";if(i){const T=ot({polyglot:e.polyglot||e.config?.polyglot,translations:h.ssrTranslations,lang:h.activeLang,pathname:f||t,attrs:y}),U=T?T+`
|
|
17
|
+
`:"",W=C.blocks?C.blocks+`
|
|
18
|
+
`:"";K=`${U}${W}<script type="module" src="${x}${r.clientScript}"${y}><\/script>`}else K=`<script type="module"${y}>
|
|
19
19
|
{
|
|
20
20
|
const brEls = document.querySelectorAll('body > :not(script):not(style)')
|
|
21
21
|
const observer = new MutationObserver((mutations) => {
|
|
@@ -32,22 +32,22 @@ ${B}
|
|
|
32
32
|
observer.observe(document.body, { childList: true })
|
|
33
33
|
}
|
|
34
34
|
<\/script>
|
|
35
|
-
<script type="module" src="${
|
|
36
|
-
${
|
|
37
|
-
`),body:
|
|
38
|
-
<html lang="${
|
|
35
|
+
<script type="module" src="${x}${r.clientScript}"${y}><\/script>`}const N={...e.config||{}};e.polyglot&&!N.polyglot&&(N.polyglot=e.polyglot);const M=N.polyglot;let I=_;if(M){const R={...M.translations||{}};for(const x in h.ssrTranslations||{})R[x]={...R[x]||{},...h.ssrTranslations[x]};I=Ae(_,$e(R,h.activeLang,M))}const E=h.diagnostics;if(E)for(const R of I.matchAll(le)){const x=R[1].trim();E.rawLabelKeys.includes(x)||E.rawLabelKeys.push(x)}const H=Nt(h.html,c);return typeof l=="string"?{html:Je(l,{headTags:`${I}
|
|
36
|
+
${C.meta}`,headEnd:[D,B,G].filter(Boolean).join(`
|
|
37
|
+
`),body:H,depth:Number.isInteger(o.depth)?o.depth:0,bootScripts:C.blocks,lang:n||(M?h.activeLang:void 0),preloadScripts:a}),route:t,brKeyCount:v,shell:!0,diagnostics:E}:{html:`<!DOCTYPE html>
|
|
38
|
+
<html lang="${S}">
|
|
39
39
|
<head>
|
|
40
|
-
${
|
|
41
|
-
${
|
|
42
|
-
${
|
|
40
|
+
${I}${r&&r.clientScript&&i?`
|
|
41
|
+
${C.meta}`:""}
|
|
42
|
+
${D}
|
|
43
43
|
|
|
44
|
-
${
|
|
45
|
-
${
|
|
44
|
+
${B}
|
|
45
|
+
${G}
|
|
46
46
|
</head>
|
|
47
47
|
<body>
|
|
48
|
-
${
|
|
48
|
+
${H}
|
|
49
49
|
${K}
|
|
50
50
|
</body>
|
|
51
|
-
</html>`,route:t,brKeyCount:
|
|
52
|
-
`):""},
|
|
53
|
-
`)}},
|
|
51
|
+
</html>`,route:t,brKeyCount:v,diagnostics:E}},De=2,Gt=/<img\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi,Kt=/\sloading\s*=/i,Nt=(e,t=De)=>{if(t===!1||t==null||typeof e!="string")return e;const o=Math.max(0,Number(t)||0);let n=0;return e.replace(Gt,s=>n++<o||Kt.test(s)?s:s.replace(/^<img\b/i,'<img loading="lazy"'))},Me=e=>{if(!e)return"";const t=e.font_family||e.fontFamily||{},o=new Set;for(const s of Object.values(t)){if(typeof s!="string")continue;const r=s.match(/'([^']+)'/);r&&o.add(r[1])}return o.size?['<link rel="preconnect" href="https://fonts.googleapis.com">','<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',`<link href="https://fonts.googleapis.com/css2?${[...o].map(s=>`family=${s.replace(/\s+/g,"+")}:wght@300;400;500;600;700`).join("&")}&display=swap" rel="stylesheet">`].join(`
|
|
52
|
+
`):""},Ht=({brRegistry:e,fetchSeed:t,adoptDom:o=!1}={})=>{const n=o&&e&&Object.keys(e).length?Ve(e):"",s=st(t);return{meta:`<meta name="brender" content="${n?"hydrate":"replace"}">`,blocks:[n,s].filter(Boolean).join(`
|
|
53
|
+
`)}},Ut=e=>{if(e==null||e==="")return"";const t=String(e);if(!/^[A-Za-z0-9+/=_-]+$/.test(t))throw new Error("brender: a nonce is base64 or base64url text");return` nonce="${t}"`};export{De as EAGER_IMAGES_DEFAULT,Se as SMBLS_SSR_SPECIFIER,So as adoptionScripts,wo as getAccumulatedEmotionCSS,Nt as markLazyImages,Ht as prerenderParts,Le as render,ko as renderElement,_o as renderPage,jo as renderRoute,xo as replaceEmotionCSS,Ro as resetGlobalCSSCache,Ft as splitCapturedCSS};
|
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,28 +104,16 @@ 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
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Every environment gets its own, empty storage. linkedom's window reads an
|
|
86
|
-
// unknown property through to globalThis, so `window.localStorage` found the
|
|
87
|
-
// FIRST environment's store (published on globalThis below) and every later
|
|
88
|
-
// render shared it: polyglot keeps the language a page switched to and the
|
|
89
|
-
// labels it loaded there, and the next page of a build booted with them
|
|
90
|
-
// (xma.info report, brender build item 7 — reset state between renders).
|
|
91
|
-
window.localStorage = createStorage()
|
|
92
|
-
window.sessionStorage = createStorage()
|
|
93
|
-
if (!globalThis.localStorage) globalThis.localStorage = window.localStorage
|
|
94
|
-
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, and runs one render at a
|
|
112
|
+
// time per process, so no two renders hold them at once (render.js "One
|
|
113
|
+
// render at a time").
|
|
114
|
+
globalThis.localStorage = own.localStorage
|
|
115
|
+
globalThis.sessionStorage = own.sessionStorage
|
|
116
|
+
if (!globalThis.history) globalThis.history = own.history
|
|
95
117
|
|
|
96
118
|
// linkedom's HTMLSelectElement has a `value` getter and NO setter: a
|
|
97
119
|
// `value` prop on a <select> threw "Cannot set property value … which has
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/brender",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.667",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -49,15 +49,15 @@
|
|
|
49
49
|
"@symbo.ls/css": "^3.14.779",
|
|
50
50
|
"@symbo.ls/default-config": "^3.14.787",
|
|
51
51
|
"@symbo.ls/element": "^3.14.805",
|
|
52
|
-
"@symbo.ls/fetch": "^3.14.
|
|
52
|
+
"@symbo.ls/fetch": "^3.14.666",
|
|
53
53
|
"@symbo.ls/helmet": "^3.14.670",
|
|
54
54
|
"@symbo.ls/scratch": "^3.14.789",
|
|
55
|
-
"@symbo.ls/utils": "^3.14.
|
|
55
|
+
"@symbo.ls/utils": "^3.14.788",
|
|
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/render.js
CHANGED
|
@@ -13,7 +13,7 @@ import { composeIntoShell } from './shell.js'
|
|
|
13
13
|
import { fetchSSRTranslations } from './prefetch.js'
|
|
14
14
|
import { createEarlySeedScript } from '@symbo.ls/fetch'
|
|
15
15
|
import { headerEntries } from '@symbo.ls/fetch/rest'
|
|
16
|
-
import { brenderRegistryBlock, escapeRegExp, jsonForScript, polyglotFallbackLang } from '@symbo.ls/utils'
|
|
16
|
+
import { brenderRegistryBlock, escapeRegExp, jsonForScript, polyglotFallbackLang, polyglotLangStorageKey, polyglotAddressLang, polyglotStartupScript } from '@symbo.ls/utils'
|
|
17
17
|
|
|
18
18
|
// funcql plugin — enables evaluation of funcql schemas as property values
|
|
19
19
|
// during SSR. Async-imported to avoid polluting non-funcql codepaths.
|
|
@@ -111,6 +111,23 @@ const ssrGetActiveLang = function () {
|
|
|
111
111
|
return activeLangOf(this?.state?.root, this?.context?.polyglot)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ── The address's language ──────────────────────────────────────────────────
|
|
115
|
+
// A page at an address whose first segment is one of the project's languages
|
|
116
|
+
// (`/en/…` with `polyglot.languages: ['ka', 'en']`) is that language's page
|
|
117
|
+
// (@symbo.ls/utils polyglotAddressLang). In the browser the app starts in it:
|
|
118
|
+
// the page's pre-boot script stores the address's language under polyglot's
|
|
119
|
+
// storage key (`storageLangKey`, utils polyglotLangStorageKey) before the app
|
|
120
|
+
// starts, and the app starts in the stored language (polyglot initPolyglot;
|
|
121
|
+
// smbls prepareState, for static labels). A render's environment starts with
|
|
122
|
+
// empty storage, so a server render started in the default language and
|
|
123
|
+
// switched only when a router guard read the address — after the shell had
|
|
124
|
+
// made its first requests: on every /en page of the xma.info test bed the
|
|
125
|
+
// header's menu was asked for, and recorded in the page's fetch seed, in
|
|
126
|
+
// `ka`; the browser asked in `en`, found no answer for that in the seed, and
|
|
127
|
+
// asked the network again on every cold /en load (2026-10-17, 13 of 13
|
|
128
|
+
// pages). render() now stores the address's language in its own
|
|
129
|
+
// environment's storage before the app starts, as the browser has it.
|
|
130
|
+
|
|
114
131
|
// Deep clone that preserves functions and avoids circular refs
|
|
115
132
|
const structuredCloneDeep = (obj, seen = new WeakMap()) => {
|
|
116
133
|
if (obj === null || typeof obj !== 'object') return obj
|
|
@@ -288,6 +305,13 @@ const resolveDomqlPackage = (ws, pkg, ...subpath) => {
|
|
|
288
305
|
//
|
|
289
306
|
// An installed smbls OLDER than the ssr entry that still carries
|
|
290
307
|
// src/createDomql.js (pre-54131b34d tarballs) keeps the source-bundle path.
|
|
308
|
+
//
|
|
309
|
+
// Loaded once per process, and the load in progress is shared: it kept the
|
|
310
|
+
// loaded module, set when the load finished, so renders that started together
|
|
311
|
+
// before then loaded one each — two esbuild runs, and two framework instances
|
|
312
|
+
// in one process (two signal and CSS engines, two @symbo.ls/fetch copies with
|
|
313
|
+
// their own query caches); the renders after them got whichever finished
|
|
314
|
+
// last. A failed load is not kept: the next render tries again.
|
|
291
315
|
let _cachedCreateDomql = null
|
|
292
316
|
|
|
293
317
|
// Exported for the packaging lock in __tests__/ssrEntryPackaging.test.js —
|
|
@@ -303,9 +327,17 @@ const importSmblsSsr = async () => {
|
|
|
303
327
|
return mod
|
|
304
328
|
}
|
|
305
329
|
|
|
306
|
-
const bundleCreateDomql =
|
|
307
|
-
if (_cachedCreateDomql)
|
|
330
|
+
const bundleCreateDomql = () => {
|
|
331
|
+
if (!_cachedCreateDomql) {
|
|
332
|
+
_cachedCreateDomql = loadCreateDomql().catch((err) => {
|
|
333
|
+
_cachedCreateDomql = null
|
|
334
|
+
throw err
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
return _cachedCreateDomql
|
|
338
|
+
}
|
|
308
339
|
|
|
340
|
+
const loadCreateDomql = async () => {
|
|
309
341
|
const ws = detectWorkspace()
|
|
310
342
|
|
|
311
343
|
// Resolve entry point: the monorepo's source, else `smbls/ssr` (one import
|
|
@@ -316,8 +348,7 @@ const bundleCreateDomql = async () => {
|
|
|
316
348
|
} else {
|
|
317
349
|
let ssrError
|
|
318
350
|
try {
|
|
319
|
-
|
|
320
|
-
return _cachedCreateDomql
|
|
351
|
+
return await importSmblsSsr()
|
|
321
352
|
} catch (err) {
|
|
322
353
|
ssrError = err
|
|
323
354
|
}
|
|
@@ -594,7 +625,6 @@ const bundleCreateDomql = async () => {
|
|
|
594
625
|
console.log('[brender] Bundle saved:', outFile)
|
|
595
626
|
}
|
|
596
627
|
|
|
597
|
-
_cachedCreateDomql = mod
|
|
598
628
|
return mod
|
|
599
629
|
}
|
|
600
630
|
|
|
@@ -705,21 +735,6 @@ const walkElements = (root, visit) => {
|
|
|
705
735
|
walk(root)
|
|
706
736
|
}
|
|
707
737
|
|
|
708
|
-
/**
|
|
709
|
-
* Renders a Symbols/DOMQL project to HTML on the server.
|
|
710
|
-
*
|
|
711
|
-
* Accepts project data as a plain object (matching what ProjectDataService provides)
|
|
712
|
-
* or as a pre-loaded smbls context. Runs DOMQL in a linkedom virtual DOM,
|
|
713
|
-
* assigns data-br keys for hydration, and extracts page metadata for SEO.
|
|
714
|
-
*
|
|
715
|
-
* @param {object} data - Project data object with: pages, components, designSystem,
|
|
716
|
-
* state, functions, methods, snippets, files, app, config/settings
|
|
717
|
-
* @param {object} [options]
|
|
718
|
-
* @param {string} [options.route='/'] - The route/page to render
|
|
719
|
-
* @param {object} [options.state] - State overrides
|
|
720
|
-
* @param {object} [options.context] - Additional context overrides
|
|
721
|
-
* @returns {Promise<{ html: string, metadata: object, registry: object, element: object }>}
|
|
722
|
-
*/
|
|
723
738
|
// The payload keys render() puts on the context itself (below `ctx`): the
|
|
724
739
|
// generic copy of every other top-level key skips them — cloning one only for
|
|
725
740
|
// the explicit key to replace it was a deep clone of `functions`, `methods`
|
|
@@ -748,7 +763,9 @@ const inFlightRequests = () => _inFlight
|
|
|
748
763
|
// can carry them (@symbo.ls/fetch createEarlySeedScript) and the hydrating
|
|
749
764
|
// client adopts them instead of asking again. A second render overlapping
|
|
750
765
|
// in the same isolate taints the recording: another project's answers must
|
|
751
|
-
// never land in this page, so an overlapped render carries none.
|
|
766
|
+
// never land in this page, so an overlapped render carries none. Renders run
|
|
767
|
+
// one at a time (see "One render at a time"), so the only render that can
|
|
768
|
+
// overlap another is one abandoned at its timeout and still unwinding.
|
|
752
769
|
// Only a GET whose headers are Accept-Language / Accept at most is recorded
|
|
753
770
|
// (never a signed-in or keyed call), only an ok JSON or text answer, at most
|
|
754
771
|
// SEED_ENTRY_MAX bytes each and SEED_TOTAL_MAX per page.
|
|
@@ -906,7 +923,147 @@ export const adoptionScripts = ({ brRegistry, fetchSeed, hydrateFlag = true } =
|
|
|
906
923
|
(fetchSeed && fetchSeed.length ? '\n' + createEarlySeedScript(fetchSeed) : '')
|
|
907
924
|
}
|
|
908
925
|
|
|
909
|
-
|
|
926
|
+
// ── One render at a time ────────────────────────────────────────────────────
|
|
927
|
+
// A render puts its environment on the process's BARE globals — `document`,
|
|
928
|
+
// `location`, `localStorage`, `sessionStorage` (swapGlobal below) and
|
|
929
|
+
// `window` (createEnv) — because the framework reads them bare, and it shares
|
|
930
|
+
// process-wide records with the code it runs (the fetch seed and diagnostics
|
|
931
|
+
// above, the data-br key counter, the CSS engine's reset). Every render owns
|
|
932
|
+
// its location, Web Storage, history (createEnv) and query store
|
|
933
|
+
// (`isolateFetchCache`), yet renders that overlapped in one process still
|
|
934
|
+
// swapped those bare globals over each other: of five overlapping renders,
|
|
935
|
+
// the page code of three read another render's address from `location`.
|
|
936
|
+
// So render() runs ONE AT A TIME per process (Nika, 2026-10-17: "Render
|
|
937
|
+
// queue"): a render starts once the render called before it has ended —
|
|
938
|
+
// returned, thrown or timed out. `smbls build` and `smbls brender` already
|
|
939
|
+
// rendered one route after another; for them nothing changes.
|
|
940
|
+
//
|
|
941
|
+
// A render that never ends would hold every later one, so each render has a
|
|
942
|
+
// time limit — `options.timeout` ms, RENDER_TIMEOUT by default, counted from
|
|
943
|
+
// when its turn starts (the wait for the renders before it does not count,
|
|
944
|
+
// and neither does building the process's framework bundle). When it is up
|
|
945
|
+
// the render is ABANDONED: the call rejects with a BRENDER_RENDER_TIMEOUT
|
|
946
|
+
// error naming the address and route, the bare globals it put in place go
|
|
947
|
+
// back to what it found, and the next render starts. Its own steps after the
|
|
948
|
+
// wait it hung on find the render closed and stop there (`live` in
|
|
949
|
+
// renderWithDiagnostics): it never puts its globals back over a later
|
|
950
|
+
// render's, never starts its flush or key pass, and its fetch seed and
|
|
951
|
+
// diagnostics stay open until it has unwound, so a render that runs meanwhile
|
|
952
|
+
// carries none — the abandoned render's late requests would land in them.
|
|
953
|
+
// What it cannot stop: framework or page code of the abandoned render that
|
|
954
|
+
// resumes with its wait (a slow shared-library or dependency load, a page's
|
|
955
|
+
// own await) runs during a later render, and what that code reads from the
|
|
956
|
+
// BARE globals, or writes through them, is the later render's — so is
|
|
957
|
+
// process-wide framework state such as the active root state that
|
|
958
|
+
// `getRootState()` falls back to. Code that goes through its own element or
|
|
959
|
+
// context (`el.context.window`, the router, the fetch plugin's store) stays
|
|
960
|
+
// in its own environment.
|
|
961
|
+
const RENDER_TIMEOUT = 30000
|
|
962
|
+
// setTimeout's ceiling: a larger delay (Infinity included) fires at once.
|
|
963
|
+
const TIMER_MAX = 2147483647
|
|
964
|
+
|
|
965
|
+
// Settles when the render called last has ended, however it ended.
|
|
966
|
+
let _renderQueue = Promise.resolve()
|
|
967
|
+
const noop = () => {}
|
|
968
|
+
|
|
969
|
+
const inRenderQueue = (run) => {
|
|
970
|
+
const turn = _renderQueue.then(run)
|
|
971
|
+
_renderQueue = turn.then(noop, noop)
|
|
972
|
+
return turn
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// `options.timeout`: milliseconds; 0, false or Infinity for no limit.
|
|
976
|
+
const renderTimeoutOf = (value) => {
|
|
977
|
+
if (value === undefined) return RENDER_TIMEOUT
|
|
978
|
+
if (value === 0 || value === false || value === Infinity) return 0
|
|
979
|
+
if (typeof value !== 'number' || !(value > 0)) {
|
|
980
|
+
throw new TypeError(`brender: options.timeout is a number of milliseconds, or 0 / false for no limit — not ${JSON.stringify(value)}`)
|
|
981
|
+
}
|
|
982
|
+
return Math.min(value, TIMER_MAX)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const renderTimeoutError = (options, timeout) => {
|
|
986
|
+
const route = options.route || '/'
|
|
987
|
+
const pathname = options.pathname || route
|
|
988
|
+
const where = pathname === route ? route : `${pathname} (route ${route})`
|
|
989
|
+
const err = new Error(`brender: the render of ${where} did not finish within ${timeout} ms and was abandoned, so the renders after it can run (options.timeout)`)
|
|
990
|
+
err.code = 'BRENDER_RENDER_TIMEOUT'
|
|
991
|
+
err.route = route
|
|
992
|
+
err.pathname = pathname
|
|
993
|
+
err.timeout = timeout
|
|
994
|
+
return err
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// The end of an abandoned render's turn: the bare globals it swapped go back
|
|
998
|
+
// now, and its own later steps find it closed.
|
|
999
|
+
const abandon = (turn) => {
|
|
1000
|
+
turn.closed = true
|
|
1001
|
+
for (const undo of turn.undo.splice(0)) undo()
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const once = (fn) => {
|
|
1005
|
+
let done = false
|
|
1006
|
+
return () => {
|
|
1007
|
+
if (done) return
|
|
1008
|
+
done = true
|
|
1009
|
+
fn()
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Renders a Symbols/DOMQL project to HTML on the server.
|
|
1015
|
+
*
|
|
1016
|
+
* Accepts project data as a plain object (matching what ProjectDataService provides)
|
|
1017
|
+
* or as a pre-loaded smbls context. Runs DOMQL in a linkedom virtual DOM,
|
|
1018
|
+
* assigns data-br keys for hydration, and extracts page metadata for SEO.
|
|
1019
|
+
* One render at a time per process: a call waits until the render called
|
|
1020
|
+
* before it has ended (see "One render at a time").
|
|
1021
|
+
*
|
|
1022
|
+
* @param {object} data - Project data object with: pages, components, designSystem,
|
|
1023
|
+
* state, functions, methods, snippets, files, app, config/settings
|
|
1024
|
+
* @param {object} [options]
|
|
1025
|
+
* @param {string} [options.route='/'] - The route/page to render
|
|
1026
|
+
* @param {string} [options.pathname] - The address the page is rendered at (default: the route)
|
|
1027
|
+
* @param {object} [options.state] - State overrides
|
|
1028
|
+
* @param {object} [options.context] - Additional context overrides
|
|
1029
|
+
* @param {boolean} [options.prefetch=false] - Fetch table/RPC labels first and wait for the page's requests
|
|
1030
|
+
* @param {number|false} [options.timeout=30000] - Milliseconds the render may run once its turn
|
|
1031
|
+
* starts; then it is abandoned and rejects with a `BRENDER_RENDER_TIMEOUT` error. 0 or false: no limit
|
|
1032
|
+
* @returns {Promise<{ html: string, metadata: object, registry: object, element: object }>}
|
|
1033
|
+
*/
|
|
1034
|
+
export const render = (data, options = {}) => {
|
|
1035
|
+
let timeout
|
|
1036
|
+
try {
|
|
1037
|
+
timeout = renderTimeoutOf(options.timeout)
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
return Promise.reject(err)
|
|
1040
|
+
}
|
|
1041
|
+
return inRenderQueue(() => renderInTurn(data, options, timeout))
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const renderInTurn = async (data, options, timeout) => {
|
|
1045
|
+
// The framework bundle is the process's, not this route's: building it
|
|
1046
|
+
// (esbuild over smbls's source on a process's first render) is not timed.
|
|
1047
|
+
await bundleCreateDomql()
|
|
1048
|
+
const turn = { closed: false, undo: [] }
|
|
1049
|
+
const work = renderOnce(data, options, turn)
|
|
1050
|
+
if (!timeout) return work
|
|
1051
|
+
let timer
|
|
1052
|
+
const limit = new Promise((resolve, reject) => {
|
|
1053
|
+
timer = setTimeout(() => {
|
|
1054
|
+
abandon(turn)
|
|
1055
|
+
reject(renderTimeoutError(options, timeout))
|
|
1056
|
+
}, timeout)
|
|
1057
|
+
})
|
|
1058
|
+
try {
|
|
1059
|
+
// An abandoned render's own rejection, later, lands here unobserved.
|
|
1060
|
+
return await Promise.race([work, limit])
|
|
1061
|
+
} finally {
|
|
1062
|
+
clearTimeout(timer)
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const renderOnce = async (data, options, turn) => {
|
|
910
1067
|
const { route = '/', pathname, state: stateOverrides, context: contextOverrides, prefetch = false } = options
|
|
911
1068
|
// pathname is the actual URL path (e.g. /podcast/abc-123), route is the page pattern (e.g. /podcast/:id)
|
|
912
1069
|
const locationPath = pathname || route
|
|
@@ -915,13 +1072,19 @@ export const render = async (data, options = {}) => {
|
|
|
915
1072
|
trackFetches()
|
|
916
1073
|
const diag = openRenderRecord(DIAG, () => ({ failures: [], labelKeys: new Set(), tainted: false }))
|
|
917
1074
|
try {
|
|
918
|
-
return await renderWithDiagnostics(data, { route, locationPath, stateOverrides, contextOverrides, prefetch }, diag.record)
|
|
1075
|
+
return await renderWithDiagnostics(data, { route, locationPath, stateOverrides, contextOverrides, prefetch }, diag.record, turn)
|
|
919
1076
|
} finally {
|
|
920
1077
|
diag.close()
|
|
921
1078
|
}
|
|
922
1079
|
}
|
|
923
1080
|
|
|
924
|
-
const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides, contextOverrides, prefetch }, diag) => {
|
|
1081
|
+
const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides, contextOverrides, prefetch }, diag, turn) => {
|
|
1082
|
+
// After every wait: a render abandoned at its timeout stops here (see
|
|
1083
|
+
// "One render at a time").
|
|
1084
|
+
const live = () => {
|
|
1085
|
+
if (turn.closed) throw new Error('brender: this render was abandoned at its timeout')
|
|
1086
|
+
}
|
|
1087
|
+
|
|
925
1088
|
// The page's data is NOT fetched here. The fetch plugin runs during the
|
|
926
1089
|
// render below with the real element — its `transform(data, el, state)`,
|
|
927
1090
|
// its `children` re-run on the answer — and the post-create flush waits
|
|
@@ -945,8 +1108,9 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
|
|
|
945
1108
|
try {
|
|
946
1109
|
ssrTranslations = await fetchSSRTranslations(data)
|
|
947
1110
|
} catch (e) {
|
|
948
|
-
console.warn('[brender] SSR translation fetch failed:', e.message)
|
|
1111
|
+
if (!turn.closed) console.warn('[brender] SSR translation fetch failed:', e.message)
|
|
949
1112
|
}
|
|
1113
|
+
live()
|
|
950
1114
|
}
|
|
951
1115
|
|
|
952
1116
|
const { window, document } = createEnv()
|
|
@@ -962,19 +1126,23 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
|
|
|
962
1126
|
// loaded there, and one shared store handed them to the next page of the
|
|
963
1127
|
// build (createEnv). Swapped by descriptor and put back however the render
|
|
964
1128
|
// ends, a throw included — a host's own Web Storage global (Node's is a
|
|
965
|
-
// lazy accessor) is never read or lost.
|
|
1129
|
+
// lazy accessor) is never read or lost. Each goes back once: at the end of
|
|
1130
|
+
// the render, or at its timeout (`turn.undo`) — then never again, when the
|
|
1131
|
+
// abandoned render unwinds over a later render's globals.
|
|
966
1132
|
const restoreGlobals = [
|
|
967
1133
|
swapGlobal('document', document),
|
|
968
1134
|
swapGlobal('location', window.location),
|
|
969
1135
|
swapGlobal('localStorage', window.localStorage),
|
|
970
1136
|
swapGlobal('sessionStorage', window.sessionStorage)
|
|
971
|
-
]
|
|
1137
|
+
].map(once)
|
|
1138
|
+
turn.undo.push(...restoreGlobals)
|
|
972
1139
|
try {
|
|
973
1140
|
|
|
974
1141
|
// Import createDomqlElement via bundled smbls source.
|
|
975
1142
|
// The smbls monorepo uses extensionless/directory imports that Node.js ESM
|
|
976
1143
|
// can't resolve natively, so we bundle it with esbuild first.
|
|
977
1144
|
const { createDomqlElement } = await bundleCreateDomql()
|
|
1145
|
+
live()
|
|
978
1146
|
|
|
979
1147
|
const app = structuredCloneDeep(data.app || {})
|
|
980
1148
|
|
|
@@ -990,6 +1158,12 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
|
|
|
990
1158
|
|
|
991
1159
|
// Inject SSR translations into polyglot config and root state
|
|
992
1160
|
const polyglotConfig = config.polyglot ? { ...config.polyglot } : undefined
|
|
1161
|
+
|
|
1162
|
+
// The address's language in this render's storage before the app starts,
|
|
1163
|
+
// as the browser has it (see "The address's language").
|
|
1164
|
+
const addressLang = polyglotAddressLang(locationPath, polyglotConfig)
|
|
1165
|
+
if (addressLang) window.localStorage.setItem(polyglotLangStorageKey(polyglotConfig), addressLang)
|
|
1166
|
+
|
|
993
1167
|
if (ssrTranslations && polyglotConfig) {
|
|
994
1168
|
polyglotConfig.translations = {
|
|
995
1169
|
...(polyglotConfig.translations || {}),
|
|
@@ -1094,6 +1268,7 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
|
|
|
1094
1268
|
|
|
1095
1269
|
// Add funcql plugin if available (enables funcql schema evaluation in exec())
|
|
1096
1270
|
const funcql = await getFuncqlPlugin()
|
|
1271
|
+
live()
|
|
1097
1272
|
if (funcql) {
|
|
1098
1273
|
ctx.plugins = ctx.plugins || []
|
|
1099
1274
|
if (!ctx.plugins.some(p => p.name === 'funcql')) {
|
|
@@ -1110,6 +1285,7 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
|
|
|
1110
1285
|
let element
|
|
1111
1286
|
try {
|
|
1112
1287
|
element = await createDomqlElement(app, ctx)
|
|
1288
|
+
live()
|
|
1113
1289
|
|
|
1114
1290
|
// Allow async operations (fetch callbacks, state updates, re-renders) to flush.
|
|
1115
1291
|
// DOMQL's fetch plugin fires on element creation and updates state
|
|
@@ -1128,11 +1304,12 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
|
|
|
1128
1304
|
const flushCap = prefetch ? 2000 : 50
|
|
1129
1305
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
1130
1306
|
await sleep(Math.min(50, flushCap))
|
|
1131
|
-
while (prefetch && inFlightRequests() > 0 && Date.now() - flushStart < flushCap) {
|
|
1307
|
+
while (prefetch && !turn.closed && inFlightRequests() > 0 && Date.now() - flushStart < flushCap) {
|
|
1132
1308
|
await sleep(10)
|
|
1133
1309
|
// a settled batch: another quiet period for what it triggers
|
|
1134
1310
|
if (inFlightRequests() === 0) await sleep(Math.max(0, Math.min(50, flushCap - (Date.now() - flushStart))))
|
|
1135
1311
|
}
|
|
1312
|
+
live()
|
|
1136
1313
|
} finally {
|
|
1137
1314
|
recording.close()
|
|
1138
1315
|
}
|
|
@@ -1146,6 +1323,7 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
|
|
|
1146
1323
|
// Build element path → brKey registry for client-side true hydration.
|
|
1147
1324
|
// Walks the element tree after mapKeysToElements has set __brKey on each element.
|
|
1148
1325
|
const { elementPath } = await import('@symbo.ls/element')
|
|
1326
|
+
live()
|
|
1149
1327
|
const brRegistry = buildPathRegistry(element, elementPath)
|
|
1150
1328
|
|
|
1151
1329
|
// Extract metadata for the rendered route
|
|
@@ -1477,17 +1655,18 @@ export const replaceEmotionCSS = (html, newCSS) => {
|
|
|
1477
1655
|
* @param {object} data - Full project data
|
|
1478
1656
|
* @param {object} [options]
|
|
1479
1657
|
* @param {string} [options.route='/'] - Route to render
|
|
1658
|
+
* @param {number|false} [options.timeout] - render()'s time limit (30 s by default)
|
|
1480
1659
|
* @returns {Promise<{ html: string, css: string, resetCss: string, fontLinks: string, metadata: object, brKeyCount: number }>}
|
|
1481
1660
|
*/
|
|
1482
1661
|
export const renderRoute = async (data, options = {}) => {
|
|
1483
1662
|
// `prefetch: false` fetches no table/RPC labels first and does not wait for
|
|
1484
1663
|
// the page's requests — the publish-time pre-render (smbls publish →
|
|
1485
1664
|
// ssrCache) renders data-free.
|
|
1486
|
-
const { route = '/', pathname, prefetch = true } = options
|
|
1665
|
+
const { route = '/', pathname, prefetch = true, timeout } = options
|
|
1487
1666
|
|
|
1488
1667
|
// Use the full render pipeline which handles polyglot, prefetch, emotion, etc.
|
|
1489
1668
|
// Pass pathname so the virtual DOM location reflects the actual URL (for dynamic routes)
|
|
1490
|
-
const result = await render(data, { route, pathname, prefetch })
|
|
1669
|
+
const result = await render(data, { route, pathname, prefetch, timeout })
|
|
1491
1670
|
if (!result) return null
|
|
1492
1671
|
|
|
1493
1672
|
const ds = data.designSystem || {}
|
|
@@ -1582,13 +1761,15 @@ const collectFetchedState = (element) => {
|
|
|
1582
1761
|
* @param {string} [options.shell] - A built app document (e.g. dist/index.html)
|
|
1583
1762
|
* to render INTO instead of emitting a standalone document (see shell.js).
|
|
1584
1763
|
* The result then carries `shell: true`.
|
|
1764
|
+
* @param {number|false} [options.timeout] - render()'s time limit (30 s by
|
|
1765
|
+
* default; 0 or false: none) — see render()
|
|
1585
1766
|
* @param {number} [options.depth] - Directory depth of the output file: rebases
|
|
1586
1767
|
* the shell's relative asset URLs (with `shell`, default 0) and the ISR
|
|
1587
1768
|
* client script path (default: derived from the route).
|
|
1588
1769
|
* @returns {Promise<{ html: string, route: string, brKeyCount: number, shell?: true }>}
|
|
1589
1770
|
*/
|
|
1590
1771
|
export const renderPage = async (data, route = '/', options = {}) => {
|
|
1591
|
-
const { lang, themeColor, isr, hydrate = true, prefetch = true, shell, adoptDom = false, pathname, eagerImages = EAGER_IMAGES_DEFAULT, preloadScripts = true, nonce } = options
|
|
1772
|
+
const { lang, themeColor, isr, hydrate = true, prefetch = true, shell, adoptDom = false, pathname, eagerImages = EAGER_IMAGES_DEFAULT, preloadScripts = true, nonce, timeout } = options
|
|
1592
1773
|
const nonceAttr = nonceAttribute(nonce)
|
|
1593
1774
|
|
|
1594
1775
|
// Detect lang from project config, app metadata, or default
|
|
@@ -1599,7 +1780,7 @@ export const renderPage = async (data, route = '/', options = {}) => {
|
|
|
1599
1780
|
// canonical/og:url built from it read it. Without it a route was rendered
|
|
1600
1781
|
// at its own pattern: the `/*` page wrote a `/*` canonical into 404.html
|
|
1601
1782
|
// (xma.info report, brender build item 7).
|
|
1602
|
-
const result = await render(data, { route, pathname, prefetch })
|
|
1783
|
+
const result = await render(data, { route, pathname, prefetch, timeout })
|
|
1603
1784
|
if (!result) return null
|
|
1604
1785
|
|
|
1605
1786
|
const metadata = { ...result.metadata }
|
|
@@ -1668,26 +1849,21 @@ export const renderPage = async (data, route = '/', options = {}) => {
|
|
|
1668
1849
|
// Seed client-side polyglot with SSR translations so that
|
|
1669
1850
|
// el.call('polyglot', key) resolves immediately during hydration
|
|
1670
1851
|
// instead of showing raw keys until the async fetch completes.
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
}
|
|
1687
|
-
if (seedEntries.length) {
|
|
1688
|
-
translationSeed = `<script${nonceAttr}>try{${seedEntries.join(';')}}catch(e){}</script>\n`
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1852
|
+
// For a visitor with no language stored yet it stores the page's
|
|
1853
|
+
// language — the address's on a /<lang>/… page (see "The address's
|
|
1854
|
+
// language") — so the client starts where the server started, and its
|
|
1855
|
+
// first requests are the ones the page's fetch seed recorded. It stored
|
|
1856
|
+
// the default language: a fresh client on a /en page booted in the
|
|
1857
|
+
// default and asked for the header's menu again. One helper with the
|
|
1858
|
+
// platform renderer (@symbo.ls/utils polyglotStartupScript).
|
|
1859
|
+
const startup = polyglotStartupScript({
|
|
1860
|
+
polyglot: data.polyglot || data.config?.polyglot,
|
|
1861
|
+
translations: result.ssrTranslations,
|
|
1862
|
+
lang: result.activeLang,
|
|
1863
|
+
pathname: pathname || route,
|
|
1864
|
+
attrs: nonceAttr
|
|
1865
|
+
})
|
|
1866
|
+
const translationSeed = startup ? startup + '\n' : ''
|
|
1691
1867
|
// The registry and the fetch seed as data blocks; the page's mark is
|
|
1692
1868
|
// its <meta name="brender"> (no inline flag a policy could block).
|
|
1693
1869
|
const blocks = prerender.blocks ? prerender.blocks + '\n' : ''
|