@rangojs/router 0.9.0 → 0.10.1

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.
Files changed (68) hide show
  1. package/AGENTS.md +1 -1
  2. package/LICENSE +21 -0
  3. package/README.md +17 -15
  4. package/dist/types/cache/cf/cf-cache-constants.d.ts +1 -1
  5. package/dist/types/cache/cf/cf-cache-store.d.ts +1 -1
  6. package/dist/types/outlet-context.d.ts +9 -0
  7. package/dist/types/outlet-provider.d.ts +2 -1
  8. package/dist/types/route-content-wrapper.d.ts +4 -1
  9. package/dist/types/rsc/nonce.d.ts +10 -0
  10. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  11. package/dist/types/rsc/types.d.ts +6 -2
  12. package/dist/types/ssr/index.d.ts +18 -0
  13. package/dist/types/ssr/ssr-root.d.ts +16 -0
  14. package/dist/types/ssr-suspension-warning.d.ts +20 -0
  15. package/dist/types/types/loader-types.d.ts +5 -1
  16. package/dist/types/types/segments.d.ts +17 -0
  17. package/dist/types/vite/discovery/state.d.ts +6 -0
  18. package/dist/types/vite/plugin-types.d.ts +27 -0
  19. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  20. package/dist/types/vite/utils/shared-utils.d.ts +1 -0
  21. package/dist/vite/index.js +56 -26
  22. package/package.json +26 -26
  23. package/skills/bundle-analysis/SKILL.md +1 -1
  24. package/skills/loader/SKILL.md +17 -2
  25. package/skills/testing/SKILL.md +1 -1
  26. package/skills/testing/bindings.md +1 -1
  27. package/skills/testing/cache-prerender.md +1 -1
  28. package/skills/testing/client-components.md +1 -1
  29. package/skills/testing/e2e-parity.md +1 -1
  30. package/skills/testing/flight.md +1 -1
  31. package/skills/testing/handles.md +1 -1
  32. package/skills/testing/loader.md +1 -1
  33. package/skills/testing/middleware.md +1 -1
  34. package/skills/testing/render-handler.md +1 -1
  35. package/skills/testing/response-routes.md +1 -1
  36. package/skills/testing/reverse-and-types.md +1 -1
  37. package/skills/testing/server-actions.md +1 -1
  38. package/skills/testing/server-tree.md +1 -1
  39. package/skills/testing/setup.md +1 -1
  40. package/src/browser/merge-segment-loaders.ts +4 -0
  41. package/src/cache/cache-runtime.ts +2 -2
  42. package/src/cache/cf/cf-cache-constants.ts +1 -1
  43. package/src/cache/cf/cf-cache-store.ts +1 -1
  44. package/src/client.tsx +2 -0
  45. package/src/outlet-context.ts +9 -0
  46. package/src/outlet-provider.tsx +4 -0
  47. package/src/route-content-wrapper.tsx +7 -0
  48. package/src/router/segment-resolution/fresh.ts +48 -17
  49. package/src/router.ts +7 -2
  50. package/src/rsc/handler.ts +4 -4
  51. package/src/rsc/nonce.ts +16 -0
  52. package/src/rsc/shell-capture-constants.ts +1 -1
  53. package/src/rsc/shell-capture.ts +41 -8
  54. package/src/rsc/types.ts +6 -2
  55. package/src/segment-system.tsx +57 -12
  56. package/src/ssr/index.tsx +53 -1
  57. package/src/ssr/ssr-root.tsx +50 -22
  58. package/src/ssr-suspension-warning.ts +68 -0
  59. package/src/types/loader-types.ts +5 -1
  60. package/src/types/segments.ts +17 -0
  61. package/src/use-loader.tsx +24 -1
  62. package/src/vite/discovery/shell-prerender-phase.ts +5 -0
  63. package/src/vite/discovery/state.ts +6 -0
  64. package/src/vite/plugin-types.ts +28 -0
  65. package/src/vite/plugins/virtual-entries.ts +26 -3
  66. package/src/vite/rango.ts +3 -0
  67. package/src/vite/router-discovery.ts +31 -19
  68. package/src/vite/utils/shared-utils.ts +8 -2
