@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 +27 -0
- package/README.md +34 -5
- package/dist/esm/index.js +1 -1
- package/dist/esm/render.js +25 -24
- package/dist/esm/shell.js +6 -6
- package/env.js +3 -2
- package/index.js +5 -3
- package/package.json +8 -8
- package/render.js +300 -56
- package/shell.js +11 -1
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
|
-
**
|
|
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
|
|
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.
|
|
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
|
|
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};
|
package/dist/esm/render.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import{resolve as m,join as Ce,dirname as x}from"path";import{existsSync as b,unlinkSync as De,readFileSync as W,realpathSync as Ie}from"fs";import{resolvePackageEntry as Oe}from"./packageEntry.js";import{tmpdir as Me}from"os";import{randomBytes as Pe}from"crypto";import{createRequire as oe}from"module";import{pathToFileURL as Fe}from"url";import{createEnv as ue}from"./env.js";import{resetKeys as de,assignKeys as me,mapKeysToElements as ge}from"./keys.js";import{extractMetadata as se,generateHeadHtml as Be}from"./metadata.js";import"./hydrate.js";import{composeIntoShell as Ke}from"./shell.js";import{fetchSSRTranslations as Ne}from"./prefetch.js";import{createEarlySeedScript as Ge}from"@symbo.ls/fetch";import{headerEntries as He}from"@symbo.ls/fetch/rest";import{brenderRegistryBlock as Je,escapeRegExp as We,jsonForScript as Ue,polyglotFallbackLang as U}from"@symbo.ls/utils";let 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
|
-
`+
|
|
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("<")){const r=n.replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&").replace(/"/g,'"').replace(/'/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
|
|
6
|
-
`),
|
|
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>`:""),
|
|
9
|
-
`),classRules:
|
|
10
|
-
`),I=
|
|
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>`,
|
|
14
|
+
</style>`,E=B?`<style data-emotion="smbls">
|
|
15
15
|
${B}
|
|
16
|
-
</style>`:"",
|
|
17
|
-
|
|
18
|
-
`:"";
|
|
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="${
|
|
36
|
-
${
|
|
37
|
-
`),body:
|
|
38
|
-
<html lang="${
|
|
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}${
|
|
41
|
-
${
|
|
42
|
-
${
|
|
40
|
+
${C}${r&&r.clientScript&&i?`
|
|
41
|
+
${q.meta}`:""}
|
|
42
|
+
${J}
|
|
43
43
|
|
|
44
44
|
${O}
|
|
45
|
-
${
|
|
45
|
+
${E}
|
|
46
46
|
</head>
|
|
47
47
|
<body>
|
|
48
|
-
${
|
|
49
|
-
${
|
|
48
|
+
${Y}
|
|
49
|
+
${G}
|
|
50
50
|
</body>
|
|
51
|
-
</html>`,route:t,brKeyCount:
|
|
52
|
-
`):""},
|
|
53
|
-
|
|
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
|
|
2
|
-
`)},
|
|
3
|
-
${t}`:
|
|
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,""")}"`}).join(""),s=i.replace(/"/g,""");n.push(l==="module"?`<link rel="modulepreload" href="${s}" fetchpriority="high"${a}>`:`<link rel="preload" as="script" href="${s}" fetchpriority="high"${a}>`)}return n.join(`
|
|
2
|
+
`)},H=(e,n)=>{const t=d(e,/<html(\s[^>]*)?>/i);if(!t)return e;const r=t[0],o=String(n).replace(/"/g,"""),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,
|
|
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
|
-
${
|
|
9
|
-
`:""}`,$=d(s,
|
|
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
|
|
112
|
-
//
|
|
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.
|
|
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.
|
|
50
|
-
"@symbo.ls/default-config": "^3.14.
|
|
51
|
-
"@symbo.ls/element": "^3.14.
|
|
52
|
-
"@symbo.ls/fetch": "^3.14.
|
|
49
|
+
"@symbo.ls/css": "^3.14.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.
|
|
55
|
-
"@symbo.ls/utils": "^3.14.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
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)
|
|
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)) {
|