@ultimat3/render 8.0.0 → 9.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,6 +4,20 @@ 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.
20
+
7
21
  `island()` is a **factory over the route's own `hydrate`**, not a ninth primitive and not a
8
22
  second render mode — the same rule `llm()` and `backfill()` follow. It adds no key to
9
23
  `defineRoute`.
@@ -68,7 +82,7 @@ for the mirror-image reason, keeping `render → pwa` refused. Never `cli` (upwa
68
82
  | Responses | return `RenderResult`. `@ultimat3/http` builds the `Response`. |
69
83
  | 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. |
70
84
  | 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. |
71
- | The loaders | `module-loader.ts` installs them at `index.ts` module scope, once. 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. |
85
+ | 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. |
72
86
  | `<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. |
73
87
  | 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. |
74
88
  | 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. |
package/README.md CHANGED
@@ -321,8 +321,32 @@ never flushed into an island that did not mount. `ISLAND_MOUNTED_ATTRIBUTE` and
321
321
  from, exported for the same reason: anything waiting for hydration has to wait at least this long,
322
322
  and a second copy of the number is a settle that shoots early and calls a healthy page broken.
323
323
 
324
+ ## Two entry points
325
+
326
+ **Split 2026-08-22, and every claim in this section holds `As of 2026-08`.**
327
+
328
+ `@ultimat3/render` is the **client** half — the `route` primitive, the JSX factory, islands,
329
+ hydration, `<head>`, the route table. It bundles for the browser, and
330
+ `scripts/browser-barrel.test.ts` builds it that way and asserts it.
331
+
332
+ `@ultimat3/render/server` is the **build-time** half — the `.tsx`/`.scss` Bun loaders and the
333
+ render pipeline. It imports `sass` and `node:url`, so it never reaches a browser bundle.
334
+
335
+ The two are **disjoint**: no name is on both, and a file needing both imports both. That is the
336
+ price of the split and it is the point of it — a single barrel could not be bundled for the
337
+ browser at all, because `node:url`'s browser polyfill exports neither `fileURLToPath` nor
338
+ `pathToFileURL` and the build fails at link time. No `sideEffects` value fixes that (measured:
339
+ `false`, `[]` and an array naming only `errors.ts` all fail identically) — only not importing it
340
+ does.
341
+
342
+ **Importing `@ultimat3/render/server` installs the `.tsx`/`.scss` loaders**, once, as a module
343
+ side effect. Anything that loads an app's source — `x dev`, `x build`, `server.ts`, a test that
344
+ `await import()`s a `page.tsx` — reaches it before the module it loads.
345
+
324
346
  ## Public API
325
347
 
348
+ `†` marks a name on `@ultimat3/render/server`.
349
+
326
350
  | Export | Owns |
327
351
  |---|---|
328
352
  | `defineRoute` | the `route` primitive |
@@ -330,9 +354,11 @@ and a second copy of the number is a settle that shoots early and calls a health
330
354
  | `MODE_SPECS`, `assertModeShape`, `assertModeInvariants` | the mode invariant table |
331
355
  | `registerRoute`, `describeRoutes`, `routeFor`, `routePathFromFile` | the route table |
332
356
  | `checkSurfaceBoundary`, `assertSurfaceBoundary`, `surfaceOf` | the hard boundary |
333
- | `renderStatic`, `enumeratePrerender` | build-time render, content hashing |
334
- | `createIsrController`, `invalidateAndRevalidate` | SWR + single-flight + tag triggers |
335
- | `renderSsr`, `streamResult` | the per-request modes |
357
+ | `renderStatic`†, `enumeratePrerender`† | build-time render, content hashing |
358
+ | `createIsrController`†, `invalidateAndRevalidate`† | SWR + single-flight + tag triggers |
359
+ | `renderSsr`†, `streamResult`† | the per-request modes |
360
+ | `renderToHtml`†, `renderComponent`†, `stylesFor`† | the server JSX writer and the surface's css |
361
+ | `installRenderLoader`†, `compileStylesheet`† | the `.tsx`/`.scss` loaders, installed on import |
336
362
  | `emitIslandAttributes`, `hydrateRuntime` | the four hydration strategies |
337
363
  | `ISLAND_MOUNTED_ATTRIBUTE`, `ISLAND_FAILED_ATTRIBUTE`, `IDLE_HYDRATE_TIMEOUT_MS` | what hydration looks like from outside the page |
338
364
  | `graphFor`, `checkBudget`, `assertBudget` | two bundle graphs, per-route budgets |
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "8.0.0",
3
+ "version": "9.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",
7
7
  "sideEffects": [
8
8
  "./src/errors.ts",
9
- "./src/index.ts"
9
+ "./src/server.ts"
10
10
  ],
11
11
  "repository": {
12
12
  "type": "git",
@@ -18,7 +18,8 @@
18
18
  "provenance": true
19
19
  },
20
20
  "exports": {
21
- ".": "./src/index.ts"
21
+ ".": "./src/index.ts",
22
+ "./server": "./src/server.ts"
22
23
  },
23
24
  "files": [
24
25
  "src",
@@ -35,10 +36,10 @@
35
36
  "test": "bun test"
36
37
  },
37
38
  "dependencies": {
38
- "@ultimat3/cache": "8.0.0",
39
- "@ultimat3/core": "8.0.0",
40
- "@ultimat3/i18n": "8.0.0",
41
- "@ultimat3/seo": "8.0.0",
39
+ "@ultimat3/cache": "9.0.0",
40
+ "@ultimat3/core": "9.0.0",
41
+ "@ultimat3/i18n": "9.0.0",
42
+ "@ultimat3/seo": "9.0.0",
42
43
  "sass": "1.102.0"
43
44
  }
44
45
  }
package/src/index.ts CHANGED
@@ -1,13 +1,8 @@
1
- /** Public API of `@ultimat3/render`: the `route` primitive, the five modes, the table. */
2
-
3
- import { installRenderLoader } from './module-loader';
4
-
5
- // A side effect on import, deliberately: a Bun plugin only transforms modules loaded AFTER it, and
6
- // every consumer that will ever load a `.tsx` route or a `.scss` module imports this package first
7
- // (an app's route file imports `defineRoute` from here before it imports anything else it owns).
8
- // Any later hook — `x dev`, `x build`, `server.ts` — would each have to remember, which is four
9
- // places one fact can be wrong instead of none.
10
- installRenderLoader();
1
+ /**
2
+ * The CLIENT half of `@ultimat3/render` — the `route` primitive, the JSX factory, islands, and the
3
+ * tables describing them — kept disjoint from `@ultimat3/render/server` because everything here
4
+ * must bundle for a browser, which the loaders cannot (axiom 6).
5
+ */
11
6
 
12
7
  /**
13
8
  * The route vocabulary is declared once, at tier 0 (`@ultimat3/core`), and re-exported here
@@ -21,8 +16,6 @@ export type { HydrateStrategy, OfflineStrategy, RenderMode } from '@ultimat3/cor
21
16
  // never had); still named here because `@ultimat3/cli`'s budget reporter reads it beside the route
22
17
  // table it prints against.
23
18
  export { formatBytes, HYDRATE_STRATEGIES, OFFLINE_STRATEGIES, RENDER_MODES } from '@ultimat3/core';
24
- export type { CompiledStylesheet } from './css-modules';
25
- export { compileStylesheet, isCssModule, isGlobalStylesheet, scopeClasses } from './css-modules';
26
19
  export { parseTtlMs } from './duration';
27
20
  export type { RenderErrorCode } from './errors';
28
21
  export {
@@ -104,15 +97,6 @@ export {
104
97
  defaultIslandBudget,
105
98
  MODE_SPECS,
106
99
  } from './modes';
107
- export type { Stylesheet } from './module-loader';
108
- export {
109
- clearStylesheets,
110
- installRenderLoader,
111
- loadStylesheet,
112
- registeredStylesheets,
113
- stylesFor,
114
- transformTsx,
115
- } from './module-loader';
116
100
  export type {
117
101
  CompiledPattern,
118
102
  RegisterRouteInput,
@@ -130,48 +114,6 @@ export {
130
114
  routeFor,
131
115
  routePathFromFile,
132
116
  } from './registry';
133
- export type { RenderHtmlOptions } from './render-html';
134
- export { ROOT_ELEMENT_ID, renderComponent, renderToHtml } from './render-html';
135
- export type {
136
- IsrController,
137
- IsrControllerOptions,
138
- IsrEntry,
139
- IsrRenderFn,
140
- IsrServeResult,
141
- IsrState,
142
- IsrStore,
143
- MemoryIsrStoreOptions,
144
- } from './render-isr';
145
- export {
146
- createIsrController,
147
- DEFAULT_ISR_MAX_ENTRIES,
148
- invalidateAndRevalidate,
149
- isrKey,
150
- memoryIsrStore,
151
- } from './render-isr';
152
- export type { SsrOptions, SsrRenderFn, SsrRenderInput } from './render-ssr';
153
- export { renderSsr, ssrHeaders } from './render-ssr';
154
- export type { StaticArtifact, StaticBuildOptions, StaticRenderFn } from './render-static';
155
- export {
156
- assertNoPerRequestState,
157
- contentHash,
158
- enumeratePrerender,
159
- fillPath,
160
- renderStatic,
161
- staticHeaders,
162
- staticResult,
163
- } from './render-static';
164
- export type { StreamHole, StreamOptions, StreamPlan } from './render-stream';
165
- export {
166
- collectStream,
167
- DEFAULT_HOLE_TIMEOUT_MS,
168
- holeId,
169
- holeMarker,
170
- REVEAL_SCRIPT,
171
- renderStreamHtml,
172
- revealChunk,
173
- streamResult,
174
- } from './render-stream';
175
117
  export type {
176
118
  LoadRequirement,
177
119
  PrerenderFn,
package/src/server.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The BUILD-TIME half of `@ultimat3/render` — the loaders and the route → bytes pipeline — split
3
+ * off because `css-modules.ts` imports `node:url`, whose browser polyfill exports neither name it
4
+ * asks for: one barrel carrying both halves could not be bundled for a browser at all (axiom 6).
5
+ * Disjoint from `@ultimat3/render` by construction, which `server.test.ts` checks.
6
+ */
7
+
8
+ import { installRenderLoader } from './module-loader';
9
+
10
+ // A side effect on import, deliberately, and the reason this barrel is named in `sideEffects`: a
11
+ // Bun plugin only transforms modules loaded AFTER it, so the install has to happen before any
12
+ // `.tsx` route or `.scss` module is imported. It lives on THIS barrel rather than on
13
+ // `@ultimat3/render` because the loader is build-time code — a browser bundle that reached it
14
+ // would carry `sass` and `node:fs`, which is the defect this split closes.
15
+ installRenderLoader();
16
+
17
+ // ---- scss → css, and the scoped class map every `import styles from` receives -------------------
18
+ export type { CompiledStylesheet } from './css-modules';
19
+ export { compileStylesheet, isCssModule, isGlobalStylesheet, scopeClasses } from './css-modules';
20
+ // ---- the two Bun loaders: `.tsx` → the server JSX factory, `.scss` → css + a class map ----------
21
+ export type { Stylesheet } from './module-loader';
22
+ export {
23
+ clearStylesheets,
24
+ installRenderLoader,
25
+ loadStylesheet,
26
+ registeredStylesheets,
27
+ stylesFor,
28
+ transformTsx,
29
+ } from './module-loader';
30
+ // ---- the render pipeline: one entry point per mode ----------------------------------------------
31
+ export type { RenderHtmlOptions } from './render-html';
32
+ export { ROOT_ELEMENT_ID, renderComponent, renderToHtml } from './render-html';
33
+ export type {
34
+ IsrController,
35
+ IsrControllerOptions,
36
+ IsrEntry,
37
+ IsrRenderFn,
38
+ IsrServeResult,
39
+ IsrState,
40
+ IsrStore,
41
+ MemoryIsrStoreOptions,
42
+ } from './render-isr';
43
+ export {
44
+ createIsrController,
45
+ DEFAULT_ISR_MAX_ENTRIES,
46
+ invalidateAndRevalidate,
47
+ isrKey,
48
+ memoryIsrStore,
49
+ } from './render-isr';
50
+ export type { SsrOptions, SsrRenderFn, SsrRenderInput } from './render-ssr';
51
+ export { renderSsr, ssrHeaders } from './render-ssr';
52
+ export type { StaticArtifact, StaticBuildOptions, StaticRenderFn } from './render-static';
53
+ export {
54
+ assertNoPerRequestState,
55
+ contentHash,
56
+ enumeratePrerender,
57
+ fillPath,
58
+ renderStatic,
59
+ staticHeaders,
60
+ staticResult,
61
+ } from './render-static';
62
+ export type { StreamHole, StreamOptions, StreamPlan } from './render-stream';
63
+ export {
64
+ collectStream,
65
+ DEFAULT_HOLE_TIMEOUT_MS,
66
+ holeId,
67
+ holeMarker,
68
+ REVEAL_SCRIPT,
69
+ renderStreamHtml,
70
+ revealChunk,
71
+ streamResult,
72
+ } from './render-stream';