@symbo.ls/brender 3.14.666 → 3.14.668

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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # @symbo.ls/brender
2
2
 
3
+ ## 3.14.668
4
+
5
+ ### Patch Changes
6
+
7
+ - XMA round 3 (performance): a pre-rendered page is adopted in short tasks; the first render puts rows on screen whole; the design system is prepared at build time and adopted by the page; `smbls build` adds V8 compile hints and (opt-in, `build.modulePreload`) lazy-module preloads; brender's preload goes after the import map; pre-built pages can be flagged stale (`fetch.seed.isStale` / `maxAge` / `onRefresh`, `<html data-smbls-stale>`); routes without their own page answer with 200.html on every deploy target; the app starts in the address's language (`addressLang`) and adopts the seeded answers; `DropdownDisclosure` and Dialog `locked`; frank-audit accepts DOM work on a bridged library's and a parsed document's own nodes; a server render waits for its own labels (label loads in flight are per app); mermaid renderProjectHtml takes a caller-resolved head (`options.metadata`) and `metadata.source` is never a tag.
8
+ - Updated dependencies
9
+ - @symbo.ls/css@3.14.780
10
+ - @symbo.ls/default-config@3.14.788
11
+ - @symbo.ls/element@3.14.806
12
+ - @symbo.ls/scratch@3.14.790
13
+ - smbls@3.14.840
14
+ - @symbo.ls/utils@3.14.789
15
+ - @symbo.ls/fetch@3.14.667
16
+
17
+ ## 3.14.667
18
+
19
+ ### Patch Changes
20
+
21
+ - Server renders run one at a time per process, and a page at /<lang>/… starts in that language.
22
+
23
+ - 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.
24
+ - 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`).
25
+ - 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`.
26
+
27
+ - Updated dependencies
28
+ - @symbo.ls/utils@3.14.788
29
+
3
30
  ## 3.14.666
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,16 +152,21 @@ 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 |
154
159
  | `hydrate` | `true` | With `isr`: boot the client over the markup (`false`: the legacy swap script) |
160
+ | `stamp` | the render time (ISO) | The build's data stamp, written into `<meta name="brender-stamp">` — a string or a number |
161
+ | `time` | now | The build time in ms, written as its `data-time`. A build passes one `stamp` and `time` to every page |
155
162
  | `nonce` | — | Content-Security-Policy nonce for the inline scripts the `isr` document still has (the label seed, the legacy swap script) — a page rendered into a shell has none |
156
163
  | `themeColor` | | See render.js. |
157
164
 
158
165
  **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
166
 
160
- **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. `smbls build` prints them; `smbls build --strict` fails on them.
167
+ **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.
168
+
169
+ **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
170
 
162
171
  **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
172
 