package/AGENTS.md CHANGED
@@ -10,4 +10,4 @@ Start with the mental model at `node_modules/@rangojs/router/skills/rango/SKILL.
10
10
 
11
11
  ## Contributing
12
12
 
13
- This package is developed in the monorepo at https://github.com/ivogt/vite-rsc. If you are contributing there rather than consuming the published package, read the root `AGENTS.md` in that repo for the pre-push gate and e2e rules — they apply to the monorepo, not to consumer apps.
13
+ This package is developed in the monorepo at https://github.com/rangojs/rango. If you are contributing there rather than consuming the published package, read the root `AGENTS.md` in that repo for the pre-push gate and e2e rules — they apply to the monorepo, not to consumer apps.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Ivo Todorov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,13 +4,14 @@ A code-first, type-safe React Server Components router. Django-inspired:
4
4
  routes are expressed in one visible tree, URLs are built from names, and
5
5
  everything past the core is opt-in.
6
6
 
7
- > **Experimental:** This package is under active development. APIs may change
8
- > between releases. Install with `@experimental` tag.
7
+ > **Pre-1.0:** Rango follows semver 0.x breaking changes land in minor
8
+ > releases and are noted in the changelog. npm `latest` is the current 0.x
9
+ > release; the `experimental` tag tracks `main` between tagged releases.
9
10
 
10
11
  This page is a tour: it builds one small shop and meets the entire core API
11
12
  along the way — about six primitives. Everything else is opt-in and linked at
12
13
  the end. For the design rationale behind these APIs, read
13
- [Why Rango](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/why-rango.md); this page shows how it feels, that page
14
+ [Why Rango](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/why-rango.md); this page shows how it feels, that page
14
15
  argues why it's right.
15
16
 
16
17
  ## Start a new app
@@ -24,7 +25,8 @@ pnpm create rango my-app
24
25
 
25
26
  Select a deployment target with `--template basic`, `--template cloudflare`, or
26
27
  `--template vercel`. The templates include streaming RSC, typed routes, Server
27
- Actions, Tailwind CSS, and production deployment configuration. For a plain
28
+ Actions, Tailwind CSS, and production deployment configuration; the scaffolder
29
+ installs the latest `@rangojs/router` release. For a plain
28
30
  JavaScript Node app, add `--template basic --js`. With npm, run
29
31
  `npm create rango@latest my-app`. The scaffolder currently requires Node.js 24
30
32
  or newer.
@@ -34,7 +36,7 @@ or newer.
34
36
  If you are adding Rango to an existing Vite RSC project:
35
37
 
36
38
  ```bash
37
- npm install @rangojs/router@experimental react @vitejs/plugin-rsc
39
+ npm install @rangojs/router@latest react @vitejs/plugin-rsc
38
40
  ```
39
41
 
40
42
  ```ts
@@ -170,7 +172,7 @@ per group — while matching stays an `O(path length)` prefix trie, identical
170
172
  in dev and production. None of this is assumed: the trie is benchmarked
171
173
  in-repo against multi-thousand-route manifests, and the lazy guarantees are
172
174
  pinned by run-count tests (see
173
- [matching & lazy discovery](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/internal/matching-and-lazy-discovery.md)).
175
+ [matching & lazy discovery](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/internal/matching-and-lazy-discovery.md)).
174
176
  Grow the tree without watching the boot time.
175
177
 
176
178
  That's a working site. Everything below adds to this app.
@@ -466,19 +468,19 @@ npx rango generate src/ # recursive scan
466
468
 
467
469
  ### Examples
468
470
 
469
- - [`e2e/mini`](https://github.com/ivogt/vite-rsc/tree/main/packages/rangojs-router/e2e/mini) — single-file demo app
470
- - [`cloudflare-basic`](https://github.com/ivogt/vite-rsc/tree/main/tests/cloudflare-basic) — Cloudflare Workers with caching, loaders, theme, and pre-rendering
471
- - [`cloudflare-multi-router`](https://github.com/ivogt/vite-rsc/tree/main/examples/cloudflare-multi-router) — multi-app host routing
472
- - [`vercel-basic`](https://github.com/ivogt/vite-rsc/tree/main/examples/vercel-basic) — Vercel deployment with `preset: "vercel"`, `VercelCacheStore`, and OTel tracing
473
- - [`vercel-multi-router`](https://github.com/ivogt/vite-rsc/tree/main/examples/vercel-multi-router) — multi-app host routing on Vercel (single function, routed by Host header)
471
+ - [`e2e/mini`](https://github.com/rangojs/rango/tree/main/packages/rangojs-router/e2e/mini) — single-file demo app
472
+ - [`cloudflare-basic`](https://github.com/rangojs/rango/tree/main/tests/cloudflare-basic) — Cloudflare Workers with caching, loaders, theme, and pre-rendering
473
+ - [`cloudflare-multi-router`](https://github.com/rangojs/rango/tree/main/examples/cloudflare-multi-router) — multi-app host routing
474
+ - [`vercel-basic`](https://github.com/rangojs/rango/tree/main/examples/vercel-basic) — Vercel deployment with `preset: "vercel"`, `VercelCacheStore`, and OTel tracing
475
+ - [`vercel-multi-router`](https://github.com/rangojs/rango/tree/main/examples/vercel-multi-router) — multi-app host routing on Vercel (single function, routed by Host header)
474
476
 
475
477
  ### Going deeper
476
478
 
477
- - [Why Rango](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/why-rango.md) — the design rationale, claim by claim
479
+ - [Why Rango](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/why-rango.md) — the design rationale, claim by claim
478
480
  - [Framework comparison](./skills/comparison/references/framework-comparison.md) — Rango vs Next.js App Router, TanStack Start, and Waku, capability by capability
479
- - [Docs index](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/README.md) — architecture, caching, prerender, testing
480
- - [Execution model](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/internal/execution-model.md) — the runtime contract
481
+ - [Docs index](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/README.md) — architecture, caching, prerender, testing
482
+ - [Execution model](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/internal/execution-model.md) — the runtime contract
481
483
 
482
484
  ## License
483
485
 
484
- MIT
486
+ [MIT](./LICENSE)
@@ -66,7 +66,7 @@ export declare const MAX_REVALIDATION_INTERVAL = 30;
66
66
  * This is the default; override per store via
67
67
  * `CFCacheStoreOptions.edgeLookupTimeoutMs` (<= 0 disables the budget).
68
68
  *
69
- * 25ms, raised from 10: production Workers logs (autobarn pilot) showed the
69
+ * 25ms, raised from 10: Workers logs (production pilot) showed the
70
70
  * 10ms budget firing frequently on cold colos where the first Cache API touch
71
71
  * is slow but healthy — each false positive downgrades a warm L1 HIT to an
72
72
  * L2/render round trip that costs far more than the 15ms of extra patience.
@@ -322,7 +322,7 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
322
322
  * this, kv.put/get reject with `414 ... exceeds key length limit of 512`
323
323
  * and the entry silently never reaches L2 (observed in production for
324
324
  * "use cache" items whose serialized args — e.g. a CMS query object — blow
325
- * the cap; autobarn pilot). Keys are opaque storage identifiers, so
325
+ * the cap; production pilot). Keys are opaque storage identifiers, so
326
326
  * normalization is semantics-preserving as long as distinct logical keys
327
327
  * stay distinct: colliding requires an identical 400-byte prefix AND a
328
328
  * 128-bit SHA-256 collision. Deterministic, so every family's read, write,
@@ -17,6 +17,15 @@ export interface OutletContextValue {
17
17
  * the resolved `loaderData` record.
18
18
  */
19
19
  loaderStreams?: Record<string, unknown>;
20
+ /**
21
+ * Loader $$ids on this segment that the render awaited before first flush
22
+ * (loader(Def, { ssr: false })). Dev-diagnostic input only: useLoader uses
23
+ * it to warn when a read still suspends during SSR on a render that awaited
24
+ * flagged loaders (ssr-suspension-warning.ts). Present only on streaming
25
+ * lanes with flagged loaders (document and shell-capture renders); absent
26
+ * on forceAwait/action lanes.
27
+ */
28
+ awaitedLoaderIds?: readonly string[];
20
29
  parent?: OutletContextValue | null;
21
30
  /** Loading component for Suspense fallback (from segment's loading() definition) */
22
31
  loading?: ReactNode;
@@ -3,12 +3,13 @@ import type { ResolvedSegment } from "./types.js";
3
3
  /**
4
4
  * Outlet content provider — stores parent context for useLoader chain walking.
5
5
  */
6
- export declare function OutletProvider({ content, parallel, segment, loaderData, loaderStreams, pending, children, }: {
6
+ export declare function OutletProvider({ content, parallel, segment, loaderData, loaderStreams, awaitedLoaderIds, pending, children, }: {
7
7
  content: ReactNode;
8
8
  parallel?: ResolvedSegment[];
9
9
  segment?: ResolvedSegment;
10
10
  loaderData?: Record<string, any>;
11
11
  loaderStreams?: Record<string, unknown>;
12
+ awaitedLoaderIds?: readonly string[];
12
13
  pending?: boolean;
13
14
  children: ReactNode;
14
15
  }): ReactNode;
@@ -71,6 +71,9 @@ export interface LoaderBoundaryProps {
71
71
  * the children; useLoader suspends per loader at the read site.
72
72
  */
73
73
  loaderStreams?: Record<string, unknown>;
74
+ /** Dev-diagnostic input for the SSR suspension warning — see
75
+ * OutletContextValue.awaitedLoaderIds. Rides the streams lane only. */
76
+ awaitedLoaderIds?: readonly string[];
74
77
  fallback?: ReactNode;
75
78
  outletKey: string;
76
79
  outletContent: ReactNode;
@@ -78,4 +81,4 @@ export interface LoaderBoundaryProps {
78
81
  parallel?: ResolvedSegment[];
79
82
  children: ReactNode;
80
83
  }
81
- export declare function LoaderBoundary({ loaderDataPromise, loaderIds, loaderStreams, fallback, outletKey, outletContent, segment, parallel, children, }: LoaderBoundaryProps): ReactNode;
84
+ export declare function LoaderBoundary({ loaderDataPromise, loaderIds, loaderStreams, awaitedLoaderIds, fallback, outletKey, outletContent, segment, parallel, children, }: LoaderBoundaryProps): ReactNode;
@@ -21,6 +21,16 @@ import type { ContextVar } from "../context-var.js";
21
21
  * scripts — useNonce() stays undefined for that request.
22
22
  */
23
23
  export declare const nonce: ContextVar<string>;
24
+ /**
25
+ * Normalize a NonceProvider return value to the nonce the request runs with.
26
+ * `true` auto-generates; `false` and the empty string are the per-request
27
+ * opt-out and normalize to undefined — without this, a falsy string would be
28
+ * threaded as-is into the render: never written to the token (`if (nonce)`)
29
+ * yet still `!== undefined` for the ppr shell gate (rsc-rendering.ts
30
+ * activeNonce), pinning a ppr route to axis 1 with no way to opt a request
31
+ * out while keeping the provider for the rest of the app.
32
+ */
33
+ export declare function resolveProviderNonce(result: string | boolean): string | undefined;
24
34
  /**
25
35
  * Generate a cryptographic nonce for CSP.
26
36
  * Returns a 16-byte random value encoded as base64.
@@ -29,7 +29,7 @@ export declare const SHELL_CAPTURE_MAX_WAIT_MS = 15000;
29
29
  * Hard cap on one capture TASK — runShellCapture end to end (both attempts +
30
30
  * the in-place retry delay). SHELL_CAPTURE_MAX_WAIT_MS arms only inside
31
31
  * captureShellHTML, AFTER the capture's router.match(); a handler wedged on a
32
- * never-settling upstream await (autobarn pilot: a 30s+ tarpitting fetch)
32
+ * never-settling upstream await (production pilot: a 30s+ tarpitting fetch)
33
33
  * wedges the task with no deadline in force. The task's settle path releases
34
34
  * the per-key stampede guard and the serialized capture-queue slot, so an
35
35
  * unbounded task strands BOTH for the isolate's lifetime.
@@ -252,9 +252,13 @@ export interface HandlerCacheConfig {
252
252
  }
253
253
  /**
254
254
  * Nonce provider function type.
255
- * Can return a nonce string, or true to auto-generate one.
255
+ * Return a nonce string to use verbatim, `true` to auto-generate one, or
256
+ * `false` (or an empty string) to serve THIS request without a nonce — the
257
+ * per-request opt-out for apps that nonce globally but declare `ppr` on some
258
+ * routes: a shell is shared per host+URL and can never bake a per-request
259
+ * nonce, so a ppr route only captures when its requests opt out here.
256
260
  */
257
- export type NonceProvider<TEnv = unknown> = (request: Request, env: TEnv) => string | true | Promise<string | true>;
261
+ export type NonceProvider<TEnv = unknown> = (request: Request, env: TEnv) => string | boolean | Promise<string | boolean>;
258
262
  /**
259
263
  * Options for creating an RSC handler
260
264
  */
@@ -19,6 +19,7 @@ interface RenderToReadableStreamOptions {
19
19
  bootstrapModules?: string[];
20
20
  nonce?: string;
21
21
  formState?: unknown;
22
+ progressiveChunkSize?: number;
22
23
  }
23
24
  /**
24
25
  * ReadableStream with the allReady promise added by react-dom/server.edge.
@@ -34,6 +35,7 @@ interface PrerenderOptions {
34
35
  bootstrapScriptContent?: string;
35
36
  bootstrapModules?: string[];
36
37
  onError?: (error: unknown) => void;
38
+ progressiveChunkSize?: number;
37
39
  }
38
40
  /**
39
41
  * Result of prerender from react-dom/static.edge. `postponed` is React's
@@ -129,6 +131,22 @@ export interface SSRDependencies<TEnv = unknown> {
129
131
  * state on upgrade (the generated entry always passes an explicit value).
130
132
  */
131
133
  headScripts?: HeadScriptsOption;
134
+ /**
135
+ * Fizz `progressiveChunkSize`, forwarded verbatim to renderToReadableStream
136
+ * (live SSR) and prerender (shell capture); resume() inherits the capture
137
+ * value from the postponed state. The generated virtual SSR entry threads
138
+ * the `rango({ progressiveChunkSize })` plugin option here (canonical docs
139
+ * on `RangoBaseOptions.progressiveChunkSize` in vite/plugin-types.ts).
140
+ *
141
+ * When UNSET, createSSRHandler auto-raises to Number.MAX_SAFE_INTEGER for
142
+ * document renders whose matched chain has a `loader(Def, { ssr: false })`
143
+ * entry (payload segments carry `awaitBeforeFlush`): the loader was awaited
144
+ * before first flush precisely so its content ships in-place, and React
145
+ * 19.2's Fizz would otherwise move any completed boundary over ~500 bytes
146
+ * to an end-of-stream `<div hidden>` + $RC reveal once the shell saturates
147
+ * the default 12800 budget. An explicit value disables the auto-raise.
148
+ */
149
+ progressiveChunkSize?: number;
132
150
  /**
133
151
  * prerender from react-dom/static.edge. Optional; required only by
134
152
  * {@link createShellCaptureHandler} for PPR shell capture.
@@ -26,6 +26,15 @@ export interface RscPayload {
26
26
  version?: string;
27
27
  };
28
28
  }
29
+ /**
30
+ * Deserialize the Flight stream branch into the payload SsrRoot renders,
31
+ * fragment expansion included. Factored out of SsrRoot so createSSRHandler can
32
+ * create the promise BEFORE fizz starts and read metadata that must influence
33
+ * the fizz call itself (the ssr:false progressiveChunkSize auto-raise) —
34
+ * awaiting it there is latency-neutral because fizz cannot emit even <html>
35
+ * until this same promise settles.
36
+ */
37
+ export declare function deserializeSsrPayload(createFromReadableStream: CreateFromReadableStream, rscStream: ReadableStream<Uint8Array>): Promise<RscPayload>;
29
38
  /**
30
39
  * Options for {@link createSsrRootComponent}.
31
40
  */
@@ -34,6 +43,13 @@ export interface SsrRootOptions {
34
43
  createFromReadableStream: CreateFromReadableStream;
35
44
  /** The Flight stream branch to deserialize into the SSR VDOM. */
36
45
  rscStream: ReadableStream<Uint8Array>;
46
+ /**
47
+ * Pre-created payload promise (deserializeSsrPayload over the SAME
48
+ * rscStream). When present SsrRoot adopts it instead of deserializing
49
+ * lazily on first render — the stream can only be consumed once, so the
50
+ * caller that already started deserialization MUST pass the promise in.
51
+ */
52
+ payload?: Promise<RscPayload>;
37
53
  /** Nonce for CSP; propagated to NonceContext. */
38
54
  nonce?: string;
39
55
  /**
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Dev-only diagnostic for the { ssr: false } SSR-completeness opt-in.
3
+ *
4
+ * A document render that awaits flagged loaders before first flush is usually
5
+ * expected to produce an SSR-complete boundary — but the await is scoped per
6
+ * LOADER (see LoaderOptions in types/loader-types.ts), so one unflagged read
7
+ * under the same Suspense boundary still suspends it and the boundary streams
8
+ * as its fallback, invisible without JS. That failure mode looks exactly like
9
+ * the flag not working; a consumer app burned hours on byte-level archaeology
10
+ * before the unflagged sibling read was identified. This warning names the
11
+ * culprit at the suspension site instead.
12
+ *
13
+ * Fires at most once per loader id per process, only during the SSR pass
14
+ * (`window` undefined), only in dev, and only when the render actually awaited
15
+ * flagged loaders (`awaitedLoaderIds`, stamped by segment resolution on
16
+ * document AND shell-capture renders — capture awaits the same flagged lane;
17
+ * its masked live-lane reads suspend by design and dedupe against the live
18
+ * render's warnings through the once-per-id set).
19
+ */
20
+ export declare function warnAwaitedSsrSuspension(loaderId: string, awaitedLoaderIds: readonly string[] | undefined, stream: Promise<unknown>, isServer?: boolean): void;
@@ -170,7 +170,11 @@ export type LoaderFn<T, TParams = Record<string, string | undefined>, TEnv = Def
170
170
  *
171
171
  * Scoped per LOADER, not per segment: an `ssr: false` loader alongside a
172
172
  * deliberately streaming sibling awaits only itself, and the sibling keeps
173
- * streaming behind its `loading()`/Suspense boundary.
173
+ * streaming behind its `loading()`/Suspense boundary. Watch the scoping when
174
+ * the boundary must be SSR-complete: ONE unflagged read under it still
175
+ * suspends the whole boundary, which then streams as its fallback — looking
176
+ * exactly like the flag not working. In dev the router warns at the read
177
+ * site naming the unflagged loader (ssr-suspension-warning.ts).
174
178
  */
175
179
  export type LoaderOptions = {
176
180
  ssr?: boolean;
@@ -140,9 +140,26 @@ export interface ResolvedSegment {
140
140
  loaderId?: string;
141
141
  _inherited?: boolean;
142
142
  loaderData?: any;
143
+ /**
144
+ * True when this loader was awaited before first flush
145
+ * (loader(Def, { ssr: false })). Stamped by resolveLoaders (fresh.ts) on
146
+ * document AND shell-capture renders — capture bakes flagged loaders and
147
+ * awaits the same lane. Feeds segment-system's settled-value delivery and
148
+ * the dev SSR suspension warning (ssr-suspension-warning.ts).
149
+ */
150
+ awaitBeforeFlush?: true;
143
151
  parallelLoading?: ReactNode;
144
152
  loaderDataPromise?: Promise<any[]> | any[];
145
153
  loaderIds?: string[];
154
+ /**
155
+ * Per-loader UNDECODED results for a layout/route stream map.
156
+ * Flagged (ssr:false) entries are settled values; unflagged siblings
157
+ * stay promises. Parallel slots do not use this channel — they pin
158
+ * loading() / live-lane holes via use(loaderDataPromise).
159
+ */
160
+ loaderStreams?: Record<string, unknown>;
161
+ /** $$ids of loaders this segment awaited before flush. Dev diagnostic. */
162
+ awaitedLoaderIds?: string[];
146
163
  error?: ErrorInfo;
147
164
  notFoundInfo?: NotFoundInfo;
148
165
  mountPath?: string;
@@ -45,6 +45,12 @@ export interface PluginOptions {
45
45
  * capture endpoint with the app's configured head-script strategy.
46
46
  */
47
47
  headScripts?: import("../plugin-types.js").HeadScriptsOption;
48
+ /**
49
+ * rango({ progressiveChunkSize }) — threaded alongside headScripts so the
50
+ * temp server's virtual SSR entry bakes the app's configured Fizz outlining
51
+ * budget into its capture/render handlers.
52
+ */
53
+ progressiveChunkSize?: number;
48
54
  }
49
55
  export interface PrecomputedEntry {
50
56
  staticPrefix: string;
@@ -135,6 +135,33 @@ interface RangoBaseOptions {
135
135
  * @default "preinit"
136
136
  */
137
137
  headScripts?: HeadScriptsOption;
138
+ /**
139
+ * React Fizz `progressiveChunkSize`, forwarded to the document renders the
140
+ * generated SSR entry performs: renderToReadableStream (live SSR) and
141
+ * prerender (PPR shell capture); resume() inherits the capture value from
142
+ * the stored postponed state.
143
+ *
144
+ * Controls COMPLETED-boundary outlining. Once the shell exceeds this budget
145
+ * (React's default is 12800 bytes — any real document), Fizz moves every
146
+ * completed Suspense boundary over ~500 bytes out of its document position
147
+ * to an end-of-stream `<div hidden>` + `$RC()` script reveal. Raise it (e.g.
148
+ * `Number.MAX_SAFE_INTEGER`) to keep completed content inline: in-place for
149
+ * non-executing HTML consumers, no reveal step. Boundaries with suspensey
150
+ * content (hoisted stylesheets) still outline — their reveal must wait for
151
+ * the CSS. Trade-off: inline content delays later shell bytes behind it,
152
+ * which is why React outlines by default.
153
+ *
154
+ * When UNSET, document renders whose matched chain has a
155
+ * `loader(Def, { ssr: false })` entry auto-raise to MAX_SAFE_INTEGER — the
156
+ * loader was awaited before first flush precisely so its content ships
157
+ * in-place, and outlining the boundary it feeds would defeat that. Setting
158
+ * an explicit value disables the auto-raise. The auto-raise is live-SSR
159
+ * only; captured shells use the explicit value or React's default.
160
+ *
161
+ * Apps with a custom SSR entry set this per-handler via
162
+ * `SSRDependencies.progressiveChunkSize`.
163
+ */
164
+ progressiveChunkSize?: number;
138
165
  /**
139
166
  * Filter which files route discovery scans, by glob. Paths are matched
140
167
  * root-relative (e.g. `src/routes/**`). `include` restricts discovery to
@@ -6,7 +6,7 @@ export declare const VIRTUAL_ENTRY_BROWSER: string;
6
6
  * lets the SSR handlers convert the bootstrap to `bootstrapModules`;
7
7
  * "preload" omits the hook and pins the handlers to the hint-only strategy.
8
8
  */
9
- export declare function getVirtualEntrySSR(headScripts?: HeadScriptsOption): string;
9
+ export declare function getVirtualEntrySSR(headScripts?: HeadScriptsOption, progressiveChunkSize?: number): string;
10
10
  /**
11
11
  * Virtual modules an RSC entry must import at startup to register the data the
12
12
  * request handler needs before the first request arrives:
@@ -45,6 +45,7 @@ export declare function createVirtualEntriesPlugin(entries: {
45
45
  kind?: "router" | "host";
46
46
  }, options?: {
47
47
  headScripts?: HeadScriptsOption;
48
+ progressiveChunkSize?: number;
48
49
  }): Plugin;
49
50
  /**
50
51
  * Suppress known harmless warnings: "use client" directives, sourcemap errors,
@@ -3476,7 +3476,18 @@ async function initializeApp() {
3476
3476
 
3477
3477
  initializeApp().catch(console.error);
3478
3478
  `.trim();
3479
- function getVirtualEntrySSR(headScripts = "preinit") {
3479
+ function emitProgressiveChunkSize(value) {
3480
+ if (value === Number.POSITIVE_INFINITY) {
3481
+ return "Number.POSITIVE_INFINITY";
3482
+ }
3483
+ if (!Number.isFinite(value)) {
3484
+ throw new Error(
3485
+ `rango({ progressiveChunkSize }) must be a finite number or Infinity, received ${String(value)}`
3486
+ );
3487
+ }
3488
+ return JSON.stringify(value);
3489
+ }
3490
+ function getVirtualEntrySSR(headScripts = "preinit", progressiveChunkSize) {
3480
3491
  const preinit = headScripts !== "preload";
3481
3492
  const depsImportNames = preinit ? "createFromReadableStream,\n setOnClientReference," : "createFromReadableStream,";
3482
3493
  const ssrImportNames = preinit ? "\n installClientReferencePreinit," : "";
@@ -3487,6 +3498,8 @@ function getVirtualEntrySSR(headScripts = "preinit") {
3487
3498
  installClientReferencePreinit(setOnClientReference);
3488
3499
  ` : "";
3489
3500
  const hs = JSON.stringify(headScripts);
3501
+ const pcs = progressiveChunkSize !== void 0 ? `
3502
+ progressiveChunkSize: ${emitProgressiveChunkSize(progressiveChunkSize)},` : "";
3490
3503
  return `
3491
3504
  import {
3492
3505
  ${depsImportNames}
@@ -3504,7 +3517,7 @@ export const renderHTML = createSSRHandler({
3504
3517
  createFromReadableStream,
3505
3518
  renderToReadableStream,
3506
3519
  injectRSCPayload,
3507
- headScripts: ${hs},
3520
+ headScripts: ${hs},${pcs}
3508
3521
  loadBootstrapScriptContent: () =>
3509
3522
  import.meta.viteRsc.loadBootstrapScriptContent("index"),
3510
3523
  });
@@ -3515,7 +3528,7 @@ export const captureShellHTML = createShellCaptureHandler({
3515
3528
  injectRSCPayload,
3516
3529
  prerender,
3517
3530
  resume,
3518
- headScripts: ${hs},
3531
+ headScripts: ${hs},${pcs}
3519
3532
  loadBootstrapScriptContent: () =>
3520
3533
  import.meta.viteRsc.loadBootstrapScriptContent("index"),
3521
3534
  });
@@ -3526,7 +3539,7 @@ export const resumeShellHTML = createShellResumeHandler({
3526
3539
  injectRSCPayload,
3527
3540
  prerender,
3528
3541
  resume,
3529
- headScripts: ${hs},
3542
+ headScripts: ${hs},${pcs}
3530
3543
  loadBootstrapScriptContent: () =>
3531
3544
  import.meta.viteRsc.loadBootstrapScriptContent("index"),
3532
3545
  });
@@ -3685,7 +3698,7 @@ import { resolve } from "node:path";
3685
3698
  // package.json
3686
3699
  var package_default = {
3687
3700
  name: "@rangojs/router",
3688
- version: "0.9.0",
3701
+ version: "0.10.1",
3689
3702
  description: "Django-inspired RSC router with composable URL patterns",
3690
3703
  keywords: [
3691
3704
  "react",
@@ -3694,15 +3707,15 @@ var package_default = {
3694
3707
  "rsc",
3695
3708
  "vite"
3696
3709
  ],
3697
- homepage: "https://github.com/ivogt/vite-rsc#readme",
3710
+ homepage: "https://github.com/rangojs/rango#readme",
3698
3711
  bugs: {
3699
- url: "https://github.com/ivogt/vite-rsc/issues"
3712
+ url: "https://github.com/rangojs/rango/issues"
3700
3713
  },
3701
3714
  license: "MIT",
3702
3715
  author: "Ivo Todorov",
3703
3716
  repository: {
3704
3717
  type: "git",
3705
- url: "git+https://github.com/ivogt/vite-rsc.git",
3718
+ url: "git+https://github.com/rangojs/rango.git",
3706
3719
  directory: "packages/rangojs-router"
3707
3720
  },
3708
3721
  bin: {
@@ -3717,7 +3730,8 @@ var package_default = {
3717
3730
  "dist",
3718
3731
  "skills",
3719
3732
  "AGENTS.md",
3720
- "README.md"
3733
+ "README.md",
3734
+ "LICENSE"
3721
3735
  ],
3722
3736
  type: "module",
3723
3737
  exports: {
@@ -5466,7 +5480,10 @@ function createVirtualEntriesPlugin(entries, routerPathRef, options) {
5466
5480
  virtualModules[VIRTUAL_IDS.browser] = VIRTUAL_ENTRY_BROWSER;
5467
5481
  }
5468
5482
  if (entries.ssr === VIRTUAL_IDS.ssr) {
5469
- virtualModules[VIRTUAL_IDS.ssr] = getVirtualEntrySSR(options?.headScripts);
5483
+ virtualModules[VIRTUAL_IDS.ssr] = getVirtualEntrySSR(
5484
+ options?.headScripts,
5485
+ options?.progressiveChunkSize
5486
+ );
5470
5487
  }
5471
5488
  const knownIds = new Set(Object.keys(virtualModules));
5472
5489
  if (entries.rsc === VIRTUAL_IDS.rsc) {
@@ -9540,6 +9557,11 @@ async function runShellPrerenderPhase(s, builder) {
9540
9557
  prerender: reactDomStatic.prerender ?? reactDomStatic.default?.prerender,
9541
9558
  injectRSCPayload: htmlStream.injectRSCPayload,
9542
9559
  headScripts: "preinit",
9560
+ // Build-captured shells must honor the explicit outlining budget: the
9561
+ // capture value rides the postponed state into every runtime resume().
9562
+ ...s.opts?.progressiveChunkSize !== void 0 && {
9563
+ progressiveChunkSize: s.opts.progressiveChunkSize
9564
+ },
9543
9565
  loadBootstrapScriptContent: async () => bootstrapContent
9544
9566
  });
9545
9567
  const rscRunner = rscEnv.runner;
@@ -10366,7 +10388,10 @@ async function createTempRscServer(state, options = {}) {
10366
10388
  return id === "virtual:entry-ssr" ? "\0rango-temp-real-ssr-entry" : null;
10367
10389
  },
10368
10390
  load(id) {
10369
- return id === "\0rango-temp-real-ssr-entry" ? getVirtualEntrySSR(state.opts?.headScripts) : null;
10391
+ return id === "\0rango-temp-real-ssr-entry" ? getVirtualEntrySSR(
10392
+ state.opts?.headScripts,
10393
+ state.opts?.progressiveChunkSize
10394
+ ) : null;
10370
10395
  }
10371
10396
  }
10372
10397
  ] : [],
@@ -11305,6 +11330,8 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
11305
11330
  if (expectedEpoch === void 0) return;
11306
11331
  void (async () => {
11307
11332
  const deadline = Date.now() + 15e3;
11333
+ let reloadBackoffMs = 100;
11334
+ let nextReloadAt = Date.now() + reloadBackoffMs;
11308
11335
  do {
11309
11336
  if (devServerClosed || expectedEpoch !== s.devDiscoveryEpoch) {
11310
11337
  return;
@@ -11325,7 +11352,14 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
11325
11352
  publishDevDiscoveryReady(expectedEpoch);
11326
11353
  return;
11327
11354
  }
11328
- reloadWorkerd();
11355
+ await response.body?.cancel().catch(() => {
11356
+ });
11357
+ const now = Date.now();
11358
+ if (now >= nextReloadAt) {
11359
+ reloadWorkerd();
11360
+ reloadBackoffMs = Math.min(reloadBackoffMs * 2, 1e3);
11361
+ nextReloadAt = now + reloadBackoffMs;
11362
+ }
11329
11363
  } catch {
11330
11364
  }
11331
11365
  } while (Date.now() < deadline);
@@ -11456,17 +11490,10 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
11456
11490
  hasClientUrls
11457
11491
  );
11458
11492
  }
11459
- if (isUseClient && hasClientUrls) {
11460
- const rscGraph = server.environments?.rsc?.moduleGraph;
11493
+ const mainRscEnv = server.environments?.rsc;
11494
+ if (isUseClient && hasClientUrls && mainRscEnv?.runner) {
11495
+ const rscGraph = mainRscEnv.moduleGraph;
11461
11496
  if (rscGraph?.getModulesByFile) {
11462
- const invalidateWithImporters = (mod, seen2) => {
11463
- if (!mod || seen2.has(mod)) return;
11464
- seen2.add(mod);
11465
- rscGraph.invalidateModule(mod);
11466
- for (const importer of mod.importers ?? []) {
11467
- invalidateWithImporters(importer, seen2);
11468
- }
11469
- };
11470
11497
  const routerSourceFiles = /* @__PURE__ */ new Set();
11471
11498
  if (s.resolvedEntryPath) {
11472
11499
  routerSourceFiles.add(resolve12(s.resolvedEntryPath));
@@ -11489,7 +11516,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
11489
11516
  continue;
11490
11517
  }
11491
11518
  for (const mod of mods) {
11492
- invalidateWithImporters(mod, seen);
11519
+ rscGraph.invalidateModule(mod, seen);
11493
11520
  }
11494
11521
  }
11495
11522
  debugDiscovery?.(
@@ -11953,7 +11980,8 @@ async function rango(options) {
11953
11980
  });
11954
11981
  plugins.push(
11955
11982
  createVirtualEntriesPlugin(finalEntries, void 0, {
11956
- headScripts: resolvedOptions.headScripts
11983
+ headScripts: resolvedOptions.headScripts,
11984
+ progressiveChunkSize: resolvedOptions.progressiveChunkSize
11957
11985
  })
11958
11986
  );
11959
11987
  plugins.push(performanceTracksPlugin());
@@ -12146,7 +12174,8 @@ If this is a multi-app host router, export a createHostRouter() instance and set
12146
12174
  });
12147
12175
  plugins.push(
12148
12176
  createVirtualEntriesPlugin(finalEntries, routerRef, {
12149
- headScripts: resolvedOptions.headScripts
12177
+ headScripts: resolvedOptions.headScripts,
12178
+ progressiveChunkSize: resolvedOptions.progressiveChunkSize
12150
12179
  })
12151
12180
  );
12152
12181
  plugins.push(performanceTracksPlugin());
@@ -12199,7 +12228,8 @@ If this is a multi-app host router, export a createHostRouter() instance and set
12199
12228
  prerenderOnError: options?.prerender?.onError,
12200
12229
  discovery: options?.discovery,
12201
12230
  clientChunkCtx,
12202
- headScripts: resolvedOptions.headScripts
12231
+ headScripts: resolvedOptions.headScripts,
12232
+ progressiveChunkSize: resolvedOptions.progressiveChunkSize
12203
12233
  })
12204
12234
  );
12205
12235
  if (preset === "vercel") {