@rangojs/router 0.19.0 → 0.20.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/dist/types/cache/cache-exec-scope.d.ts +6 -0
- package/dist/types/cache/cache-tag.d.ts +24 -1
- package/dist/types/server/context.d.ts +38 -1
- package/dist/types/ssr/entry-preload-priority.d.ts +69 -0
- package/dist/types/ssr/index.d.ts +1 -0
- package/dist/types/ssr/preinit-client-references.d.ts +17 -5
- package/dist/types/vite/plugin-types.d.ts +20 -6
- package/dist/types/vite/plugins/virtual-entries.d.ts +3 -1
- package/dist/types/vite/utils/client-chunks.d.ts +22 -2
- package/dist/vite/index.js +43 -10
- package/package.json +1 -1
- package/skills/bundle-analysis/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +3 -3
- package/skills/caching/SKILL.md +4 -4
- package/skills/parallel/SKILL.md +1 -1
- package/skills/rango/SKILL.md +2 -2
- package/skills/router-setup/SKILL.md +8 -2
- package/skills/use-cache/SKILL.md +8 -5
- package/src/cache/cache-exec-scope.ts +6 -0
- package/src/cache/cache-runtime.ts +5 -0
- package/src/cache/cache-tag.ts +104 -0
- package/src/router/loader-resolution.ts +15 -3
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/server/context.ts +75 -1
- package/src/server/request-context.ts +11 -3
- package/src/ssr/entry-preload-priority.ts +308 -0
- package/src/ssr/index.tsx +46 -9
- package/src/ssr/preinit-client-references.ts +17 -5
- package/src/vite/plugin-types.ts +20 -6
- package/src/vite/plugins/virtual-entries.ts +9 -2
- package/src/vite/rango.ts +2 -0
- package/src/vite/utils/client-chunks.ts +80 -14
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
*/
|
|
31
31
|
export interface CacheExecScope {
|
|
32
32
|
readonly parent?: CacheExecScope;
|
|
33
|
+
/**
|
|
34
|
+
* Loader values its chain read through an already-started execution, to
|
|
35
|
+
* their loader ids (cache-tag.ts readStartedLoaderValue), checked again
|
|
36
|
+
* before its write (#1011).
|
|
37
|
+
*/
|
|
38
|
+
loaderReads?: Map<object, string>;
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
35
41
|
* Run a callback whose identity reads never reach a shared entry, so
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* profile/DSL tags before storing.
|
|
11
11
|
*/
|
|
12
12
|
import { type RequestContext } from "../server/request-context.js";
|
|
13
|
-
import { type LoaderIdentityRead } from "../server/context.js";
|
|
13
|
+
import { type LoaderIdentityRead, type LoaderRunIdentity } from "../server/context.js";
|
|
14
|
+
import { type CacheExecScope } from "./cache-exec-scope.js";
|
|
14
15
|
import type { ExecutionStart } from "./tag-invalidation.js";
|
|
15
16
|
/**
|
|
16
17
|
* Call `fn` outside any "use cache" tag scope: detached work a cached body
|
|
@@ -153,6 +154,28 @@ export declare function markIdentityRead(set: Set<string>, read: LoaderIdentityR
|
|
|
153
154
|
* recorded, nearest first (breadth-first, so the execution's own read wins).
|
|
154
155
|
*/
|
|
155
156
|
export declare function recordedIdentityRead(root: Set<string>): LoaderIdentityRead | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* `value` is the promise of the execution `run` records, started by the
|
|
159
|
+
* current execution (if any), which reads it. Both loader runners call it
|
|
160
|
+
* (loader-resolution.ts createLoaderExecutor, request-context.ts
|
|
161
|
+
* createUseFunction).
|
|
162
|
+
*/
|
|
163
|
+
export declare function trackLoaderRun(value: object, run: LoaderRunIdentity): void;
|
|
164
|
+
/**
|
|
165
|
+
* A ctx.use(Loader) or getRequestContext().use(Loader) read that returns an
|
|
166
|
+
* already-started value (the request memo, a loader cache() binding's value).
|
|
167
|
+
* The current loader execution, if any, links it (it reads the value).
|
|
168
|
+
* Inside a "use cache" body the value is refused when its execution read
|
|
169
|
+
* request identity, as a body entered there would have been
|
|
170
|
+
* (guardIdentityRead). The check runs once the value settles, so a read the
|
|
171
|
+
* body makes after an await counts. Each enclosing execution also notes the
|
|
172
|
+
* value, and checks it again after encoding its result
|
|
173
|
+
* (assertLoaderReadsClean): a read that settles after the value (a nested
|
|
174
|
+
* promise in it) then fails the write instead.
|
|
175
|
+
*/
|
|
176
|
+
export declare function readStartedLoaderValue<T>(value: Promise<T>, loaderId: string): Promise<T>;
|
|
177
|
+
/** Throw when a loader value `scope` read carries an identity read. */
|
|
178
|
+
export declare function assertLoaderReadsClean(scope: CacheExecScope): void;
|
|
156
179
|
/** `root`'s tags plus those of every set it links to, transitively. */
|
|
157
180
|
export declare function flattenRecordedTags(root: Set<string>): Set<string>;
|
|
158
181
|
/** Record where the execution recording into `set` started. */
|
|
@@ -539,8 +539,44 @@ export interface LoaderIdentityRead {
|
|
|
539
539
|
* (cache-tag.ts recordedIdentityRead), so both orders fail the same way.
|
|
540
540
|
* Response directives do not record: a key cannot make a skipped body's side
|
|
541
541
|
* effect reach a HIT.
|
|
542
|
+
*
|
|
543
|
+
* The read also lands on the execution's LoaderRunIdentity (#1011), which
|
|
544
|
+
* every execution has, from either loader runner (runInsideLoaderRun).
|
|
542
545
|
*/
|
|
543
546
|
export declare function recordLoaderIdentityRead(surface: string, verb?: LoaderIdentityReadVerb): void;
|
|
547
|
+
/**
|
|
548
|
+
* One loader execution's request-identity record (#1011): the first identity
|
|
549
|
+
* read its body made, and the executions whose values it read with
|
|
550
|
+
* ctx.use(Loader). Every execution has one; the recorded-tag sets that carry
|
|
551
|
+
* the same reads for a loader cache() fill (cache-tag.ts) exist only in a
|
|
552
|
+
* request that binds one.
|
|
553
|
+
*
|
|
554
|
+
* A "use cache" function that reads a loader's value through the request
|
|
555
|
+
* memo (a handler, the route's loader() binding or a parent layout's started
|
|
556
|
+
* it first) does not run the body, so guardIdentityRead never sees the read.
|
|
557
|
+
* Scar: the entry, keyed without the cookie, stored the first visitor's value
|
|
558
|
+
* and served it to the next. cache-tag.ts readStartedLoaderValue refuses such
|
|
559
|
+
* a value through this record.
|
|
560
|
+
*/
|
|
561
|
+
export interface LoaderRunIdentity {
|
|
562
|
+
readonly loaderId: string;
|
|
563
|
+
read?: LoaderIdentityRead;
|
|
564
|
+
reads?: Set<LoaderRunIdentity>;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Run a loader body with `run` as its identity record: both loader runners
|
|
568
|
+
* (loader-resolution.ts createLoaderExecutor, request-context.ts
|
|
569
|
+
* createUseFunction) enter it around the loader function.
|
|
570
|
+
*/
|
|
571
|
+
export declare function runInsideLoaderRun<T>(run: LoaderRunIdentity, fn: () => T): T;
|
|
572
|
+
/** The innermost loader execution's identity record (runInsideLoaderRun). */
|
|
573
|
+
export declare function getLoaderRunIdentity(): LoaderRunIdentity | undefined;
|
|
574
|
+
/**
|
|
575
|
+
* The error a "use cache" function's read of a loader value fails with when
|
|
576
|
+
* the loader's execution, or one whose value it read, made `read` (#1011).
|
|
577
|
+
* Worded like guardIdentityRead's "use cache" refusal, naming the loaders.
|
|
578
|
+
*/
|
|
579
|
+
export declare function useCacheLoaderIdentityError(read: LoaderIdentityRead, loaderId: string): Error;
|
|
544
580
|
/** The error a loader cache() fill with no declared key fails with (#972). */
|
|
545
581
|
export declare function loaderCacheIdentityError(read: LoaderIdentityRead, cachedLoaderId: string): Error;
|
|
546
582
|
/**
|
|
@@ -632,7 +668,8 @@ export declare function runInsideLoaderScope<T>(fn: () => T): T;
|
|
|
632
668
|
* and handler-invoked via ctx.use) so request-scoped reads inside a loader
|
|
633
669
|
* never trip the cache() guard — a route cache() never stores loader values.
|
|
634
670
|
* A "use cache" body the loader was entered in still refuses them
|
|
635
|
-
* (guardIdentityRead)
|
|
671
|
+
* (guardIdentityRead); one that reads the value later refuses what the run
|
|
672
|
+
* recorded (runInsideLoaderRun, cache-tag.ts readStartedLoaderValue).
|
|
636
673
|
*/
|
|
637
674
|
export declare function runInsideLoaderBodyScope<T>(fn: () => T, loaderId?: string, tags?: Set<string>): T;
|
|
638
675
|
/** The innermost loader body's execution tag set (runInsideLoaderBodyScope). */
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client entry's head hint at default fetch priority, after the head chunk
|
|
3
|
+
* scripts (issue #1025).
|
|
4
|
+
*
|
|
5
|
+
* For `bootstrapModules`, Fizz writes `<link rel="modulepreload"
|
|
6
|
+
* fetchPriority="low">` into the head and claims the URL
|
|
7
|
+
* (`moduleScriptResources[url] = null`, react-dom 19.3.0
|
|
8
|
+
* `react-dom-server.edge.production.js:546-565`) before the render starts, so
|
|
9
|
+
* no `preloadModule`/`preinitModule` call can replace it, and the
|
|
10
|
+
* `bootstrapModules` entry shape has no priority field. Chromium fetches that
|
|
11
|
+
* hint at Low, and once it boosts in-viewport images to High after first
|
|
12
|
+
* layout, the entry queues behind them; the executing end-of-shell module
|
|
13
|
+
* script reuses the in-flight Low fetch through the module map. Hydration
|
|
14
|
+
* then waits on a few-hundred-byte file while the head chunk scripts it
|
|
15
|
+
* imports (High) have long arrived: measured +3.8 s on a priority-honouring
|
|
16
|
+
* HTTP/2 link at Slow 4G.
|
|
17
|
+
*
|
|
18
|
+
* The fix serves that one tag without the attribute, so it fetches at
|
|
19
|
+
* Chromium's modulepreload default (High), and moves it after the head
|
|
20
|
+
* chunk scripts. Fizz flushes the bootstrap hint before the head chunk scripts; at
|
|
21
|
+
* High in that position it took one of HTTP/1.1's six connections ahead of
|
|
22
|
+
* the react chunk and delayed the last head chunk (hydration +91 ms, LCP
|
|
23
|
+
* +84 ms measured). After the scripts it is the last High request in the
|
|
24
|
+
* queue, so it never displaces a chunk hydration also needs. The accepted
|
|
25
|
+
* cost: an HTTP/2 server that sends equal-priority streams first in, first
|
|
26
|
+
* out delivers it with the last head chunk instead of before it (text-only
|
|
27
|
+
* pages +12 to +56 ms measured, against -0.15 to -3.3 s hydration on image
|
|
28
|
+
* pages; CHANGELOG has the matrix). Everything else about the bootstrap pair
|
|
29
|
+
* stays Fizz's: the nonce, the executing `id="_R_"` tag, the URL claim that
|
|
30
|
+
* keeps plugin-rsc's and the preinit hook's calls for the entry inert, and the
|
|
31
|
+
* PPR prelude/resume behaviour.
|
|
32
|
+
*
|
|
33
|
+
* The match is the exact tag Fizz writes for a string `bootstrapModules`
|
|
34
|
+
* entry (`rel`, `fetchPriority`, `nonce`, `href` in that order), not a
|
|
35
|
+
* pattern, and only before the first `<body`. A Fizz version that writes it
|
|
36
|
+
* differently leaves the bytes untouched: the output keeps the Low hint.
|
|
37
|
+
*
|
|
38
|
+
* Where the tag goes: right after the run of Fizz resource tags that directly
|
|
39
|
+
* follows the hint, before the first token that is anything else (`</head>`,
|
|
40
|
+
* the app's own head children, or body content in a head-less document).
|
|
41
|
+
* Fizz flushes the head chunk scripts and the bulk preloads straight after
|
|
42
|
+
* the bootstrap hint (react-dom 19.3.0 `flushCompletedQueues`, `:7321-7325`),
|
|
43
|
+
* as `<script …></script>` with no body and `<link …/>`; the scan accepts
|
|
44
|
+
* only those two shapes and stops at any other token without reading into it.
|
|
45
|
+
* It never searches the app's head content for `</head>` or `<body`: React
|
|
46
|
+
* does not escape either inside `<script>` or `<style>` text, so a head script
|
|
47
|
+
* holding the string `"<body>"` once received the tag inside its source and
|
|
48
|
+
* threw. Fizz escapes `>` in attribute values, so the first `>` ends a tag.
|
|
49
|
+
* Every insertion point is the boundary between two complete tags; a stream
|
|
50
|
+
* that ends mid-run, or a run tag longer than {@link MAX_HELD_TAG}, gets the
|
|
51
|
+
* tag before the unfinished one. Only that one tag is held back while the run
|
|
52
|
+
* streams; each complete run tag is forwarded as soon as it is scanned.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Rewrite a complete document prefix (the PPR shell capture's prelude).
|
|
56
|
+
* Returns `bytes` unchanged when no hint precedes the first `<body`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function rewriteEntryPreloadPriority(bytes: Uint8Array, entryUrl: string, nonce?: string): Uint8Array;
|
|
59
|
+
/**
|
|
60
|
+
* Streaming form for live SSR: the same rewrite as
|
|
61
|
+
* {@link rewriteEntryPreloadPriority}, then pass-through. Before the hint is
|
|
62
|
+
* found, only a chunk tail that may be the start of a split `<body` or hint
|
|
63
|
+
* tag is held back. After it, complete run tags are forwarded as they are
|
|
64
|
+
* scanned and only an unfinished one (at most {@link MAX_HELD_TAG} bytes) is
|
|
65
|
+
* held, so every byte is scanned a bounded number of times. Once the tag is
|
|
66
|
+
* placed, or once `<body` passes without a hint, chunks are forwarded
|
|
67
|
+
* untouched and unscanned.
|
|
68
|
+
*/
|
|
69
|
+
export declare function entryPreloadPriorityTransform(entryUrl: string, nonce?: string): TransformStream<Uint8Array, Uint8Array>;
|
|
@@ -145,6 +145,7 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
145
145
|
* undefined keeps the inline bootstrap verbatim, so a custom SSR entry that
|
|
146
146
|
* never installed the preinit hook cannot drift into the half-converted
|
|
147
147
|
* state on upgrade (the generated entry always passes an explicit value).
|
|
148
|
+
* Any other value throws when the handler is created.
|
|
148
149
|
*/
|
|
149
150
|
headScripts?: HeadScriptsOption;
|
|
150
151
|
/**
|
|
@@ -55,11 +55,23 @@ export declare function runWithPreinitNonce<T>(nonce: string | undefined, fn: ()
|
|
|
55
55
|
*
|
|
56
56
|
* Known trades (deliberate, measured neutral-to-positive on the e2e apps —
|
|
57
57
|
* PR #694 has the Lighthouse/hydration numbers):
|
|
58
|
-
* - Fetch priority:
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
58
|
+
* - Fetch priority: Chromium gives a parser-inserted `async` module script
|
|
59
|
+
* initial priority High: below the render-blocking stylesheet (Highest),
|
|
60
|
+
* and alongside the LCP image once Chromium boosts the in-viewport image to
|
|
61
|
+
* High (#1021). Every head chunk gets the same priority: the shared
|
|
62
|
+
* dependency chunks (react, router, entry.rsc and the bundler runtime) and
|
|
63
|
+
* every client-component chunk. The entry itself is a Fizz bootstrapModules
|
|
64
|
+
* hint, not a preinit (Fizz claims its URL, so this hook's call for it is
|
|
65
|
+
* inert); the handlers serve that hint at default priority, High as well,
|
|
66
|
+
* after these scripts (entry-preload-priority.ts, #1025).
|
|
67
|
+
* Lowering the chunks to `fetchpriority="low"` was measured and rejected
|
|
68
|
+
* (#1021; Chromium 153, 20 interleaved runs per variant and cell). On pages
|
|
69
|
+
* with images above the fold the Low chunks queue behind the boosted
|
|
70
|
+
* images: hydration came 206 to 6407 ms later on HTTP/1.1 and 65 to 642 ms
|
|
71
|
+
* later on HTTP/2. The one gain, FCP -136 ms, needed an image-free page
|
|
72
|
+
* with 35 KB of render-blocking CSS on HTTP/1.1 Slow 4G, and hydration was
|
|
73
|
+
* still 98 ms later there. `headScripts: "preload"` is the way to get Low
|
|
74
|
+
* chunk fetches.
|
|
63
75
|
* - Build only: plugin-rsc's dev load path reports `js: []` per reference, so
|
|
64
76
|
* dev documents have no head chunk scripts — a client module whose module
|
|
65
77
|
* scope assumes body-parsed DOM can break in production only. The
|
|
@@ -68,6 +68,12 @@ export interface ClientChunkMeta {
|
|
|
68
68
|
* strategy**. It splits app `"use client"` modules by **route id** — the segment
|
|
69
69
|
* after a route-root directory (`routes`, `app`, `pages`, `features`, `handlers`,
|
|
70
70
|
* …) — so `routes/dashboard/**` becomes `app-dashboard` at any nesting depth.
|
|
71
|
+
* The first marker in the path wins, except that an `app/` directly followed
|
|
72
|
+
* by another marker with a directory after it is treated as a source root:
|
|
73
|
+
* `app/routes/product/**` becomes `app-product`, while `app/dashboard/**`
|
|
74
|
+
* (Next-style), `app/components/**` and files directly in `app/routes/` stay
|
|
75
|
+
* `app-dashboard` / `app-components` / `app-routes`. The same route id under
|
|
76
|
+
* different markers is one group.
|
|
71
77
|
* Where it finds NO route structure (a flat `src/components/`, or host sub-apps
|
|
72
78
|
* already split by a dynamic `import()`), it inherits the default grouping
|
|
73
79
|
* unchanged — so the shared `src/components` chunk stays shared and host apps do
|
|
@@ -79,10 +85,14 @@ export interface ClientChunkMeta {
|
|
|
79
85
|
* back to the default grouping for that one module. Forwarded directly to
|
|
80
86
|
* `@vitejs/plugin-rsc`'s `clientChunks`.
|
|
81
87
|
*
|
|
82
|
-
* Every module maps to exactly one group, so there is no byte duplication
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
88
|
+
* Every module maps to exactly one group, so there is no byte duplication. A
|
|
89
|
+
* group is also the loading unit: rendering any one member downloads the whole
|
|
90
|
+
* group's chunk, every other member included. A component used by two routes
|
|
91
|
+
* lives in one group, and rendering it on either route fetches that entire
|
|
92
|
+
* group. Put genuinely shared client components OUTSIDE route directories so
|
|
93
|
+
* they land in the shared group rather than one route's chunk, and keep an
|
|
94
|
+
* always-rendered component (a layout header) out of a large group.
|
|
95
|
+
* `DEBUG=rango:chunks vite build` logs each group's module count and size.
|
|
86
96
|
*
|
|
87
97
|
* @default true
|
|
88
98
|
*/
|
|
@@ -114,14 +124,18 @@ interface RangoBaseOptions {
|
|
|
114
124
|
* - `"preinit"` (**default**): client-reference chunks render as EXECUTING
|
|
115
125
|
* `<script type="module" async>` tags hoisted into `<head>` (upgrading
|
|
116
126
|
* plugin-rsc's modulepreload hints in place), and the browser entry ships
|
|
117
|
-
* as Fizz `bootstrapModules` — a
|
|
118
|
-
*
|
|
127
|
+
* as Fizz `bootstrapModules` — a `modulepreload` hint after the head
|
|
128
|
+
* chunk scripts (default priority; Fizz's `fetchpriority=low` is dropped,
|
|
129
|
+
* #1025)
|
|
130
|
+
* plus the executing end-of-shell `id="_R_"` module script. Chunk
|
|
119
131
|
* execution overlaps body streaming instead of waiting for the hydration
|
|
120
132
|
* import walk; under PPR everything lands in the stored shell prelude.
|
|
121
133
|
* - `"preload"`: the previous behavior — `<link rel="modulepreload">` hints
|
|
122
134
|
* only, entry as an inline `import()` script at end of shell. Chunks
|
|
123
135
|
* fetch+compile early but execute only when hydration imports them.
|
|
124
136
|
*
|
|
137
|
+
* Any other value throws when the generated SSR entry is built.
|
|
138
|
+
*
|
|
125
139
|
* Build-only for the chunk half: plugin-rsc resolves no JS deps per client
|
|
126
140
|
* reference in dev, so dev documents carry no head chunk scripts in either
|
|
127
141
|
* mode (the bootstrap conversion does apply in dev). Trades and upstream
|
|
@@ -5,7 +5,9 @@ export declare const VIRTUAL_ENTRY_BROWSER: string;
|
|
|
5
5
|
* option: "preinit" (default) installs the client-reference preinit hook and
|
|
6
6
|
* threads `getClientEntryUrl` so Fizz emits `bootstrapModules`;
|
|
7
7
|
* "preload" omits the hook and uses the deprecated inline
|
|
8
|
-
* `loadBootstrapScriptContent` bootstrap.
|
|
8
|
+
* `loadBootstrapScriptContent` bootstrap. Any other value throws: an unknown
|
|
9
|
+
* string such as "prenit" used to run as "preinit" silently
|
|
10
|
+
* (`headScripts !== "preload"` since #694).
|
|
9
11
|
*/
|
|
10
12
|
export declare function getVirtualEntrySSR(headScripts?: HeadScriptsOption, progressiveChunkSize?: number): string;
|
|
11
13
|
/**
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
1
2
|
import type { ClientChunkMeta, ClientChunks } from "../plugin-types.js";
|
|
3
|
+
import { type Debugger } from "../debug.js";
|
|
2
4
|
/** The callback shape @vitejs/plugin-rsc's `clientChunks` option accepts. */
|
|
3
5
|
export type RscClientChunksFn = (meta: ClientChunkMeta) => string | undefined;
|
|
4
6
|
/**
|
|
@@ -36,8 +38,11 @@ export interface ClientChunkContext {
|
|
|
36
38
|
* 1. Shared runtime (React / router / node_modules) -> `undefined` (never split).
|
|
37
39
|
* 2. A registered error/notFound fallback (`ctx.fallbackRefs`) -> `app-fallback`,
|
|
38
40
|
* regardless of location, so the error UI is decoupled from the happy path.
|
|
39
|
-
* 3.
|
|
40
|
-
* next segment (the route id), robust to any nesting depth.
|
|
41
|
+
* 3. The first {@link ROUTE_ROOT_DIRS} marker with a directory after it -> key on
|
|
42
|
+
* that next segment (the route id), robust to any nesting depth. A
|
|
43
|
+
* {@link SOURCE_ROOT_DIRS} marker (`app`) directly followed by another marker
|
|
44
|
+
* with a directory after it defers to that inner marker:
|
|
45
|
+
* `app/routes/<id>/…` -> `app-<id>`, while `app/<segment>/…` -> `app-<segment>`.
|
|
41
46
|
* 4. Otherwise `undefined` (inherit the default `serverChunk` grouping).
|
|
42
47
|
*/
|
|
43
48
|
export declare function directoryClientChunks(meta: ClientChunkMeta, ctx?: ClientChunkContext): string | undefined;
|
|
@@ -53,3 +58,18 @@ export declare function directoryClientChunks(meta: ClientChunkMeta, ctx?: Clien
|
|
|
53
58
|
* fallback refinement does not apply — the consumer owns the grouping).
|
|
54
59
|
*/
|
|
55
60
|
export declare function resolveClientChunks(option: ClientChunks | undefined, ctx?: ClientChunkContext): RscClientChunksFn | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Build-end group report under `DEBUG=rango:chunks`: one line per client
|
|
63
|
+
* reference group with its client-reference count and the size of the group's
|
|
64
|
+
* own chunk (code split into shared chunks such as `react`/`router` is not
|
|
65
|
+
* counted). A group is the loading unit (plugin-rsc imports the group's virtual
|
|
66
|
+
* module to resolve any one member), so a large group that an always-rendered
|
|
67
|
+
* component belongs to is downloaded on every route; this line makes that
|
|
68
|
+
* visible. Covers every group, including ones named by a custom `clientChunks`
|
|
69
|
+
* function or by plugin-rsc's default `serverChunk` grouping. `undefined` when
|
|
70
|
+
* the namespace is off, so a normal build does not register it. Group names are
|
|
71
|
+
* printed with NUL escaped: plugin-rsc's default group is named
|
|
72
|
+
* `facade:\0virtual:…`, and a raw NUL makes `grep` treat the output as binary
|
|
73
|
+
* and drop that line, usually the largest group. `log` is injectable for tests.
|
|
74
|
+
*/
|
|
75
|
+
export declare function clientChunksReport(log?: Debugger | undefined): Plugin | undefined;
|
package/dist/vite/index.js
CHANGED
|
@@ -4369,7 +4369,12 @@ function emitProgressiveChunkSize(value) {
|
|
|
4369
4369
|
return JSON.stringify(value);
|
|
4370
4370
|
}
|
|
4371
4371
|
function getVirtualEntrySSR(headScripts = "preinit", progressiveChunkSize) {
|
|
4372
|
-
|
|
4372
|
+
if (headScripts !== "preinit" && headScripts !== "preload") {
|
|
4373
|
+
throw new Error(
|
|
4374
|
+
`rango({ headScripts }) must be "preinit" or "preload", received ${JSON.stringify(headScripts)}`
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
const preinit = headScripts === "preinit";
|
|
4373
4378
|
const depsImportNames = preinit ? "createFromReadableStream,\n setOnClientReference,\n getClientEntryUrl," : "createFromReadableStream,";
|
|
4374
4379
|
const ssrImportNames = preinit ? "\n installClientReferencePreinit," : "";
|
|
4375
4380
|
const install = preinit ? `
|
|
@@ -4578,7 +4583,7 @@ import { resolve } from "node:path";
|
|
|
4578
4583
|
// package.json
|
|
4579
4584
|
var package_default = {
|
|
4580
4585
|
name: "@rangojs/router",
|
|
4581
|
-
version: "0.
|
|
4586
|
+
version: "0.20.0",
|
|
4582
4587
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
4583
4588
|
keywords: [
|
|
4584
4589
|
"react",
|
|
@@ -6443,6 +6448,7 @@ function getManualChunks(id) {
|
|
|
6443
6448
|
// src/vite/utils/client-chunks.ts
|
|
6444
6449
|
init_debug();
|
|
6445
6450
|
init_client_ref_hashing();
|
|
6451
|
+
import { gzipSync } from "node:zlib";
|
|
6446
6452
|
var debugChunks = createRangoDebugger(NS.chunks);
|
|
6447
6453
|
function isSharedRuntime(meta) {
|
|
6448
6454
|
return [meta.id, meta.normalizedId].some(
|
|
@@ -6468,6 +6474,7 @@ var ROUTE_ROOT_DIRS = /* @__PURE__ */ new Set([
|
|
|
6468
6474
|
"screens",
|
|
6469
6475
|
"sections"
|
|
6470
6476
|
]);
|
|
6477
|
+
var SOURCE_ROOT_DIRS = /* @__PURE__ */ new Set(["app"]);
|
|
6471
6478
|
function directoryClientChunks(meta, ctx) {
|
|
6472
6479
|
if (isSharedRuntime(meta)) {
|
|
6473
6480
|
return void 0;
|
|
@@ -6478,14 +6485,13 @@ function directoryClientChunks(meta, ctx) {
|
|
|
6478
6485
|
}
|
|
6479
6486
|
const segments = meta.normalizedId.split("/").filter(Boolean);
|
|
6480
6487
|
const dirCount = segments.length - 1;
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
}
|
|
6488
|
+
const isMarkerAt = (i) => i < dirCount - 1 && ROUTE_ROOT_DIRS.has(segments[i].toLowerCase());
|
|
6489
|
+
for (let i = 0; i < dirCount - 1; i++) {
|
|
6490
|
+
if (!isMarkerAt(i)) continue;
|
|
6491
|
+
const marker = SOURCE_ROOT_DIRS.has(segments[i].toLowerCase()) && isMarkerAt(i + 1) ? i + 1 : i;
|
|
6492
|
+
const group = `app-${sanitizeGroup(segments[marker + 1])}`;
|
|
6493
|
+
debugChunks?.("split %s -> %s", meta.normalizedId, group);
|
|
6494
|
+
return group;
|
|
6489
6495
|
}
|
|
6490
6496
|
debugChunks?.(
|
|
6491
6497
|
"shared %s (no route-root marker; inherits default grouping)",
|
|
@@ -6498,6 +6504,30 @@ function resolveClientChunks(option, ctx) {
|
|
|
6498
6504
|
if (option === true) return (meta) => directoryClientChunks(meta, ctx);
|
|
6499
6505
|
return option;
|
|
6500
6506
|
}
|
|
6507
|
+
var CLIENT_GROUP_PREFIX = "\0virtual:vite-rsc/client-references/group/";
|
|
6508
|
+
function clientChunksReport(log = debugChunks) {
|
|
6509
|
+
if (!log) return void 0;
|
|
6510
|
+
return {
|
|
6511
|
+
name: "@rangojs/router:client-chunks-report",
|
|
6512
|
+
apply: "build",
|
|
6513
|
+
applyToEnvironment: (env) => env.name === "client",
|
|
6514
|
+
generateBundle(_options, bundle) {
|
|
6515
|
+
for (const chunk of Object.values(bundle)) {
|
|
6516
|
+
if (chunk.type !== "chunk") continue;
|
|
6517
|
+
const facade = chunk.facadeModuleId;
|
|
6518
|
+
if (!facade?.startsWith(CLIENT_GROUP_PREFIX)) continue;
|
|
6519
|
+
log(
|
|
6520
|
+
"group %s: %d client reference(s), %d B (%d B gzip) -> %s",
|
|
6521
|
+
facade.slice(CLIENT_GROUP_PREFIX.length).replaceAll("\0", "\\0"),
|
|
6522
|
+
this.getModuleInfo(facade)?.importedIds.length ?? 0,
|
|
6523
|
+
Buffer.byteLength(chunk.code),
|
|
6524
|
+
gzipSync(chunk.code).length,
|
|
6525
|
+
chunk.fileName
|
|
6526
|
+
);
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
};
|
|
6530
|
+
}
|
|
6501
6531
|
|
|
6502
6532
|
// src/vite/plugins/vercel-output.ts
|
|
6503
6533
|
import { rm, mkdir, cp, writeFile } from "node:fs/promises";
|
|
@@ -6957,6 +6987,7 @@ var NON_CACHEABLE_READ = {
|
|
|
6957
6987
|
...requestReadCaptureFix("per-request variables")
|
|
6958
6988
|
}
|
|
6959
6989
|
};
|
|
6990
|
+
var loaderRunALS = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:loader-run-identity")] ??= new AsyncLocalStorage2();
|
|
6960
6991
|
function getLoaderBodyTags() {
|
|
6961
6992
|
return loaderBodyScopeALS.getStore()?.tags;
|
|
6962
6993
|
}
|
|
@@ -9938,6 +9969,7 @@ globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:identity-read-recorder")]
|
|
|
9938
9969
|
if (set === void 0 || identityReads.has(set)) return;
|
|
9939
9970
|
identityReads.set(set, { surface, verb, bodyId: getCurrentLoaderBodyId() });
|
|
9940
9971
|
};
|
|
9972
|
+
var loaderRuns = globalThis[/* @__PURE__ */ Symbol.for("rangojs-router:loader-runs")] ??= /* @__PURE__ */ new WeakMap();
|
|
9941
9973
|
|
|
9942
9974
|
// src/router/router-context.ts
|
|
9943
9975
|
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
@@ -13347,6 +13379,7 @@ If this is a multi-app host router, export a createHostRouter() instance and set
|
|
|
13347
13379
|
);
|
|
13348
13380
|
plugins.push(clientRefDedup());
|
|
13349
13381
|
}
|
|
13382
|
+
plugins.push(clientChunksReport());
|
|
13350
13383
|
plugins.push({
|
|
13351
13384
|
name: "@rangojs/router:client-component-hmr",
|
|
13352
13385
|
hotUpdate(ctx) {
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ Your app builds in three Vite environments — `client`, `ssr`, and `rsc` — an
|
|
|
15
15
|
1. **Server code leaking into the client.** A file that imports `node:fs`, calls a database, or contains action logic ends up in the client bundle because a client component pulled it in transitively. Symptom: your client bundle is much larger than expected, sometimes with imports that fail at runtime.
|
|
16
16
|
2. **Both dev and prod React in the SSR/RSC bundle.** When `process.env.NODE_ENV` isn't folded at build time, React's CJS files ship both `.development.js` _and_ `.production.js` variants — doubling React's footprint. The Cloudflare Vite plugin and rango's `vercel` preset fold NODE_ENV automatically; on the default `node` preset, `vite build` does it for the client but not always for SSR/RSC, so add the `define` from Step 2.
|
|
17
17
|
3. **An oversized routes-manifest in your RSC worker.** The `virtual:rsc-router/routes-manifest/<routerId>` chunk holds your route trie and precomputed entries — large only in proportion to your route count. If it's surprisingly big, you may have unintentionally generated routes (e.g., parametrized fixtures) that bloated the trie.
|
|
18
|
-
4. **Poor client-chunk grouping.** `rango({ clientChunks })` decides how `"use client"` modules are grouped into browser chunks. The default groups them by route directory (`routes/<id>/**` → `app-<id>`), with flat folders like `src/components/` in one shared chunk; `clientChunks: false` ships one client chunk per router. A route that pulls in unrelated routes' client code, or one oversized shared chunk, points at the grouping.
|
|
18
|
+
4. **Poor client-chunk grouping.** `rango({ clientChunks })` decides how `"use client"` modules are grouped into browser chunks. The default groups them by route directory (`routes/<id>/**` → `app-<id>`), with flat folders like `src/components/` in one shared chunk; `clientChunks: false` ships one client chunk per router. A group is the loading unit: rendering any one member downloads the whole group's chunk. A route that pulls in unrelated routes' client code, or one oversized shared chunk, points at the grouping; `DEBUG=rango:chunks vite build` logs each group's size at build end.
|
|
19
19
|
|
|
20
20
|
Tree-shaking does _not_ catch generated data inlined as string literals, or data-dependent conditionals like React's `NODE_ENV` branches. You need a visualizer.
|
|
21
21
|
|
|
@@ -560,9 +560,9 @@ Loaders are exempt from the `cache()` guard because they run outside the cache
|
|
|
560
560
|
scope and resolve fresh every request. A loader with its own `cache()` does not:
|
|
561
561
|
its value is stored, so it falls under the loader `cache()` guard above. Nor
|
|
562
562
|
does a loader awaited inside a `"use cache"` body: its value is part of what the
|
|
563
|
-
function returns, so the same reads throw there
|
|
564
|
-
the
|
|
565
|
-
|
|
563
|
+
function returns, so the same reads throw there, whether the cached function
|
|
564
|
+
starts the loader or reads a value a handler or a `loader()` binding already
|
|
565
|
+
started.
|
|
566
566
|
|
|
567
567
|
## Loaders Are Always Fresh
|
|
568
568
|
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -870,10 +870,10 @@ This is the **consumption-lane rule**, and it holds for every shared
|
|
|
870
870
|
artifact — `cache()`, `"use cache"`, and the PPR shell (`/ppr`): handler
|
|
871
871
|
consumption = baked copy; client-side `useLoader` = live. The tiers differ on
|
|
872
872
|
identity reads inside a handler-consumed loader: `cache()` permits them (the
|
|
873
|
-
leak above); `"use cache"` throws
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
873
|
+
leak above); `"use cache"` throws, because the loader's value is part of the
|
|
874
|
+
result: inside the loader body when the cached function starts it, and at
|
|
875
|
+
`ctx.use()` when a handler or a `loader()` binding started it first and its
|
|
876
|
+
run made such a read; a PPR shell capture
|
|
877
877
|
refuses them and the route stays uncached. It is stated once in `/rango` → Invariants.
|
|
878
878
|
|
|
879
879
|
```typescript
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -280,7 +280,7 @@ layout(<AccountLayout />, () => [
|
|
|
280
280
|
|
|
281
281
|
A slot's `loading()` (whether from `handler.use` or explicit) makes that slot an independent streaming unit, exactly as in the **Streaming Behavior** section above.
|
|
282
282
|
|
|
283
|
-
Under a shared artifact (`cache()`, `"use cache"`, a PPR shell), the server-side `await ctx.use(CartLoader)` above is the BAKED lane — the capture-time value freezes into the artifact. An identity read (`cookies()`, `headers()`, `ctx.request.headers`) inside the loader bakes too under `cache()`; inside `"use cache"` it throws
|
|
283
|
+
Under a shared artifact (`cache()`, `"use cache"`, a PPR shell), the server-side `await ctx.use(CartLoader)` above is the BAKED lane — the capture-time value freezes into the artifact. An identity read (`cookies()`, `headers()`, `ctx.request.headers`) inside the loader bakes too under `cache()`; inside `"use cache"` it throws, whichever code started the loader first; under a PPR shell it refuses the capture, so the route stays uncached. Consume the loader client-side (`useLoader` in a `"use client"` component, the slot's `loading()` as its boundary) to keep the slot live per request. One rule, stated once: `/rango` → Invariants ("the consumption-lane rule").
|
|
284
284
|
|
|
285
285
|
The `parallel` mount site has the narrowest allow-list for `handler.use` items — slots cannot bring their own middleware or layout, only `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, and `transition`. See [skills/handler-use](../handler-use/SKILL.md) for the full table and merge rules.
|
|
286
286
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -135,8 +135,8 @@ stated, greppable contract.
|
|
|
135
135
|
`ctx.request.headers`, a `{ cache: false }` variable) inside that loader
|
|
136
136
|
are permitted under a route `cache()` (a documented footgun; see
|
|
137
137
|
`/caching` → "Cache purity & tainted objects"). They throw inside
|
|
138
|
-
`"use cache"` (
|
|
139
|
-
|
|
138
|
+
`"use cache"` (whichever code started the loader first: a handler, a
|
|
139
|
+
`loader()` binding, or the cached function itself) and REFUSE a PPR capture: the route serves uncached until
|
|
140
140
|
the read moves to a live loader. Client-side consumption
|
|
141
141
|
(`useLoader` in a `"use client"` component) is the LIVE lane. DSL
|
|
142
142
|
`loader()` segments follow their PPR lane (only `ssr: false` bakes; see
|
|
@@ -234,8 +234,14 @@ Notes:
|
|
|
234
234
|
|
|
235
235
|
- `clientChunks` default groups app client components by the directory after a
|
|
236
236
|
route root (`routes/`, `app/`, `pages/`, `features/`, `handlers/`, ...), so
|
|
237
|
-
`routes/dashboard/**` becomes chunk `app-dashboard`.
|
|
238
|
-
|
|
237
|
+
`routes/dashboard/**` becomes chunk `app-dashboard`. An `app/` directly
|
|
238
|
+
followed by another marker with a directory after it is a source root, so
|
|
239
|
+
`app/routes/product/**` is `app-product`; `app/<segment>/**`,
|
|
240
|
+
`app/components/**` and files directly in `app/routes/` keep `app-<segment>`
|
|
241
|
+
/ `app-components` / `app-routes`. Flat folders such as
|
|
242
|
+
`src/components/` stay in the shared chunk. A group is the loading unit:
|
|
243
|
+
rendering any member downloads the whole group's chunk;
|
|
244
|
+
`DEBUG=rango:chunks vite build` logs each group's size. `false` restores
|
|
239
245
|
`@vitejs/plugin-rsc`'s grouping (one client chunk per router). A function
|
|
240
246
|
`(meta) => string | undefined` names the group per module (`undefined` keeps
|
|
241
247
|
the default); `directoryClientChunks(meta)`, the default strategy, is
|
|
@@ -283,11 +283,14 @@ is part of that body: its value is captured into the shared cache entry like
|
|
|
283
283
|
any other computed data, so a `{ cache: false }` variable read there throws,
|
|
284
284
|
exactly as `cookies()`, `headers()`, `ctx.request.headers` and a theme read do
|
|
285
285
|
(one guard follows the
|
|
286
|
-
cached body's whole async chain, loaders included). The
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
286
|
+
cached body's whole async chain, loaders included). The order does not
|
|
287
|
+
matter: a loader something else in the request already started (a handler's
|
|
288
|
+
`ctx.use()`, the route's or a parent layout's `loader()`) returns its memoized
|
|
289
|
+
value, and `ctx.use()` (or `getRequestContext().use()`) inside the cached function rejects with the same error
|
|
290
|
+
when that run, or a loader it read, made such a read. A read the run makes
|
|
291
|
+
after its value settled (a nested promise in the value) fails the entry's
|
|
292
|
+
write instead: the caller gets its own value, nothing is stored, and `onError`
|
|
293
|
+
reports it. Read the value outside and pass it in as an argument. Handler/cached-scope consumption = baked copy,
|
|
291
294
|
client-side `useLoader` = live (the consumption-lane rule, `/rango` →
|
|
292
295
|
Invariants). Under a route `cache()` a loader body's reads stay allowed (the
|
|
293
296
|
route entry never stores a loader's value); during a PPR capture they refuse
|
|
@@ -34,6 +34,12 @@ import { _setCacheExecScopeProbe } from "./taint.js";
|
|
|
34
34
|
*/
|
|
35
35
|
export interface CacheExecScope {
|
|
36
36
|
readonly parent?: CacheExecScope;
|
|
37
|
+
/**
|
|
38
|
+
* Loader values its chain read through an already-started execution, to
|
|
39
|
+
* their loader ids (cache-tag.ts readStartedLoaderValue), checked again
|
|
40
|
+
* before its write (#1011).
|
|
41
|
+
*/
|
|
42
|
+
loaderReads?: Map<object, string>;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
const cacheExecStorage = new AsyncLocalStorage<CacheExecScope>();
|
|
@@ -47,6 +47,7 @@ import { encodeKV } from "../encode-kv.js";
|
|
|
47
47
|
import { runBackground } from "./background-task.js";
|
|
48
48
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
49
49
|
import {
|
|
50
|
+
assertLoaderReadsClean,
|
|
50
51
|
normalizeTags,
|
|
51
52
|
outsideCacheTagScope,
|
|
52
53
|
recordRequestTags,
|
|
@@ -753,6 +754,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
753
754
|
// An error row would replace the stale entry; the catch
|
|
754
755
|
// below reports it and the stale entry keeps serving.
|
|
755
756
|
if (flightErrors.length > 0) throw flightErrors[0];
|
|
757
|
+
assertLoaderReadsClean(bgScope);
|
|
756
758
|
const freshTags = entryTags(scoped.tags);
|
|
757
759
|
// One of the refreshed value's tags was invalidated after
|
|
758
760
|
// the refresh started: written now, a value that may
|
|
@@ -972,6 +974,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
972
974
|
// promise in the result or a handle value, as an error row and
|
|
973
975
|
// completes normally; stored, every hit would serve that error.
|
|
974
976
|
if (flightErrors.length > 0) throw flightErrors[0];
|
|
977
|
+
// The encode awaited the result's nested promises: an identity read a
|
|
978
|
+
// loader value made after it settled is recorded now (#1011).
|
|
979
|
+
assertLoaderReadsClean(execScope);
|
|
975
980
|
} catch (buildError) {
|
|
976
981
|
// Serialize/handle-encode failed: no envelope for followers (they run
|
|
977
982
|
// fresh) and nothing to write.
|