@@ -171,9 +180,12 @@ No inline script — a strict Content-Security-Policy (`script-src 'self'`, no `
171
180
  | `data-br="br-N"` | each server element node | The node's key |
172
181
  | `<script type="application/json" data-brender="registry">{…}</script>` | after the markup | Element path → `data-br` key (`__root` for the app's root); adoption pages only |
173
182
  | `<script type="application/json" data-fetch-seed>[…]</script>` | after the markup | The anonymous GET answers the render got (`@symbo.ls/fetch`), adopted by the client request that sends exactly the same request |
183
+ | `<meta name="brender-stamp" content="<stamp>" data-time="<ms>">` | `<head>`, after the mark | The build the page comes from: `stamp` (the `stamp` option, else the render time as an ISO string) and the render time in ms. App code reads it as `el.context.prerender` (`{ stamp, time }`) |
174
184
 
175
185
  The runtime also reads what pages from brender up to 3.14.664 carry — the inline `window.__BRENDER__` flag, `window.__BR_REGISTRY__`, `window.__BRENDER_HYDRATE__` — and, when a policy blocked that flag, takes a `data-br` element directly in the mount node for the mark. An smbls runtime older than this format does not know the `<meta>`: keep `smbls` and `@symbo.ls/brender` at the same release.
176
186
 
187
+ **The data is the build's.** A pre-rendered page shows the answers its render got until it is rendered again — a `smbls build` page until the next build. A page flagged stale (`<html data-smbls-stale>` from the host, or the project's `fetch.seed.isStale(stamp)` / `fetch.seed.maxAge`) shows them at once and then, after the adoption, asks again and swaps in what changed; `fetch.seed: false` keeps the answers out of the page. See `@symbo.ls/fetch`, "A pre-built page shows the data of its build".
188
+
177
189
  What is recorded for the seed: a GET whose only headers are `Accept` / `Accept-Language` — so an anonymous read of a keyed backend counts when `fetch.reads` moves its key into the query string (`reads: { query: { apikey: 'apikey' }, rpc: 'get' }`, read-only RPCs as GETs) — with an ok JSON or text answer, at most 256 KB each and 1 MB per page. Never a POST, a call with `Authorization` or another header, or anything from a render that overlapped another.
178
190
 
179
191
  ### `prefetchPageData(data, route, options?)` (deprecated)
@@ -281,16 +293,31 @@ Output directory defaults to `dist-brender` (or `brenderDistDir` from `symbols.j
281
293
 
282
294
  Param routes (e.g. `/blog/:id`) are skipped unless `brender.paths` lists addresses for them.
283
295
 
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.
296
+ `smbls brender` and `smbls build` share one route loop: the same `brender` settings from `symbols.json` (`context`, `paths`, `exclude`, `strict`, `eagerImages`, `timeout`, `stamp`), `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
297
 
286
298
  ### `smbls build`
287
299
 
288
300
  With brender on (the default), `smbls build` pre-renders every static route INTO the bundler's `dist/index.html`:
289
301
 
290
302
  - `/about` → `about.html`, `/` → `index.html`, `/*` → `404.html` rendered at `/404`. Two routes that end in one file (`/404` and `/*`) are written once — the first in page order — and the other is reported.
291
- - The bundler's own document is kept as **`200.html`** — serve it for client-side routes (an SPA rewrite, a Worker's catch-all), never `index.html`, which is now the home page. A `200.html` already in the output (`public/`) is not replaced.
303
+ - The bundler's own document is kept as **`200.html`** — serve it for client-side routes, never `index.html`, which is now the home page (see below). A `200.html` already in the output (`public/`) is not replaced.
292
304
  - Every `file:` URL in a page (a `new URL('…', import.meta.url)` asset) is mapped to the file the bundler emitted from the same source (found by content), or copied into the output under a content hash. A `file:` URL that names no project file fails the build.
293
305
  - A page made with failed data or label requests, or with raw label keys, is reported; `smbls build --strict` (or `brender.strict`) fails the build on it.
306
+ - Every page carries the data of the build time until the next build, and says which build that is: one stamp per build (`<meta name="brender-stamp">`). `brender.stamp` in `symbols.json` (a string or a number), else `brender.stamp` in the project's `config.js` — a value, or a function `({ time, data }) => stamp` run once per build (it may be async: the data's own version, such as the time of its latest change) — else the build time. A stamp function that throws fails the build. How a page learns that its data moved on: `@symbo.ls/fetch`, "A pre-built page shows the data of its build".
307
+
308
+ #### Routes without their own page: 200.html
309
+
310
+ **Serve 200.html, never another page, for a route without its own page** — a route with parameters that `brender.paths` does not list (`/campaigns/:id`), a page with `prerender: false`, an excluded or failed one. `index.html` is now the HOME page: a host's single-page fallback that serves it shows the home page first, then the client throws its nodes away and draws the right page (XMA: `removed: 439` on a campaign page). A host's 404 handling serves the pre-rendered not-found page, with a 404 status, under the right page's client. `smbls build` writes the rules into `<outDir>/_redirects` (after the project's own rules from `public/`; `brender.redirects: false` writes none). A pre-rendered address that a route pattern also matches gets a rule to itself first — Cloudflare follows a rule even where a file matches. The rule per host:
311
+
312
+ | Host | The rule | Written by |
313
+ |------|----------|------------|
314
+ | Cloudflare Workers (static assets) | `_redirects` in the output: each route without its own page → `/200` with status `200`; `assets.not_found_handling`: `"404-page"` with a 404 page, else `"none"` — never `"single-page-application"`, which serves index.html | `smbls build` (`_redirects`), `smbls deploy --target workers` (`wrangler.jsonc`) |
315
+ | Cloudflare Pages | the same `_redirects`; every other address gets 404.html. Without a 404.html, Pages falls back to index.html: give the project a `/*` page | `smbls build` |
316
+ | Netlify | the same `_redirects` (a file always wins over a rewrite there) | `smbls build` |
317
+ | Vercel | `vercel.json`: `"cleanUrls": true` and `"rewrites": [{ "source": "/(.*)", "destination": "/200" }]` — Vercel checks the filesystem first, so every pre-rendered page keeps its own file; an unknown address gets the app's not-found page with status 200 | `smbls deploy --provider vercel` |
318
+ | GitHub Pages | no rewrites: an address with no file gets 404.html (status 404). Without a 404 page of the project's own, copy 200.html to 404.html; better, pre-render the routes with parameters (`brender.paths`) | `smbls deploy --provider github-pages` (the workflow) |
319
+ | Surge | serves 200.html for every unknown address by itself | — |
320
+ | Your own server or Worker | try the file, then `<path>.html`, then 200.html — nginx: `try_files $uri $uri.html /200.html;`; a Worker: `env.ASSETS.fetch(new URL('/200', request.url))` | — |
294
321
 
295
322
  ### symbols.json
296
323
 
@@ -312,7 +339,8 @@ With brender on (the default), `smbls build` pre-renders every static route INTO
312
339
  "exclude": ["/admin*"],
313
340
  "paths": { "/:lang": ["/en"], "/:lang/about": ["/en/about"] },
314
341
  "strict": true,
315
- "eagerImages": 2
342
+ "eagerImages": 2,
343
+ "timeout": 30000
316
344
  }
317
345
  }
318
346
  ```
@@ -325,6 +353,7 @@ With brender on (the default), `smbls build` pre-renders every static route INTO
325
353
  | `strict` | As `--strict`. |
326
354
  | `eagerImages` | As renderPage's. |
327
355
  | `hydrate` | `true` (default): every page ships its element-path registry and the client adopts it; `false`: replace pages (no registry; the client renders fresh). |
356
+ | `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
357
 
329
358
  ## Examples
330
359
 
@@ -429,7 +458,7 @@ The client finds an element's node through the registry the page carries (elemen
429
458
  - `renderElement()` uses `@domql/element` create directly — lightweight, no smbls bootstrap. Good for individual components
430
459
  - `render()` uses the full `smbls/src/createDomql.js` pipeline — handles routing, designSystem initialization, uikit defaults, the works. Needed for complete apps
431
460
  - 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.location` before each render to match the linkedom virtual env, then restores them after. This allows the bundled smbls code (which reads `window = globalThis`) to work in SSR
461
+ - `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
462
  - `hydrate.js` is browser-only code (no linkedom dependency) — it's exported separately via `@symbo.ls/brender/hydrate`
434
463
  - `createEnv()` sets `globalThis.window/document/Node/HTMLElement` because `@domql/utils` `isDOMNode` uses `instanceof` checks against global constructors
435
464
  - CSS is extracted from the atomic CSS engine's generated rules
package/dist/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{createEnv as e}from"./env.js";import{resetKeys as r,assignKeys as t,mapKeysToElements as o}from"./keys.js";import{loadProject as m,loadAndRenderAll as a}from"./load.js";import{render as p,renderElement as n,renderRoute as d,renderPage as i,resetGlobalCSSCache as l,getAccumulatedEmotionCSS as c,replaceEmotionCSS as f,adoptionScripts as s,prerenderParts as S}from"./render.js";import{extractMetadata as g,generateHeadHtml as h}from"./metadata.js";import{collectBrNodes as E,hydrate as P}from"./hydrate.js";import{generateSitemap as u}from"./sitemap.js";import{prefetchPageData as y,injectPrefetchedState as C}from"./prefetch.js";import{composeIntoShell as x}from"./shell.js";var G={createEnv:e,resetKeys:r,assignKeys:t,mapKeysToElements:o,loadProject:m,loadAndRenderAll:a,render:p,renderElement:n,renderRoute:d,renderPage:i,resetGlobalCSSCache:l,getAccumulatedEmotionCSS:c,replaceEmotionCSS:f,extractMetadata:g,generateHeadHtml:h,collectBrNodes:E,hydrate:P,generateSitemap:u,prefetchPageData:y,injectPrefetchedState:C,composeIntoShell:x,adoptionScripts:s,prerenderParts:S};export{s as adoptionScripts,t as assignKeys,E as collectBrNodes,x as composeIntoShell,e as createEnv,G as default,g as extractMetadata,h as generateHeadHtml,u as generateSitemap,c as getAccumulatedEmotionCSS,P as hydrate,C as injectPrefetchedState,a as loadAndRenderAll,m as loadProject,o as mapKeysToElements,y as prefetchPageData,S as prerenderParts,p as render,n as renderElement,i as renderPage,d as renderRoute,f as replaceEmotionCSS,l as resetGlobalCSSCache,r as resetKeys};
1
+ import{createEnv as e}from"./env.js";import{resetKeys as r,assignKeys as t,mapKeysToElements as o}from"./keys.js";import{loadProject as m,loadAndRenderAll as a}from"./load.js";import{render as p,renderElement as d,renderRoute as n,renderPage as i,resetGlobalCSSCache as l,getAccumulatedEmotionCSS as c,replaceEmotionCSS as f,adoptionScripts as s,prerenderParts as S,buildStamp as g}from"./render.js";import{extractMetadata as h,generateHeadHtml as u}from"./metadata.js";import{collectBrNodes as E,hydrate as P}from"./hydrate.js";import{generateSitemap as y}from"./sitemap.js";import{prefetchPageData as C,injectPrefetchedState as x}from"./prefetch.js";import{composeIntoShell as A}from"./shell.js";var I={createEnv:e,resetKeys:r,assignKeys:t,mapKeysToElements:o,loadProject:m,loadAndRenderAll:a,render:p,renderElement:d,renderRoute:n,renderPage:i,resetGlobalCSSCache:l,getAccumulatedEmotionCSS:c,replaceEmotionCSS:f,extractMetadata:h,generateHeadHtml:u,collectBrNodes:E,hydrate:P,generateSitemap:y,prefetchPageData:C,injectPrefetchedState:x,composeIntoShell:A,adoptionScripts:s,prerenderParts:S,buildStamp:g};export{s as adoptionScripts,t as assignKeys,g as buildStamp,E as collectBrNodes,A as composeIntoShell,e as createEnv,I as default,h as extractMetadata,u as generateHeadHtml,y as generateSitemap,c as getAccumulatedEmotionCSS,P as hydrate,x as injectPrefetchedState,a as loadAndRenderAll,m as loadProject,o as mapKeysToElements,C as prefetchPageData,S as prerenderParts,p as render,d as renderElement,i as renderPage,n as renderRoute,f as replaceEmotionCSS,l as resetGlobalCSSCache,r as resetKeys};
@@ -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 X=null;const ze=async()=>{if(X)return X;try{return X=(await import("@symbo.ls/funcql")).funcqlPlugin,X}catch{return null}};import"linkedom";import{createEarlySeedBlock as Ye}from"@symbo.ls/fetch";import{reset as Ve}from"@symbo.ls/css";const Z=(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 l=Z(n,e);if(l!==void 0)return l}}if(s?.translations){const n=s.translations[t];if(n){const l=Z(n,e);if(l!==void 0)return l}}const r=U(void 0,o);if(r!==t&&o?.translations){const n=o.translations[r];if(n){const l=Z(n,e);if(l!==void 0)return l}}},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 l=ie(ce);return s&&l&&l.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:a=>a};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 a=$.resolve("smbls/package.json");s=x(a)}catch{let a=e;for(;a!==x(a);){const y=m(a,"node_modules","smbls");if(b(m(y,"package.json"))){s=y;break}a=x(a)}}let r=$;if(s)try{r=oe(m(s,"package.json"))}catch{}let n,l=process.cwd();for(;l!==x(l);){if(b(m(l,"package.json"))){n=l;break}l=x(l)}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},Qt=(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},eo=(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 Q=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=()=>(Q||(Q=tt().catch(e=>{throw Q=null,e})),Q),tt=async()=>{const e=Ze();let t;if($&&e.isMonorepo)t=m(e.monorepoRoot,"packages","smbls","src","createDomql.js");else{let f;try{return await Qe()}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,l=n?new Set(n.workspacePackageRoots().map(f=>n.workspacePackageAt(f).name)):null,u=/^(?:smbls(?:\/.*)?|domql|css-in-props(?:\/.*)?|@symbo\.ls\/[^/]+)$/,a={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(!l.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:[a],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 y},ot={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"}},st=(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)},nt=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,rt=256*1024,ct=1024*1024,it=new Set(["accept-language","accept"]),xe={renders:0,record:null},lt=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]}},le=(e,t,o)=>{!e||e.tainted||e.failures.length>=lt||e.failures.push({url:at(t),reason:o})},ve=e=>e&&typeof e=="object"&&typeof e.url=="string"?e.url:String(e),ft=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(),l={},u=a=>{if(!a)return;const y=!Array.isArray(a)&&typeof a.entries!="function";for(const[f,i]of He(a))y&&i==null||(l[String(f).toLowerCase()]=String(i))};return s&&u(t.headers),o&&u(o.headers),{url:r,method:n,headers:l}},pt=(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(l=>{e.tainted||l.length>rt||e.bytes+l.length>ct||(e.bytes+=l.length,e.entries.push({url:t.url,lang:t.headers["accept-language"]||"",status:o.status,type:r,body:l}))},()=>{}))},ut=()=>{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--})},l=ie(xe),u=ie(ce);let a=null;if(l)try{a=ft(o),(a.method!=="GET"||!/^https?:/i.test(a.url)||Object.keys(a.headers).some(i=>!it.has(i)))&&(a=null)}catch{a=null}const y=()=>ve(o[0]);let f;try{f=e.apply(this,o)}catch(i){throw le(u,y(),i&&i.message||"failed"),r(),i}return f&&typeof f.then=="function"?f.then(i=>{a&&pt(l,a,i,n),u&&i&&i.ok===!1&&le(u,y(),"HTTP "+i.status),r()},i=>{le(u,y(),i&&i.message||"network error"),r()}):r(),f};t[Re]=!0,globalThis.fetch=t},to=({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
- `+Ge(t):""):`<script>window.__BR_REGISTRY__=${s}<\/script>`},_e=async(e,t={})=>{const{route:o="/",pathname:s,state:r,context:n,prefetch:l=!1}=t,u=s||o;ut();const a=je(ce,()=>({failures:[],labelKeys:new Set,tainted:!1}));try{return await dt(e,{route:o,locationPath:u,stateOverrides:r,contextOverrides:n,prefetch:l},a.record)}finally{a.close()}},dt=async(e,{route:t,locationPath:o,stateOverrides:s,contextOverrides:r,prefetch:n},l)=>{let u;if(n)try{u=await Ne(e)}catch(c){console.warn("[brender] SSR translation fetch failed:",c.message)}const{window:a,document:y}=ue(),f=y.body;a.location.pathname=o;const i=[ee("document",y),ee("location",a.location),ee("localStorage",a.localStorage),ee("sessionStorage",a.sessionStorage)];try{const{createDomqlElement:c}=await et(),d=_(e.app||{}),p={...e.config||{}};e.polyglot&&!p.polyglot&&(p.polyglot=e.polyglot),e.fetch&&!p.fetch&&(p.fetch=e.fetch),e.router&&!p.router&&(p.router=e.router);for(const S of bt)e[S]!=null&&p[S]==null&&(p[S]=e[S]);const h=p.polyglot?{...p.polyglot}:void 0;u&&h&&(h.translations={...h.translations||{},...u});const g=_(e.state||{});(u||h)&&(g.root||(g.root={}),h&&(g.root.lang=U(g.root.lang,h)),u&&(g.root.translations={...g.root.translations||{},...u})),Ve();const q={};for(const S in e){if(nt.has(S))continue;const R=e[S];q[S]=R&&typeof R=="object"?_(R):R}const T={...q,state:g,...s?{state:{...g,...s}}:{},dependencies:_(e.dependencies||{}),components:_(e.components||{}),snippets:_(e.snippets||{}),pages:_(e.pages||{}),functions:{...e.functions||{},polyglot:Xe,getActiveLang:ye,getLang:ye},methods:e.methods||{},designSystem:_(e.designSystem||{}),files:e.files||{},sharedLibraries:e.sharedLibraries||[],...p,...h?{polyglot:h}:{},document:y,window:a,parent:{node:f},initOptions:{},domqlOptions:{sourcemap:!1},firstRender:!1,sync:!1,inspect:!1,notifications:!1,isolateDesignSystem:!0,isolateFetchCache:!0,...r||{}},B=await ze();B&&(T.plugins=T.plugins||[],T.plugins.some(S=>S.name==="funcql")||T.plugins.push(B)),de();const I=je(xe,()=>({entries:[],bytes:0,tainted:!1})),O=I.record;let j;try{j=await c(d,T);const S=Date.now(),R=n?2e3:50,L=M=>new Promise(J=>setTimeout(J,M));for(await L(Math.min(50,R));n&&we()>0&&Date.now()-S<R;)await L(10),we()===0&&await L(Math.max(0,Math.min(50,R-(Date.now()-S))))}finally{I.close()}const z=O&&!O.tainted?O.entries.slice():[];me(f);const Y=ge(j),{elementPath:A}=await import("@symbo.ls/element"),K=st(j,A),N=se(e,t,j,j?.state),v=[],C=y.head||y.querySelector("head");if(C)for(const S of C.querySelectorAll("style")){if(S.sheet&&S.sheet.cssRules)for(const R of S.sheet.cssRules)R.cssText&&v.push(R);if(!v.length){const R=S.textContent||"";R&&v.push({cssText:R})}}const D=v.map(S=>S.cssText),{globalCSS:V,classRules:pe}=wt(v);let k=Le(f.innerHTML);const w=j?.state?.root,E=ne(w,h),G=mt(u,w);G&&(k=Te(k,Ee(G,E,h)));const H=l&&!l.tainted?{failedRequests:l.failures.slice(),rawLabelKeys:yt(l.labelKeys,k,E,h,w)}:null;return{html:k,metadata:N,registry:Y,brRegistry:K,element:j,emotionCSS:D,globalCSS:V,classRules:pe,fetchSeed:z,document:y,window:a,ssrTranslations:G||void 0,activeLang:E,diagnostics:H}}finally{for(const c of i)c()}},ee=(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]})},ae=/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g,fe=e=>e&&typeof e.parse=="function"?e.parse():e,mt=(e,t)=>{const o=fe(t?.translations),s={};for(const r of[e,o])if(!(!r||typeof r!="object"))for(const n in r){const l=fe(r[n]);l&&typeof l=="object"&&(s[n]={...s[n]||{},...l})}return Object.keys(s).length?s:null},Ee=(e,t,o)=>e[t]||e[U(void 0,o)]||Object.values(e)[0]||{},Te=(e,t)=>e.replace(ae,(o,s)=>Z(t,s.trim())??o),gt=/<script\b[^>]*\btype\s*=\s*["']?application\/json["']?[^>]*>[\s\S]*?<\/script\s*>/gi,ht=(e,t)=>new RegExp(`(?:^|[^\\w.-])${We(t)}(?![\\w-]|\\.\\w)`).test(e),yt=(e,t,o,s,r)=>{const n=new Set,l=t.replace(gt,"");for(const u of e)/\s/.test(u)||he(u,o,s,r)===void 0&&ht(l,u)&&n.add(u);for(const u of l.matchAll(ae))n.add(u[1].trim());return[...n]},oo=async(e,t={})=>{const{context:o={}}=t,{window:s,document:r}=ue(),n=r.body,{create:l}=await import("@symbo.ls/element"),u=await import("@symbo.ls/utils"),a={...ot,...o.components||{}},y={...u,...o.utils||{},...o.functions||{}};de();let f;try{f=l(e,{node:n},"root",{context:{document:r,window:s,...o,components:a,utils:y}})}catch{}me(n);const i=f?ge(f):{};return{html:Le(n.innerHTML),registry:i,element:f}},Le=e=>e.replace(/(<svg\b[^>]*>)([\s\S]*?)(<\/svg>)/gi,(t,o,s,r)=>{if(s.includes("&lt;")){const n=s.replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,"&").replace(/&quot;/g,'"').replace(/&#39;/g,"'");return o+n+r}return t}),bt=["globalTheme","themeStorageKey","themeRoot","useReset","useVariable","useFontImport","useIconSprite","useSvgSprite","useDocumentTheme","useDefaultConfig","useDefaultIcons","useThemeSuffixedVars","verbose","semanticIcons"],St=/\.[A-Za-z_\\-]/,kt=e=>{if(typeof e!="string"||!e)return!1;const t=e.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,'""').replace(/\[[^\]]*\]/g,"[]");return!St.test(t)},Rt=/^\s*(:root|\[data-theme|\[data-skin)/,$e=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 kt(e.selectorText)?Rt.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 s=new Set(o.map($e));return s.has(null)||s.size!==1?s.has(null)?null:"reset":[...s][0]}return null},wt=e=>{const t={root:[],reset:[],keyframes:[]},o=[];for(const s of e||[]){const r=$e(s);r?t[r].push(s.cssText):s&&s.cssText&&o.push(s.cssText)}return{globalCSS:{rootRule:t.root.join(`
1
+ import{resolve as u,join as Fe,dirname as _}from"path";import{existsSync as y,unlinkSync as Be,readFileSync as U,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 re}from"module";import{pathToFileURL as Ue}from"url";import{createEnv as ge}from"./env.js";import{resetKeys as he,assignKeys as ye,mapKeysToElements as be}from"./keys.js";import{extractMetadata as ae,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,brenderStampMeta as Qe,escapeRegExp as Ze,jsonForScript as et,polyglotFallbackLang as X,polyglotLangStorageKey as tt,polyglotAddressLang as ot,polyglotStartupScript as nt}from"@symbo.ls/utils";let V=null;const st=async()=>{if(V)return V;try{return V=(await import("@symbo.ls/funcql")).funcqlPlugin,V}catch{return null}};import"linkedom";import{createEarlySeedBlock as rt}from"@symbo.ls/fetch";import{reset as at}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 n=e;for(const s of o){if(n==null||typeof n!="object")return;n=n[s]}return n},Z=(e,t)=>X(e?.lang,t),Se=(e,t,o,n)=>{if(o?.translations){const r=o.translations[t];if(r){const i=Q(r,e);if(i!==void 0)return i}}if(n?.translations){const r=n.translations[t];if(r){const i=Q(r,e);if(i!==void 0)return i}}const s=X(void 0,o);if(s!==t&&o?.translations){const r=o.translations[s];if(r){const i=Q(r,e);if(i!==void 0)return i}}},it=function(e,t){if(!e)return"";const o=this?.context,n=o?.polyglot,s=this?.state?.root||o?.state?.root,r=Se(e,t||Z(s,n),n,s);if(r!==void 0)return r;const i=fe(le);return n&&i&&i.labelKeys.add(e),e},we=function(){return Z(this?.state?.root,this?.context?.polyglot)},T=(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?T(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?T(s,t):s}return o};let $=null;try{import.meta.url&&($=re(import.meta.url))}catch{}const ct=()=>{if(!import.meta.url||!$)return{isMonorepo:!1,monorepoRoot:null,resolvePackage:l=>l};const e=Ge(new URL(".",import.meta.url).pathname),t=u(e,"../..");if(y(u(t,"packages","smbls","src","createDomql.js")))return{isMonorepo:!0,monorepoRoot:t};let n;try{const l=$.resolve("smbls/package.json");n=_(l)}catch{let l=e;for(;l!==_(l);){const g=u(l,"node_modules","smbls");if(y(u(g,"package.json"))){n=g;break}l=_(l)}}let s=$;if(n)try{s=re(u(n,"package.json"))}catch{}let r,i=process.cwd();for(;i!==_(i);){if(y(u(i,"package.json"))){r=i;break}i=_(i)}let d=s;if(r)try{d=re(u(r,"package.json"))}catch{}return{isMonorepo:!1,smblsRoot:n,brenderDir:e,projectRoot:r,_smblsRequire:s,_projectRequire:d}},P=(e,t)=>{const o=e.isMonorepo?[$]:[e._smblsRequire,e._projectRequire,$].filter(Boolean);for(const n of o)try{return n.resolve(t)}catch{}return null},wo=(e,t,...o)=>{if(e.isMonorepo)return u(e.monorepoRoot,"packages",t,...o);const n=P(e,`${t}/package.json`);return n?u(_(n),...o):null},ko=(e,t,...o)=>{if(e.isMonorepo)return u(e.monorepoRoot,"plugins",t,...o);const n=P(e,`@symbo.ls/${t}/package.json`);return n?u(_(n),...o):null},ie=(e,t,...o)=>{if(e.isMonorepo){for(const s of["packages","plugins"]){const r=u(e.monorepoRoot,s,t,...o);if(y(r))return r}return null}const n=P(e,`@symbo.ls/${t}/package.json`);return n?u(_(n),...o):null},ke=(e,t,...o)=>{if(e.isMonorepo)return u(e.monorepoRoot,"packages","domql","packages",t,...o);const n=P(e,`@symbo.ls/${t}/package.json`);return n?u(_(n),...o):null};let ee=null;const Re="smbls/ssr",lt=async()=>{const e=await import("smbls/ssr");if(typeof e?.createDomqlElement!="function")throw new Error("smbls/ssr does not export createDomqlElement");return e},xe=()=>(ee||(ee=ft().catch(e=>{throw ee=null,e})),ee),ft=async()=>{const e=ct();let t;if($&&e.isMonorepo)t=u(e.monorepoRoot,"packages","smbls","src","createDomql.js");else{let f;try{return await lt()}catch(c){f=c}if(!$)throw new Error(`brender: ${Re} 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 a=e.smblsRoot&&u(e.smblsRoot,"src","createDomql.js");if(a&&y(a))t=a;else throw new Error(`brender: cannot load ${Re} (${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:/^[^./]/},a=>{if(d.test(a.path))return;const c=a.path.split("/"),p=a.path.startsWith("@")?c.slice(0,2).join("/"):c[0];if(!i.has(p))return;const m=r.resolveWorkspaceSource(a.path,a.importer||t);return m?{path:m}:{errors:[{text:`"${a.path}" names no source file in the workspace`}]}}),f.onLoad({filter:/[\\/]dist[\\/]/},a=>r.isWorkspaceDist(a.path)?{errors:[{text:`${a.path} is a BUILD of a workspace package \u2014 the render bundles source`}]}:void 0)),f.onResolve({filter:/^smbls/},a=>{const c=a.path.replace(/^smbls\/?/,""),p=e.isMonorepo?u(e.monorepoRoot,"packages","smbls"):e.smblsRoot;if(!p)return;if(!c){const S=s(p);return S?{path:S}:void 0}const m=u(p,c);if(y(m))return{path:m};if(y(m+".js"))return{path:m+".js"};const b=u(m,"index.js");if(y(b))return{path:b}}),f.onResolve({filter:/^domql$/},a=>{if(e.isMonorepo){const c=u(e.monorepoRoot,"packages","domql","src","index.js");if(y(c))return{path:c};const p=u(e.monorepoRoot,"packages","domql","index.js");if(y(p))return{path:p}}else try{const c=_require.resolve("domql/package.json"),p=s(_(c));if(p)return{path:p}}catch{}}),f.onResolve({filter:/^@symbo\.ls\//},a=>{const c=a.path.replace("@symbo.ls/","");if(c==="sync")return{path:"sync-stub",namespace:"brender-stub"};if(e.isMonorepo){for(const m of["packages","plugins"]){const b=u(e.monorepoRoot,m,c,"src","index.js");if(y(b))return{path:b};const S=u(e.monorepoRoot,m,c,"index.js");if(y(S))return{path:S}}const p=u(e.monorepoRoot,"packages","default-config","blank","index.js");if(c==="default-config"&&y(p))return{path:p}}else{const p=ie(e,c,"src","index.js");if(p&&y(p))return{path:p};const m=ie(e,c,"index.js");if(m&&y(m))return{path:m};if(c==="default-config"){const b=ie(e,"default-config","blank","index.js");if(b&&y(b))return{path:b}}}}),f.onResolve({filter:/^@domql\//},a=>{const c=a.path.replace("@symbo.ls/","");if(e.isMonorepo){const p=u(e.monorepoRoot,"packages","domql","packages",c,"src","index.js");if(y(p))return{path:p};const m=u(e.monorepoRoot,"packages","domql","packages",c,"index.js");if(y(m))return{path:m}}else{const p=ke(e,c,"src","index.js");if(p&&y(p))return{path:p};const m=ke(e,c,"index.js");if(m&&y(m))return{path:m}}}),f.onResolve({filter:/^css-in-props/},a=>{let c;if(e.isMonorepo)c=u(e.monorepoRoot,"packages","css-in-props");else{const b=P(e,"css-in-props/package.json");b&&(c=_(b))}if(!c)return;const p=a.path.replace(/^css-in-props\/?/,"");if(p){const b=u(c,p),S=u(b,"index.js");if(y(S))return{path:S};if(y(b+".js"))return{path:b+".js"};if(y(b))return{path:b}}const m=s(c);if(m)return{path:m}}),f.onResolve({filter:/^@emotion\//},a=>{let c;if(e.isMonorepo)c=u(e.monorepoRoot,"node_modules",a.path);else{const p=P(e,`${a.path}/package.json`);if(!p)return;c=_(p)}if(y(c)){const p=u(c,"package.json");if(y(p))try{const m=JSON.parse(U(p,"utf8")),b=m.module||m.main||"dist/emotion-css.esm.js";return{path:u(c,b)}}catch{}return{path:c}}}),f.onResolve({filter:/\.json$/},a=>{if(a.resolveDir){const c=u(a.resolveDir,a.path);if(y(c))return{path:c}}}),f.onLoad({filter:/smbls\/.*options\.js$/},async a=>{if(!a.path.includes("smbls/")||!a.path.endsWith("options.js"))return;let c=U(a.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 a=>{if(!a.path.includes("smbls/")||!a.path.endsWith("init.js"))return;let c=U(a.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 a=>{let c=U(a.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 a=>{if(!a.path.includes("smbls/"))return;let c=U(a.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: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=>y(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},pt={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"}},ut=(e,t)=>{const o={};return je(e,(n,s)=>{if(!s.__brKey)return;const r=t(n);o[r===""?"__root":r]=s.__brKey}),o},je=(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)},dt=new Set(["app","config","pages","components","state","snippets","designSystem","dependencies","functions","methods","files","sharedLibraries"]);let F=0;const _e=Symbol.for("brender.fetchTracked"),ce=()=>F,mt=256*1024,gt=1024*1024,ht=new Set(["accept-language","accept"]),Ee={renders:0,record:null},yt=50,le={renders:0,record:null},ve=(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)}}},fe=e=>e.record&&!e.record.tainted?e.record:null,bt=e=>{try{const t=new URL(e);return t.origin+t.pathname}catch{return String(e).split("?")[0]}},pe=(e,t,o)=>{!e||e.tainted||e.failures.length>=yt||e.failures.push({url:bt(t),reason:o})},Te=e=>e&&typeof e=="object"&&typeof e.url=="string"?e.url:String(e),St=e=>{const[t,o]=e,n=t&&typeof t=="object"&&typeof t.url=="string",s=Te(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,a]of Xe(l))g&&a==null||(i[String(f).toLowerCase()]=String(a))};return n&&d(t.headers),o&&d(o.headers),{url:s,method:r,headers:i}},wt=(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>mt||e.bytes+i.length>gt||(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[_e])return;const t=function(...o){F++;let n=!1;const s=()=>{n||(n=!0,F--)},r=a=>{F++,a.then(()=>{F--},()=>{F--})},i=fe(Ee),d=fe(le);let l=null;if(i)try{l=St(o),(l.method!=="GET"||!/^https?:/i.test(l.url)||Object.keys(l.headers).some(a=>!ht.has(a)))&&(l=null)}catch{l=null}const g=()=>Te(o[0]);let f;try{f=e.apply(this,o)}catch(a){throw pe(d,g(),a&&a.message||"failed"),s(),a}return f&&typeof f.then=="function"?f.then(a=>{l&&wt(i,l,a,r),d&&a&&a.ok===!1&&pe(d,g(),"HTTP "+a.status),s()},a=>{pe(d,g(),a&&a.message||"network error"),s()}):s(),f};t[_e]=!0,globalThis.fetch=t},Ro=({brRegistry:e,fetchSeed:t,hydrateFlag:o=!0}={})=>{if(!e||!Object.keys(e).length)return"";const n=et(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,xt=2147483647;let Le=Promise.resolve();const De=()=>{},jt=e=>{const t=Le.then(e);return Le=t.then(De,De),t},_t=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,xt)},Et=(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},vt=e=>{e.closed=!0;for(const t of e.undo.splice(0))t()},Tt=e=>{let t=!1;return()=>{t||(t=!0,e())}},$e=(e,t={})=>{let o;try{o=_t(t.timeout)}catch(n){return Promise.reject(n)}return jt(()=>Lt(e,t,o))},Lt=async(e,t,o)=>{await xe();const n={closed:!1,undo:[]},s=Dt(e,t,n);if(!o)return s;let r;const i=new Promise((d,l)=>{r=setTimeout(()=>{vt(n),l(Et(t,o))},o)});try{return await Promise.race([s,i])}finally{clearTimeout(r)}},Dt=async(e,t,o)=>{const{route:n="/",pathname:s,state:r,context:i,prefetch:d=!1}=t,l=s||n;kt();const g=ve(le,()=>({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:a}=ge(),c=a.body;f.location.pathname=o;const p=[te("document",a),te("location",f.location),te("localStorage",f.localStorage),te("sessionStorage",f.sessionStorage)].map(Tt);d.undo.push(...p);try{const{createDomqlElement:m}=await xe();l();const b=T(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 Ot)e[k]!=null&&S[k]==null&&(S[k]=e[k]);const R=S.polyglot?{...S.polyglot}:void 0,W=ot(o,R);W&&f.localStorage.setItem(tt(R),W),g&&R&&(R.translations={...R.translations||{},...g});const h=T(e.state||{});(g||R)&&(h.root||(h.root={}),R&&(h.root.lang=X(h.root.lang,R)),g&&(h.root.translations={...h.root.translations||{},...g})),at();const A={};for(const k in e){if(dt.has(k))continue;const x=e[k];A[k]=x&&typeof x=="object"?T(x):x}const L={...A,state:h,...n?{state:{...h,...n}}:{},dependencies:T(e.dependencies||{}),components:T(e.components||{}),snippets:T(e.snippets||{}),pages:T(e.pages||{}),functions:{...e.functions||{},polyglot:it,getActiveLang:we,getLang:we},methods:e.methods||{},designSystem:T(e.designSystem||{}),files:e.files||{},sharedLibraries:e.sharedLibraries||[],...S,...R?{polyglot:R}:{},document:a,window:f,parent:{node:c},initOptions:{},domqlOptions:{sourcemap:!1},firstRender:!1,sync:!1,inspect:!1,notifications:!1,isolateDesignSystem:!0,isolateFetchCache:!0,captureDesignSystem:!0,...s||{}},B=await st();l(),B&&(L.plugins=L.plugins||[],L.plugins.some(k=>k.name==="funcql")||L.plugins.push(B)),he();const I=ve(Ee,()=>({entries:[],bytes:0,tainted:!1})),O=I.record;let E;try{E=await m(b,L),l();const k=Date.now(),x=r?2e3:50,H=se=>new Promise(Pe=>setTimeout(Pe,se));for(await H(Math.min(50,x));r&&!d.closed&&ce()>0&&Date.now()-k<x;)await H(10),ce()===0&&await H(Math.max(0,Math.min(50,x-(Date.now()-k))));if(l(),r&&await At(E,R)){l();const se=Date.now();for(await H(Math.min(50,x));!d.closed&&ce()>0&&Date.now()-se<x;)await H(10);l()}}finally{I.close()}const J=O&&!O.tainted&&!(S.fetch&&S.fetch.seed===!1)?O.entries.slice():[];ye(c);const z=be(E),{elementPath:q}=await import("@symbo.ls/element");l();const G=ut(E,q),K=ae(e,t,E,E?.state),v=[],C=a.head||a.querySelector("head");if(C)for(const k of C.querySelectorAll("style")){if(k.sheet&&k.sheet.cssRules)for(const x of k.sheet.cssRules)x.cssText&&v.push(x);if(!v.length){const x=k.textContent||"";x&&v.push({cssText:x})}}const M=v.map(k=>k.cssText),{globalCSS:Y,classRules:me}=Gt(v);let w=Ce(c.innerHTML);const j=E?.state?.root,D=Z(j,R),N=qt(g,j);N&&(w=qe(w,Ae(N,D,R)));const ne=i&&!i.tainted?{failedRequests:i.failures.slice(),rawLabelKeys:It(i.labelKeys,w,D,R,j)}:null;return{html:w,metadata:K,registry:z,brRegistry:G,element:E,emotionCSS:M,globalCSS:Y,classRules:me,fetchSeed:J,document:a,window:f,ssrTranslations:N||void 0,activeLang:D,diagnostics:ne,preparedDesignSystem:L.preparedDesignSystem||null}}finally{for(const m of p)m()}},te=(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]})},At=async(e,t)=>{if(!t||!t.fetch||!e||typeof e.call!="function")return!1;const o=e.state&&e.state.root;if(!o)return!1;const n=Z(o,t),s=o.translations&&o.translations[n];if(s&&typeof s=="object"&&Object.keys(s).length||t.translations&&t.translations[n])return!1;try{await e.call("loadTranslations",n,{inFlight:!0})}catch{}return!0},ue=/\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g,de=e=>e&&typeof e.parse=="function"?e.parse():e,qt=(e,t)=>{const o=de(t?.translations),n={};for(const s of[e,o])if(!(!s||typeof s!="object"))for(const r in s){const i=de(s[r]);i&&typeof i=="object"&&(n[r]={...n[r]||{},...i})}return Object.keys(n).length?n:null},Ae=(e,t,o)=>e[t]||e[X(void 0,o)]||Object.values(e)[0]||{},qe=(e,t)=>e.replace(ue,(o,n)=>Q(t,n.trim())??o),Ct=/<script\b[^>]*\btype\s*=\s*["']?application\/json["']?[^>]*>[\s\S]*?<\/script\s*>/gi,Mt=(e,t)=>new RegExp(`(?:^|[^\\w.-])${Ze(t)}(?![\\w-]|\\.\\w)`).test(e),It=(e,t,o,n,s)=>{const r=new Set,i=t.replace(Ct,"");for(const d of e)/\s/.test(d)||Se(d,o,n,s)===void 0&&Mt(i,d)&&r.add(d);for(const d of i.matchAll(ue))r.add(d[1].trim());return[...r]},xo=async(e,t={})=>{const{context:o={}}=t,{window:n,document:s}=ge(),r=s.body,{create:i}=await import("@symbo.ls/element"),d=await import("@symbo.ls/utils"),l={...pt,...o.components||{}},g={...d,...o.utils||{},...o.functions||{}};he();let f;try{f=i(e,{node:r},"root",{context:{document:s,window:n,...o,components:l,utils:g}})}catch{}ye(r);const a=f?be(f):{};return{html:Ce(r.innerHTML),registry:a,element:f}},Ce=e=>e.replace(/(<svg\b[^>]*>)([\s\S]*?)(<\/svg>)/gi,(t,o,n,s)=>{if(n.includes("&lt;")){const r=n.replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,"&").replace(/&quot;/g,'"').replace(/&#39;/g,"'");return o+r+s}return t}),Ot=["globalTheme","themeStorageKey","themeRoot","useReset","useVariable","useFontImport","useIconSprite","useSvgSprite","useDocumentTheme","useDefaultConfig","useDefaultIcons","useThemeSuffixedVars","verbose","semanticIcons"],Pt=/\.[A-Za-z_\\-]/,Ft=e=>{if(typeof e!="string"||!e)return!1;const t=e.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,'""').replace(/\[[^\]]*\]/g,"[]");return!Pt.test(t)},Bt=/^\s*(:root|\[data-theme|\[data-skin)/,Me=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 Ft(e.selectorText)?Bt.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(Me));return n.has(null)||n.size!==1?n.has(null)?null:"reset":[...n][0]}return null},Gt=e=>{const t={root:[],reset:[],keyframes:[]},o=[];for(const n of e||[]){const s=Me(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 te=new Set;const so=()=>{te=new Set},no=()=>Array.from(te).join(`
6
- `),ro=(e,t)=>e.replace(/<style data-emotion="smbls">[\s\S]*?<\/style>/,t?`<style data-emotion="smbls">
5
+ `),fontFaceCSS:""},classRules:o}};let oe=new Set;const jo=()=>{oe=new Set},_o=()=>Array.from(oe).join(`
6
+ `),Eo=(e,t)=>e.replace(/<style data-emotion="smbls">[\s\S]*?<\/style>/,t?`<style data-emotion="smbls">
7
7
  ${t}
8
- </style>`:""),co=async(e,t={})=>{const{route:o="/",pathname:s,prefetch:r=!0}=t,n=await _e(e,{route:o,pathname:s,prefetch:r});if(!n)return null;const l=e.designSystem||{},u=n.globalCSS;let a=null;try{const f=xt(n.element);Object.keys(f).length&&(a=f)}catch{}const y=n.activeLang;return{html:n.html,css:n.classRules.join(`
9
- `),classRules:n.classRules.slice(),globalCSS:u,resetCss:u.resetRules,fontLinks:Ae(l),metadata:n.metadata||se(e,o),brKeyCount:n.registry?Object.keys(n.registry).length:0,brRegistry:n.brRegistry||{},fetchSeed:n.fetchSeed||[],ssrTranslations:n.ssrTranslations,prefetchedState:a,activeLang:y,diagnostics:n.diagnostics}},xt=e=>{const t={};return ke(e,o=>{const s=o.fetch;if(!(!s||typeof s!="object"||!o.state))for(const r of Array.isArray(s)?s:[s]){const n=r&&typeof r=="object"&&typeof r.as=="string"?r.as:null;if(!n)continue;const l=n.startsWith("~/"),u=l?n.slice(2):n,a=l?o.state.root:o.state,y=fe(a&&a[u]);u&&y!=null&&typeof y!="function"&&(t[u]=y)}}),t},io=async(e,t="/",o={})=>{const{lang:s,themeColor:r,isr:n,hydrate:l=!0,prefetch:u=!0,shell:a,adoptDom:y=!1,pathname:f,eagerImages:i=qe,preloadScripts:c=!0,nonce:d}=o,p=Tt(d),h=s||e.state?.lang||e.app?.metadata?.lang||"en",g=await _e(e,{route:t,pathname:f,prefetch:u});if(!g)return null;const q={...g.metadata};if(r&&(q["theme-color"]=r),typeof a=="string"&&!e.name){const k="\0brender-undeclared-title\0";if(se({...e,name:k},t,g.element,g.element?.state).title===k)for(const E of["title","og:title","twitter:title"])q[E]===g.metadata.title&&delete q[E]}const T=Be(q);for(const k of g.classRules)k&&te.add(k);const B=Array.from(te).join(`
10
- `),I=g.globalCSS,O=`<style data-brender>
8
+ </style>`:""),vo=async(e,t={})=>{const{route:o="/",pathname:n,prefetch:s=!0,timeout:r}=t,i=await $e(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 a=Kt(i.element);Object.keys(a).length&&(g=a)}catch{}const f=i.activeLang;return{html:i.html,css:i.classRules.join(`
9
+ `),classRules:i.classRules.slice(),globalCSS:l,resetCss:l.resetRules,fontLinks:Oe(d),metadata:i.metadata||ae(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,preparedDesignSystem:i.preparedDesignSystem||null}},Kt=e=>{const t={};return je(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=de(l&&l[d]);d&&g!=null&&typeof g!="function"&&(t[d]=g)}}),t},To=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:a=Ie,preloadScripts:c=!0,nonce:p,timeout:m,stamp:b,time:S}=o,R=zt(p),W=n||e.state?.lang||e.app?.metadata?.lang||"en",h=await $e(e,{route:t,pathname:f,prefetch:d,timeout:m});if(!h)return null;const A={...h.metadata};if(s&&(A["theme-color"]=s),typeof l=="string"&&!e.name){const w="\0brender-undeclared-title\0";if(ae({...e,name:w},t,h.element,h.element?.state).title===w)for(const D of["title","og:title","twitter:title"])A[D]===h.metadata.title&&delete A[D]}const L=We(A);for(const w of h.classRules)w&&oe.add(w);const B=Array.from(oe).join(`
10
+ `),I=h.globalCSS,O=`<style data-brender>
11
11
  ${I.rootRule||""}
12
12
  ${I.resetRules||""}
13
13
  ${I.keyframeRules||""}
14
- </style>`,j=B?`<style data-emotion="smbls">
14
+ </style>`,E=B?`<style data-emotion="smbls">
15
15
  ${B}
16
- </style>`:"",z=Ae(e.designSystem||{}),Y=Object.keys(g.registry).length,A=Et({brRegistry:g.brRegistry,fetchSeed:g.fetchSeed,adoptDom:y});let K="";if(n&&n.clientScript){const k=Number.isInteger(o.depth)?o.depth:t==="/"?0:t.replace(/^\/|\/$/g,"").split("/").length,w=k>0?"../".repeat(k):"./";if(l){let E="";if(g.ssrTranslations){const H=e.polyglot||e.config?.polyglot,S=H?.storagePrefix||"",R=H?.storageLangKey||"",L=[];for(const M in g.ssrTranslations){const J=g.ssrTranslations[M];J&&typeof J=="object"&&L.push(`localStorage.setItem(${JSON.stringify(S+M)},${JSON.stringify(JSON.stringify(J))})`)}if(R){const M=U(void 0,H);L.push(`if(!localStorage.getItem(${JSON.stringify(R)}))localStorage.setItem(${JSON.stringify(R)},${JSON.stringify(M)})`)}L.length&&(E=`<script${p}>try{${L.join(";")}}catch(e){}<\/script>
17
- `)}const G=A.blocks?A.blocks+`
18
- `:"";K=`${E}${G}<script type="module" src="${w}${n.clientScript}"${p}><\/script>`}else K=`<script type="module"${p}>
16
+ </style>`:"",J=Oe(e.designSystem||{}),z=Object.keys(h.registry).length,q=Wt({brRegistry:h.brRegistry,fetchSeed:h.fetchSeed,adoptDom:g,stamp:b,time:S});let G="";if(r&&r.clientScript){const w=Number.isInteger(o.depth)?o.depth:t==="/"?0:t.replace(/^\/|\/$/g,"").split("/").length,j=w>0?"../".repeat(w):"./";if(i){const D=nt({polyglot:e.polyglot||e.config?.polyglot,translations:h.ssrTranslations,lang:h.activeLang,pathname:f||t,attrs:R}),N=D?D+`
17
+ `:"",ne=q.blocks?q.blocks+`
18
+ `:"";G=`${N}${ne}<script type="module" src="${j}${r.clientScript}"${R}><\/script>`}else G=`<script type="module"${R}>
19
19
  {
20
20
  const brEls = document.querySelectorAll('body > :not(script):not(style)')
21
21
  const observer = new MutationObserver((mutations) => {
@@ -32,22 +32,23 @@ ${B}
32
32
  observer.observe(document.body, { childList: true })
33
33
  }
34
34
  <\/script>
35
- <script type="module" src="${w}${n.clientScript}"${p}><\/script>`}const N={...e.config||{}};e.polyglot&&!N.polyglot&&(N.polyglot=e.polyglot);const v=N.polyglot;let C=T;if(v){const k={...v.translations||{}};for(const w in g.ssrTranslations||{})k[w]={...k[w]||{},...g.ssrTranslations[w]};C=Te(T,Ee(k,g.activeLang,v))}const D=g.diagnostics;if(D)for(const k of C.matchAll(ae)){const w=k[1].trim();D.rawLabelKeys.includes(w)||D.rawLabelKeys.push(w)}const V=_t(g.html,i);return typeof a=="string"?{html:Ke(a,{headTags:`${C}
36
- ${A.meta}`,headEnd:[z,O,j].filter(Boolean).join(`
37
- `),body:V,depth:Number.isInteger(o.depth)?o.depth:0,bootScripts:A.blocks,lang:s||(v?g.activeLang:void 0),preloadScripts:c}),route:t,brKeyCount:Y,shell:!0,diagnostics:D}:{html:`<!DOCTYPE html>
38
- <html lang="${h}">
35
+ <script type="module" src="${j}${r.clientScript}"${R}><\/script>`}const K={...e.config||{}};e.polyglot&&!K.polyglot&&(K.polyglot=e.polyglot);const v=K.polyglot;let C=L;if(v){const w={...v.translations||{}};for(const j in h.ssrTranslations||{})w[j]={...w[j]||{},...h.ssrTranslations[j]};C=qe(L,Ae(w,h.activeLang,v))}const M=h.diagnostics;if(M)for(const w of C.matchAll(ue)){const j=w[1].trim();M.rawLabelKeys.includes(j)||M.rawLabelKeys.push(j)}const Y=Ut(h.html,a);return typeof l=="string"?{html:Je(l,{headTags:`${C}
36
+ ${q.meta}`,headEnd:[J,O,E].filter(Boolean).join(`
37
+ `),body:Y,depth:Number.isInteger(o.depth)?o.depth:0,bootScripts:q.blocks,lang:n||(v?h.activeLang:void 0),preloadScripts:c}),route:t,brKeyCount:z,shell:!0,diagnostics:M,preparedDesignSystem:h.preparedDesignSystem||null}:{html:`<!DOCTYPE html>
38
+ <html lang="${W}">
39
39
  <head>
40
- ${C}${n&&n.clientScript&&l?`
41
- ${A.meta}`:""}
42
- ${z}
40
+ ${C}${r&&r.clientScript&&i?`
41
+ ${q.meta}`:""}
42
+ ${J}
43
43
 
44
44
  ${O}
45
- ${j}
45
+ ${E}
46
46
  </head>
47
47
  <body>
48
- ${V}
49
- ${K}
48
+ ${Y}
49
+ ${G}
50
50
  </body>
51
- </html>`,route:t,brKeyCount:Y,diagnostics:D}},qe=2,jt=/<img\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi,vt=/\sloading\s*=/i,_t=(e,t=qe)=>{if(t===!1||t==null||typeof e!="string")return e;const o=Math.max(0,Number(t)||0);let s=0;return e.replace(jt,r=>s++<o||vt.test(r)?r:r.replace(/^<img\b/i,'<img loading="lazy"'))},Ae=e=>{if(!e)return"";const t=e.font_family||e.fontFamily||{},o=new Set;for(const r of Object.values(t)){if(typeof r!="string")continue;const n=r.match(/'([^']+)'/);n&&o.add(n[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(r=>`family=${r.replace(/\s+/g,"+")}:wght@300;400;500;600;700`).join("&")}&display=swap" rel="stylesheet">`].join(`
52
- `):""},Et=({brRegistry:e,fetchSeed:t,adoptDom:o=!1}={})=>{const s=o&&e&&Object.keys(e).length?Je(e):"",r=Ye(t);return{meta:`<meta name="brender" content="${s?"hydrate":"replace"}">`,blocks:[s,r].filter(Boolean).join(`
53
- `)}},Tt=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{qe as EAGER_IMAGES_DEFAULT,Se as SMBLS_SSR_SPECIFIER,to as adoptionScripts,no as getAccumulatedEmotionCSS,_t as markLazyImages,Et as prerenderParts,_e as render,oo as renderElement,io as renderPage,co as renderRoute,ro as replaceEmotionCSS,so as resetGlobalCSSCache,wt as splitCapturedCSS};
51
+ </html>`,route:t,brKeyCount:z,diagnostics:M,preparedDesignSystem:h.preparedDesignSystem||null}},Ie=2,Nt=/<img\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi,Ht=/\sloading\s*=/i,Ut=(e,t=Ie)=>{if(t===!1||t==null||typeof e!="string")return e;const o=Math.max(0,Number(t)||0);let n=0;return e.replace(Nt,s=>n++<o||Ht.test(s)?s:s.replace(/^<img\b/i,'<img loading="lazy"'))},Oe=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
+ `):""},Wt=({brRegistry:e,fetchSeed:t,adoptDom:o=!1,stamp:n,time:s}={})=>{const r=o&&e&&Object.keys(e).length?Ve(e):"",i=rt(t);return{meta:`<meta name="brender" content="${r?"hydrate":"replace"}">
53
+ `+Qe(Jt(n,s)),blocks:[r,i].filter(Boolean).join(`
54
+ `)}},Jt=(e,t)=>{const o=typeof t=="number"&&isFinite(t)?Math.round(t):Date.now();return{stamp:e!=null&&e!==""?String(e):new Date(o).toISOString(),time:o}},zt=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{Ie as EAGER_IMAGES_DEFAULT,Re as SMBLS_SSR_SPECIFIER,Ro as adoptionScripts,Jt as buildStamp,_o as getAccumulatedEmotionCSS,Ut as markLazyImages,Wt as prerenderParts,$e as render,xo as renderElement,To as renderPage,vo as renderRoute,Eo as replaceEmotionCSS,jo as resetGlobalCSSCache,Gt as splitCapturedCSS};
package/dist/esm/shell.js CHANGED
@@ -1,9 +1,9 @@
1
- const I=/<head(\s[^>]*)?>/i,P=/<body(\s[^>]*)?>/i,f=(e,n)=>{const t=new RegExp(`\\s${n}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`,"i").exec(e);return t?t[2]??t[3]??t[4]??"":null},S=e=>{const n=e.slice(0,12).toLowerCase();if(n.startsWith("<title"))return"title";if(n.startsWith("<meta")){if(f(e,"charset")!==null)return"charset";for(const t of["name","property","http-equiv","itemprop"]){const r=f(e,t);if(r!==null)return`${t}:${r.toLowerCase()}`}return null}if(n.startsWith("<link")){const t=(f(e,"rel")||"").toLowerCase();return t==="canonical"?"link:canonical":t==="icon"||t==="shortcut icon"?`link:${t}:${f(e,"href")||""}`:t==="alternate"&&f(e,"hreflang")!==null?`link:alternate:${(f(e,"hreflang")||"").toLowerCase()}`:null}return null},v=/<title[^>]*>[\s\S]*?<\/title\s*>|<meta\b[^>]*>|<link\b[^>]*>/gi,_=/<(script|style|noscript|template|textarea)\b[\s\S]*?<\/\1\s*>/gi,x=e=>[...e.matchAll(_)].map(n=>[n.index,n.index+n[0].length,n[0]]),L=(e,n,t=r=>r)=>{let r="",i=0;for(const[o,l,a]of x(e))r+=n(e.slice(i,o))+t(a),i=l;return r+n(e.slice(i))},d=(e,n,t=0)=>{const r=x(e),i=new RegExp(n.source,n.flags.includes("g")?n.flags:n.flags+"g");i.lastIndex=t;let o;for(;o=i.exec(e);){const l=o.index,a=r.find(([s,u])=>l>s&&l<u);if(!a)return o;i.lastIndex=a[1]}return null},j=e=>!!e&&!/^(?:[a-z][a-z0-9+.-]*:|\/|#|\?)/i.test(e),E=(e,n)=>e.replace(/(\s(?:src|href)\s*=\s*)(?:(["'])([^"']*)\2|([^\s"'>]+))/gi,(t,r,i,o,l)=>{const a=i?o:l;if(!j(a))return t;const s=i||"";return`${r}${s}${n}${a.replace(/^\.\//,"")}${s}`}),q=(e,n)=>{if(!n)return e;const t="../".repeat(n);return L(e,r=>r.replace(/<link\b[^>]*>/gi,i=>E(i,t)),r=>/^<script\b/i.test(r)?r.replace(/^<script\b[^>]*>/i,i=>E(i,t)):r)},W=new Set(["","module","text/javascript","application/javascript"]),m=(e,n)=>new RegExp(`\\s${n}(?:\\s*=|\\s|>|/)`,"i").test(e),B=e=>{const n=[],t=new Set;for(const[,,r]of x(e)){if(!/^<script\b/i.test(r))continue;const i=r.slice(0,r.indexOf(">")+1),o=f(i,"src");if(!o||t.has(o))continue;const l=(f(i,"type")||"").trim().toLowerCase();if(!W.has(l)||m(i,"async")||m(i,"nomodule"))continue;t.add(o);const a=["crossorigin","integrity","referrerpolicy"].map(u=>{const h=f(i,u);return h===null?m(i,u)?` ${u}`:"":` ${u}="${h.replace(/"/g,"&quot;")}"`}).join(""),s=o.replace(/"/g,"&quot;");n.push(l==="module"?`<link rel="modulepreload" href="${s}" fetchpriority="high"${a}>`:`<link rel="preload" as="script" href="${s}" fetchpriority="high"${a}>`)}return n.join(`
2
- `)},D=(e,n)=>{const t=d(e,/<html(\s[^>]*)?>/i);if(!t)return e;const r=t[0],i=String(n).replace(/"/g,"&quot;"),o=f(r,"lang")!==null?r.replace(/(\slang\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/i,`$1"${i}"`):r.replace(/^<html/i,`<html lang="${i}"`);return e.slice(0,t.index)+o+e.slice(t.index+r.length)},H=(e,{headTags:n="",headEnd:t="",body:r="",depth:i=0,bootScripts:o="",lang:l,preloadScripts:a=!1}={})=>{if(typeof e!="string"||!e.trim())throw new Error("brender: the shell document is empty");let s=q(e,i);l&&(s=D(s,l));const u=a?B(s):"";u&&(t=t?`${u}
3
- ${t}`:u);const h=new Set;for(const c of n.matchAll(v)){const p=S(c[0]);p&&h.add(p)}const w=c=>L(c,p=>p.replace(v,R=>h.has(S(R))?"":R)),y=n?`
1
+ import{insertAfterImportMap as E}from"@symbo.ls/utils";const P=/<head(\s[^>]*)?>/i,_=/<body(\s[^>]*)?>/i,f=(e,n)=>{const t=new RegExp(`\\s${n}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`,"i").exec(e);return t?t[2]??t[3]??t[4]??"":null},S=e=>{const n=e.slice(0,12).toLowerCase();if(n.startsWith("<title"))return"title";if(n.startsWith("<meta")){if(f(e,"charset")!==null)return"charset";for(const t of["name","property","http-equiv","itemprop"]){const r=f(e,t);if(r!==null)return`${t}:${r.toLowerCase()}`}return null}if(n.startsWith("<link")){const t=(f(e,"rel")||"").toLowerCase();return t==="canonical"?"link:canonical":t==="icon"||t==="shortcut icon"?`link:${t}:${f(e,"href")||""}`:t==="alternate"&&f(e,"hreflang")!==null?`link:alternate:${(f(e,"hreflang")||"").toLowerCase()}`:null}return null},v=/<title[^>]*>[\s\S]*?<\/title\s*>|<meta\b[^>]*>|<link\b[^>]*>/gi,j=/<(script|style|noscript|template|textarea)\b[\s\S]*?<\/\1\s*>/gi,x=e=>[...e.matchAll(j)].map(n=>[n.index,n.index+n[0].length,n[0]]),L=(e,n,t=r=>r)=>{let r="",o=0;for(const[i,l,a]of x(e))r+=n(e.slice(o,i))+t(a),o=l;return r+n(e.slice(o))},d=(e,n,t=0)=>{const r=x(e),o=new RegExp(n.source,n.flags.includes("g")?n.flags:n.flags+"g");o.lastIndex=t;let i;for(;i=o.exec(e);){const l=i.index,a=r.find(([s,p])=>l>s&&l<p);if(!a)return i;o.lastIndex=a[1]}return null},q=e=>!!e&&!/^(?:[a-z][a-z0-9+.-]*:|\/|#|\?)/i.test(e),I=(e,n)=>e.replace(/(\s(?:src|href)\s*=\s*)(?:(["'])([^"']*)\2|([^\s"'>]+))/gi,(t,r,o,i,l)=>{const a=o?i:l;if(!q(a))return t;const s=o||"";return`${r}${s}${n}${a.replace(/^\.\//,"")}${s}`}),W=(e,n)=>{if(!n)return e;const t="../".repeat(n);return L(e,r=>r.replace(/<link\b[^>]*>/gi,o=>I(o,t)),r=>/^<script\b/i.test(r)?r.replace(/^<script\b[^>]*>/i,o=>I(o,t)):r)},B=new Set(["","module","text/javascript","application/javascript"]),m=(e,n)=>new RegExp(`\\s${n}(?:\\s*=|\\s|>|/)`,"i").test(e),D=e=>{const n=[],t=new Set;for(const[,,r]of x(e)){if(!/^<script\b/i.test(r))continue;const o=r.slice(0,r.indexOf(">")+1),i=f(o,"src");if(!i||t.has(i))continue;const l=(f(o,"type")||"").trim().toLowerCase();if(!B.has(l)||m(o,"async")||m(o,"nomodule"))continue;t.add(i);const a=["crossorigin","integrity","referrerpolicy"].map(p=>{const h=f(o,p);return h===null?m(o,p)?` ${p}`:"":` ${p}="${h.replace(/"/g,"&quot;")}"`}).join(""),s=i.replace(/"/g,"&quot;");n.push(l==="module"?`<link rel="modulepreload" href="${s}" fetchpriority="high"${a}>`:`<link rel="preload" as="script" href="${s}" fetchpriority="high"${a}>`)}return n.join(`
2
+ `)},H=(e,n)=>{const t=d(e,/<html(\s[^>]*)?>/i);if(!t)return e;const r=t[0],o=String(n).replace(/"/g,"&quot;"),i=f(r,"lang")!==null?r.replace(/(\slang\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/i,`$1"${o}"`):r.replace(/^<html/i,`<html lang="${o}"`);return e.slice(0,t.index)+i+e.slice(t.index+r.length)},z=(e,{headTags:n="",headEnd:t="",body:r="",depth:o=0,bootScripts:i="",lang:l,preloadScripts:a=!1}={})=>{if(typeof e!="string"||!e.trim())throw new Error("brender: the shell document is empty");let s=W(e,o);l&&(s=H(s,l));const p=a?D(s):"";if(p){const c=E(s,p);c!==null?s=c:t=t?`${p}
3
+ ${t}`:p}const h=new Set;for(const c of n.matchAll(v)){const u=S(c[0]);u&&h.add(u)}const w=c=>L(c,u=>u.replace(v,R=>h.has(S(R))?"":R)),y=n?`
4
4
  ${n}`:"",O=t?`
5
5
  ${t}
6
- `:"",g=d(s,I),b=g&&d(s,/<\/head\s*>/i,g.index);if(g&&b){const c=g.index+g[0].length,p=b.index;s=s.slice(0,c)+y+w(s.slice(c,p))+O+s.slice(p)}else{s=w(s);const c=d(s,/<html(\s[^>]*)?>/i)||d(s,/<!doctype[^>]*>/i),p=c?c.index+c[0].length:0;s=s.slice(0,p)+y+O+s.slice(p)}const k=`
6
+ `:"",g=d(s,P),b=g&&d(s,/<\/head\s*>/i,g.index);if(g&&b){const c=g.index+g[0].length,u=b.index;s=s.slice(0,c)+y+w(s.slice(c,u))+O+s.slice(u)}else{s=w(s);const c=d(s,/<html(\s[^>]*)?>/i)||d(s,/<!doctype[^>]*>/i),u=c?c.index+c[0].length:0;s=s.slice(0,u)+y+O+s.slice(u)}const k=`
7
7
  ${r}
8
- ${o?o+`
9
- `:""}`,$=d(s,P);if($){const c=$.index+$[0].length;return s.slice(0,c)+k+s.slice(c)}const C=d(s,/<\/(?:body|html)\s*>/i),A=C?C.index:s.length;return s.slice(0,A)+k+s.slice(A)};export{H as composeIntoShell,S as headTagKey,B as scriptPreloads};
8
+ ${i?i+`
9
+ `:""}`,$=d(s,_);if($){const c=$.index+$[0].length;return s.slice(0,c)+k+s.slice(c)}const A=d(s,/<\/(?:body|html)\s*>/i),C=A?A.index:s.length;return s.slice(0,C)+k+s.slice(C)};export{z as composeIntoShell,S as headTagKey,D as scriptPreloads};
package/env.js CHANGED
@@ -108,8 +108,9 @@ export const createEnv = (html = '<!DOCTYPE html><html><head></head><body></body
108
108
  // them): `localStorage` / `sessionStorage` are the newest environment's
109
109
  // stores, and `history` is the first environment's stub when the host has
110
110
  // none. render() swaps the bare storage and location to the rendering
111
- // environment's own for the length of a render; overlapping renders still
112
- // share them (a render queue or per-render globals — an open decision).
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").
113
114
  globalThis.localStorage = own.localStorage
114
115
  globalThis.sessionStorage = own.sessionStorage
115
116
  if (!globalThis.history) globalThis.history = own.history
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createEnv } from './env.js'
2
2
  import { resetKeys, assignKeys, mapKeysToElements } from './keys.js'
3
3
  import { loadProject, loadAndRenderAll } from './load.js'
4
- import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache, getAccumulatedEmotionCSS, replaceEmotionCSS, adoptionScripts, prerenderParts } from './render.js'
4
+ import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache, getAccumulatedEmotionCSS, replaceEmotionCSS, adoptionScripts, prerenderParts, buildStamp } from './render.js'
5
5
  import { extractMetadata, generateHeadHtml } from './metadata.js'
6
6
  import { collectBrNodes, hydrate } from './hydrate.js'
7
7
  import { generateSitemap } from './sitemap.js'
@@ -31,7 +31,8 @@ export {
31
31
  injectPrefetchedState,
32
32
  composeIntoShell,
33
33
  adoptionScripts,
34
- prerenderParts
34
+ prerenderParts,
35
+ buildStamp
35
36
  }
36
37
 
37
38
  export default {
@@ -57,5 +58,6 @@ export default {
57
58
  injectPrefetchedState,
58
59
  composeIntoShell,
59
60
  adoptionScripts,
60
- prerenderParts
61
+ prerenderParts,
62
+ buildStamp
61
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/brender",
3
- "version": "3.14.666",
3
+ "version": "3.14.668",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "module": "./dist/esm/index.js",
@@ -46,18 +46,18 @@
46
46
  "prepublishOnly": "npm run build"
47
47
  },
48
48
  "dependencies": {
49
- "@symbo.ls/css": "^3.14.779",
50
- "@symbo.ls/default-config": "^3.14.787",
51
- "@symbo.ls/element": "^3.14.805",
52
- "@symbo.ls/fetch": "^3.14.666",
49
+ "@symbo.ls/css": "^3.14.780",
50
+ "@symbo.ls/default-config": "^3.14.788",
51
+ "@symbo.ls/element": "^3.14.806",
52
+ "@symbo.ls/fetch": "^3.14.667",
53
53
  "@symbo.ls/helmet": "^3.14.670",
54
- "@symbo.ls/scratch": "^3.14.789",
55
- "@symbo.ls/utils": "^3.14.787",
54
+ "@symbo.ls/scratch": "^3.14.790",
55
+ "@symbo.ls/utils": "^3.14.789",
56
56
  "esbuild": "^0.28.1",
57
57
  "linkedom": "^0.16.8"
58
58
  },
59
59
  "peerDependencies": {
60
- "smbls": "^3.14.839"
60
+ "smbls": "^3.14.840"
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, brenderStampMeta, 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
@@ -718,21 +735,6 @@ const walkElements = (root, visit) => {
718
735
  walk(root)
719
736
  }
720
737
 
721
- /**
722
- * Renders a Symbols/DOMQL project to HTML on the server.
723
- *
724
- * Accepts project data as a plain object (matching what ProjectDataService provides)
725
- * or as a pre-loaded smbls context. Runs DOMQL in a linkedom virtual DOM,
726
- * assigns data-br keys for hydration, and extracts page metadata for SEO.
727
- *
728
- * @param {object} data - Project data object with: pages, components, designSystem,
729
- * state, functions, methods, snippets, files, app, config/settings
730
- * @param {object} [options]
731
- * @param {string} [options.route='/'] - The route/page to render
732
- * @param {object} [options.state] - State overrides
733
- * @param {object} [options.context] - Additional context overrides
734
- * @returns {Promise<{ html: string, metadata: object, registry: object, element: object }>}
735
- */
736
738
  // The payload keys render() puts on the context itself (below `ctx`): the
737
739
  // generic copy of every other top-level key skips them — cloning one only for
738
740
  // the explicit key to replace it was a deep clone of `functions`, `methods`
@@ -761,7 +763,9 @@ const inFlightRequests = () => _inFlight
761
763
  // can carry them (@symbo.ls/fetch createEarlySeedScript) and the hydrating
762
764
  // client adopts them instead of asking again. A second render overlapping
763
765
  // in the same isolate taints the recording: another project's answers must
764
- // 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.
765
769
  // Only a GET whose headers are Accept-Language / Accept at most is recorded
766
770
  // (never a signed-in or keyed call), only an ok JSON or text answer, at most
767
771
  // SEED_ENTRY_MAX bytes each and SEED_TOTAL_MAX per page.
@@ -919,7 +923,147 @@ export const adoptionScripts = ({ brRegistry, fetchSeed, hydrateFlag = true } =
919
923
  (fetchSeed && fetchSeed.length ? '\n' + createEarlySeedScript(fetchSeed) : '')
920
924
  }
921
925
 
922
- export const render = async (data, options = {}) => {
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) => {
923
1067
  const { route = '/', pathname, state: stateOverrides, context: contextOverrides, prefetch = false } = options
924
1068
  // pathname is the actual URL path (e.g. /podcast/abc-123), route is the page pattern (e.g. /podcast/:id)
925
1069
  const locationPath = pathname || route
@@ -928,13 +1072,19 @@ export const render = async (data, options = {}) => {
928
1072
  trackFetches()
929
1073
  const diag = openRenderRecord(DIAG, () => ({ failures: [], labelKeys: new Set(), tainted: false }))
930
1074
  try {
931
- return await renderWithDiagnostics(data, { route, locationPath, stateOverrides, contextOverrides, prefetch }, diag.record)
1075
+ return await renderWithDiagnostics(data, { route, locationPath, stateOverrides, contextOverrides, prefetch }, diag.record, turn)
932
1076
  } finally {
933
1077
  diag.close()
934
1078
  }
935
1079
  }
936
1080
 
937
- 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
+
938
1088
  // The page's data is NOT fetched here. The fetch plugin runs during the
939
1089
  // render below with the real element — its `transform(data, el, state)`,
940
1090
  // its `children` re-run on the answer — and the post-create flush waits
@@ -958,8 +1108,9 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
958
1108
  try {
959
1109
  ssrTranslations = await fetchSSRTranslations(data)
960
1110
  } catch (e) {
961
- console.warn('[brender] SSR translation fetch failed:', e.message)
1111
+ if (!turn.closed) console.warn('[brender] SSR translation fetch failed:', e.message)
962
1112
  }
1113
+ live()
963
1114
  }
964
1115
 
965
1116
  const { window, document } = createEnv()
@@ -975,19 +1126,23 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
975
1126
  // loaded there, and one shared store handed them to the next page of the
976
1127
  // build (createEnv). Swapped by descriptor and put back however the render
977
1128
  // ends, a throw included — a host's own Web Storage global (Node's is a
978
- // 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.
979
1132
  const restoreGlobals = [
980
1133
  swapGlobal('document', document),
981
1134
  swapGlobal('location', window.location),
982
1135
  swapGlobal('localStorage', window.localStorage),
983
1136
  swapGlobal('sessionStorage', window.sessionStorage)
984
- ]
1137
+ ].map(once)
1138
+ turn.undo.push(...restoreGlobals)
985
1139
  try {
986
1140
 
987
1141
  // Import createDomqlElement via bundled smbls source.
988
1142
  // The smbls monorepo uses extensionless/directory imports that Node.js ESM
989
1143
  // can't resolve natively, so we bundle it with esbuild first.
990
1144
  const { createDomqlElement } = await bundleCreateDomql()
1145
+ live()
991
1146
 
992
1147
  const app = structuredCloneDeep(data.app || {})
993
1148
 
@@ -1003,6 +1158,12 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
1003
1158
 
1004
1159
  // Inject SSR translations into polyglot config and root state
1005
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
+
1006
1167
  if (ssrTranslations && polyglotConfig) {
1007
1168
  polyglotConfig.translations = {
1008
1169
  ...(polyglotConfig.translations || {}),
@@ -1101,12 +1262,19 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
1101
1262
  // not carry them: the browser asked for them again after boot (xma.info,
1102
1263
  // 2026-10-16: index.html 10 seed entries, about.html 3).
1103
1264
  isolateFetchCache: true,
1265
+ // Keep the design system this render prepared — the processed config and
1266
+ // its global CSS — for the page to adopt instead of preparing it again
1267
+ // (smbls prepare.js, "The design system prepared at build time"; every
1268
+ // render returns it as `preparedDesignSystem`, and `smbls build` writes
1269
+ // it into one file each page preloads).
1270
+ captureDesignSystem: true,
1104
1271
  // Caller overrides
1105
1272
  ...(contextOverrides || {})
1106
1273
  }
1107
1274
 
1108
1275
  // Add funcql plugin if available (enables funcql schema evaluation in exec())
1109
1276
  const funcql = await getFuncqlPlugin()
1277
+ live()
1110
1278
  if (funcql) {
1111
1279
  ctx.plugins = ctx.plugins || []
1112
1280
  if (!ctx.plugins.some(p => p.name === 'funcql')) {
@@ -1123,6 +1291,7 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
1123
1291
  let element
1124
1292
  try {
1125
1293
  element = await createDomqlElement(app, ctx)
1294
+ live()
1126
1295
 
1127
1296
  // Allow async operations (fetch callbacks, state updates, re-renders) to flush.
1128
1297
  // DOMQL's fetch plugin fires on element creation and updates state
@@ -1141,15 +1310,32 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
1141
1310
  const flushCap = prefetch ? 2000 : 50
1142
1311
  const sleep = (ms) => new Promise(r => setTimeout(r, ms))
1143
1312
  await sleep(Math.min(50, flushCap))
1144
- while (prefetch && inFlightRequests() > 0 && Date.now() - flushStart < flushCap) {
1313
+ while (prefetch && !turn.closed && inFlightRequests() > 0 && Date.now() - flushStart < flushCap) {
1145
1314
  await sleep(10)
1146
1315
  // a settled batch: another quiet period for what it triggers
1147
1316
  if (inFlightRequests() === 0) await sleep(Math.max(0, Math.min(50, flushCap - (Date.now() - flushStart))))
1148
1317
  }
1318
+ live()
1319
+
1320
+ // The page's labels, when the project's polyglot.fetch is still loading
1321
+ // them: the render waits for that load past the flush's cap — a page
1322
+ // without its labels is a failed page, not a slower one — bounded by the
1323
+ // render's own time limit (see "One render at a time"). See awaitLabels.
1324
+ if (prefetch && await awaitLabels(element, polyglotConfig)) {
1325
+ live()
1326
+ // what the labels' answer triggers: another quiet period, and the
1327
+ // requests it starts, under the flush's cap once more
1328
+ const labelsAt = Date.now()
1329
+ await sleep(Math.min(50, flushCap))
1330
+ while (!turn.closed && inFlightRequests() > 0 && Date.now() - labelsAt < flushCap) await sleep(10)
1331
+ live()
1332
+ }
1149
1333
  } finally {
1150
1334
  recording.close()
1151
1335
  }
1152
- const fetchSeed = seed && !seed.tainted ? seed.entries.slice() : []
1336
+ // `fetch.seed: false` — the project adopts no recorded answer (the client
1337
+ // asks for everything itself): the page carries none.
1338
+ const fetchSeed = seed && !seed.tainted && !(config.fetch && config.fetch.seed === false) ? seed.entries.slice() : []
1153
1339
 
1154
1340
  // Assign data-br keys for hydration
1155
1341
  assignKeys(body)
@@ -1159,6 +1345,7 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
1159
1345
  // Build element path → brKey registry for client-side true hydration.
1160
1346
  // Walks the element tree after mapKeysToElements has set __brKey on each element.
1161
1347
  const { elementPath } = await import('@symbo.ls/element')
1348
+ live()
1162
1349
  const brRegistry = buildPathRegistry(element, elementPath)
1163
1350
 
1164
1351
  // Extract metadata for the rendered route
@@ -1210,7 +1397,7 @@ const renderWithDiagnostics = async (data, { route, locationPath, stateOverrides
1210
1397
  }
1211
1398
  : null
1212
1399
 
1213
- return { html, metadata, registry, brRegistry, element, emotionCSS, globalCSS, classRules, fetchSeed, document, window, ssrTranslations: labels || undefined, activeLang, diagnostics }
1400
+ return { html, metadata, registry, brRegistry, element, emotionCSS, globalCSS, classRules, fetchSeed, document, window, ssrTranslations: labels || undefined, activeLang, diagnostics, preparedDesignSystem: ctx.preparedDesignSystem || null }
1214
1401
  } finally {
1215
1402
  for (const restore of restoreGlobals) restore()
1216
1403
  }
@@ -1233,6 +1420,39 @@ const swapGlobal = (name, value) => {
1233
1420
  }
1234
1421
  }
1235
1422
 
1423
+ // ── Labels the render is still loading ──────────────────────────────────────
1424
+ // A function `polyglot.fetch` loads the page's labels inside the render (the
1425
+ // polyglot plugin calls it with the root element, see fetchSSRTranslations).
1426
+ // The flush waits for requests up to its cap; the labels' load is more than
1427
+ // a request (the project's loader, its own awaits, the body, the answer into
1428
+ // the root state), and the first render of a process — a cold connection,
1429
+ // a cold JIT, a page that renders a lot — took longer than the cap: `smbls
1430
+ // build` of xma.info wrote `/` with 49 raw label keys while its labels landed
1431
+ // 2.9 s after the render had ended (2026-10-18). So when the root has no
1432
+ // labels for its language yet, the render waits for the load in flight for
1433
+ // this app's root: the polyglot plugin's `loadTranslations(lang, { inFlight:
1434
+ // true })` hands it back (@symbo.ls/polyglot loadsInFlight) and starts
1435
+ // nothing — a load that already ended, failed or answered "unchanged", is not
1436
+ // asked again. True when it asked (the render then gives the answer a quiet
1437
+ // period).
1438
+ const awaitLabels = async (element, poly) => {
1439
+ if (!poly || !poly.fetch || !element || typeof element.call !== 'function') return false
1440
+ const root = element.state && element.state.root
1441
+ if (!root) return false
1442
+ const lang = activeLangOf(root, poly)
1443
+ const have = root.translations && root.translations[lang]
1444
+ if (have && typeof have === 'object' && Object.keys(have).length) return false
1445
+ if (poly.translations && poly.translations[lang]) return false
1446
+ try {
1447
+ // undefined at once when no load is in flight
1448
+ await element.call('loadTranslations', lang, { inFlight: true })
1449
+ } catch (e) {
1450
+ // the load reports its own failure (polyglot logs it; the request shows
1451
+ // in the diagnostics); the raw keys check names what is missing
1452
+ }
1453
+ return true
1454
+ }
1455
+
1236
1456
  // ── Labels after a render ───────────────────────────────────────────────────
1237
1457
 
1238
1458
  const LABEL_TEMPLATE = /\{\{\s*([^|{}]+?)\s*\|\s*polyglot\s*\}\}/g
@@ -1490,17 +1710,18 @@ export const replaceEmotionCSS = (html, newCSS) => {
1490
1710
  * @param {object} data - Full project data
1491
1711
  * @param {object} [options]
1492
1712
  * @param {string} [options.route='/'] - Route to render
1713
+ * @param {number|false} [options.timeout] - render()'s time limit (30 s by default)
1493
1714
  * @returns {Promise<{ html: string, css: string, resetCss: string, fontLinks: string, metadata: object, brKeyCount: number }>}
1494
1715
  */
1495
1716
  export const renderRoute = async (data, options = {}) => {
1496
1717
  // `prefetch: false` fetches no table/RPC labels first and does not wait for
1497
1718
  // the page's requests — the publish-time pre-render (smbls publish →
1498
1719
  // ssrCache) renders data-free.
1499
- const { route = '/', pathname, prefetch = true } = options
1720
+ const { route = '/', pathname, prefetch = true, timeout } = options
1500
1721
 
1501
1722
  // Use the full render pipeline which handles polyglot, prefetch, emotion, etc.
1502
1723
  // Pass pathname so the virtual DOM location reflects the actual URL (for dynamic routes)
1503
- const result = await render(data, { route, pathname, prefetch })
1724
+ const result = await render(data, { route, pathname, prefetch, timeout })
1504
1725
  if (!result) return null
1505
1726
 
1506
1727
  const ds = data.designSystem || {}
@@ -1536,7 +1757,11 @@ export const renderRoute = async (data, options = {}) => {
1536
1757
  activeLang,
1537
1758
  // failed requests and raw label keys (see `DIAG`); null when an
1538
1759
  // overlapping render made them unknowable
1539
- diagnostics: result.diagnostics
1760
+ diagnostics: result.diagnostics,
1761
+ // the design system this render prepared (smbls prepare.js), for the
1762
+ // caller to hand the page (@symbo.ls/utils designSystemLink /
1763
+ // designSystemBlock)
1764
+ preparedDesignSystem: result.preparedDesignSystem || null
1540
1765
  }
1541
1766
  }
1542
1767
 
@@ -1595,13 +1820,19 @@ const collectFetchedState = (element) => {
1595
1820
  * @param {string} [options.shell] - A built app document (e.g. dist/index.html)
1596
1821
  * to render INTO instead of emitting a standalone document (see shell.js).
1597
1822
  * The result then carries `shell: true`.
1823
+ * @param {number|false} [options.timeout] - render()'s time limit (30 s by
1824
+ * default; 0 or false: none) — see render()
1825
+ * @param {string|number} [options.stamp] - The build's data stamp, written into
1826
+ * the page (see buildStamp); default: the render time as an ISO string
1827
+ * @param {number} [options.time] - The build time in ms (see buildStamp);
1828
+ * default: now. A build passes one stamp and time to every page.
1598
1829
  * @param {number} [options.depth] - Directory depth of the output file: rebases
1599
1830
  * the shell's relative asset URLs (with `shell`, default 0) and the ISR
1600
1831
  * client script path (default: derived from the route).
1601
1832
  * @returns {Promise<{ html: string, route: string, brKeyCount: number, shell?: true }>}
1602
1833
  */
1603
1834
  export const renderPage = async (data, route = '/', options = {}) => {
1604
- const { lang, themeColor, isr, hydrate = true, prefetch = true, shell, adoptDom = false, pathname, eagerImages = EAGER_IMAGES_DEFAULT, preloadScripts = true, nonce } = options
1835
+ const { lang, themeColor, isr, hydrate = true, prefetch = true, shell, adoptDom = false, pathname, eagerImages = EAGER_IMAGES_DEFAULT, preloadScripts = true, nonce, timeout, stamp, time } = options
1605
1836
  const nonceAttr = nonceAttribute(nonce)
1606
1837
 
1607
1838
  // Detect lang from project config, app metadata, or default
@@ -1612,7 +1843,7 @@ export const renderPage = async (data, route = '/', options = {}) => {
1612
1843
  // canonical/og:url built from it read it. Without it a route was rendered
1613
1844
  // at its own pattern: the `/*` page wrote a `/*` canonical into 404.html
1614
1845
  // (xma.info report, brender build item 7).
1615
- const result = await render(data, { route, pathname, prefetch })
1846
+ const result = await render(data, { route, pathname, prefetch, timeout })
1616
1847
  if (!result) return null
1617
1848
 
1618
1849
  const metadata = { ...result.metadata }
@@ -1660,7 +1891,7 @@ export const renderPage = async (data, route = '/', options = {}) => {
1660
1891
  // and the data blocks after the markup — the registry under `adoptDom`,
1661
1892
  // and the recorded anonymous answers, so the client renders from the data
1662
1893
  // the server used instead of asking for all of it again.
1663
- const prerender = prerenderParts({ brRegistry: result.brRegistry, fetchSeed: result.fetchSeed, adoptDom })
1894
+ const prerender = prerenderParts({ brRegistry: result.brRegistry, fetchSeed: result.fetchSeed, adoptDom, stamp, time })
1664
1895
 
1665
1896
  // ISR: include client SPA bundle for hydration + data fetching
1666
1897
  let isrBody = ''
@@ -1681,26 +1912,21 @@ export const renderPage = async (data, route = '/', options = {}) => {
1681
1912
  // Seed client-side polyglot with SSR translations so that
1682
1913
  // el.call('polyglot', key) resolves immediately during hydration
1683
1914
  // instead of showing raw keys until the async fetch completes.
1684
- let translationSeed = ''
1685
- if (result.ssrTranslations) {
1686
- const polyglotCfg = data.polyglot || data.config?.polyglot
1687
- const storagePrefix = polyglotCfg?.storagePrefix || ''
1688
- const storageLangKey = polyglotCfg?.storageLangKey || ''
1689
- const seedEntries = []
1690
- for (const lang in result.ssrTranslations) {
1691
- const map = result.ssrTranslations[lang]
1692
- if (map && typeof map === 'object') {
1693
- seedEntries.push(`localStorage.setItem(${JSON.stringify(storagePrefix + lang)},${JSON.stringify(JSON.stringify(map))})`)
1694
- }
1695
- }
1696
- if (storageLangKey) {
1697
- const defaultLang = polyglotFallbackLang(undefined, polyglotCfg)
1698
- seedEntries.push(`if(!localStorage.getItem(${JSON.stringify(storageLangKey)}))localStorage.setItem(${JSON.stringify(storageLangKey)},${JSON.stringify(defaultLang)})`)
1699
- }
1700
- if (seedEntries.length) {
1701
- translationSeed = `<script${nonceAttr}>try{${seedEntries.join(';')}}catch(e){}</script>\n`
1702
- }
1703
- }
1915
+ // For a visitor with no language stored yet it stores the page's
1916
+ // language — the address's on a /<lang>/… page (see "The address's
1917
+ // language") — so the client starts where the server started, and its
1918
+ // first requests are the ones the page's fetch seed recorded. It stored
1919
+ // the default language: a fresh client on a /en page booted in the
1920
+ // default and asked for the header's menu again. One helper with the
1921
+ // platform renderer (@symbo.ls/utils polyglotStartupScript).
1922
+ const startup = polyglotStartupScript({
1923
+ polyglot: data.polyglot || data.config?.polyglot,
1924
+ translations: result.ssrTranslations,
1925
+ lang: result.activeLang,
1926
+ pathname: pathname || route,
1927
+ attrs: nonceAttr
1928
+ })
1929
+ const translationSeed = startup ? startup + '\n' : ''
1704
1930
  // The registry and the fetch seed as data blocks; the page's mark is
1705
1931
  // its <meta name="brender"> (no inline flag a policy could block).
1706
1932
  const blocks = prerender.blocks ? prerender.blocks + '\n' : ''
@@ -1773,7 +1999,7 @@ export const renderPage = async (data, route = '/', options = {}) => {
1773
1999
  lang: lang || (polyglotCfg ? result.activeLang : undefined),
1774
2000
  preloadScripts
1775
2001
  })
1776
- return { html, route, brKeyCount, shell: true, diagnostics }
2002
+ return { html, route, brKeyCount, shell: true, diagnostics, preparedDesignSystem: result.preparedDesignSystem || null }
1777
2003
  }
1778
2004
 
1779
2005
  // (The empty line after the font links held a @font-face <style> that
@@ -1793,7 +2019,7 @@ ${isrBody}
1793
2019
  </body>
1794
2020
  </html>`
1795
2021
 
1796
- return { html, route, brKeyCount, diagnostics }
2022
+ return { html, route, brKeyCount, diagnostics, preparedDesignSystem: result.preparedDesignSystem || null }
1797
2023
  }
1798
2024
 
1799
2025
  // ── Images below the fold ───────────────────────────────────────────────────
@@ -1862,17 +2088,35 @@ const generateFontLinks = (ds) => {
1862
2088
  // again. Placed after the markup, so they never delay its paint;
1863
2089
  // the app's scripts run after them either way.
1864
2090
  // `adoptionScripts` above stays for older callers (the platform's renderer).
1865
- export const prerenderParts = ({ brRegistry, fetchSeed, adoptDom = false } = {}) => {
2091
+ // The build's stamp (XMA round 3, item B) goes into <head> after the mark:
2092
+ // `<meta name="brender-stamp">` (@symbo.ls/utils brenderStampMeta) — read by
2093
+ // the client's fetch plugin, which decides whether the page is stale
2094
+ // (@symbo.ls/fetch early.js "Stale pages"), by app code
2095
+ // (`el.context.prerender`, smbls prerender.js) and by a host that compares
2096
+ // it with its data's last change.
2097
+ export const prerenderParts = ({ brRegistry, fetchSeed, adoptDom = false, stamp, time } = {}) => {
1866
2098
  const registry = adoptDom && brRegistry && Object.keys(brRegistry).length
1867
2099
  ? brenderRegistryBlock(brRegistry)
1868
2100
  : ''
1869
2101
  const seed = createEarlySeedBlock(fetchSeed)
1870
2102
  return {
1871
- meta: `<meta name="brender" content="${registry ? 'hydrate' : 'replace'}">`,
2103
+ meta: `<meta name="brender" content="${registry ? 'hydrate' : 'replace'}">\n` + brenderStampMeta(buildStamp(stamp, time)),
1872
2104
  blocks: [registry, seed].filter(Boolean).join('\n')
1873
2105
  }
1874
2106
  }
1875
2107
 
2108
+ // ── The build's stamp ───────────────────────────────────────────────────────
2109
+ // A pre-rendered page carries the data of the time it was rendered until it
2110
+ // is rendered again (`smbls build` pages: until the next build). Each page
2111
+ // says when that was — `time`, ms since the epoch — and which data it holds:
2112
+ // `stamp`, a project's own version of its data (`brender.stamp`, run once
2113
+ // per build by the CLI), else the render time as an ISO string. One build
2114
+ // passes one stamp and time to every page.
2115
+ export const buildStamp = (stamp, time) => {
2116
+ const at = typeof time === 'number' && isFinite(time) ? Math.round(time) : Date.now()
2117
+ return { stamp: stamp != null && stamp !== '' ? String(stamp) : new Date(at).toISOString(), time: at }
2118
+ }
2119
+
1876
2120
  // ` nonce="…"` for an inline script, or '' — a value that could break out of
1877
2121
  // the attribute is refused.
1878
2122
  const nonceAttribute = (nonce) => {
package/shell.js CHANGED
@@ -32,6 +32,8 @@
32
32
  * DOM parse + serialize round-trip does not guarantee.
33
33
  */
34
34
 
35
+ import { insertAfterImportMap } from '@symbo.ls/utils'
36
+
35
37
  const HEAD_OPEN = /<head(\s[^>]*)?>/i
36
38
  const BODY_OPEN = /<body(\s[^>]*)?>/i
37
39
 
@@ -199,8 +201,16 @@ export const composeIntoShell = (shell, { headTags = '', headEnd = '', body = ''
199
201
  }
200
202
  let html = rebaseAssets(shell, depth)
201
203
  if (lang) html = setHtmlLang(html, lang)
204
+ // The app's preloads go right after the shell's import map when it has one
205
+ // (parcel's page does): a modulepreload before it gets the map rejected,
206
+ // and the app's lazy bundles resolve through it. Else first in what the
207
+ // render adds before </head>.
202
208
  const preloads = preloadScripts ? scriptPreloads(html) : ''
203
- if (preloads) headEnd = headEnd ? `${preloads}\n${headEnd}` : preloads
209
+ if (preloads) {
210
+ const placed = insertAfterImportMap(html, preloads)
211
+ if (placed !== null) html = placed
212
+ else headEnd = headEnd ? `${preloads}\n${headEnd}` : preloads
213
+ }
204
214
 
205
215
  const owned = new Set()
206
216
  for (const m of headTags.matchAll(HEAD_TAG)) {