@rangojs/router 0.0.0-experimental.144 → 0.0.0-experimental.146
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/bin/rango.js +1 -40
- package/dist/vite/index.js +35 -9
- package/package.json +1 -1
- package/skills/ppr/SKILL.md +29 -23
- package/src/browser/logging.ts +18 -0
- package/src/browser/rsc-router.tsx +43 -0
- package/src/cache/cache-runtime.ts +41 -51
- package/src/cache/cache-scope.ts +30 -1
- package/src/cache/cf/cf-cache-store.ts +4 -0
- package/src/cache/handle-snapshot.ts +22 -1
- package/src/cache/shell-snapshot.ts +47 -0
- package/src/cache/types.ts +31 -4
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/deps/ssr.ts +4 -1
- package/src/router/loader-resolution.ts +16 -0
- package/src/router/match-api.ts +9 -2
- package/src/router/match-handlers.ts +13 -0
- package/src/router/segment-resolution/loader-cache.ts +19 -3
- package/src/router/segment-resolution/loader-mask.ts +4 -11
- package/src/router/segment-resolution/loader-snapshot.ts +14 -6
- package/src/router/segment-resolution/mask-nested.ts +99 -0
- package/src/rsc/rsc-rendering.ts +139 -16
- package/src/rsc/shell-capture.ts +122 -0
- package/src/rsc/shell-serve.ts +37 -6
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +123 -9
- package/src/server/request-context.ts +47 -0
- package/src/ssr/index.tsx +118 -18
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/preinit-client-references.ts +106 -0
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +33 -0
- package/src/vite/plugins/virtual-entries.ts +37 -4
- package/src/vite/rango.ts +10 -2
- package/src/vite/utils/shared-utils.ts +4 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { preinitModule } from "react-dom";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JS/CSS asset deps plugin-rsc resolves for a client reference. Structural
|
|
6
|
+
* mirror of @vitejs/plugin-rsc's ResolvedAssetDeps — deliberately not imported:
|
|
7
|
+
* @rangojs/router/ssr never imports plugin-rsc directly; every plugin binding
|
|
8
|
+
* is injected by the virtual SSR entry (see SSRDependencies).
|
|
9
|
+
*/
|
|
10
|
+
export interface ClientReferenceDeps {
|
|
11
|
+
js: string[];
|
|
12
|
+
css: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Callback shape of @vitejs/plugin-rsc/ssr's setOnClientReference. Fired
|
|
17
|
+
* (synchronously, inside the Fizz render) whenever a client reference module
|
|
18
|
+
* is accessed during SSR — the same moment plugin-rsc issues its own
|
|
19
|
+
* ReactDOM.preloadModule calls for the reference's chunks.
|
|
20
|
+
*/
|
|
21
|
+
export type OnClientReference = (reference: {
|
|
22
|
+
id: string;
|
|
23
|
+
deps: ClientReferenceDeps;
|
|
24
|
+
}) => void;
|
|
25
|
+
|
|
26
|
+
/** setOnClientReference from @vitejs/plugin-rsc/ssr (injected). */
|
|
27
|
+
export type SetOnClientReference = (
|
|
28
|
+
callback: OnClientReference | undefined,
|
|
29
|
+
) => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-request CSP nonce channel for the preinit hook. The hook is installed
|
|
33
|
+
* once per isolate while the nonce is per request, and Fizz interleaves
|
|
34
|
+
* concurrent renders at task granularity — a module-scoped variable would race
|
|
35
|
+
* across requests and stamp request A's scripts with request B's nonce. ALS is
|
|
36
|
+
* the only channel that survives from the render call into the client-reference
|
|
37
|
+
* proxy access. A lost context degrades to nonce-less preinit (CSP blocks the
|
|
38
|
+
* head script; hydration still works through the nonce'd bootstrap), never to a
|
|
39
|
+
* wrong nonce.
|
|
40
|
+
*/
|
|
41
|
+
const preinitNonceStorage = new AsyncLocalStorage<string | undefined>();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run a Fizz render (renderToReadableStream / prerender / resume) with the
|
|
45
|
+
* request's nonce visible to the client-reference preinit hook. Nonce-less
|
|
46
|
+
* requests (the common non-CSP configuration) skip the ALS frame entirely —
|
|
47
|
+
* getStore() on an unentered storage already returns undefined, so the hook
|
|
48
|
+
* reads the same value either way.
|
|
49
|
+
*/
|
|
50
|
+
export function runWithPreinitNonce<T>(
|
|
51
|
+
nonce: string | undefined,
|
|
52
|
+
fn: () => T,
|
|
53
|
+
): T {
|
|
54
|
+
return nonce === undefined ? fn() : preinitNonceStorage.run(nonce, fn);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Upgrade plugin-rsc's client-reference modulepreload hints to executing
|
|
59
|
+
* scripts: for every JS chunk a client reference needs, emit
|
|
60
|
+
* `<script type="module" src async>` hoisted into the document head instead of
|
|
61
|
+
* only `<link rel="modulepreload">`.
|
|
62
|
+
*
|
|
63
|
+
* Why: modulepreload fetches + compiles but never executes; the chunks then
|
|
64
|
+
* execute only when the entry's hydration import walks the graph — after the
|
|
65
|
+
* whole document has streamed. preinitModule starts execution as soon as each
|
|
66
|
+
* chunk arrives, overlapping it with body streaming (the pattern Next.js uses
|
|
67
|
+
* via ReactDOM.preinit for all non-bootstrap chunks). Under PPR the preinits
|
|
68
|
+
* run during shell capture, so the executing tags live in the stored prelude
|
|
69
|
+
* and chunk execution starts on the first flushed bytes.
|
|
70
|
+
*
|
|
71
|
+
* No duplicate tags: plugin-rsc's preloadModule fires first in the same
|
|
72
|
+
* synchronous block; Fizz's preinitModuleScript then clears the queued preload
|
|
73
|
+
* chunks for that URL and adopts its credentials (ReactFizzConfigDOM,
|
|
74
|
+
* renderState.preloads.moduleScripts). Capture→resume double-emission is
|
|
75
|
+
* prevented the same way: the `moduleScriptResources[src] = null` markers
|
|
76
|
+
* serialize inside the postponed state, so the resume pass preinits only
|
|
77
|
+
* references the shell never saw.
|
|
78
|
+
*
|
|
79
|
+
* crossOrigin "" matches plugin-rsc's preloadModule creds so the upgrade path
|
|
80
|
+
* reuses the same resource instead of forking on credential mismatch.
|
|
81
|
+
*
|
|
82
|
+
* Known trades (deliberate, measured neutral-to-positive on the e2e apps —
|
|
83
|
+
* PR #694 has the Lighthouse/hydration numbers):
|
|
84
|
+
* - Fetch priority: an executing async module script fetches at Chromium's
|
|
85
|
+
* async-script priority, below a bare modulepreload hint; preinitModule
|
|
86
|
+
* forwards no fetchPriority (react-dom's public API drops it — only
|
|
87
|
+
* `preinit` forwards it). Execution-overlap is bought with hint priority,
|
|
88
|
+
* the same trade Next.js ships via ReactDOM.preinit.
|
|
89
|
+
* - Build only: plugin-rsc's dev load path reports `js: []` per reference, so
|
|
90
|
+
* dev documents have no head chunk scripts — a client module whose module
|
|
91
|
+
* scope assumes body-parsed DOM can break in production only. The
|
|
92
|
+
* `rango({ headScripts: "preload" })` escape hatch restores hint-only.
|
|
93
|
+
* - plugin-rsc's setOnClientReference is a single-slot, last-write-wins
|
|
94
|
+
* setter: another registrant in the SSR environment silently replaces this
|
|
95
|
+
* hook (or is replaced by it). No composition API exists upstream yet.
|
|
96
|
+
*/
|
|
97
|
+
export function installClientReferencePreinit(
|
|
98
|
+
setOnClientReference: SetOnClientReference,
|
|
99
|
+
): void {
|
|
100
|
+
setOnClientReference(({ deps }) => {
|
|
101
|
+
const nonce = preinitNonceStorage.getStore();
|
|
102
|
+
for (const href of deps.js) {
|
|
103
|
+
preinitModule(href, { as: "script", crossOrigin: "", nonce });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
package/src/vite/index.ts
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
export { rango } from "./rango.js";
|
|
10
10
|
export { poke } from "./plugins/refresh-cmd.js";
|
|
11
|
+
// The built-in clientChunks strategy, exported so a custom `clientChunks`
|
|
12
|
+
// function can OVERLAY it (route a few modules to a dedicated chunk, delegate
|
|
13
|
+
// the rest) instead of replacing the whole route/marker grouping. Without this
|
|
14
|
+
// a consumer override silently loses app-fallback/route splitting for the
|
|
15
|
+
// entire app. Note: called without a ClientChunkContext the fallbackRefs-based
|
|
16
|
+
// `app-fallback` split is inactive — discovery wires it only for the built-in.
|
|
17
|
+
export { directoryClientChunks } from "./utils/client-chunks.js";
|
|
11
18
|
|
|
12
19
|
export type {
|
|
13
20
|
RangoNodeOptions,
|
|
@@ -17,6 +24,7 @@ export type {
|
|
|
17
24
|
RangoOptions,
|
|
18
25
|
ClientChunks,
|
|
19
26
|
ClientChunkMeta,
|
|
27
|
+
HeadScriptsOption,
|
|
20
28
|
BuildEnvOption,
|
|
21
29
|
BuildEnvFactory,
|
|
22
30
|
BuildEnvFactoryContext,
|
package/src/vite/plugin-types.ts
CHANGED
|
@@ -107,6 +107,11 @@ export type ClientChunks =
|
|
|
107
107
|
|
|
108
108
|
// -- Plugin options ---------------------------------------------------------
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Document script strategy. See {@link RangoBaseOptions.headScripts}.
|
|
112
|
+
*/
|
|
113
|
+
export type HeadScriptsOption = "preinit" | "preload";
|
|
114
|
+
|
|
110
115
|
/**
|
|
111
116
|
* Base options shared by all presets
|
|
112
117
|
*/
|
|
@@ -126,6 +131,34 @@ interface RangoBaseOptions {
|
|
|
126
131
|
*/
|
|
127
132
|
clientChunks?: ClientChunks;
|
|
128
133
|
|
|
134
|
+
/**
|
|
135
|
+
* How the document ships its JavaScript.
|
|
136
|
+
*
|
|
137
|
+
* - `"preinit"` (**default**): client-reference chunks render as EXECUTING
|
|
138
|
+
* `<script type="module" async>` tags hoisted into `<head>` (upgrading
|
|
139
|
+
* plugin-rsc's modulepreload hints in place), and the browser entry ships
|
|
140
|
+
* as Fizz `bootstrapModules` — a head `modulepreload fetchpriority=low`
|
|
141
|
+
* hint plus the executing end-of-shell `id="_R_"` module script. Chunk
|
|
142
|
+
* execution overlaps body streaming instead of waiting for the hydration
|
|
143
|
+
* import walk; under PPR everything lands in the stored shell prelude.
|
|
144
|
+
* - `"preload"`: the previous behavior — `<link rel="modulepreload">` hints
|
|
145
|
+
* only, entry as an inline `import()` script at end of shell. Chunks
|
|
146
|
+
* fetch+compile early but execute only when hydration imports them.
|
|
147
|
+
*
|
|
148
|
+
* Build-only for the chunk half: plugin-rsc resolves no JS deps per client
|
|
149
|
+
* reference in dev, so dev documents carry no head chunk scripts in either
|
|
150
|
+
* mode (the bootstrap conversion does apply in dev). Trades and upstream
|
|
151
|
+
* limits are documented in src/ssr/preinit-client-references.ts.
|
|
152
|
+
*
|
|
153
|
+
* Wired in the generated virtual SSR entry
|
|
154
|
+
* (`src/ssr/preinit-client-references.ts` has the mechanism); apps with a
|
|
155
|
+
* custom SSR entry choose per-handler via `SSRDependencies.headScripts` and
|
|
156
|
+
* `installClientReferencePreinit`.
|
|
157
|
+
*
|
|
158
|
+
* @default "preinit"
|
|
159
|
+
*/
|
|
160
|
+
headScripts?: HeadScriptsOption;
|
|
161
|
+
|
|
129
162
|
/**
|
|
130
163
|
* Filter which files route discovery scans, by glob. Paths are matched
|
|
131
164
|
* root-relative (e.g. `src/routes/**`). `include` restricts discovery to
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { HeadScriptsOption } from "../plugin-types.js";
|
|
2
|
+
|
|
1
3
|
export const VIRTUAL_ENTRY_BROWSER: string = `
|
|
2
4
|
import {
|
|
3
5
|
createFromReadableStream,
|
|
@@ -36,21 +38,49 @@ async function initializeApp() {
|
|
|
36
38
|
initializeApp().catch(console.error);
|
|
37
39
|
`.trim();
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Generate the virtual SSR entry. `headScripts` mirrors the rango() plugin
|
|
43
|
+
* option: "preinit" (default) installs the client-reference preinit hook and
|
|
44
|
+
* lets the SSR handlers convert the bootstrap to `bootstrapModules`;
|
|
45
|
+
* "preload" omits the hook and pins the handlers to the hint-only strategy.
|
|
46
|
+
*/
|
|
47
|
+
export function getVirtualEntrySSR(
|
|
48
|
+
headScripts: HeadScriptsOption = "preinit",
|
|
49
|
+
): string {
|
|
50
|
+
const preinit = headScripts !== "preload";
|
|
51
|
+
// The preload variant drops exactly three preinit-only lines, all built
|
|
52
|
+
// here so the template below stays a single unconditional shape.
|
|
53
|
+
const depsImportNames = preinit
|
|
54
|
+
? "createFromReadableStream,\n setOnClientReference,"
|
|
55
|
+
: "createFromReadableStream,";
|
|
56
|
+
const ssrImportNames = preinit ? "\n installClientReferencePreinit," : "";
|
|
57
|
+
const install = preinit
|
|
58
|
+
? `
|
|
59
|
+
// Upgrade client-reference modulepreload hints to executing module scripts in
|
|
60
|
+
// the document head, for every render pass (live SSR, shell capture, resume).
|
|
61
|
+
// See src/ssr/preinit-client-references.ts for the full rationale.
|
|
62
|
+
installClientReferencePreinit(setOnClientReference);
|
|
63
|
+
`
|
|
64
|
+
: "";
|
|
65
|
+
const hs = JSON.stringify(headScripts);
|
|
66
|
+
return `
|
|
67
|
+
import {
|
|
68
|
+
${depsImportNames}
|
|
69
|
+
} from "@rangojs/router/internal/deps/ssr";
|
|
41
70
|
import { renderToReadableStream, resume } from "react-dom/server.edge";
|
|
42
71
|
import { prerender } from "react-dom/static.edge";
|
|
43
72
|
import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
44
73
|
import {
|
|
45
74
|
createSSRHandler,
|
|
46
75
|
createShellCaptureHandler,
|
|
47
|
-
createShellResumeHandler
|
|
76
|
+
createShellResumeHandler,${ssrImportNames}
|
|
48
77
|
} from "@rangojs/router/ssr";
|
|
49
|
-
|
|
78
|
+
${install}
|
|
50
79
|
export const renderHTML = createSSRHandler({
|
|
51
80
|
createFromReadableStream,
|
|
52
81
|
renderToReadableStream,
|
|
53
82
|
injectRSCPayload,
|
|
83
|
+
headScripts: ${hs},
|
|
54
84
|
loadBootstrapScriptContent: () =>
|
|
55
85
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
56
86
|
});
|
|
@@ -61,6 +91,7 @@ export const captureShellHTML = createShellCaptureHandler({
|
|
|
61
91
|
injectRSCPayload,
|
|
62
92
|
prerender,
|
|
63
93
|
resume,
|
|
94
|
+
headScripts: ${hs},
|
|
64
95
|
loadBootstrapScriptContent: () =>
|
|
65
96
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
66
97
|
});
|
|
@@ -71,10 +102,12 @@ export const resumeShellHTML = createShellResumeHandler({
|
|
|
71
102
|
injectRSCPayload,
|
|
72
103
|
prerender,
|
|
73
104
|
resume,
|
|
105
|
+
headScripts: ${hs},
|
|
74
106
|
loadBootstrapScriptContent: () =>
|
|
75
107
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
76
108
|
});
|
|
77
109
|
`.trim();
|
|
110
|
+
}
|
|
78
111
|
|
|
79
112
|
/**
|
|
80
113
|
* Virtual modules an RSC entry must import at startup to register the data the
|
package/src/vite/rango.ts
CHANGED
|
@@ -239,7 +239,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
239
239
|
},
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
plugins.push(
|
|
242
|
+
plugins.push(
|
|
243
|
+
createVirtualEntriesPlugin(finalEntries, undefined, {
|
|
244
|
+
headScripts: resolvedOptions.headScripts,
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
243
247
|
plugins.push(performanceTracksPlugin());
|
|
244
248
|
plugins.push(
|
|
245
249
|
rsc({
|
|
@@ -476,7 +480,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
476
480
|
},
|
|
477
481
|
});
|
|
478
482
|
|
|
479
|
-
plugins.push(
|
|
483
|
+
plugins.push(
|
|
484
|
+
createVirtualEntriesPlugin(finalEntries, routerRef, {
|
|
485
|
+
headScripts: resolvedOptions.headScripts,
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
480
488
|
plugins.push(performanceTracksPlugin());
|
|
481
489
|
plugins.push(
|
|
482
490
|
rsc({
|
|
@@ -5,12 +5,13 @@ import { getPublishedPackageName } from "./package-resolution.js";
|
|
|
5
5
|
import { performanceTracksOptimizeDepsPlugin } from "../plugins/performance-tracks.js";
|
|
6
6
|
import {
|
|
7
7
|
VIRTUAL_ENTRY_BROWSER,
|
|
8
|
-
|
|
8
|
+
getVirtualEntrySSR,
|
|
9
9
|
getVirtualEntryRSC,
|
|
10
10
|
getVirtualEntryRSCHost,
|
|
11
11
|
getVirtualVersionContent,
|
|
12
12
|
VIRTUAL_IDS,
|
|
13
13
|
} from "../plugins/virtual-entries.js";
|
|
14
|
+
import type { HeadScriptsOption } from "../plugin-types.js";
|
|
14
15
|
|
|
15
16
|
// Cloudflare preset: @cloudflare/vite-plugin sets optimizeDeps.entries (string
|
|
16
17
|
// or array) on the rsc environment. Single source for both the discovery plugin
|
|
@@ -109,6 +110,7 @@ export function normalizeHostRouterEntry(
|
|
|
109
110
|
export function createVirtualEntriesPlugin(
|
|
110
111
|
entries: { client: string; ssr: string; rsc?: string },
|
|
111
112
|
routerPathRef?: { path?: string; kind?: "router" | "host" },
|
|
113
|
+
options?: { headScripts?: HeadScriptsOption },
|
|
112
114
|
): Plugin {
|
|
113
115
|
// Build virtual modules map based on which entries use virtual IDs
|
|
114
116
|
const virtualModules: Record<string, string> = {};
|
|
@@ -117,7 +119,7 @@ export function createVirtualEntriesPlugin(
|
|
|
117
119
|
virtualModules[VIRTUAL_IDS.browser] = VIRTUAL_ENTRY_BROWSER;
|
|
118
120
|
}
|
|
119
121
|
if (entries.ssr === VIRTUAL_IDS.ssr) {
|
|
120
|
-
virtualModules[VIRTUAL_IDS.ssr] =
|
|
122
|
+
virtualModules[VIRTUAL_IDS.ssr] = getVirtualEntrySSR(options?.headScripts);
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
// RSC entry is resolved lazily in load() because routerPath may be
|