@ultimat3/render
Advanced tools
+5
-0
@@ -60,2 +60,3 @@ # @ultimat3/render — boundary | ||
| | Island bytes | **not this package's**, `As of 2026-08-23`. `routeJsBytes`, `graphFor`, `checkBudget`, `checkBudgets`, `assertBudget` and the `Island` / `BundleGraph` / `RouteBytes` / `BudgetReport` types were exported from the barrel and called by NOTHING outside this package's own tests — the near-miss is `@ultimat3/cli`'s own `checkBudgets` in `packages/cli/src/budgets.ts`, which measures the EMITTED document and is the gate that runs. Deleted rather than wired, because two answers to "what does this route weigh" — one of them never asked — is axiom 1, and a build error nothing calls is not a build error. What survives here is the budget GRAMMAR (`parseByteBudget`, which the CLI does import) and `defaultIslandBudget`, which `registerRoute` reaches. **Breaking**: the six functions and five types are gone from the public API. | | ||
| | The hydration runtime's CSP | `HYDRATE_RUNTIME_BODIES` — every body `hydrateRuntime` can emit, one per non-empty subset of the three strategies, seven in all. It is emitted INLINE in every document carrying an island, and `@ultimat3/http`'s `script-src` is `'self' 'wasm-unsafe-eval'`, so under the enforced policy a container serves (`dev: false`) **no island ever booted** — invisible in `x dev`, where the policy is report-only. `@ultimat3/cli`'s `script-csp.ts` hashes this list at boot, the mirror of `style-csp.ts`. Hashes and not a nonce, for `cspHashSource`'s own reason: a `render: 'static'` page is a file on disk. Never restate the concatenation — `runtimeBody` is the one place the served text and the hashed text are the same string. **Still uncovered**: `render-stream.ts`'s per-hole `<script>$X("id")</script>`, whose body is per-response and cannot be hashed. Unreachable today (`dev-render.ts` passes `holes: []`), and the first real hole needs a nonce, not a hash. | | ||
| | Island markup | the props `<script>` is emitted INSIDE the wrapper by `render-html.ts`, so a document assembler has exactly one thing left to remember: `hydrateRuntime(directives)`. | | ||
@@ -76,2 +77,5 @@ | Island boot | `el.__x` holds the boot PROMISE, never a boolean. As a flag, a second interaction while the chunk was still loading got a resolved promise back and the replay queue flushed into an island that had not mounted — the events went nowhere and the listeners were already removed. | | ||
| | `X_ROUTE_LOAD_FAILED` computes its pathname BEFORE the try | `new URL(ctx.url)` inside the catch made a relative `ctx.url` — what a prerender pass, `x build` and every test harness hand in — throw a bare `TypeError` on the one path whose job is a coded error. `pathnameOf` never throws. | | ||
| | The ISR key | `isrKey(url, locale)` — pathname, the negotiated LOCALE in the reserved `__x_locale` parameter, then the query, params sorted. The locale is a REQUIRED argument, so every call site has to answer: a document is rendered with `<html lang>` and every `t()` in the request's own locale, so one entry per path served visitor 2 the document negotiated for visitor 1 — for the whole TTL, and `s-maxage` told the CDN to do the same. `examples/dummy` ships en + es and three `isr` routes, so it was all three. A parameter and not a prefix because `routePathOf` splits a key at its `?`: a `es:/blog` key matches no route, so `descriptorFor` answers `undefined` and a declared `ttl` silently becomes tag-only. The time zone is deliberately NOT a dimension — unbounded where a locale set is declared — so a date on an `isr` page belongs in a zone the page itself names. `toResult` emits `vary: accept-language` for the CDN half; the rest of the shared key is added by `@ultimat3/http`'s `cache-headers` stage, which sees the actor this function cannot. | | ||
| | A bust that lands MID-render | fenced with `@ultimat3/cache`'s `sampleFence({ key, tags })`, taken before `render()` and asked before `store.set` — the same mechanism `CacheStack`'s read-through fill uses, never a second one grown here. `regenerate` rendered and then wrote `{ stale: false }` unconditionally, so a `markStale` arriving in between was ERASED by HTML built from pre-write rows; for a tag-only route `isFresh` is then true forever and the process serves it for the rest of its life. `registerPath` runs BEFORE the render for the other half: `revalidateByTags` reads the graph, so a bust could not see a cold path whose first render was still in flight. | | ||
| | Marking a page stale | `IsrStore.markStale(path)`, in place — never `set({ ...entry, stale: true })`. `set` means "this page was just generated" and the default store orders eviction by exactly that, so the read-modify-write made the STALEST page the newest: a tag bust protected the pages that most needed regenerating and evicted the freshest one instead. **Breaking**: `markStale` is a required member of `IsrStore`. | | ||
| | ISR store bound | `memoryIsrStore()` caps at `DEFAULT_ISR_MAX_ENTRIES` (1,000), least recently generated first — a route table supports `:params` and `*`, so `/blog/:slug` retains one full HTML string per slug ever requested, 404-shaped ones included. | | ||
@@ -86,2 +90,3 @@ | Route path from file | ONE reader of the surface segment: `locateSurface()` answers which surface AND where it starts. `registry.ts` sliced at `indexOf('app/')` instead, which matched inside `myapp/`, so every route under `apps/myapp/app/` served at `/app/…`. Never re-derive the offset from the surface NAME. | | ||
| | Responses | return `RenderResult`. `@ultimat3/http` builds the `Response`. | | ||
| | Who owns `cache-control` | a render mode states the MODE's intent; `@ultimat3/http`'s `cache-headers` stage makes the final answer and may overrule it. `ssrHeaders` writes `s-maxage=30` for any route without a `policy` — and `meta.auth` is `'public' \| 'required'`, so the page that greets a signed-in visitor by name is a `'public'` route whose own header offered it to a CDN. This package cannot see the actor and must never try: the fix is not a second actor check here. | | ||
| | Solid | no `solid-js` import anywhere in this package — `type-pins.tsx` satisfies its `JSX.Element` structurally, through `jsxImportSource`, and never names it. The JSX factory in `jsx.ts` builds inert nodes — it is not a Solid renderer and must never become one. The client half runs in an island chunk, which `@ultimat3/cli`'s `solid-loader.ts` compiles with `babel-preset-solid`: Solid's reactivity is a COMPILE-time contract, so nothing this package could inject would substitute for it. `router-client.ts` was the one file built on that premise ("inject primitives") and it never had a caller. | | ||
@@ -88,0 +93,0 @@ | Root element | `ROOT_ELEMENT_ID` (`render-html.ts`), the id every document's body wraps its component in. It was `SPA_ROOT_ID` in `render-spa.ts`, naming a mode that never used it and that no longer exists. | |
+5
-5
| { | ||
| "name": "@ultimat3/render", | ||
| "version": "10.0.0", | ||
| "version": "11.0.0", | ||
| "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.", | ||
@@ -39,8 +39,8 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@ultimat3/cache": "10.0.0", | ||
| "@ultimat3/core": "10.0.0", | ||
| "@ultimat3/i18n": "10.0.0", | ||
| "@ultimat3/seo": "10.0.0", | ||
| "@ultimat3/cache": "11.0.0", | ||
| "@ultimat3/core": "11.0.0", | ||
| "@ultimat3/i18n": "11.0.0", | ||
| "@ultimat3/seo": "11.0.0", | ||
| "sass": "1.102.0" | ||
| } | ||
| } |
+1
-1
@@ -366,3 +366,3 @@ # 🖼 @ultimat3/render | ||
| | `installRenderLoader`†, `compileStylesheet`† | the `.tsx`/`.scss` loaders, installed on import | | ||
| | `emitIslandAttributes`, `hydrateRuntime` | the four hydration strategies | | ||
| | `emitIslandAttributes`, `hydrateRuntime`, `HYDRATE_RUNTIME_BODIES` | the four hydration strategies, and every body the runtime script can hold — a host hashes that list into `script-src`, because the runtime is emitted inline and no `render: 'static'` file can receive a nonce | | ||
| | `ISLAND_MOUNTED_ATTRIBUTE`, `ISLAND_FAILED_ATTRIBUTE`, `IDLE_HYDRATE_TIMEOUT_MS` | what hydration looks like from outside the page | | ||
@@ -369,0 +369,0 @@ | `parseByteBudget`, `defaultIslandBudget` | the `'40kb'` budget grammar, and the ceiling a declared island earns | |
+44
-5
@@ -9,2 +9,3 @@ /** | ||
| import type { HydrateStrategy } from '@ultimat3/core'; | ||
| import { HYDRATE_STRATEGIES } from '@ultimat3/core'; | ||
| import { escapeAttribute, escapeJsonContent } from './html'; | ||
@@ -89,3 +90,6 @@ | ||
| return ( | ||
| `<script type="application/json" data-x-props="${directive.islandId}">` + | ||
| // The id goes through the SAME escaper every other attribute in this file does. Safe today — | ||
| // `islandModuleId` reduces to `[a-z0-9-]` — which is exactly why it costs nothing to route it | ||
| // now, rather than after an id starts being derived from something an author did not type. | ||
| `<script type="application/json" data-x-props="${escapeAttribute(directive.islandId)}">` + | ||
| `${escapeJsonContent(JSON.stringify(directive.props))}</script>` | ||
@@ -175,2 +179,40 @@ ); | ||
| /** | ||
| * Emission order, and the order the subsets below are enumerated in. Derived from core's one | ||
| * declaration of the vocabulary, never restated: a second literal of this set is what | ||
| * `bun run render-modes` refuses, and it refuses it on the MEMBERS, not on the name. | ||
| */ | ||
| const RUNTIME_ORDER: readonly Exclude<HydrateStrategy, 'never'>[] = HYDRATE_STRATEGIES.filter( | ||
| (strategy): strategy is Exclude<HydrateStrategy, 'never'> => strategy !== 'never', | ||
| ); | ||
| /** | ||
| * The TEXT of the runtime script for exactly these strategies — the body a CSP `script-src` hash | ||
| * is taken over. Split out of `hydrateRuntime` so the served document and the policy that admits | ||
| * it read one function: a second copy of this concatenation is a hash that stops matching the | ||
| * moment the runtime changes, and the failure it produces is an island that never boots on a page | ||
| * that otherwise looks correct. | ||
| */ | ||
| const runtimeBody = (needed: ReadonlySet<Exclude<HydrateStrategy, 'never'>>): string => | ||
| [ | ||
| RUNTIME_PRELUDE, | ||
| ...RUNTIME_ORDER.filter((strategy) => needed.has(strategy)).map( | ||
| (strategy) => RUNTIME_PARTS[strategy], | ||
| ), | ||
| ].join('\n'); | ||
| /** | ||
| * Every body `hydrateRuntime` can emit: one per non-empty subset of the three strategies, seven in | ||
| * all, deterministic. A policy that admits inline script by HASH has to enumerate them before the | ||
| * socket opens — the alternative is a per-response nonce, which a `render: 'static'` page (a file | ||
| * on disk) can never receive. Enumerated rather than derived from the route table because the | ||
| * runtime is a function of the SET a document needs, and a table read at boot cannot answer for | ||
| * a document assembled later. | ||
| */ | ||
| export const HYDRATE_RUNTIME_BODIES: readonly string[] = Array.from( | ||
| { length: 2 ** RUNTIME_ORDER.length - 1 }, | ||
| (_unused, index) => | ||
| runtimeBody(new Set(RUNTIME_ORDER.filter((_s, bit) => (((index + 1) >> bit) & 1) === 1))), | ||
| ); | ||
| /** | ||
| * Emit only the runtime the page's islands actually use. A page of `never` islands gets | ||
@@ -182,6 +224,3 @@ * the empty string — an unused strategy must not cost a byte. | ||
| if (needed.size === 0) return ''; | ||
| const parts = (['idle', 'visible', 'interaction'] as const) | ||
| .filter((strategy) => needed.has(strategy)) | ||
| .map((strategy) => RUNTIME_PARTS[strategy]); | ||
| return `<script type="module">${RUNTIME_PRELUDE}\n${parts.join('\n')}</script>`; | ||
| return `<script type="module">${runtimeBody(needed)}</script>`; | ||
| } | ||
@@ -188,0 +227,0 @@ |
+1
-0
@@ -60,2 +60,3 @@ /** | ||
| emitIslandProps, | ||
| HYDRATE_RUNTIME_BODIES, | ||
| hydrateRuntime, | ||
@@ -62,0 +63,0 @@ hydrateRuntimeBytes, |
+63
-9
@@ -12,4 +12,6 @@ /** | ||
| invalidateTags, | ||
| markInvalidated, | ||
| registerDependent, | ||
| registerRevalidator, | ||
| sampleFence, | ||
| unregisterDependent, | ||
@@ -44,2 +46,9 @@ } from '@ultimat3/cache'; | ||
| set(entry: IsrEntry): void; | ||
| /** | ||
| * Mark a held page stale IN PLACE — `false` when the store does not hold it. Its own method and | ||
| * not `set({ ...entry, stale: true })`, because `set` means "this page was just generated" and a | ||
| * store is entitled to order its eviction by that: the read-modify-write made the STALEST page | ||
| * the newest, so a tag bust protected exactly the pages that most needed regenerating. | ||
| */ | ||
| markStale(path: string): boolean; | ||
| delete(path: string): void; | ||
@@ -78,2 +87,10 @@ paths(): readonly string[]; | ||
| }, | ||
| // In place: `map.set` on a key the Map already holds keeps its position, and that position is | ||
| // the eviction order. Never `delete` + `set` here — that is the bug this method exists to fix. | ||
| markStale: (path) => { | ||
| const entry = map.get(path); | ||
| if (entry === undefined) return false; | ||
| map.set(path, { ...entry, stale: true }); | ||
| return true; | ||
| }, | ||
| delete: (path) => { | ||
@@ -87,3 +104,12 @@ map.delete(path); | ||
| /** | ||
| * The ISR store key for one request URL: pathname plus the query, **params sorted**. | ||
| * The reserved query parameter the negotiated locale rides in. A parameter and not a prefix | ||
| * because `routePathOf` splits a key at its `?`: a `es:/blog` key would match no route, so | ||
| * `descriptorFor` would answer `undefined` and a declared `revalidate: { ttl }` would silently | ||
| * become tag-only. Reserved spelling, so an app's own `?locale=` stays its own dimension. | ||
| */ | ||
| export const ISR_LOCALE_PARAM = '__x_locale'; | ||
| /** | ||
| * The ISR store key for one request URL: pathname, the negotiated LOCALE, and the query, **params | ||
| * sorted**. | ||
| * | ||
@@ -94,2 +120,9 @@ * Exported because deriving it is the caller's job and there may only be ONE derivation — a | ||
| * | ||
| * The locale is REQUIRED, and required as an argument rather than read from the ambient context so | ||
| * that every call site has to answer: a document is rendered with `<html lang>` and every `t()` in | ||
| * the request's own locale, so one entry per path served visitor 2 the document negotiated for | ||
| * visitor 1 — for the whole TTL, and with `s-maxage` telling the CDN to do the same. The time zone | ||
| * is deliberately NOT a dimension: it is unbounded where a locale set is declared, and an `isr` | ||
| * page is a shared artifact, so a date on one belongs in an explicit zone the page itself names. | ||
| * | ||
| * A query-carrying URL therefore gets its own entry, which is correct and is not free: a crawler | ||
@@ -100,5 +133,5 @@ * appending `?utm_source=…` mints one entry per value. That is bounded, not unbounded — | ||
| */ | ||
| export function isrKey(url: URL): string { | ||
| if (url.search === '') return url.pathname; | ||
| export function isrKey(url: URL, locale: string): string { | ||
| const params = new URLSearchParams(url.search); | ||
| params.set(ISR_LOCALE_PARAM, locale); | ||
| params.sort(); | ||
@@ -222,2 +255,15 @@ return `${url.pathname}?${params.toString()}`; | ||
| const work = (async (): Promise<IsrEntry> => { | ||
| // BEFORE the render, not after: `revalidateByTags` reads the graph, so a bust arriving while | ||
| // a cold path's first render was in flight could not see the page it was invalidating — | ||
| // which is the window in which the bust that matters most arrives. | ||
| registerPath(path, descriptor); | ||
| // Sampled before the render for the reason `@ultimat3/cache`'s read-through fill samples | ||
| // before its `load()` (`tiers.ts`): the HTML below is built from rows read in the past, and | ||
| // a `markStale` landing in between was then ERASED by `store.set({ stale: false })`. For a | ||
| // tag-only route `isFresh` is true forever, so the process went on serving pre-write HTML | ||
| // for the rest of its life. One mechanism, not a second one grown here. | ||
| const fence = sampleFence({ | ||
| key: path, | ||
| tags: (descriptor?.revalidateTags ?? []).map(parseWireTag), | ||
| }); | ||
| const html = await render(path); | ||
@@ -232,4 +278,6 @@ const entry: IsrEntry = { | ||
| }; | ||
| store.set(entry); | ||
| registerPath(path, descriptor); | ||
| // Refused, never published stale-flagged: the next request re-renders from rows that now | ||
| // include the write, where a stored-but-stale entry would serve this pre-write body once | ||
| // more before doing the same thing. | ||
| if (fence.isValid()) store.set(entry); | ||
| forgetEvictedPaths(); | ||
@@ -245,6 +293,7 @@ return entry; | ||
| function markStale(path: string): boolean { | ||
| const entry = store.get(path); | ||
| if (entry === undefined) return false; | ||
| store.set({ ...entry, stale: true }); | ||
| return true; | ||
| // The mark is recorded whether or not the store holds the page: a regeneration already in | ||
| // flight for a path this store has never held is exactly the case the fence above exists for, | ||
| // and `invalidateTags`' own fanout only marks the TAGS. | ||
| markInvalidated({ key: path }); | ||
| return store.markStale(path); | ||
| } | ||
@@ -374,2 +423,7 @@ | ||
| 'cache-control': cacheControl(entry.ttlMs), | ||
| // The store keys on the locale; a shared cache in front of it has to as well, or the CDN | ||
| // repeats the bug this entry was split to fix. `ssrHeaders`' own line, for the same reason. | ||
| // The rest of the shared key — the cookie, the zone — is added by `@ultimat3/http`'s | ||
| // `cache-headers` stage, which sees the actor this function cannot. | ||
| vary: 'accept-language', | ||
| }; | ||
@@ -376,0 +430,0 @@ if (servedStale) headers['x-ultimate-isr'] = 'stale'; |
+1
-0
@@ -46,2 +46,3 @@ /** | ||
| DEFAULT_ISR_MAX_ENTRIES, | ||
| ISR_LOCALE_PARAM, | ||
| invalidateAndRevalidate, | ||
@@ -48,0 +49,0 @@ isrKey, |
246857
4.05%4093
2.27%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated