@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.
- package/AGENTS.md +1 -1
- package/LICENSE +21 -0
- package/README.md +17 -15
- package/dist/types/cache/cf/cf-cache-constants.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +1 -1
- package/dist/types/outlet-context.d.ts +9 -0
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/route-content-wrapper.d.ts +4 -1
- package/dist/types/rsc/nonce.d.ts +10 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/types.d.ts +6 -2
- package/dist/types/ssr/index.d.ts +18 -0
- package/dist/types/ssr/ssr-root.d.ts +16 -0
- package/dist/types/ssr-suspension-warning.d.ts +20 -0
- package/dist/types/types/loader-types.d.ts +5 -1
- package/dist/types/types/segments.d.ts +17 -0
- package/dist/types/vite/discovery/state.d.ts +6 -0
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/utils/shared-utils.d.ts +1 -0
- package/dist/vite/index.js +56 -26
- package/package.json +26 -26
- package/skills/bundle-analysis/SKILL.md +1 -1
- package/skills/loader/SKILL.md +17 -2
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/bindings.md +1 -1
- package/skills/testing/cache-prerender.md +1 -1
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/e2e-parity.md +1 -1
- package/skills/testing/flight.md +1 -1
- package/skills/testing/handles.md +1 -1
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +1 -1
- package/skills/testing/response-routes.md +1 -1
- package/skills/testing/reverse-and-types.md +1 -1
- package/skills/testing/server-actions.md +1 -1
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/browser/merge-segment-loaders.ts +4 -0
- package/src/cache/cache-runtime.ts +2 -2
- package/src/cache/cf/cf-cache-constants.ts +1 -1
- package/src/cache/cf/cf-cache-store.ts +1 -1
- package/src/client.tsx +2 -0
- package/src/outlet-context.ts +9 -0
- package/src/outlet-provider.tsx +4 -0
- package/src/route-content-wrapper.tsx +7 -0
- package/src/router/segment-resolution/fresh.ts +48 -17
- package/src/router.ts +7 -2
- package/src/rsc/handler.ts +4 -4
- package/src/rsc/nonce.ts +16 -0
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +41 -8
- package/src/rsc/types.ts +6 -2
- package/src/segment-system.tsx +57 -12
- package/src/ssr/index.tsx +53 -1
- package/src/ssr/ssr-root.tsx +50 -22
- package/src/ssr-suspension-warning.ts +68 -0
- package/src/types/loader-types.ts +5 -1
- package/src/types/segments.ts +17 -0
- package/src/use-loader.tsx +24 -1
- package/src/vite/discovery/shell-prerender-phase.ts +5 -0
- package/src/vite/discovery/state.ts +6 -0
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/virtual-entries.ts +26 -3
- package/src/vite/rango.ts +3 -0
- package/src/vite/router-discovery.ts +31 -19
- 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/
|
|
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
|
-
> **
|
|
8
|
-
>
|
|
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/
|
|
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
|
|
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@
|
|
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/
|
|
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/
|
|
470
|
-
- [`cloudflare-basic`](https://github.com/
|
|
471
|
-
- [`cloudflare-multi-router`](https://github.com/
|
|
472
|
-
- [`vercel-basic`](https://github.com/
|
|
473
|
-
- [`vercel-multi-router`](https://github.com/
|
|
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/
|
|
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/
|
|
480
|
-
- [Execution model](https://github.com/
|
|
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:
|
|
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;
|
|
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 (
|
|
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
|
-
*
|
|
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 |
|
|
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,
|
package/dist/vite/index.js
CHANGED
|
@@ -3476,7 +3476,18 @@ async function initializeApp() {
|
|
|
3476
3476
|
|
|
3477
3477
|
initializeApp().catch(console.error);
|
|
3478
3478
|
`.trim();
|
|
3479
|
-
function
|
|
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.
|
|
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/
|
|
3710
|
+
homepage: "https://github.com/rangojs/rango#readme",
|
|
3698
3711
|
bugs: {
|
|
3699
|
-
url: "https://github.com/
|
|
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/
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
11460
|
-
|
|
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
|
-
|
|
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") {
|