@ultimat3/render 20.2.1 → 22.0.0

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/CLAUDE.md CHANGED
@@ -4,107 +4,102 @@ Owns: the `route` primitive, the four render modes, the route table, the surface
4
4
  islands + budgets, hydration directives, `<head>` merge, **the server JSX runtime and the two Bun
5
5
  loaders that make an app's `.tsx` and `.scss` runnable**.
6
6
 
7
- **Two entry points, disjoint, split 2026-08-22** — the same `"."` / `"./server"` shape
8
- `@ultimat3/realtime` took, for the same reason. `"."` (`index.ts`) is the CLIENT half and BUNDLES
9
- for the browser; `"./server"` (`server.ts`) is the build-time half — `css-modules`,
10
- `module-loader`, `render-html`, `render-isr`, `render-ssr`, `render-static`, `render-stream` — and
11
- does not. `css-modules.ts` imports `fileURLToPath`/`pathToFileURL` from `node:url`, which Bun's
12
- browser polyfill exports NEITHER of, so the single barrel was not a fat bundle, it was a
13
- `bun build --target=browser` that FAILED at link time on any app entry that reached this package.
14
- Measured: no `sideEffects` value repairs it (`false`, `[]`, an array naming only `errors.ts` — all
15
- fail identically), which is where this split differs from realtime's, where the array alone was
16
- enough. Only not importing the module does. Never re-export a name from both barrels: disjointness
17
- is what makes "which half does this live in" a mechanical fact, and `index.test.ts` asserts both
18
- the empty name intersection AND that `index.ts`'s transitive runtime import graph reaches none of
19
- the seven modules above. `scripts/browser-barrel.test.ts` holds the end property, both directions.
7
+ **Two entry points, disjoint.** `"."` (`index.ts`) is the CLIENT half and bundles for the browser;
8
+ `"./server"` (`server.ts`) is the build-time half — `css-modules`, `module-loader`, `render-html`,
9
+ `render-isr`, `render-ssr`, `render-static`, `render-stream` — and does not (`css-modules.ts` needs
10
+ `node:url`, which a browser build cannot link; no `sideEffects` value repairs that). Never re-export
11
+ a name from both barrels: `index.test.ts` asserts the empty name intersection and that `index.ts`'s
12
+ runtime import graph reaches none of those seven modules; `scripts/browser-barrel.test.ts` holds
13
+ the end property.
20
14
 
21
- `island()` is a **factory over the route's own `hydrate`**, not a ninth primitive and not a
22
- second render mode — the same rule `llm()` and `backfill()` follow. It adds no key to
23
- `defineRoute`.
15
+ `island()` is a **factory over the route's own `hydrate`**, not a ninth primitive and not a second
16
+ render mode. It adds no key to `defineRoute`.
24
17
 
25
18
  Tier 4. May import tiers 0–3: `core`, `schema`, `i18n`, `money`, `time`, `cache`, `seo`,
26
- `entity`, `policy`, `http`, `action`, `query`. **Never** `pwa`, `mcp`, `ai`, `manifest`, `ui`
27
- — all tier 4, so **sideways**, and an undeclared sideways edge is a build error. `ui` moved 5 → 4
28
- in 2026-08 and is held level with this package deliberately (`FLOOR_ABOVE` in
29
- `scripts/lib/tiers.ts`), so `render → ui` stays refused; this package sits above its own floor of 2
30
- for the mirror-image reason, keeping `render → pwa` refused. Never `cli` (upward).
19
+ `entity`, `policy`, `http`, `action`, `query`. **Never** `pwa`, `mcp`, `ai`, `manifest`, `ui` — all
20
+ tier 4, so sideways. This package sits above its floor of 2 so `render → pwa` stays refused, and
21
+ `ui` is held level with it so `render → ui` stays refused (`FLOOR_ABOVE` in `scripts/lib/tiers.ts`;
22
+ axiom 6). Never `cli` (upward).
31
23
 
32
24
  | Rule | Detail |
33
25
  |---|---|
34
26
  | `offline`, `meta` | required by `RouteDefinition`. Never make them optional. |
35
- | `hydrate` | **optional since 1.2.0, derived from `island()`** — `'interaction'` when the module declared one, `'never'` when it did not. Declaring it still wins and is the only way to reach `idle` / `visible`. Not a widening of the contract: it is the one key the declaration above it already answers, and requiring it meant two failures (`X_ISLAND_NOT_HYDRATED`, and a `site/` route refused for a missing `budget.js`) for one omission. Never give the island its own strategy instead — `RouteDescriptor.hydrate` is read by `sw.js`, the web manifest and `x routes`, and two islands wanting different timings would leave it with no true answer. |
36
- | `defineRoute` shape | exactly the contract's 9 keys. New route *metadata* still goes inside `meta` — `load` is not metadata, it is the data `meta` already took a parameter for. |
37
- | `load` | optional, and the ONE server-side data seam. Resolved once per render by `routeDataFor()` and handed to **both** `meta` and the page component. Two resolutions is a `<title>` describing content the body does not contain. Absent `load`, the context IS the data (`{ params, url }`), which is what `meta` received before the key existed — so no consumer branches on whether a route declared one. |
38
- | `load` is required when the context cannot supply the data | `LoadRequirement<TData>` in `defineRoute`'s parameter — `unknown` when `RouteContext` satisfies `TData`, a required `load` when it does not. That is what makes the no-`load` fallback true rather than asserted: it was `ctx as unknown as TData`, so a `meta` reading `data.post` off a route that loads nothing type-checked and rendered `undefined` in a `<title>`. `RouteContext` is a type ALIAS for the same reason — only an alias carries the implicit index signature that makes it a `RouteData`; as an `interface` the compiler cannot see it and the cast comes back. |
39
- | A loader's own error | rethrown only when `isUltimateError` says so — core's brand, never a `code` property. Every `ENOENT` is an `Error` with a string `code`, and the duck-type that preceded this let all of them out of `routeDataFor` unwrapped: no `X_ROUTE_LOAD_FAILED`, no fix line, no route named. A tier-0 error (`@ultimat3/schema` cannot import core) is branded, not a subclass — never narrow this to `instanceof UltimateError`. |
40
- | A loader's own STATUS | `withStatus(status, data)` in `route-status.ts` — the ONE way a page answers 404 (or 410, or 503) while still rendering its own component inside the app's shell. Measured in ai-maxxing 2026-09-07: the only route to a status was a throw, which is the framework's error page OUTSIDE the shell, so `/fleet/nope` rendered the right page and answered 200. The status rides on the data by IDENTITY, in a `WeakMap` — the same object comes back, so `load`'s type, `routeDataFor`'s signature and every consumer that never asks are untouched, and a frozen or class-instance result is never written into. Never a `RouteContext` method: every builder of a context (`x dev`, the prerenderer, the SEO scan, both scaffold templates) would have to supply it, and an optional method is a second way. `routeStatusOf(data)` is the reader, TOTAL, 200 when nothing asked. A 3xx is `X_ROUTE_STATUS_INVALID` — a redirect is a `Location` and no body — and the range is `finiteStatus`'s. A 4xx/5xx is `robots.index = false` BY CONSTRUCTION in `defineRoute`'s `meta` wrapper, the one function every `<head>` renderer calls; a 200 hands the author's `meta` object back by reference. The response status itself is minted by `@ultimat3/cli`'s `dev-render.ts` (`resultFor`), which reads `routeStatusOf(data)` once and hands it to the mode — this package owns the seam, never the `Response`. |
41
- | Type claims | `type-pins.tsx`, never a `.test.ts` — `tsconfig.json` excludes tests, so `tsc` never reads one. `.tsx` since 1.2.0: the island-as-JSX claim is only decidable by writing the JSX an author writes, checked against the same `solid-js` `JSX.Element` a page is. |
27
+ | `hydrate` | optional, **derived from `island()`** — `'interaction'` when the module declared one, `'never'` when not. Declaring it still wins and is the only way to reach `idle` / `visible`. Never give an island its own strategy: `RouteDescriptor.hydrate` is read by `sw.js`, the web manifest and `x routes`. |
28
+ | `defineRoute` shape | exactly the contract's 9 keys. New route *metadata* goes inside `meta`. |
29
+ | `load` | optional, and the ONE server-side data seam. Resolved once per render by `routeDataFor()` and handed to **both** `meta` and the page component. Absent `load`, the context IS the data (`{ params, url }`). |
30
+ | `load` is required when the context cannot supply the data | `LoadRequirement<TData>` in `defineRoute`'s parameter. `RouteContext` is a type ALIAS on purpose — only an alias carries the implicit index signature that makes it a `RouteData`. |
31
+ | A loader's own error | rethrown only when `isUltimateError` says so (core's brand), never a `code` property and never `instanceof UltimateError` — a tier-0 error is branded, not a subclass. Everything else is `X_ROUTE_LOAD_FAILED`. |
32
+ | A loader's own STATUS | `withStatus(status, data)` (`route-status.ts`) — the ONE way a page answers 404/410/503 while rendering its own component in the app's shell. The status rides on the data by IDENTITY in a `WeakMap`; `routeStatusOf(data)` is the total reader (200 by default). A 3xx is `X_ROUTE_STATUS_INVALID`. A 4xx/5xx is `robots.index = false` by construction in `defineRoute`'s `meta` wrapper. The `Response` status is minted by `@ultimat3/cli`'s `dev-render.ts`. |
33
+ | Type claims | `type-pins.tsx`, never a `.test.ts` — `tsconfig.json` excludes tests. `.tsx` so the island-as-JSX claim is checked against the JSX an author writes. |
42
34
  | Descriptor `meta` / `load` | always `(x) => Promise<…>`. Authors may declare either sync; consumers never branch. |
43
- | Descriptor `budget` | always an object, `{}` when undeclared. Its *fields* stay optional — `budget.js === undefined` is the site/ hydration failure. |
44
- | `RouteBudget` keys | **`js` and `lcp`, and every key is PROJECTED** (`As of 2026-08-24`). `css`, `cls` and `tbt` were declared on the contract, flattened away by `registerRoute` — the descriptor carries `budgetJs` + `budgetLcp` and nothing else — and read by NOTHING: `x verify`'s `budgets` step, `x.manifest.json`, `x routes` and the dev dashboard all read those two. So `budget: { cls: 0.1 }` was accepted, normalised, stored and ignored while the gate that exists to enforce budgets reported green — the shape `jobs.driver` and `realtime.tier` had one declaration surface over. **Deleted, not wired**, for `PwaConfig.installPrompt`'s reason: the measuring half does not exist either. `type-pins.tsx` derives the allowed set from `RouteDescriptor`'s own `budget*` keys, so a key added here without a projection is a build error; `scripts/declaration-readers.ts` is the rule that finds the class across every declaration. **Breaking**: three keys are gone from `RouteBudget`. |
45
- | No `describe()` on a route | `describeRoutes()` is the one route list. A per-route projection would be a second one. |
46
- | Mode invariants | `modes.ts` only. Never inline a mode check in a render-\* file. **Four modes, and every one renders the route's component** — `spa` did not, and `renderSpa` never read `entry.component`, so a `spa` route served `<div id="x-root"></div>` in every app that ever declared one. A gated page is `ssr`; a body that belongs in the browser is an `island({ src })`. A fifth mode has to name the function that renders it. |
47
- | Island declaration | `island({ src })` — a **specifier**, never an import. That is the whole boundary: a string has no scope to close over and no edge for a bundler or `checkSurfaceBoundary` to follow, so a `static` page's graph cannot grow the island's dependencies. Never add an overload that takes a component. |
48
- | Island filename | `*.island.tsx`, `ISLAND_EXTENSION` — one spelling, same rule as `page.tsx`: a module ships to the browser iff its name says so. Never widen it to accept a second, and never make it optional. |
49
- | Island timing | the route's `hydrate` and nothing else — derived from the declaration, never declared on the island. An island declaring its own strategy would be a second way to say "this route hydrates" (axiom 1) and a second thing `budget.js` would have to chase. `hydrate: 'never'` + an island is still `X_ISLAND_NOT_HYDRATED`, and so is an `island()` call *below* the `defineRoute` that drains it. The `fix:` names exactly ONE of the two edits — `islandNeverDrained(spec)` tells the causes apart at the throw site, and a message offering both makes half the instruction wrong for every reader. |
50
- | Island node shape | a **branded array** (`IslandNode extends Array<never>`), and every walker tests `isIslandNode` BEFORE `Array.isArray`. An app types JSX with `jsxImportSource: solid-js`, whose `JSX.Element` is a type ALIAS — unaugmentable — and whose only object-shaped member is `ArrayElement`; a plain object was TS2786 at every `<Island />`, so the feature only worked through `h(Island, …)`, which is what render's own tests used. The array stays empty; the shell is `props.children`. Never satisfy this by importing solid's union. |
51
- | Island declaration order | `island()` above `defineRoute`, drained by it (`drainDeclaredIslands`). Package-internal — reachable from `./island`, never re-exported by `src/index.ts`: an app calling the drain between its `island()` and its `defineRoute` would silently un-declare the islands the route derives everything from, and a public export is semver-locked the moment it ships. Ambient, and NOT the thing the collector refuses to be: that one is per RENDER, where two requests would bill each other; this one is per MODULE, evaluated once, before any request — and `src` is resolved relative to the route file, so an `island()` call is route-module-local by construction. |
52
- | Derived budget | `registry.ts`, not `defineRoute`: a ceiling is only meaningful against a surface baseline, and the surface is a fact of the file path the route table already reads. `site/` → `20kb`, `app/` → `34kb` (`DEFAULT_ISLAND_JS_BYTES` above `jsBaselineBytes`). **Calibrated on a Solid island** `As of 2026-08`: it was `4kb`, sized from `contact-sales.island.tsx`, which imports no `solid-js` at all — and `render(() => <p>hello</p>, el)` measures 12,588 B, so the default sat a factor of three below the floor of every island that uses the JSX runtime, on every surface. A declared `budget.js` wins; a `'never'` route gets none, so the contradiction stays visible. |
53
- | `RouteEntry.islands` | filled from `config.islands` at registration, and from nothing else — `RegisterRouteInput` has no `islands` key. It was `input.islands ?? []`, undocumented and passed by nothing, so the now-deleted `routeJsBytes`'s "what registration declared" half read `[]` on every route in the framework's history; keeping it as a fallback would be a second answer to one question that can only ever weaken it, since a caller passing `[]` un-declares an island. The field survives its one former reader: it is the only record a build has of an island a page declared but did not render on a given pass. |
54
- | Island props | declared, JSON-safe, under `ISLAND_PROPS_MAX_BYTES` — `island-props.ts` is the one gate. A structural walk, never a `JSON.stringify` round trip: stringify drops a function and an `undefined` silently, which is the footgun rather than the check. The cap is **16 KiB** `As of 2026-09-05` (was 4; a 34-row catalog at 8,812 B was a runtime 500) and its doc comment carries the arithmetic — the bag is inlined as a JSON script `measureDocumentJs` counts as zero JS, so this constant is the only ceiling on it. Over the cap, the cause names the **heaviest props with their bytes** and the fix names the endpoint pattern (`models: []` + `modelsEndpoint`), never "raise the cap". `X_ISLAND_PROPS_INVALID` has a **row** in `@ultimat3/http`'s table (500): unclassified, `problem+json` blanked the one sentence that is the instruction. |
55
- | A prop lands via `Object.defineProperty` | never `out[key] = v`. For exactly one name — `__proto__`, which `JSON.parse` mints as a real OWN key off any request body — the assignment runs `Object.prototype`'s setter: the prop was DROPPED from the browser payload (the footgun the walk exists to prevent), the record handed back as `IslandProps` carried a prototype built from request data, so a later `bag.row.isAdmin` on the SERVER read attacker-chosen values, and `ISLAND_PROPS_MAX_BYTES` under-counted because `JSON.stringify` could not see it. Same shape `@ultimat3/mcp`'s `validate-args.ts` uses for the same class. |
56
- | An attribute alias is a `Map` | never a record — an object lookup walks the prototype chain, so `<div {...row} />` with a column named `toString` resolved the alias to a FUNCTION and `attribute.toLowerCase()` threw a bare `TypeError`: no code, no fix, the whole page 500s off a `load()` result. Same reason `MODE_SPECS[config.render]` in `modes.ts` is guarded by `Object.hasOwn`, where `render: 'constructor'` returned a frozen descriptor for a mode nothing implements. |
57
- | An attribute NAME is validated too | `ATTRIBUTE_NAME` in `html.ts` — `/^[A-Za-z_:][-A-Za-z0-9_:.]*$/`, refused as `null`. A name is emitted VERBATIM before the `=` and is escaped nowhere, so `{ 'q onmouseover=alert(1) r': 'ok' }` shipped a live event handler out of an object KEY. The handler check on the line under it folds case for the same reason: it was `name.startsWith('on')` while the two checks below it lowercased, so `ONERROR="alert(1)"` went out on the wire. Both are reachable by `<div {...row} />` over a JSON body or a JSONB column. `head.ts`'s `renderTag` is the package's OTHER attribute sink and shares the predicate (`isAttributeName`) — it emitted `<meta name="q" r onmouseover=alert(1) s="ok">` from an `attrs` key. One predicate, never two. |
58
- | Which attributes take a URL | `URL_BEARING_ATTRIBUTES` in `html.ts` — core's four (`href`, `src`, `action`, `formaction`) plus `data`, `poster`, `ping` and `xlink:href`, each of which a browser FOLLOWS. `srcdoc` is refused outright: its value is entity-decoded and THEN parsed as HTML, so `escapeAttribute`'s `&lt;script&gt;` becomes a live `<script>` on this origin — escaping cannot make markup inert, so the attribute is never emitted, the same way `innerHTML` stays the one explicit escape hatch. |
59
- | Island collection | per render, passed as `renderToHtml(tree, { islands })`. Never module-global and never on an ambient context — two concurrent requests would bill one page for the other's JS, and `assertNoPerRequestState` refuses a live context under `static` anyway. |
60
- | A byte count in a message | `formatBytes` from `@ultimat3/core`, never a local one. This package's copy stopped at `kb`, so a 5 MB route read `5120kb` in `X_BUDGET_EXCEEDED` while `@ultimat3/pwa`'s own copy said `5mb` for the same bytes — two halves of one build disagreeing about the size of one artifact. Still on this barrel, because `@ultimat3/cli`'s budget reporter reads it beside the route table. |
61
- | 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. |
62
- | 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. |
63
- | 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)`. |
64
- | 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. |
65
- | Island mount markers | `data-x-mounted=""` when `mount()` RESOLVED, `data-x-failed="<message>"` when it rejected — set by the runtime, never by `emitIslandAttributes`, because the server does not know the answer. `el.__x` alone cannot carry this: it is assigned when `import()` is CALLED, so a chunk still downloading and one whose `mount()` threw were the same observable, and telling those apart is what `x shot` gates on. Two attributes rather than two values of one, so `[data-x-mounted]` never counts a failure as a success. The rejection handler RETHROWS — swallowing it resolves `el.__x` and the interaction runtime flushes its replay queue into an island that never mounted, which is the row above reintroduced one layer out. Costs 129 B of shared prelude: `idle` 774, `visible` 846, `interaction` 1,251 (`As of 2026-08-25`, the numbers `DEFAULT_ISLAND_JS_BYTES` is derived from — `interaction` moved 1,067 -> 1,251 for the replay-target row below, and neither of the other two changed). |
66
- | A runtime that calls `boot` | TERMINATES the chain, because `boot` rethrows. `idle` and `visible` end in `.catch(hush)`; `interaction` passes `off` as the rejection arm of its `then`. A bare `boot(el)` produced a fresh rejected promise per call — one unhandled rejection per user event on an island whose `mount()` threw — and, on `interaction`, left `done` false, the listeners attached and the queue growing by one retained `Event` (each with a live `target`) per click, for an island that will never mount. Nothing is lost by swallowing here: the DOM already carries the failure as `data-x-failed`, which is the row above and the documented observable. `hydrate-runtime.test.ts` runs all three against a real module; Bun's runner fails a test on an unhandled rejection, so the omission reds the suite by itself. |
67
- | Where `interaction` replays | `aim(el, ev)` in `hydrate.ts`, never `ev.target`. Every island's `mount` opens with `el.textContent = ''` — the documented idiom, and what `settings`, `feed` and `like` all do — so the node the visitor pressed is DETACHED by the time the replay runs and `ev.target.dispatchEvent(c)` reached nothing: the first press did nothing and the second worked, which reads as a slow network and is never filed as a bug. `examples/dummy/apps/web/app/posts/[id]/page.tsx` declares `hydrate: 'idle'` in writing to avoid it. The runtime CAN tell the two mounts apart — `el.contains(ev.target)` AFTER the mount is the exact question — so this is a repair and not a refusal: refusing the pairing would delete a strategy that works today for a takeover-style island (`contact-sales.island.tsx` attaches to the server's own form and replaces nothing). Kept → the original target. Replaced → `document.elementFromPoint(ev.clientX, ev.clientY)`, which is where the event would land had the visitor pressed a moment later. The island ROOT is the last resort and never the repair: Solid's delegated listener sits on `document` and walks UP from the target (`solid-js/web`'s `eventHandler`), so a handler on a CHILD of the root is never visited and dispatching at the root fixes nothing for the canonical island. A hit landing outside this island falls back to the root too — synthesizing a click on an element the visitor never pressed is worse than losing the replay. `typeof ev.clientX === 'number'`, never `ev.clientX || ev.clientY`: (0, 0) is a coordinate. `hydrate-replay.test.ts` holds it, and it is a separate file because `hydrate-runtime.test.ts`'s element is BOTH the island root and every event's target — the two answers are the same node there, which is how this survived. |
68
- | `idle`'s deadline | `IDLE_HYDRATE_TIMEOUT_MS`, interpolated INTO the runtime string. Exported because a second reader has to agree — `x shot` waits before it photographs, and a settle shorter than this deadline reports an unhydrated page for one that hydrates perfectly. A constant the emitted string restates instead of reading is worse than no constant. |
69
- | Route truth | `registry.ts`. Never keep a second route list anywhere, and never a second *matcher*: this package's `matchRoute` was deleted in 2026-08 with zero consumers, because `@ultimat3/http`'s trie (`stages.ts`) is the one that serves requests and two matchers with different precedence rules is two answers to "which route is this?". `routeFor` is an exact-path `Map` lookup, not a pattern matcher. |
70
- | Route filename | `page.tsx` under `site/`/`app/`, `route.ts` under `api/` — `ROUTE_FILENAME`, one per surface. The URL is the directory path. Anything else is `X_ROUTE_FILE_INVALID`; never widen the table to accept a second spelling. |
71
- | Registry input | descriptors only. `registerRoute` refuses a raw declaration with `X_ROUTE_UNNORMALIZED` — `defineRoute` is the one normalizer of everything the declaration alone decides, and every reader downstream assumes it ran. The registry fills in exactly one value on top: the island budget, which needs the surface, which is a fact of the file path only the route table reads. |
72
- | Descriptors | `describeRoutes()` must stay JSON-safe, sorted by path, deterministic. |
73
- | Every string order in this package | `byCodeUnit` (`code-unit-order.ts`), never `localeCompare`. With no locale argument `localeCompare` answers from the runtime's ICU **default locale** and **collation version**: `'/A'` sorted after `'/a'` on one machine and before it on the next, and `'/zoo'` before `'/ärzte'` under `sv-SE` but after it under `en-US` — for the same route table. Three sites shipped it: `routeEntries()` (so `describeRoutes()`, and with it `x.manifest.json`, the sitemap and `sw.js`'s rule table), `checkSurfaceBoundary`'s report, and `pageComponentOf`'s fallback, whose own comment promised "the same one on every machine". Package-internal on purpose — a comparator is not public API. Same rule `@ultimat3/pwa`'s `precache.ts` and `service-worker.ts` state for the artifact they emit. |
35
+ | Descriptor `budget` | always an object, `{}` when undeclared. `budget.js === undefined` is the site/ hydration failure. |
36
+ | `RouteBudget` keys | **`js` and `lcp` only, and every key is PROJECTED** (`budgetJs`, `budgetLcp`). `type-pins.tsx` derives the allowed set from `RouteDescriptor`'s `budget*` keys, so an unprojected key is a build error; `scripts/declaration-readers.ts` finds the class across every declaration. |
37
+ | No `describe()` on a route | `describeRoutes()` is the one route list. |
38
+ | Mode invariants | `modes.ts` only; never inline a mode check in a render-\* file. **Four modes, and every one renders the route's component.** A fifth mode has to name the function that renders it. |
39
+ | Island declaration | `island({ src })` — a **specifier**, never an import, so a `static` page's graph cannot grow the island's dependencies. Never add an overload that takes a component. |
40
+ | Island filename | `*.island.tsx`, `ISLAND_EXTENSION` — one spelling. Never widen it. |
41
+ | Island timing | the route's `hydrate` and nothing else. `hydrate: 'never'` + an island, or an `island()` call below the `defineRoute` that drains it, is `X_ISLAND_NOT_HYDRATED`; `islandNeverDrained(spec)` tells the two causes apart so the `fix:` names exactly one edit. |
42
+ | Island node shape | a **branded array** (`IslandNode extends Array<never>`); every walker tests `isIslandNode` BEFORE `Array.isArray`. Solid's `JSX.Element` is an unaugmentable alias whose only object member is `ArrayElement`. Never import solid's union. |
43
+ | Island declaration order | `island()` above `defineRoute`, drained by it (`drainDeclaredIslands`), reachable from `./island` and never from `src/index.ts`. Per MODULE (evaluated once), unlike the per-render collector. |
44
+ | Derived budget | `registry.ts`, from the surface: `site/` → `20kb`, `app/` → `34kb` (`DEFAULT_ISLAND_JS_BYTES` above `jsBaselineBytes`), calibrated on a Solid island. A declared `budget.js` wins; a `'never'` route gets none. |
45
+ | `RouteEntry.islands` | filled from `config.islands` at registration and nothing else — `RegisterRouteInput` has no `islands` key. |
46
+ | Island props | declared, JSON-safe, under `ISLAND_PROPS_MAX_BYTES` (16 KiB; the doc comment carries the arithmetic) — `island-props.ts` is the one gate, a structural walk, never a `JSON.stringify` round trip. Over the cap the cause names the heaviest props and the fix names the endpoint pattern, never "raise the cap". `X_ISLAND_PROPS_INVALID` has a row in `@ultimat3/http`'s table (500). |
47
+ | A prop lands via `Object.defineProperty` | never `out[key] = v` — `__proto__` (a real own key off `JSON.parse`) would run the prototype setter. |
48
+ | An attribute alias is a `Map` | never a record (`toString` resolved to a function). `MODE_SPECS[config.render]` is guarded by `Object.hasOwn` for the same reason. |
49
+ | An attribute NAME is validated too | `ATTRIBUTE_NAME` in `html.ts` (`/^[A-Za-z_:][-A-Za-z0-9_:.]*$/`), refused as `null`; the `on*` handler check folds case. `head.ts`'s `renderTag` shares the predicate (`isAttributeName`). One predicate, never two. |
50
+ | Which attributes take a URL | `URL_BEARING_ATTRIBUTES` in `html.ts` — core's four plus `data`, `poster`, `ping`, `xlink:href`. `srcdoc` is refused outright (entity-decoded, then parsed as HTML). |
51
+ | Island collection | per render, `renderToHtml(tree, { islands })`. Never module-global and never on an ambient context. |
52
+ | A byte count in a message | `formatBytes` from `@ultimat3/core`, never a local one. |
53
+ | Island bytes | **not this package's**: `@ultimat3/cli`'s `packages/cli/src/budgets.ts` measures the EMITTED document and is the gate. What stays here is the budget grammar (`parseByteBudget`) and `defaultIslandBudget`. |
54
+ | The hydration runtime's CSP | `HYDRATE_RUNTIME_BODIES` — every body `hydrateRuntime` can emit (seven subsets). It is inline in every document with an island and `@ultimat3/http`'s `script-src` is `'self' 'wasm-unsafe-eval'`, so `@ultimat3/cli`'s `script-csp.ts` hashes this list at boot. `runtimeBody` is the one place the served and hashed text are one string. **Uncovered**: `render-stream.ts`'s per-hole `<script>$X("id")</script>` (unreachable today; the first real hole needs a nonce). |
55
+ | Island markup | the props `<script>` is emitted INSIDE the wrapper by `render-html.ts`; an assembler only adds `hydrateRuntime(directives)`. |
56
+ | Island boot | `el.__x` holds the boot PROMISE, never a boolean. |
57
+ | Island mount markers | `data-x-mounted=""` when `mount()` resolved, `data-x-failed="<message>"` when it rejected — set by the runtime, never by `emitIslandAttributes`. The rejection handler RETHROWS. Prelude sizes `As of 2026-09-22`: `idle` 1,744, `visible` 846, `interaction` 1,629 B (what `DEFAULT_ISLAND_JS_BYTES` derives from). |
58
+ | A runtime that calls `boot` | TERMINATES the chain: `idle`/`visible` end in `.catch(hush)`, `interaction` passes `off` as the rejection arm. `hydrate-runtime.test.ts` reds on an unhandled rejection. |
59
+ | `idle` replays too | `idle` and `interaction` share ONE `catchUp(el)` in `hydrate.ts` (capture listeners for `data-x-events`, default `click`; a queue; `aim`; one flush). A caught event wakes an `idle` island early; each event replays once. `hydrate-replay.test.ts`. |
60
+ | Where `interaction` replays | `aim(el, ev)`, never `ev.target` — `mount` usually clears the root, detaching the pressed node. Kept → original target; replaced → `path(el, target)`'s structural walk (same tag, same place), then `document.elementFromPoint(ev.clientX, ev.clientY)` (unless `ev.detail === 0`), then the island root. A hit outside the island falls back to the root. `hydrate-replay.test.ts`. |
61
+ | `idle`'s deadline | `IDLE_HYDRATE_TIMEOUT_MS`, interpolated INTO the runtime string and exported for `x shot`'s settle. |
62
+ | Route truth | `registry.ts`. Never a second route list and never a second matcher — `@ultimat3/http`'s trie (`stages.ts`) serves requests. `routeFor` is an exact-path `Map` lookup. |
63
+ | Route filename | `page.tsx` under `site/`/`app/`, `route.ts` under `api/` — `ROUTE_FILENAME`. Anything else is `X_ROUTE_FILE_INVALID`. |
64
+ | Registry input | descriptors only. `registerRoute` refuses a raw declaration with `X_ROUTE_UNNORMALIZED`; it fills in only the island budget. |
65
+ | Descriptors | `describeRoutes()` stays JSON-safe, sorted by path, deterministic. |
66
+ | Every string order | `byCodeUnit` (`code-unit-order.ts`), never `localeCompare` (ICU default locale and collation version vary by machine). |
74
67
  | Boundary | `surfaces.ts` throws; it never warns. Type-only edges are not violations. |
75
- | Stream cancellation | the underlying source has a `cancel()`, and `write` is guarded on it. A client that disconnects mid-stream aborts `StreamHole.resolve(signal)` and every later `write`/`close` is a no-op — `settle()` on a cancelled controller threw out of a `void`ed promise, one unhandled rejection per response, while the resolved holes kept doing their database work with nowhere to write. |
76
- | ISR detach | `attach()`'s returned function clears the revalidator as well as the dependents — and only if the slot is still its own, tracked in `installedRevalidator` because `@ultimat3/cache` holds ONE and offers no read back. Left installed, a detached controller and its whole store stayed reachable and kept receiving revalidations while the live one's pages never went stale. |
77
- | "Is this a TTL?" has one reader | `parseTtlMs` in `duration.ts`, below both `modes.ts` and `render-isr.ts` (importing the latter from the former is a cycle through `registry.ts`). `hasRevalidateTrigger` accepted any non-empty string, so `revalidate: { ttl: '5 minutes' }` passed registration and parsed to `null` at serve time: generated ONCE, served for the life of the process, while the CDN was told `s-maxage=60` — the exact costume the `isr`-needs-a-trigger check refuses. |
78
- | A build-time frame reads a throw with `renderThrowable` | `render-html.ts`, `render-static.ts`, `css-modules.ts`, `module-loader.ts`, `route-data.ts` — never `error instanceof Error ? error.message : String(error)`. A component that throws `Object.create(null)` escaped as a bare `TypeError` and one whose `message` getter throws as a bare `Error`, where `X_PRERENDER_FAILED` naming the file belongs. `bun run error-render` does NOT see this shape: it follows a direct interpolation, not a value laundered through a file-local `describe()` helper. |
79
- | `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. |
80
- | 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. |
81
- | 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. |
82
- | 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`. |
83
- | 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. |
84
- | A `RenderResult.status` | `finiteStatus(subject, status)` in `finite-status.ts`, at every site that takes one from a caller (`renderSsr`'s `options.status`, `streamResult`'s third argument, `withStatus`'s first, and an `IsrRenderFn` answering `{ html, status }` at generation — `isRenderStatus`, the same range as a predicate, is the TOTAL read of a stored `IsrEntry.status` on the request path, because a custom store may JSON-round-trip one and a throw there is a 500 for the page's whole TTL) — 200–599, whole. It reaches `new Response(body, { status })`, which answers a bare `RangeError` for anything else: `NaN` arrives there as `The status provided (-9223372036854775808)`, no code, no fix, and the render fails two frames above the route that set it. The screen is NARROWER than the boundary on purpose — `new Response` also takes `101`, and no rendered document is a protocol switch. The name carries `finite` because `bun run finite-bounds` recognises a repair by the shape of the CALL; spelled `renderStatus` it read as no screen at all. |
85
- | `IsrEntry.ttlMs` off a store | normalised by `entryTtlMs`, TOTAL, never a throw — a ttl that is not a POSITIVE FINITE number of ms is the tag-only `null` `parseTtlMs` would have answered. `IsrStore` is a driver seam, and one backed by Redis round-trips the entry through JSON where a `ttlMs` nobody wrote reads back as `undefined`, so `entry.ttlMs === null` is false. Two failures, neither raising: `now - generatedAt < NaN` is false so the page is NEVER fresh and every request regenerates it, and the CDN is handed `s-maxage=NaN` — a directive a conforming cache IGNORES, dropping the page to heuristic caching. Read on the request path, so a refusal would turn a bad stored entry into a 500; the `isr.entry_ttl_invalid` warning is what keeps it from being silent. |
86
- | 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. |
87
- | An undecodable path segment | not a match, never a throw — `decodeSegment` in `registry.ts` is the one reader. `decodeURIComponent('%zz')` is a bare `URIError`, so a typo in a path segment was a 500 where `@ultimat3/http`'s router already answers "this branch does not match". A literal route matching the same text still wins. `router-client.ts` was the second reader and went with `createRouter`. |
88
- | ISR registration | reconciled against `store.paths()` after every generation (`forgetEvictedPaths`). The store is bounded and evicts silently; `registered` and the cache graph behind it only ever grew, one edge per slug ever requested. Never a store callback — a custom `IsrStore` has none. |
89
- | Stream hole deadline | `DEFAULT_HOLE_TIMEOUT_MS` (15s), `holeTimeoutMs: null` to opt out, and a declared one is a whole number of at least **1** — a deadline is the bound a non-finite value does not disable but MOVES: `setTimeout(fn, NaN)` is `setTimeout(fn, 0)`, so every hole misses a deadline nobody set and the document is all fallbacks. There is no spelling of `holeTimeoutMs` that means "immediately". A hole is app code the framework `await`s and nothing else bounds it; one that never settles held the response and its whole closure open for the life of the process. A hole reveals exactly once — its promise, its rejection, or its deadline, whichever lands first. |
68
+ | Stream cancellation | the source has a `cancel()` and `write` is guarded on it; a disconnect aborts `StreamHole.resolve(signal)`. |
69
+ | ISR detach | `attach()`'s returned function clears the revalidator too — only if the slot is still its own (`installedRevalidator`). |
70
+ | "Is this a TTL?" has one reader | `parseTtlMs` in `duration.ts`, below `modes.ts` and `render-isr.ts`. |
71
+ | A build-time frame reads a throw with `renderThrowable` | `render-html.ts`, `render-static.ts`, `css-modules.ts`, `module-loader.ts`, `route-data.ts`. `bun run error-render` cannot see a value laundered through a local helper. |
72
+ | `X_ROUTE_LOAD_FAILED` computes its pathname BEFORE the try | `pathnameOf` never throws on a relative `ctx.url`. |
73
+ | The ISR key | `isrKey(url, locale)` — pathname, the negotiated locale in the reserved `__x_locale` parameter, then the sorted query. The locale is REQUIRED. The time zone is deliberately not a dimension. `toResult` emits `vary: accept-language`; the rest of the shared key is `@ultimat3/http`'s `cache-headers` stage. |
74
+ | A bust that lands MID-render | fenced with `@ultimat3/cache`'s `sampleFence({ key, tags })`, taken before `render()` and asked before `store.set`. `registerPath` runs BEFORE the render. |
75
+ | Marking a page stale | `IsrStore.markStale(path)`, in place — never `set({ ...entry, stale: true })` (`set` means "just generated" and orders eviction). |
76
+ | ISR store bound | `memoryIsrStore()` caps at `DEFAULT_ISR_MAX_ENTRIES` (1,000), least recently generated first. |
77
+ | A `RenderResult.status` | `finiteStatus(subject, status)` (`finite-status.ts`), 200–599 whole, at every site that takes one from a caller; `isRenderStatus` is the TOTAL read of a stored `IsrEntry.status`. The name carries `finite` so `bun run finite-bounds` recognises it. |
78
+ | `IsrEntry.ttlMs` off a store | normalised by `entryTtlMs`, TOTAL — anything not positive-finite is tag-only `null`, with an `isr.entry_ttl_invalid` warning. |
79
+ | Route path from file | `locateSurface()` answers which surface AND where it starts. Never re-derive the offset from the surface name. |
80
+ | An undecodable path segment | not a match, never a throw — `decodeSegment` in `registry.ts`. |
81
+ | ISR registration | reconciled against `store.paths()` after every generation (`forgetEvictedPaths`). |
82
+ | Stream hole deadline | `DEFAULT_HOLE_TIMEOUT_MS` (15 s), `holeTimeoutMs: null` to opt out, otherwise a whole number ≥ 1. A hole reveals exactly once — promise, rejection or deadline. |
90
83
  | Errors | `errors.ts` subclasses only. Never a bare `Error`, never a bare `TODO`. |
91
84
  | Policy | render checks *presence* only. Evaluation belongs to `@ultimat3/policy`. |
92
- | A gated route is never a cached one | `modes.ts` refuses `policy` on BOTH `static` and `isr` (`X_ROUTE_MODE_INVALID`, `modes.test.ts`). `isr` was not refused until 2026-08 and `dev-render.ts` keys the cache on `url.pathname` alone — no actor, no query string — so a gated ISR route rendered the first actor's document and served it to every later actor who passed the same policy. Keying on more is a trap, not a fix: the key would have to enumerate everything a `policy` and a `load` can read. `ssr` is the one gated mode there is. |
85
+ | A gated route is never a cached one | `modes.ts` refuses `policy` on both `static` and `isr` (`X_ROUTE_MODE_INVALID`, `modes.test.ts`). `ssr` is the one gated mode. |
93
86
  | Responses | return `RenderResult`. `@ultimat3/http` builds the `Response`. |
94
- | 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. |
95
- | 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. |
96
- | 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. |
97
- | The loaders | `module-loader.ts` installs them at **`server.ts`** module scope, once — `index.ts` until the barrel split, and it cannot be there again: the client barrel would carry `sass` and `node:url`. A plugin only affects modules loaded after it, so a second install point is a page that renders in one entry point and not another. Anything that loads an app's `.tsx` reaches `@ultimat3/render/server` first, which is why `packages/cli/src/app-load.ts` imports it for the side effect and nothing else. |
98
- | `<head>` baseline | `documentBaseline()` in `head.ts` — charset, viewport, `color-scheme` — merged FIRST so a route can still override any of them. Absent until `As of 2026-08`, and the missing `viewport` is why every deployed app rendered zoomed-out on a phone whatever its CSS said. |
99
- | Escaping | `html.ts` only, and that now includes `render-stream.ts` (`holeMarker`'s attribute, and `revealChunk`'s `$X(...)` argument via `JSON.stringify` — an id containing `")` closed the call and ran the rest) and `head.ts`'s `themeScript` (`storageKey`/`attribute` as JS string LITERALS). All author-controlled today — the identical status `emitIslandAttributes` had before the last sweep. `render-spa.ts` was the third entry here and went with the mode. A second escaper is how one of them ends up missing a character, and a missing character in an attribute is an injection. `escapeAttribute` itself is `@ultimat3/seo`'s (tier 1), re-exported by `html.ts` rather than reimplemented — the copy that lived here was the second escaper this row forbids, and `pwa/CLAUDE.md` already named seo's as the one. `head.ts` and `hydrate.ts` each had a private copy; both now import — `escapeAttribute` for every attribute value (`emitIslandAttributes` interpolated all five of its own raw until 2026-08, while this row already claimed otherwise) and `escapeJsonContent` for a JSON script body. |
100
- | Script and style CONTENT | never emitted raw. Three rules, one choice: HTML text (`escapeText`), raw text for code (`escapeRawTextContent`: `</` → `<\/`, `<!--` → `<\!--`), and the total JSON rule for a `type` ending in `json` (`escapeJsonContent`: `<`, `>`, `&`, U+2028/9 → `\uXXXX`, still valid JSON). `meta.ld` is built from route data, and it was emitted VERBATIM until `As of 2026-08` — a title could close the element. Never HTML-escape a script body: a character reference is not decoded there, so `&lt;` corrupts the code AND leaves the hole. |
101
- | Which export is the page | `route-component.ts`, one precedence: `Page` → a single `…Page` → a single capitalised function. Never a per-generator name table. |
102
- | Stylesheets | compiled by `css-modules.ts`, still grouped per surface, and served by the CLI as **one content-hashed file per surface** (`@ultimat3/cli`'s `style-bundle.ts`) rather than inlined — measured 2026-09-06, the inline block was 156,738 bytes inside a `no-store` document, re-sent on every navigation. `stylesFor` is unchanged and is what that file is built from. `sass` is this package's only third-party dependency and its only reason to exist here. |
103
- | CSS order | `stylesFor` sorts **globals before modules** (`isGlobalStylesheet`), never plain insertion order — the reset styles bare elements at the lowest specificity there is, so whichever page loaded first must not decide who wins a tie. `shared/` is carried by both graphs, like a package sheet: it is where an app's own global layer lives, and filtering it out is what made every deployed app render token-less. |
104
- | The global layer | this package may not import `@ultimat3/ui` (tier 4, the same tier — sideways, not upward: `ui` moved 5 → 4 in 2026-08 and `render → ui` stays forbidden because a same-tier edge has to be declared in `scripts/lib/tiers.ts`, and this one deliberately is not — the static bundle graph may not reach the design system, axiom 6), so the app's source graph carries it: one `shared/global.scss` that `@use`s `@ultimat3/ui/global.scss`, side-effect-imported by `shared/global.ts`. One file, because each stylesheet is its own Sass compilation — a token file `@use`d per module duplicates its `:root` block per module. `x verify` fails with `X_STYLES_GLOBAL_MISSING` when a surface's document defines none. |
87
+ | Who owns `cache-control` | a mode states intent; `@ultimat3/http`'s `cache-headers` stage decides and may overrule. This package cannot see the actor and never tries. |
88
+ | Solid | no `solid-js` import anywhere in this package; `type-pins.tsx` satisfies `JSX.Element` structurally. `jsx.ts` builds inert nodes. Islands are compiled by `@ultimat3/cli`'s `solid-loader.ts` with `babel-preset-solid`. |
89
+ | Root element | `ROOT_ELEMENT_ID` (`render-html.ts`). |
90
+ | The loaders | `module-loader.ts` installs them at **`server.ts`** module scope, once. `packages/cli/src/app-load.ts` imports `@ultimat3/render/server` for that side effect. |
91
+ | Client sync tags | `client-sync-tags.ts` — `ultimate-sync`, `ultimate-build`, `ultimate-sync-worker`. Principal-free, on every document. |
92
+ | Client scope tag | `client-scope-tag.ts` — `<meta name="ultimate-scope">`, read by core's `pageClient()`. `documentCarriesScope(headers)` is the one rule: only a `private` document carries it; absent means "not rendered for anyone". The literal is duplicated in core until core exports a constant. |
93
+ | `<head>` baseline | `documentBaseline()` in `head.ts` — charset, viewport, `color-scheme` — merged FIRST so a route can override any of them. |
94
+ | Escaping | `html.ts` only — including `render-stream.ts`'s `holeMarker` and `revealChunk` (`JSON.stringify`), and `head.ts`'s `themeScript`. `escapeAttribute` is `@ultimat3/seo`'s, re-exported by `html.ts`. |
95
+ | Script and style CONTENT | never raw: `escapeText`, `escapeRawTextContent` (`</` → `<\/`, `<!--` → `<\!--`), or `escapeJsonContent` for a `type` ending in `json`. Never HTML-escape a script body. |
96
+ | Which export is the page | `route-component.ts`: `Page` → a single `…Page` → a single capitalised function. |
97
+ | Stylesheets | compiled by `css-modules.ts`, grouped per surface, served by the CLI as one content-hashed file per surface (`@ultimat3/cli`'s `style-bundle.ts`) from `stylesFor`. `sass` is this package's only third-party dependency. |
98
+ | CSS order | `stylesFor` sorts **globals before modules** (`isGlobalStylesheet`). `shared/` is carried by both graphs. |
99
+ | The global layer | the app's `shared/global.scss` `@use`s `@ultimat3/ui/global.scss`, side-effect-imported by `shared/global.ts` (this package may not import `ui`). `x verify` fails with `X_STYLES_GLOBAL_MISSING` when a surface's document defines none. |
105
100
  | Colours | tokens and `data-theme` only. No hex in `head.ts` or any emitted script. |
106
- | An island's `mount` | may return `() => void`, its disposer — `return render(…)`. The runtime resolves `el.__x` with it and never calls it; `@ultimat3/testing`'s `mountIsland` does, on dispose. |
107
- | `<head>` binding | `head.ts` stays injection-only (testable with no catalog); `head-seo.ts` is the ONE binding of `HeadRenderers` to `@ultimat3/seo`. A caller writing its own converter is the drift this file prevents. |
101
+ | An island's `mount` | may return `() => void`, its disposer; the runtime never calls it, `@ultimat3/testing`'s `mountIsland` does. |
102
+ | `<head>` binding | `head.ts` stays injection-only; `head-seo.ts` is the ONE binding of `HeadRenderers` to `@ultimat3/seo`. |
108
103
 
109
104
  Cross-package: `@ultimat3/pwa` consumes route descriptors as **data**, never by import.
110
105
  Keep `RouteDescriptor` additive — removing a field breaks `sw.js` generation.
@@ -114,3 +109,5 @@ bun test # from packages/render
114
109
  bun run typecheck
115
110
  bun run --cwd ../.. verify # the contract
116
111
  ```
112
+
113
+ Why each rule above is shaped the way it is: [`docs/history/render.md`](../../docs/history/render.md).
package/README.md CHANGED
@@ -210,17 +210,17 @@ production Solid, `As of 2026-08`:
210
210
  | `render(() => <p>hello</p>, el)` — the floor, before an author writes a line | 12,588 |
211
211
  | a signal, a button and reactive text | 13,663 |
212
212
  | `settings.island.tsx`, the heaviest island this repo ships | 17,797 |
213
- | one directive's hydration runtime at `hydrate: 'idle'` | 774 |
214
- | the same at `'interaction'`, which is what an island route declaring no `hydrate` gets | 1,251 |
213
+ | one directive's hydration runtime at `hydrate: 'idle'`, what an `app/` island gets from `defaultHydrate` | 1,744 |
214
+ | the same at `'interaction'`, which is what an island route declaring no `hydrate` gets | 1,629 |
215
215
 
216
- 17,797 + 1,251 = **19,048** — the heaviest island this repo ships, plus the runtime an app pays
217
- without writing a number down. `DEFAULT_ISLAND_HYDRATE` is `'interaction'`
218
- ([`route.ts:33`](src/route.ts)), applied at `:253` to any island route that states no `hydrate`, so
219
- `idle`'s 774 is the cheaper case and not the one a budget has to clear.
216
+ 17,797 + 1,744 = **19,541** — the heaviest island this repo ships, plus the costlier of the two
217
+ runtimes an app pays without writing a number down. `idle` became the costlier on 2026-09-22, when
218
+ it learned to catch a press made before it mounted and replay it through the same `catchUp` as
219
+ `interaction`, and both learned to aim a keyboard press by the pressed node's path (774 -> 1,744);
220
+ `DEFAULT_ISLAND_HYDRATE` is `'interaction'` ([`route.ts:34`](src/route.ts)) at 1,629.
220
221
 
221
- The default is **20,480** (20kb), which is not that number rounded: the next whole kilobyte above
222
- it is 19,456, and clearing today's worst island by 408 bytes is a ceiling the next line anyone
223
- writes breaks. 20kb leaves 1,432 B, and stays under 2× 19,048 — so a route that bundles the same
222
+ The default is **20,480** (20kb), the next whole kilobyte above that number: 939 B of headroom,
223
+ and under 2× 19,541 — so a route that bundles the same
224
224
  island twice is still refused. All three clauses are assertions in
225
225
  [`island-budget.test.ts`](src/island-budget.test.ts)'s `DEFAULT_ISLAND_JS_BYTES` block, against the
226
226
  measured table above; a default that stopped clearing the floor, or stopped being a ceiling, is red.
@@ -433,6 +433,28 @@ does.
433
433
  side effect. Anything that loads an app's source — `x dev`, `x build`, `server.ts`, a test that
434
434
  `await import()`s a `page.tsx` — reaches it before the module it loads.
435
435
 
436
+ ## Error classes
437
+
438
+ Every error class `src/index.ts` exports, for `instanceof` inside one process. Across a wire or
439
+ a job boundary the class is gone and the `code` is what survives — match on that.
440
+
441
+ | Class | Code | Declared in |
442
+ |---|---|---|
443
+ | `BudgetExceededError` | `X_BUDGET_EXCEEDED` | `src/errors.ts` |
444
+ | `IslandInvalidError` | `X_ISLAND_INVALID` | `src/errors.ts` |
445
+ | `IslandNotHydratedError` | `X_ISLAND_NOT_HYDRATED` | `src/errors.ts` |
446
+ | `IslandPropsInvalidError` | `X_ISLAND_PROPS_INVALID` | `src/errors.ts` |
447
+ | `PrerenderFailedError` | `X_PRERENDER_FAILED` | `src/errors.ts` |
448
+ | `RouteDuplicateError` | `X_ROUTE_DUPLICATE` | `src/errors.ts` |
449
+ | `RouteFileInvalidError` | `X_ROUTE_FILE_INVALID` | `src/errors.ts` |
450
+ | `RouteLoadFailedError` | `X_ROUTE_LOAD_FAILED` | `src/errors.ts` |
451
+ | `RouteLoadInvalidError` | `X_ROUTE_LOAD_INVALID` | `src/errors.ts` |
452
+ | `RouteMetaMissingError` | `X_ROUTE_META_MISSING` | `src/errors.ts` |
453
+ | `RouteModeInvalidError` | `X_ROUTE_MODE_INVALID` | `src/errors.ts` |
454
+ | `RouteOfflineMissingError` | `X_ROUTE_OFFLINE_MISSING` | `src/errors.ts` |
455
+ | `RouteStatusInvalidError` | `X_ROUTE_STATUS_INVALID` | `src/errors.ts` |
456
+ | `SurfaceBoundaryError` | `X_SURFACE_BOUNDARY` | `src/errors.ts` |
457
+
436
458
  ## Public API
437
459
 
438
460
  `†` marks a name on `@ultimat3/render/server`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "20.2.1",
3
+ "version": "22.0.0",
4
4
  "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,10 +36,10 @@
36
36
  "test": "bun test"
37
37
  },
38
38
  "dependencies": {
39
- "@ultimat3/cache": "20.2.1",
40
- "@ultimat3/core": "20.2.1",
41
- "@ultimat3/i18n": "20.2.1",
42
- "@ultimat3/seo": "20.2.1",
43
- "sass": "1.102.0"
39
+ "@ultimat3/cache": "22.0.0",
40
+ "@ultimat3/core": "22.0.0",
41
+ "@ultimat3/i18n": "22.0.0",
42
+ "@ultimat3/seo": "22.0.0",
43
+ "sass": "1.104.0"
44
44
  }
45
45
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The page's client scope, as a `<head>` tag: `<meta name="ultimate-scope" content="…">`, read
3
+ * once by `@ultimat3/core`'s `pageClient()` when the page's client handle is created. Only a
4
+ * PRIVATE document carries one (`documentCarriesScope`): anything a shared cache may hold would
5
+ * serve one visitor's scope to the next, and a store fenced as the wrong principal is the leak the
6
+ * fence exists to stop.
7
+ */
8
+
9
+ import { CLIENT_PERSIST_META, CLIENT_SCOPE_META } from '@ultimat3/core';
10
+ import type { HeadTag } from './head';
11
+
12
+ /** The `name` core's reader matches — core's constant, so the writer and the reader are one literal. */
13
+ export { CLIENT_PERSIST_META, CLIENT_SCOPE_META };
14
+
15
+ /** `scope` is opaque (`@ultimat3/auth`'s `clientScopeOf`), and `''` is the anonymous page. */
16
+ export function clientScopeTag(scope: string): HeadTag {
17
+ return {
18
+ kind: 'meta',
19
+ key: `meta:${CLIENT_SCOPE_META}`,
20
+ attrs: { name: CLIENT_SCOPE_META, content: scope },
21
+ };
22
+ }
23
+
24
+ /**
25
+ * The record types this app keeps on disk (`entity(name, { persist: true })`), for the page's
26
+ * persister. Beside the scope and ONLY beside it: persistence is per principal, so a document with
27
+ * no scope has nothing to persist under. None persisted is no tag — absent already means "none".
28
+ */
29
+ export function clientPersistTags(types: readonly string[]): readonly HeadTag[] {
30
+ if (types.length === 0) return [];
31
+ return [
32
+ {
33
+ kind: 'meta',
34
+ key: `meta:${CLIENT_PERSIST_META}`,
35
+ attrs: { name: CLIENT_PERSIST_META, content: [...types].sort().join(',') },
36
+ },
37
+ ];
38
+ }
39
+
40
+ /**
41
+ * Whether a document with these response headers may carry a scope: its `cache-control` says
42
+ * `private`. Read off the headers the mode ALREADY decided — `ssrHeaders` makes a gated page
43
+ * private and an ungated one `public, s-maxage`, a stream is always private, `static`/`isr` never —
44
+ * so this is not a second opinion on what is shareable.
45
+ */
46
+ export function documentCarriesScope(headers: Readonly<Record<string, string>>): boolean {
47
+ const cacheControl = headers['cache-control'] ?? '';
48
+ return cacheControl.split(',').some((directive) => directive.trim().toLowerCase() === 'private');
49
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The page's sync target as `<head>` tags, read by `@ultimat3/realtime`'s tab-side socket host:
3
+ * where the one socket dials, which build the page was rendered by, and which worker script hosts
4
+ * the socket. Principal-free, so a shareable document may carry them — unlike the scope tag.
5
+ */
6
+
7
+ import { CLIENT_BUILD_META, CLIENT_SYNC_META, CLIENT_SYNC_WORKER_META } from '@ultimat3/core';
8
+ import type { HeadTag } from './head';
9
+
10
+ /** Core's names (`page-meta.ts`), so the writer here and every reader share one literal each. */
11
+ export { CLIENT_BUILD_META, CLIENT_SYNC_META, CLIENT_SYNC_WORKER_META };
12
+
13
+ export interface ClientSyncHead {
14
+ readonly syncUrl: string;
15
+ readonly buildId: string;
16
+ readonly workerUrl?: string | undefined;
17
+ /** `/_x/page-boot/<hash>.js` — realtime's page boot, when the app has realtime. */
18
+ readonly bootUrl?: string | undefined;
19
+ }
20
+
21
+ export function clientSyncTags(head: ClientSyncHead): readonly HeadTag[] {
22
+ return [
23
+ meta(CLIENT_SYNC_META, head.syncUrl),
24
+ meta(CLIENT_BUILD_META, head.buildId),
25
+ ...(head.workerUrl === undefined ? [] : [meta(CLIENT_SYNC_WORKER_META, head.workerUrl)]),
26
+ ];
27
+ }
28
+
29
+ const meta = (name: string, content: string): HeadTag => ({
30
+ kind: 'meta',
31
+ key: `meta:${name}`,
32
+ attrs: { name, content },
33
+ });
34
+
35
+ /**
36
+ * The page boot as one deferred classic script: it runs before every island module that follows it
37
+ * in the document, and ONCE per page — the disk restore and the outbox are the page's job, never
38
+ * each island's. Rendered only where there is a principal to restore for (`dev-render.ts`).
39
+ */
40
+ export function clientBootTags(head: ClientSyncHead): readonly HeadTag[] {
41
+ return head.bootUrl === undefined
42
+ ? []
43
+ : [{ kind: 'script', key: 'script:ultimate-boot', attrs: { src: head.bootUrl, defer: true } }];
44
+ }
@@ -8,7 +8,7 @@ import { existsSync } from 'node:fs';
8
8
  import { basename, dirname, resolve } from 'node:path';
9
9
  import { fileURLToPath, pathToFileURL } from 'node:url';
10
10
  import { renderThrowable } from '@ultimat3/core';
11
- import * as sass from 'sass';
11
+ import type * as Sass from 'sass';
12
12
  import { PrerenderFailedError } from './errors';
13
13
  import { contentHash } from './render-static';
14
14
 
@@ -38,7 +38,7 @@ export function isGlobalStylesheet(file: string): boolean {
38
38
  * Sass resolves relative `@use` itself; a bare specifier is Bun's job, because `@ultimat3/ui/tokens`
39
39
  * is an `exports` entry and only the module resolver knows where that lands.
40
40
  */
41
- const packageImporter = (from: string): sass.FileImporter<'sync'> => ({
41
+ const packageImporter = (from: string): Sass.FileImporter<'sync'> => ({
42
42
  findFileUrl(url: string, context: { readonly containingUrl?: URL | null }): URL | null {
43
43
  // Sass routes every load inside a file THIS importer supplied back to this importer, including
44
44
  // `_index.scss`'s own relative `@forward`s — so the filesystem lookup has to live here too, or
@@ -185,11 +185,24 @@ export function stripCharset(css: string): string {
185
185
  return css.replace(CHARSET_HEAD, '');
186
186
  }
187
187
 
188
+ let loadedSass: typeof Sass | undefined;
189
+
190
+ /**
191
+ * Dart Sass, loaded on the first compile and never at import: it is ~290ms of module evaluation,
192
+ * measured, and every process importing `@ultimat3/render/server` paid it — `x --help`, `x doctor`,
193
+ * a container role that renders no stylesheet. `require` and not `import()` because this function
194
+ * is synchronous: Bun's loader-plugin `onLoad` path in `module-loader.ts` calls it inline.
195
+ */
196
+ const sassCompiler = (): typeof Sass => {
197
+ loadedSass ??= require('sass') as typeof Sass;
198
+ return loadedSass;
199
+ };
200
+
188
201
  export function compileStylesheet(file: string, source: string): CompiledStylesheet {
189
202
  let css: string;
190
203
  try {
191
204
  css = stripCharset(
192
- sass.compileString(source, {
205
+ sassCompiler().compileString(source, {
193
206
  url: pathToFileURL(file),
194
207
  loadPaths: [dirname(file)],
195
208
  importers: [packageImporter(dirname(file))],
package/src/duration.ts CHANGED
@@ -6,13 +6,14 @@
6
6
  * the process while the CDN was told `s-maxage=60`.
7
7
  */
8
8
 
9
- const DURATION_UNITS: Readonly<Record<string, number>> = {
10
- ms: 1,
11
- s: 1_000,
12
- m: 60_000,
13
- h: 3_600_000,
14
- d: 86_400_000,
15
- };
9
+ /** A `Map`, never an object literal: a unit is read by a string key, and a `Map` has no prototype chain to answer for it. */
10
+ const DURATION_UNITS: ReadonlyMap<string, number> = new Map([
11
+ ['ms', 1],
12
+ ['s', 1_000],
13
+ ['m', 60_000],
14
+ ['h', 3_600_000],
15
+ ['d', 86_400_000],
16
+ ]);
16
17
 
17
18
  /** `'5m'` → 300000. Numbers pass through as milliseconds. */
18
19
  export function parseTtlMs(ttl: string | number | null | undefined): number | null {
@@ -22,6 +23,6 @@ export function parseTtlMs(ttl: string | number | null | undefined): number | nu
22
23
  const amount = match?.[1];
23
24
  const unit = match?.[2];
24
25
  if (amount === undefined || unit === undefined) return null;
25
- const factor = DURATION_UNITS[unit];
26
+ const factor = DURATION_UNITS.get(unit);
26
27
  return factor === undefined ? null : Number(amount) * factor;
27
28
  }