@rangojs/router 0.11.0 → 0.12.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/testing/vitest.js +1 -1
- package/dist/types/deps/rsc-client.d.ts +1 -0
- package/dist/types/deps/rsc.d.ts +1 -1
- package/dist/types/deps/ssr.d.ts +1 -1
- package/dist/types/rsc/types.d.ts +9 -9
- package/dist/types/ssr/index.d.ts +27 -4
- package/dist/types/testing/flight.d.ts +4 -3
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +2 -0
- package/dist/types/testing/vitest.d.ts +2 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +3 -2
- package/dist/vite/index.js +53 -12
- package/package.json +8 -3
- package/skills/testing/setup.md +6 -6
- package/src/cache/cache-runtime.ts +1 -1
- package/src/cache/segment-codec.ts +2 -2
- package/src/deps/rsc-client.ts +8 -0
- package/src/deps/rsc.ts +4 -2
- package/src/deps/ssr.ts +1 -0
- package/src/rsc/handler.ts +3 -3
- package/src/rsc/types.ts +9 -9
- package/src/ssr/index.tsx +132 -51
- package/src/testing/flight.ts +4 -3
- package/src/testing/vitest-stubs/plugin-rsc.ts +13 -5
- package/src/testing/vitest.ts +3 -2
- package/src/vite/plugins/use-cache-transform.ts +65 -1
- package/src/vite/plugins/virtual-entries.ts +14 -11
package/dist/testing/vitest.js
CHANGED
|
@@ -14,7 +14,7 @@ function rangoTestAliases(opts = {}) {
|
|
|
14
14
|
replacement: here("src/testing/vitest-stubs/version.ts")
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
find: /^@vitejs\/plugin-rsc\/rsc
|
|
17
|
+
find: /^@vitejs\/plugin-rsc\/rsc(\/(server|client))?$/,
|
|
18
18
|
replacement: here("src/testing/vitest-stubs/plugin-rsc.ts")
|
|
19
19
|
}
|
|
20
20
|
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createFromReadableStream, encodeReply, createClientTemporaryReferenceSet, } from "@vitejs/plugin-rsc/rsc/client";
|
package/dist/types/deps/rsc.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { renderToReadableStream, decodeReply, createTemporaryReferenceSet, loadServerAction, decodeAction, decodeFormState, } from "@vitejs/plugin-rsc/rsc";
|
|
1
|
+
export { renderToReadableStream, decodeReply, createTemporaryReferenceSet, loadServerAction, decodeAction, decodeFormState, } from "@vitejs/plugin-rsc/rsc/server";
|
package/dist/types/deps/ssr.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createFromReadableStream, setOnClientReference, } from "@vitejs/plugin-rsc/ssr";
|
|
1
|
+
export { createFromReadableStream, setOnClientReference, getClientEntryUrl, } from "@vitejs/plugin-rsc/ssr";
|
|
@@ -107,37 +107,37 @@ export interface RscPayload {
|
|
|
107
107
|
*/
|
|
108
108
|
export type ReactFormState = unknown;
|
|
109
109
|
/**
|
|
110
|
-
* RSC dependencies from @vitejs/plugin-rsc/rsc
|
|
110
|
+
* RSC dependencies from @vitejs/plugin-rsc/rsc/server
|
|
111
111
|
*/
|
|
112
112
|
export interface RSCDependencies {
|
|
113
113
|
/**
|
|
114
|
-
* renderToReadableStream from @vitejs/plugin-rsc/rsc
|
|
114
|
+
* renderToReadableStream from @vitejs/plugin-rsc/rsc/server
|
|
115
115
|
*/
|
|
116
116
|
renderToReadableStream: <T>(payload: T, options?: {
|
|
117
117
|
temporaryReferences?: unknown;
|
|
118
118
|
onError?: (error: unknown) => string | void;
|
|
119
119
|
}) => ReadableStream<Uint8Array>;
|
|
120
120
|
/**
|
|
121
|
-
* decodeReply from @vitejs/plugin-rsc/rsc
|
|
121
|
+
* decodeReply from @vitejs/plugin-rsc/rsc/server
|
|
122
122
|
*/
|
|
123
123
|
decodeReply: (body: FormData | string, options?: {
|
|
124
124
|
temporaryReferences?: unknown;
|
|
125
125
|
}) => Promise<unknown[]>;
|
|
126
126
|
/**
|
|
127
|
-
* createTemporaryReferenceSet from @vitejs/plugin-rsc/rsc
|
|
127
|
+
* createTemporaryReferenceSet from @vitejs/plugin-rsc/rsc/server
|
|
128
128
|
*/
|
|
129
129
|
createTemporaryReferenceSet: () => unknown;
|
|
130
130
|
/**
|
|
131
|
-
* loadServerAction from @vitejs/plugin-rsc/rsc
|
|
131
|
+
* loadServerAction from @vitejs/plugin-rsc/rsc/server
|
|
132
132
|
*/
|
|
133
133
|
loadServerAction: (actionId: string) => Promise<Function>;
|
|
134
134
|
/**
|
|
135
|
-
* decodeAction from @vitejs/plugin-rsc/rsc
|
|
135
|
+
* decodeAction from @vitejs/plugin-rsc/rsc/server
|
|
136
136
|
* Decodes a FormData into a bound action function (for useActionState forms)
|
|
137
137
|
*/
|
|
138
138
|
decodeAction: (body: FormData) => Promise<() => Promise<unknown>>;
|
|
139
139
|
/**
|
|
140
|
-
* decodeFormState from @vitejs/plugin-rsc/rsc
|
|
140
|
+
* decodeFormState from @vitejs/plugin-rsc/rsc/server
|
|
141
141
|
* Decodes the action result into a ReactFormState for useActionState progressive enhancement
|
|
142
142
|
*/
|
|
143
143
|
decodeFormState: (actionResult: unknown, body: FormData) => Promise<ReactFormState | null>;
|
|
@@ -268,8 +268,8 @@ export interface CreateRSCHandlerOptions<TEnv = unknown, TRoutes extends Record<
|
|
|
268
268
|
*/
|
|
269
269
|
router: RangoInternal<TEnv, TRoutes>;
|
|
270
270
|
/**
|
|
271
|
-
* RSC dependencies from @vitejs/plugin-rsc/rsc.
|
|
272
|
-
* Defaults to the exports from @vitejs/plugin-rsc/rsc.
|
|
271
|
+
* RSC dependencies from @vitejs/plugin-rsc/rsc/server.
|
|
272
|
+
* Defaults to the exports from @vitejs/plugin-rsc/rsc/server.
|
|
273
273
|
*/
|
|
274
274
|
deps?: RSCDependencies;
|
|
275
275
|
/**
|
|
@@ -117,10 +117,19 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
117
117
|
*/
|
|
118
118
|
injectRSCPayload: (rscStream: ReadableStream<Uint8Array>, options?: InjectRSCPayloadOptions) => TransformStream<Uint8Array, Uint8Array>;
|
|
119
119
|
/**
|
|
120
|
-
* Function to load bootstrap script content
|
|
121
|
-
*
|
|
120
|
+
* Function to load bootstrap script content.
|
|
121
|
+
* Required unless `getClientEntryUrl` is provided with `headScripts: "preinit"`.
|
|
122
|
+
* Custom SSR entries typically: `() => import.meta.viteRsc.loadBootstrapScriptContent("index")`
|
|
123
|
+
* (deprecated in `@vitejs/plugin-rsc` 0.5.33 in favor of `getClientEntryUrl`).
|
|
122
124
|
*/
|
|
123
|
-
loadBootstrapScriptContent
|
|
125
|
+
loadBootstrapScriptContent?: () => Promise<string>;
|
|
126
|
+
/**
|
|
127
|
+
* Client entry URL from `@vitejs/plugin-rsc/ssr` `getClientEntryUrl()`.
|
|
128
|
+
* Preferred when `headScripts` is `"preinit"`: Fizz receives `bootstrapModules`
|
|
129
|
+
* without the deprecated `loadBootstrapScriptContent` round-trip. Custom SSR
|
|
130
|
+
* entries can omit this and keep the inline bootstrap path.
|
|
131
|
+
*/
|
|
132
|
+
getClientEntryUrl?: () => string;
|
|
124
133
|
/**
|
|
125
134
|
* Document script strategy; the generated virtual SSR entry threads the
|
|
126
135
|
* `rango({ headScripts })` plugin option here (canonical docs on
|
|
@@ -251,7 +260,10 @@ interface ShellResumeOptions {
|
|
|
251
260
|
* @example
|
|
252
261
|
* ```tsx
|
|
253
262
|
* import { createSSRHandler } from "@rangojs/router/ssr";
|
|
254
|
-
* import {
|
|
263
|
+
* import {
|
|
264
|
+
* createFromReadableStream,
|
|
265
|
+
* getClientEntryUrl,
|
|
266
|
+
* } from "@rangojs/router/internal/deps/ssr";
|
|
255
267
|
* import { renderToReadableStream } from "react-dom/server.edge";
|
|
256
268
|
* import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
257
269
|
*
|
|
@@ -259,6 +271,17 @@ interface ShellResumeOptions {
|
|
|
259
271
|
* createFromReadableStream,
|
|
260
272
|
* renderToReadableStream,
|
|
261
273
|
* injectRSCPayload,
|
|
274
|
+
* getClientEntryUrl,
|
|
275
|
+
* headScripts: "preinit", // getClientEntryUrl is only used under "preinit"
|
|
276
|
+
* });
|
|
277
|
+
* ```
|
|
278
|
+
*
|
|
279
|
+
* Custom SSR entries that still use the deprecated bootstrap helper:
|
|
280
|
+
* ```tsx
|
|
281
|
+
* export const renderHTML = createSSRHandler({
|
|
282
|
+
* createFromReadableStream,
|
|
283
|
+
* renderToReadableStream,
|
|
284
|
+
* injectRSCPayload,
|
|
262
285
|
* loadBootstrapScriptContent: () =>
|
|
263
286
|
* import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
264
287
|
* });
|
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
* the same react-server-dom serializer the router uses at runtime. It runs in
|
|
7
7
|
* plain node (no Vite, no browser), but ONLY under the `react-server` export
|
|
8
8
|
* condition. The serializer is the VENDORED build shipped with
|
|
9
|
-
* @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc` entry
|
|
10
|
-
* imports Vite virtual modules and is not usable outside a Vite
|
|
9
|
+
* @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc/server` entry
|
|
10
|
+
* top-level imports Vite virtual modules and is not usable outside a Vite
|
|
11
|
+
* build.
|
|
11
12
|
*
|
|
12
13
|
* Run the example/tests for this module via the dedicated rsc vitest project
|
|
13
14
|
* (vitest.rsc.config.ts), which forces `--conditions=react-server` on the
|
|
14
15
|
* worker. The main vitest project must NOT use that condition (it would flip
|
|
15
16
|
* React to the no-hooks server build and break the ~50 tests that mock
|
|
16
|
-
* @vitejs/plugin-rsc/rsc).
|
|
17
|
+
* @vitejs/plugin-rsc/rsc/server).
|
|
17
18
|
*
|
|
18
19
|
* Scope / limitations (v1):
|
|
19
20
|
* - Server-only / leaf trees. A tree containing a CLIENT component emits an
|
|
@@ -5,3 +5,5 @@ export declare const decodeReply: () => undefined;
|
|
|
5
5
|
export declare const decodeAction: () => undefined;
|
|
6
6
|
export declare const decodeFormState: () => undefined;
|
|
7
7
|
export declare const createTemporaryReferenceSet: () => Record<string, never>;
|
|
8
|
+
export declare const encodeReply: () => never;
|
|
9
|
+
export declare const createClientTemporaryReferenceSet: () => Record<string, never>;
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* `@rangojs/router` specifier to its react-server entry (real impls) while
|
|
19
19
|
* leaving React as the client build — which is exactly what this helper does.
|
|
20
20
|
* - The build-only `@rangojs/router:version` virtual and `@vitejs/plugin-rsc/rsc`
|
|
21
|
-
* (whose real body imports unresolvable
|
|
21
|
+
* plus `/rsc/server`, `/rsc/client` (whose real body imports unresolvable
|
|
22
|
+
* Vite virtuals) are stubbed.
|
|
22
23
|
* - Cloudflare apps additionally import the `cloudflare:workers` /
|
|
23
24
|
* `cloudflare:email` runtime virtuals; pass `{ preset: "cloudflare" }` to stub them.
|
|
24
25
|
*
|
|
@@ -3,8 +3,9 @@ export declare const VIRTUAL_ENTRY_BROWSER: string;
|
|
|
3
3
|
/**
|
|
4
4
|
* Generate the virtual SSR entry. `headScripts` mirrors the rango() plugin
|
|
5
5
|
* option: "preinit" (default) installs the client-reference preinit hook and
|
|
6
|
-
*
|
|
7
|
-
* "preload" omits the hook and
|
|
6
|
+
* threads `getClientEntryUrl` so Fizz emits `bootstrapModules`;
|
|
7
|
+
* "preload" omits the hook and uses the deprecated inline
|
|
8
|
+
* `loadBootstrapScriptContent` bootstrap.
|
|
8
9
|
*/
|
|
9
10
|
export declare function getVirtualEntrySSR(headScripts?: HeadScriptsOption, progressiveChunkSize?: number): string;
|
|
10
11
|
/**
|
package/dist/vite/index.js
CHANGED
|
@@ -3048,6 +3048,7 @@ function useCacheTransform() {
|
|
|
3048
3048
|
} catch {
|
|
3049
3049
|
return;
|
|
3050
3050
|
}
|
|
3051
|
+
stripNullDirectiveFields(ast);
|
|
3051
3052
|
const filePath = normalizePath(path5.relative(projectRoot, id));
|
|
3052
3053
|
const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
|
|
3053
3054
|
if (hasDirective(ast.body, "use cache")) {
|
|
@@ -3058,7 +3059,8 @@ function useCacheTransform() {
|
|
|
3058
3059
|
id,
|
|
3059
3060
|
isBuild,
|
|
3060
3061
|
isLayoutOrTemplate,
|
|
3061
|
-
transformWrapExport
|
|
3062
|
+
transformWrapExport,
|
|
3063
|
+
hasDirective
|
|
3062
3064
|
);
|
|
3063
3065
|
}
|
|
3064
3066
|
const functionResult = transformFunctionLevelUseCache(
|
|
@@ -3077,7 +3079,7 @@ function useCacheTransform() {
|
|
|
3077
3079
|
}
|
|
3078
3080
|
};
|
|
3079
3081
|
}
|
|
3080
|
-
function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild, isLayoutOrTemplate, transformWrapExport) {
|
|
3082
|
+
function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild, isLayoutOrTemplate, transformWrapExport, hasDirective) {
|
|
3081
3083
|
const unconfirmedExports = [];
|
|
3082
3084
|
const { exportNames, output } = transformWrapExport(code, ast, {
|
|
3083
3085
|
runtime: (value, name) => {
|
|
@@ -3087,6 +3089,11 @@ function transformFileLevelUseCache(code, ast, filePath, sourceId, isBuild, isLa
|
|
|
3087
3089
|
rejectNonAsyncFunction: false,
|
|
3088
3090
|
filter: (name, meta) => {
|
|
3089
3091
|
if (name === "default" && isLayoutOrTemplate) return false;
|
|
3092
|
+
if (name.startsWith("$$hoist_")) return false;
|
|
3093
|
+
if (isHoistedServerReferenceRebind(meta.valueNode)) return false;
|
|
3094
|
+
if (functionHasUseServerDirective(meta.valueNode, hasDirective)) {
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3090
3097
|
if (meta.isFunction !== true) {
|
|
3091
3098
|
unconfirmedExports.push(name);
|
|
3092
3099
|
return false;
|
|
@@ -3158,6 +3165,36 @@ function transformFunctionLevelUseCache(code, ast, filePath, sourceId, isBuild,
|
|
|
3158
3165
|
return;
|
|
3159
3166
|
}
|
|
3160
3167
|
}
|
|
3168
|
+
function stripNullDirectiveFields(node) {
|
|
3169
|
+
if (!node || typeof node !== "object") return;
|
|
3170
|
+
const rec = node;
|
|
3171
|
+
if (rec.type === "ExpressionStatement" && typeof rec.directive !== "string") {
|
|
3172
|
+
delete rec.directive;
|
|
3173
|
+
}
|
|
3174
|
+
for (const value of Object.values(rec)) {
|
|
3175
|
+
if (Array.isArray(value)) {
|
|
3176
|
+
for (const item of value) stripNullDirectiveFields(item);
|
|
3177
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
3178
|
+
stripNullDirectiveFields(value);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
function isHoistedServerReferenceRebind(valueNode) {
|
|
3183
|
+
if (!valueNode || valueNode.type !== "CallExpression") return false;
|
|
3184
|
+
const first = valueNode.arguments[0];
|
|
3185
|
+
return first !== void 0 && first.type === "Identifier" && first.name.startsWith("$$hoist_");
|
|
3186
|
+
}
|
|
3187
|
+
function functionHasUseServerDirective(valueNode, hasDirective) {
|
|
3188
|
+
if (!valueNode || !("body" in valueNode)) return false;
|
|
3189
|
+
const { body } = valueNode;
|
|
3190
|
+
if (!body || Array.isArray(body) || body.type !== "BlockStatement") {
|
|
3191
|
+
return false;
|
|
3192
|
+
}
|
|
3193
|
+
return hasDirective(
|
|
3194
|
+
body.body,
|
|
3195
|
+
"use server"
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3161
3198
|
function findFileLevelDirective(ast) {
|
|
3162
3199
|
for (const node of ast.body ?? []) {
|
|
3163
3200
|
if (node.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string" && node.expression.value.startsWith("use cache")) {
|
|
@@ -3501,7 +3538,7 @@ function emitProgressiveChunkSize(value) {
|
|
|
3501
3538
|
}
|
|
3502
3539
|
function getVirtualEntrySSR(headScripts = "preinit", progressiveChunkSize) {
|
|
3503
3540
|
const preinit = headScripts !== "preload";
|
|
3504
|
-
const depsImportNames = preinit ? "createFromReadableStream,\n setOnClientReference," : "createFromReadableStream,";
|
|
3541
|
+
const depsImportNames = preinit ? "createFromReadableStream,\n setOnClientReference,\n getClientEntryUrl," : "createFromReadableStream,";
|
|
3505
3542
|
const ssrImportNames = preinit ? "\n installClientReferencePreinit," : "";
|
|
3506
3543
|
const install = preinit ? `
|
|
3507
3544
|
// Upgrade client-reference modulepreload hints to executing module scripts in
|
|
@@ -3509,6 +3546,8 @@ function getVirtualEntrySSR(headScripts = "preinit", progressiveChunkSize) {
|
|
|
3509
3546
|
// See src/ssr/preinit-client-references.ts for the full rationale.
|
|
3510
3547
|
installClientReferencePreinit(setOnClientReference);
|
|
3511
3548
|
` : "";
|
|
3549
|
+
const bootstrapDep = preinit ? "getClientEntryUrl," : `loadBootstrapScriptContent: () =>
|
|
3550
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),`;
|
|
3512
3551
|
const hs = JSON.stringify(headScripts);
|
|
3513
3552
|
const pcs = progressiveChunkSize !== void 0 ? `
|
|
3514
3553
|
progressiveChunkSize: ${emitProgressiveChunkSize(progressiveChunkSize)},` : "";
|
|
@@ -3530,8 +3569,7 @@ export const renderHTML = createSSRHandler({
|
|
|
3530
3569
|
renderToReadableStream,
|
|
3531
3570
|
injectRSCPayload,
|
|
3532
3571
|
headScripts: ${hs},${pcs}
|
|
3533
|
-
|
|
3534
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
3572
|
+
${bootstrapDep}
|
|
3535
3573
|
});
|
|
3536
3574
|
|
|
3537
3575
|
export const captureShellHTML = createShellCaptureHandler({
|
|
@@ -3541,8 +3579,7 @@ export const captureShellHTML = createShellCaptureHandler({
|
|
|
3541
3579
|
prerender,
|
|
3542
3580
|
resume,
|
|
3543
3581
|
headScripts: ${hs},${pcs}
|
|
3544
|
-
|
|
3545
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
3582
|
+
${bootstrapDep}
|
|
3546
3583
|
});
|
|
3547
3584
|
|
|
3548
3585
|
export const resumeShellHTML = createShellResumeHandler({
|
|
@@ -3552,8 +3589,7 @@ export const resumeShellHTML = createShellResumeHandler({
|
|
|
3552
3589
|
prerender,
|
|
3553
3590
|
resume,
|
|
3554
3591
|
headScripts: ${hs},${pcs}
|
|
3555
|
-
|
|
3556
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
3592
|
+
${bootstrapDep}
|
|
3557
3593
|
});
|
|
3558
3594
|
`.trim();
|
|
3559
3595
|
}
|
|
@@ -3710,7 +3746,7 @@ import { resolve } from "node:path";
|
|
|
3710
3746
|
// package.json
|
|
3711
3747
|
var package_default = {
|
|
3712
3748
|
name: "@rangojs/router",
|
|
3713
|
-
version: "0.
|
|
3749
|
+
version: "0.12.0",
|
|
3714
3750
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
3715
3751
|
keywords: [
|
|
3716
3752
|
"react",
|
|
@@ -3798,6 +3834,11 @@ var package_default = {
|
|
|
3798
3834
|
"react-server": "./src/deps/rsc.ts",
|
|
3799
3835
|
default: "./src/deps/rsc.ts"
|
|
3800
3836
|
},
|
|
3837
|
+
"./internal/deps/rsc-client": {
|
|
3838
|
+
types: "./dist/types/deps/rsc-client.d.ts",
|
|
3839
|
+
"react-server": "./src/deps/rsc-client.ts",
|
|
3840
|
+
default: "./src/deps/rsc-client.ts"
|
|
3841
|
+
},
|
|
3801
3842
|
"./internal/deps/html-stream-client": {
|
|
3802
3843
|
types: "./dist/types/deps/html-stream-client.d.ts",
|
|
3803
3844
|
default: "./src/deps/html-stream-client.ts"
|
|
@@ -3903,7 +3944,7 @@ var package_default = {
|
|
|
3903
3944
|
},
|
|
3904
3945
|
dependencies: {
|
|
3905
3946
|
"@types/debug": "^4.1.12",
|
|
3906
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
3947
|
+
"@vitejs/plugin-rsc": "^0.5.34",
|
|
3907
3948
|
debug: "^4.4.1",
|
|
3908
3949
|
"magic-string": "^0.30.17",
|
|
3909
3950
|
picomatch: "^4.0.4",
|
|
@@ -3936,7 +3977,7 @@ var package_default = {
|
|
|
3936
3977
|
"@playwright/test": "^1.49.1",
|
|
3937
3978
|
"@testing-library/react": ">=16",
|
|
3938
3979
|
"@vercel/functions": "^3.0.0",
|
|
3939
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
3980
|
+
"@vitejs/plugin-rsc": "^0.5.34",
|
|
3940
3981
|
react: ">=19.2.8 <20",
|
|
3941
3982
|
"react-dom": ">=19.2.8 <20",
|
|
3942
3983
|
vite: "^8.0.16",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -88,6 +88,11 @@
|
|
|
88
88
|
"react-server": "./src/deps/rsc.ts",
|
|
89
89
|
"default": "./src/deps/rsc.ts"
|
|
90
90
|
},
|
|
91
|
+
"./internal/deps/rsc-client": {
|
|
92
|
+
"types": "./dist/types/deps/rsc-client.d.ts",
|
|
93
|
+
"react-server": "./src/deps/rsc-client.ts",
|
|
94
|
+
"default": "./src/deps/rsc-client.ts"
|
|
95
|
+
},
|
|
91
96
|
"./internal/deps/html-stream-client": {
|
|
92
97
|
"types": "./dist/types/deps/html-stream-client.d.ts",
|
|
93
98
|
"default": "./src/deps/html-stream-client.ts"
|
|
@@ -179,7 +184,7 @@
|
|
|
179
184
|
},
|
|
180
185
|
"dependencies": {
|
|
181
186
|
"@types/debug": "^4.1.12",
|
|
182
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
187
|
+
"@vitejs/plugin-rsc": "^0.5.34",
|
|
183
188
|
"debug": "^4.4.1",
|
|
184
189
|
"magic-string": "^0.30.17",
|
|
185
190
|
"picomatch": "^4.0.4",
|
|
@@ -212,7 +217,7 @@
|
|
|
212
217
|
"@playwright/test": "^1.49.1",
|
|
213
218
|
"@testing-library/react": ">=16",
|
|
214
219
|
"@vercel/functions": "^3.0.0",
|
|
215
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
220
|
+
"@vitejs/plugin-rsc": "^0.5.34",
|
|
216
221
|
"react": ">=19.2.8 <20",
|
|
217
222
|
"react-dom": ">=19.2.8 <20",
|
|
218
223
|
"vite": "^8.0.16",
|
package/skills/testing/setup.md
CHANGED
|
@@ -14,11 +14,11 @@ Real machinery: Vite transpiles `@rangojs/router`'s shipped TS source and resolv
|
|
|
14
14
|
|
|
15
15
|
### Functions
|
|
16
16
|
|
|
17
|
-
| Function | Returns | Use
|
|
18
|
-
| --------------------------- | ----------------------------------------- |
|
|
19
|
-
| `rangoTestConfig(opts?)` | `{ alias, server: { deps: { inline } } }` | Recommended. Spread into the node/DOM project's `test` block. Bundles the resolve aliases AND `server.deps.inline`.
|
|
20
|
-
| `rangoTestAliases(opts?)` | `TestAlias[]` (`{ find, replacement }[]`) | Lower-level. The bare `@rangojs/router` -> `index.rsc.ts` alias plus the `:version` / `@vitejs/plugin-rsc/rsc` stubs (and CF stubs under `preset:"cloudflare"`). Used in the rsc project's `resolve.alias`. |
|
|
21
|
-
| `rangoUseClientTransform()` | a Vite plugin (`{ name, transform }`) | Add to the rsc project `plugins`. Applies the `"use client"` transform so `renderServerTree` auto-discovers client islands from the server tree's imports.
|
|
17
|
+
| Function | Returns | Use |
|
|
18
|
+
| --------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
19
|
+
| `rangoTestConfig(opts?)` | `{ alias, server: { deps: { inline } } }` | Recommended. Spread into the node/DOM project's `test` block. Bundles the resolve aliases AND `server.deps.inline`. |
|
|
20
|
+
| `rangoTestAliases(opts?)` | `TestAlias[]` (`{ find, replacement }[]`) | Lower-level. The bare `@rangojs/router` -> `index.rsc.ts` alias plus the `:version` / `@vitejs/plugin-rsc/rsc` (`/rsc/server`, `/rsc/client`) stubs (and CF stubs under `preset:"cloudflare"`). Used in the rsc project's `resolve.alias`. |
|
|
21
|
+
| `rangoUseClientTransform()` | a Vite plugin (`{ name, transform }`) | Add to the rsc project `plugins`. Applies the `"use client"` transform so `renderServerTree` auto-discovers client islands from the server tree's imports. |
|
|
22
22
|
|
|
23
23
|
### Returns — `RangoTestConfig` (from `rangoTestConfig`)
|
|
24
24
|
|
|
@@ -112,7 +112,7 @@ Scripts:
|
|
|
112
112
|
- The rsc project needs BOTH `resolve.conditions: ["react-server"]` AND the bare `@rangojs/router` -> `index.rsc.ts` alias from `rangoTestAliases({ preset })`. `resolve.conditions` alone is not reliably applied to bare-package export resolution; without the alias a handler/component reading `getRequestContext()` / `cookies()` resolves the throwing out-of-react-server stub (symptom: `renderHandler` returns `tree: undefined`). `renderToFlightString` / `renderServerTree` now self-diagnose this exact misconfiguration — they reject with an actionable message naming `rangoTestAliases`, rather than surfacing the opaque stub error.
|
|
113
113
|
- `NODE_ENV` must be `"production"` in the rsc project. Dev `NODE_ENV` crashes the bare worker (jsxDEV owner-stack machinery uninitialized) and emits volatile debug rows that defeat stable Flight snapshots.
|
|
114
114
|
- The forked rsc worker (`pool: "forks"`) must force the condition via `execArgv: ["--conditions=react-server"]`, or React throws "the react-server condition must be enabled".
|
|
115
|
-
- The `@rangojs/router:version` and `@vitejs/plugin-rsc/rsc` virtuals must be stubbed; the preset does it. A bare router import without stubbing throws.
|
|
115
|
+
- The `@rangojs/router:version` and `@vitejs/plugin-rsc/rsc` (`/rsc/server`, `/rsc/client`) virtuals must be stubbed; the preset does it. A bare router import without stubbing throws.
|
|
116
116
|
- The rango fragment goes under `test` (`test.alias` + `test.server.deps.inline`, both returned by `rangoTestConfig`), NOT under top-level `resolve`.
|
|
117
117
|
- Wire `rangoUseClientTransform()` into the rsc project `plugins` so islands auto-discover from the server tree imports (see `./server-tree.md`); without it, register islands explicitly with `clientComponents`.
|
|
118
118
|
|
|
@@ -15,8 +15,8 @@ import { segmentFragment } from "../segment-fragments.js";
|
|
|
15
15
|
import {
|
|
16
16
|
renderToReadableStream,
|
|
17
17
|
createTemporaryReferenceSet,
|
|
18
|
-
} from "
|
|
19
|
-
import { createFromReadableStream } from "
|
|
18
|
+
} from "../deps/rsc.js";
|
|
19
|
+
import { createFromReadableStream } from "../deps/rsc-client.js";
|
|
20
20
|
|
|
21
21
|
// Preserve embedded server references on a cache/prerender HIT so they
|
|
22
22
|
// re-serialize to the client instead of resolving to a raw function React
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
2
|
+
// RSC-environment *client* protocol (deserialize / encodeReply). Kept as its
|
|
3
|
+
// own module so a server-only importer of `./rsc.ts` does not pull this side.
|
|
4
|
+
export {
|
|
5
|
+
createFromReadableStream,
|
|
6
|
+
encodeReply,
|
|
7
|
+
createClientTemporaryReferenceSet,
|
|
8
|
+
} from "@vitejs/plugin-rsc/rsc/client";
|
package/src/deps/rsc.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
2
|
-
// Re-export
|
|
2
|
+
// Re-export the RSC-environment *server* runtime for virtual entries.
|
|
3
|
+
// Prefer `@vitejs/plugin-rsc/rsc/server` over the combined `/rsc` barrel so
|
|
4
|
+
// Vite can skip bundling the unused `react-server-dom` client protocol.
|
|
3
5
|
export {
|
|
4
6
|
renderToReadableStream,
|
|
5
7
|
decodeReply,
|
|
@@ -7,4 +9,4 @@ export {
|
|
|
7
9
|
loadServerAction,
|
|
8
10
|
decodeAction,
|
|
9
11
|
decodeFormState,
|
|
10
|
-
} from "@vitejs/plugin-rsc/rsc";
|
|
12
|
+
} from "@vitejs/plugin-rsc/rsc/server";
|
package/src/deps/ssr.ts
CHANGED
package/src/rsc/handler.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
_getRequestContext,
|
|
17
17
|
createRequestContext,
|
|
18
18
|
} from "../server/request-context.js";
|
|
19
|
-
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
19
|
+
import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
|
|
20
20
|
import type {
|
|
21
21
|
RscPayload,
|
|
22
22
|
CreateRSCHandlerOptions,
|
|
@@ -137,7 +137,7 @@ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
|
137
137
|
* @example With custom deps (advanced)
|
|
138
138
|
* ```tsx
|
|
139
139
|
* import { createRSCHandler } from "@rangojs/router/rsc";
|
|
140
|
-
* import * as rsc from "@vitejs/plugin-rsc/rsc";
|
|
140
|
+
* import * as rsc from "@vitejs/plugin-rsc/rsc/server";
|
|
141
141
|
* import { router } from "./router.js";
|
|
142
142
|
*
|
|
143
143
|
* export default createRSCHandler({
|
|
@@ -176,7 +176,7 @@ export function createRSCHandler<
|
|
|
176
176
|
// stores not covered by the app-level ctx._cacheStore.
|
|
177
177
|
const explicitTaggedStores = new Set<SegmentCacheStore>();
|
|
178
178
|
|
|
179
|
-
// Use provided deps or default to @vitejs/plugin-rsc/rsc exports
|
|
179
|
+
// Use provided deps or default to @vitejs/plugin-rsc/rsc/server exports
|
|
180
180
|
const deps = options.deps ?? rscDeps;
|
|
181
181
|
const {
|
|
182
182
|
renderToReadableStream,
|
package/src/rsc/types.ts
CHANGED
|
@@ -103,11 +103,11 @@ export interface RscPayload {
|
|
|
103
103
|
export type ReactFormState = unknown;
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* RSC dependencies from @vitejs/plugin-rsc/rsc
|
|
106
|
+
* RSC dependencies from @vitejs/plugin-rsc/rsc/server
|
|
107
107
|
*/
|
|
108
108
|
export interface RSCDependencies {
|
|
109
109
|
/**
|
|
110
|
-
* renderToReadableStream from @vitejs/plugin-rsc/rsc
|
|
110
|
+
* renderToReadableStream from @vitejs/plugin-rsc/rsc/server
|
|
111
111
|
*/
|
|
112
112
|
renderToReadableStream: <T>(
|
|
113
113
|
payload: T,
|
|
@@ -118,7 +118,7 @@ export interface RSCDependencies {
|
|
|
118
118
|
) => ReadableStream<Uint8Array>;
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* decodeReply from @vitejs/plugin-rsc/rsc
|
|
121
|
+
* decodeReply from @vitejs/plugin-rsc/rsc/server
|
|
122
122
|
*/
|
|
123
123
|
decodeReply: (
|
|
124
124
|
body: FormData | string,
|
|
@@ -126,23 +126,23 @@ export interface RSCDependencies {
|
|
|
126
126
|
) => Promise<unknown[]>;
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
* createTemporaryReferenceSet from @vitejs/plugin-rsc/rsc
|
|
129
|
+
* createTemporaryReferenceSet from @vitejs/plugin-rsc/rsc/server
|
|
130
130
|
*/
|
|
131
131
|
createTemporaryReferenceSet: () => unknown;
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* loadServerAction from @vitejs/plugin-rsc/rsc
|
|
134
|
+
* loadServerAction from @vitejs/plugin-rsc/rsc/server
|
|
135
135
|
*/
|
|
136
136
|
loadServerAction: (actionId: string) => Promise<Function>;
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
* decodeAction from @vitejs/plugin-rsc/rsc
|
|
139
|
+
* decodeAction from @vitejs/plugin-rsc/rsc/server
|
|
140
140
|
* Decodes a FormData into a bound action function (for useActionState forms)
|
|
141
141
|
*/
|
|
142
142
|
decodeAction: (body: FormData) => Promise<() => Promise<unknown>>;
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
* decodeFormState from @vitejs/plugin-rsc/rsc
|
|
145
|
+
* decodeFormState from @vitejs/plugin-rsc/rsc/server
|
|
146
146
|
* Decodes the action result into a ReactFormState for useActionState progressive enhancement
|
|
147
147
|
*/
|
|
148
148
|
decodeFormState: (
|
|
@@ -300,8 +300,8 @@ export interface CreateRSCHandlerOptions<
|
|
|
300
300
|
router: RangoInternal<TEnv, TRoutes>;
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
|
-
* RSC dependencies from @vitejs/plugin-rsc/rsc.
|
|
304
|
-
* Defaults to the exports from @vitejs/plugin-rsc/rsc.
|
|
303
|
+
* RSC dependencies from @vitejs/plugin-rsc/rsc/server.
|
|
304
|
+
* Defaults to the exports from @vitejs/plugin-rsc/rsc/server.
|
|
305
305
|
*/
|
|
306
306
|
deps?: RSCDependencies;
|
|
307
307
|
|
package/src/ssr/index.tsx
CHANGED
|
@@ -154,10 +154,20 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
154
154
|
) => TransformStream<Uint8Array, Uint8Array>;
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* Function to load bootstrap script content
|
|
158
|
-
*
|
|
157
|
+
* Function to load bootstrap script content.
|
|
158
|
+
* Required unless `getClientEntryUrl` is provided with `headScripts: "preinit"`.
|
|
159
|
+
* Custom SSR entries typically: `() => import.meta.viteRsc.loadBootstrapScriptContent("index")`
|
|
160
|
+
* (deprecated in `@vitejs/plugin-rsc` 0.5.33 in favor of `getClientEntryUrl`).
|
|
159
161
|
*/
|
|
160
|
-
loadBootstrapScriptContent
|
|
162
|
+
loadBootstrapScriptContent?: () => Promise<string>;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Client entry URL from `@vitejs/plugin-rsc/ssr` `getClientEntryUrl()`.
|
|
166
|
+
* Preferred when `headScripts` is `"preinit"`: Fizz receives `bootstrapModules`
|
|
167
|
+
* without the deprecated `loadBootstrapScriptContent` round-trip. Custom SSR
|
|
168
|
+
* entries can omit this and keep the inline bootstrap path.
|
|
169
|
+
*/
|
|
170
|
+
getClientEntryUrl?: () => string;
|
|
161
171
|
|
|
162
172
|
/**
|
|
163
173
|
* Document script strategy; the generated virtual SSR entry threads the
|
|
@@ -438,36 +448,105 @@ interface ShellResumeOptions {
|
|
|
438
448
|
const BOOTSTRAP_IMPORT_ONLY_RE =
|
|
439
449
|
/^\s*import\(\s*(["'])([^"'\\]+)\1\s*\)\s*;?\s*$/;
|
|
440
450
|
|
|
451
|
+
const MISSING_BOOTSTRAP_MSG =
|
|
452
|
+
"[ssr] Missing bootstrap dependency: provide loadBootstrapScriptContent(), " +
|
|
453
|
+
'or getClientEntryUrl with headScripts: "preinit".';
|
|
454
|
+
|
|
441
455
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* reaches an opaque inline script that only reveals the URL once executed.
|
|
448
|
-
* Fizz stamps the request nonce on both tags (the inline form needed that
|
|
449
|
-
* too), and under PPR both land in the stored prelude; on resume React has
|
|
450
|
-
* already cleared the bootstrap fields from the postponed state, so nothing
|
|
451
|
-
* re-emits.
|
|
456
|
+
* Construction-time guard for {@link resolveBootstrap}: a handler whose deps
|
|
457
|
+
* can never produce a bootstrap must fail at startup, not 500 per request.
|
|
458
|
+
* getClientEntryUrl only counts under an explicit `headScripts: "preinit"` —
|
|
459
|
+
* any other headScripts keeps the inline path, so its presence alone is a
|
|
460
|
+
* misconfiguration worth flagging rather than silently ignoring.
|
|
452
461
|
*/
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
462
|
+
function assertBootstrapDeps(deps: SSRDependencies): void {
|
|
463
|
+
const preinit = deps.headScripts === "preinit";
|
|
464
|
+
if (deps.getClientEntryUrl && !preinit) {
|
|
465
|
+
console.warn(
|
|
466
|
+
'[ssr] getClientEntryUrl is ignored without headScripts: "preinit"; ' +
|
|
467
|
+
"the inline loadBootstrapScriptContent path is used instead.",
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (
|
|
471
|
+
!(preinit && deps.getClientEntryUrl) &&
|
|
472
|
+
!deps.loadBootstrapScriptContent
|
|
473
|
+
) {
|
|
474
|
+
throw new Error(MISSING_BOOTSTRAP_MSG);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
type BootstrapOptions = Pick<
|
|
457
479
|
RenderToReadableStreamOptions,
|
|
458
480
|
"bootstrapScriptContent" | "bootstrapModules"
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
481
|
+
>;
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Resolve Fizz's bootstrap options from the deps.
|
|
485
|
+
*
|
|
486
|
+
* Prefer bootstrapModules over the inline import() bootstrap: with
|
|
487
|
+
* `headScripts: "preinit"`, getClientEntryUrl() (sync — nothing to race)
|
|
488
|
+
* short-circuits to bootstrapModules, and inline content that is exactly
|
|
489
|
+
* `import("<entry-url>")` converts to the URL. React then emits a
|
|
490
|
+
* `<link rel="modulepreload" fetchpriority="low">` hint in the head plus the
|
|
491
|
+
* executing `<script type="module" src async>` at end of shell — the entry
|
|
492
|
+
* fetch starts with the first flushed bytes instead of when the parser reaches
|
|
493
|
+
* an opaque inline script that only reveals the URL once executed. Fizz stamps
|
|
494
|
+
* the request nonce on both tags, and under PPR both land in the stored
|
|
495
|
+
* prelude; on resume React has already cleared the bootstrap fields from the
|
|
496
|
+
* postponed state, so nothing re-emits. The conversion is an explicit opt-in:
|
|
497
|
+
* undefined headScripts (a custom SSR entry that predates the option, which
|
|
498
|
+
* also never installed the preinit hook) keeps the inline bootstrap
|
|
499
|
+
* byte-for-byte — converting by default would break CSPs that allowlist the
|
|
500
|
+
* known inline import() via a script hash.
|
|
501
|
+
*
|
|
502
|
+
* With `deadline` (shell capture), the inline load races it: a load that never
|
|
503
|
+
* resolves within the deadline is the same bounded no-shell degrade as a shell
|
|
504
|
+
* that never goes quiet — resolves `null`, the caller's degrade sentinel
|
|
505
|
+
* (disjoint from the load's string). A load that REJECTS is a genuine error
|
|
506
|
+
* and still propagates. The no-op catch keeps a late rejection off the
|
|
507
|
+
* unhandledRejection path when the deadline already won; a rejection that
|
|
508
|
+
* lands first still propagates out.
|
|
509
|
+
*/
|
|
510
|
+
async function resolveBootstrap(
|
|
511
|
+
deps: SSRDependencies,
|
|
512
|
+
): Promise<BootstrapOptions>;
|
|
513
|
+
async function resolveBootstrap(
|
|
514
|
+
deps: SSRDependencies,
|
|
515
|
+
deadline: Promise<void>,
|
|
516
|
+
): Promise<BootstrapOptions | null>;
|
|
517
|
+
async function resolveBootstrap(
|
|
518
|
+
deps: SSRDependencies,
|
|
519
|
+
deadline?: Promise<void>,
|
|
520
|
+
): Promise<BootstrapOptions | null> {
|
|
521
|
+
const preinit = deps.headScripts === "preinit";
|
|
522
|
+
if (preinit) {
|
|
523
|
+
// Truthy on purpose, and the ONLY predicate on the URL: an empty string is
|
|
524
|
+
// an unusable entry URL and falls through to the inline path.
|
|
525
|
+
const url = deps.getClientEntryUrl?.();
|
|
526
|
+
if (url) {
|
|
527
|
+
return { bootstrapModules: [url] };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!deps.loadBootstrapScriptContent) {
|
|
531
|
+
throw new Error(MISSING_BOOTSTRAP_MSG);
|
|
532
|
+
}
|
|
533
|
+
let content: string;
|
|
534
|
+
if (deadline) {
|
|
535
|
+
const load = deps.loadBootstrapScriptContent();
|
|
536
|
+
load.catch(() => {});
|
|
537
|
+
const raced = await Promise.race([load, deadline.then(() => null)]);
|
|
538
|
+
if (raced === null) return null;
|
|
539
|
+
content = raced;
|
|
540
|
+
} else {
|
|
541
|
+
content = await deps.loadBootstrapScriptContent();
|
|
542
|
+
}
|
|
543
|
+
if (preinit) {
|
|
544
|
+
const match = BOOTSTRAP_IMPORT_ONLY_RE.exec(content);
|
|
545
|
+
return match
|
|
546
|
+
? { bootstrapModules: [match[2]!] }
|
|
547
|
+
: { bootstrapScriptContent: content };
|
|
466
548
|
}
|
|
467
|
-
|
|
468
|
-
return match
|
|
469
|
-
? { bootstrapModules: [match[2]!] }
|
|
470
|
-
: { bootstrapScriptContent: content };
|
|
549
|
+
return { bootstrapScriptContent: content };
|
|
471
550
|
}
|
|
472
551
|
|
|
473
552
|
/**
|
|
@@ -476,7 +555,10 @@ function resolveBootstrapOptions(
|
|
|
476
555
|
* @example
|
|
477
556
|
* ```tsx
|
|
478
557
|
* import { createSSRHandler } from "@rangojs/router/ssr";
|
|
479
|
-
* import {
|
|
558
|
+
* import {
|
|
559
|
+
* createFromReadableStream,
|
|
560
|
+
* getClientEntryUrl,
|
|
561
|
+
* } from "@rangojs/router/internal/deps/ssr";
|
|
480
562
|
* import { renderToReadableStream } from "react-dom/server.edge";
|
|
481
563
|
* import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
482
564
|
*
|
|
@@ -484,6 +566,17 @@ function resolveBootstrapOptions(
|
|
|
484
566
|
* createFromReadableStream,
|
|
485
567
|
* renderToReadableStream,
|
|
486
568
|
* injectRSCPayload,
|
|
569
|
+
* getClientEntryUrl,
|
|
570
|
+
* headScripts: "preinit", // getClientEntryUrl is only used under "preinit"
|
|
571
|
+
* });
|
|
572
|
+
* ```
|
|
573
|
+
*
|
|
574
|
+
* Custom SSR entries that still use the deprecated bootstrap helper:
|
|
575
|
+
* ```tsx
|
|
576
|
+
* export const renderHTML = createSSRHandler({
|
|
577
|
+
* createFromReadableStream,
|
|
578
|
+
* renderToReadableStream,
|
|
579
|
+
* injectRSCPayload,
|
|
487
580
|
* loadBootstrapScriptContent: () =>
|
|
488
581
|
* import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
489
582
|
* });
|
|
@@ -494,9 +587,9 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
494
587
|
createFromReadableStream,
|
|
495
588
|
renderToReadableStream,
|
|
496
589
|
injectRSCPayload,
|
|
497
|
-
loadBootstrapScriptContent,
|
|
498
590
|
onError,
|
|
499
591
|
} = deps;
|
|
592
|
+
assertBootstrapDeps(deps);
|
|
500
593
|
|
|
501
594
|
/**
|
|
502
595
|
* Render RSC stream to HTML stream
|
|
@@ -541,8 +634,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
541
634
|
origin,
|
|
542
635
|
});
|
|
543
636
|
|
|
544
|
-
|
|
545
|
-
const bootstrapScriptContent = await loadBootstrapScriptContent();
|
|
637
|
+
const bootstrap = await resolveBootstrap(deps);
|
|
546
638
|
|
|
547
639
|
// ssr:false auto-raise (see SSRDependencies.progressiveChunkSize).
|
|
548
640
|
// Awaiting the payload here is latency-neutral: fizz cannot emit even
|
|
@@ -563,7 +655,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
563
655
|
// isolate-global, the nonce per request).
|
|
564
656
|
const htmlStream = await runWithPreinitNonce(nonce, () =>
|
|
565
657
|
renderToReadableStream(<SsrRoot />, {
|
|
566
|
-
...
|
|
658
|
+
...bootstrap,
|
|
567
659
|
formState,
|
|
568
660
|
nonce,
|
|
569
661
|
...(progressiveChunkSize !== undefined && { progressiveChunkSize }),
|
|
@@ -600,8 +692,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
600
692
|
export function createShellCaptureHandler<TEnv = unknown>(
|
|
601
693
|
deps: SSRDependencies<TEnv>,
|
|
602
694
|
) {
|
|
603
|
-
const { createFromReadableStream,
|
|
604
|
-
deps;
|
|
695
|
+
const { createFromReadableStream, prerender } = deps;
|
|
605
696
|
const onError = deps.onError;
|
|
606
697
|
|
|
607
698
|
if (!prerender) {
|
|
@@ -610,6 +701,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
610
701
|
"PPR shell capture requires the prerender export; wire it in the SSR virtual entry.",
|
|
611
702
|
);
|
|
612
703
|
}
|
|
704
|
+
assertBootstrapDeps(deps);
|
|
613
705
|
|
|
614
706
|
/**
|
|
615
707
|
* Prerender the shell and return the stored artifacts, or null when the
|
|
@@ -659,21 +751,10 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
659
751
|
origin: opts.origin,
|
|
660
752
|
});
|
|
661
753
|
|
|
662
|
-
// Bootstrap
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
// the deadline sentinel — disjoint from the load's `Promise<string>`, so
|
|
667
|
-
// the race narrows to `string | null` with no wrapper. The no-op catch
|
|
668
|
-
// keeps a late rejection off the unhandledRejection path when the deadline
|
|
669
|
-
// already won; a rejection that lands first still propagates out.
|
|
670
|
-
const load = loadBootstrapScriptContent();
|
|
671
|
-
load.catch(() => {});
|
|
672
|
-
const bootstrapScriptContent = await Promise.race([
|
|
673
|
-
load,
|
|
674
|
-
deadline.promise.then(() => null),
|
|
675
|
-
]);
|
|
676
|
-
if (bootstrapScriptContent === null) {
|
|
754
|
+
// Bootstrap resolution raced against the deadline (see resolveBootstrap):
|
|
755
|
+
// null means the deadline won — the bounded no-shell degrade.
|
|
756
|
+
const bootstrap = await resolveBootstrap(deps, deadline.promise);
|
|
757
|
+
if (bootstrap === null) {
|
|
677
758
|
return null;
|
|
678
759
|
}
|
|
679
760
|
|
|
@@ -689,7 +770,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
689
770
|
const abortReason = { rangoShellCaptureAbort: true };
|
|
690
771
|
const prerenderPromise = prerender(<SsrRoot />, {
|
|
691
772
|
signal: controller.signal,
|
|
692
|
-
...
|
|
773
|
+
...bootstrap,
|
|
693
774
|
// Explicit option only — the ssr:false auto-raise is live-SSR scoped
|
|
694
775
|
// (RangoBaseOptions.progressiveChunkSize documents the contract); the
|
|
695
776
|
// capture handler starts prerender without deserializing the payload,
|
package/src/testing/flight.ts
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* the same react-server-dom serializer the router uses at runtime. It runs in
|
|
8
8
|
* plain node (no Vite, no browser), but ONLY under the `react-server` export
|
|
9
9
|
* condition. The serializer is the VENDORED build shipped with
|
|
10
|
-
* @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc` entry
|
|
11
|
-
* imports Vite virtual modules and is not usable outside a Vite
|
|
10
|
+
* @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc/server` entry
|
|
11
|
+
* top-level imports Vite virtual modules and is not usable outside a Vite
|
|
12
|
+
* build.
|
|
12
13
|
*
|
|
13
14
|
* Run the example/tests for this module via the dedicated rsc vitest project
|
|
14
15
|
* (vitest.rsc.config.ts), which forces `--conditions=react-server` on the
|
|
15
16
|
* worker. The main vitest project must NOT use that condition (it would flip
|
|
16
17
|
* React to the no-hooks server build and break the ~50 tests that mock
|
|
17
|
-
* @vitejs/plugin-rsc/rsc).
|
|
18
|
+
* @vitejs/plugin-rsc/rsc/server).
|
|
18
19
|
*
|
|
19
20
|
* Scope / limitations (v1):
|
|
20
21
|
* - Server-only / leaf trees. A tree containing a CLIENT component emits an
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// Stub for `@vitejs/plugin-rsc/rsc
|
|
2
|
-
// per-file `vi.mock(...)`.
|
|
3
|
-
//
|
|
4
|
-
// plain node. The
|
|
5
|
-
//
|
|
1
|
+
// Stub for `@vitejs/plugin-rsc/rsc` and the split `/rsc/server`, `/rsc/client`
|
|
2
|
+
// entries, shipped so consumers do not have to write a per-file `vi.mock(...)`.
|
|
3
|
+
// Importing a router internal transitively pulls this module, whose real
|
|
4
|
+
// top-level body imports Vite virtuals that do not resolve in plain node. The
|
|
5
|
+
// unit/integration primitives (dispatch/runLoader/runMiddleware) never render
|
|
6
|
+
// RSC, so empty fns suffice.
|
|
6
7
|
export const createFromReadableStream = (): never => {
|
|
7
8
|
throw new Error("plugin-rsc stub: createFromReadableStream not available");
|
|
8
9
|
};
|
|
@@ -14,3 +15,10 @@ export const decodeReply = (): undefined => undefined;
|
|
|
14
15
|
export const decodeAction = (): undefined => undefined;
|
|
15
16
|
export const decodeFormState = (): undefined => undefined;
|
|
16
17
|
export const createTemporaryReferenceSet = (): Record<string, never> => ({});
|
|
18
|
+
export const encodeReply = (): never => {
|
|
19
|
+
throw new Error("plugin-rsc stub: encodeReply not available");
|
|
20
|
+
};
|
|
21
|
+
export const createClientTemporaryReferenceSet = (): Record<
|
|
22
|
+
string,
|
|
23
|
+
never
|
|
24
|
+
> => ({});
|
package/src/testing/vitest.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* `@rangojs/router` specifier to its react-server entry (real impls) while
|
|
19
19
|
* leaving React as the client build — which is exactly what this helper does.
|
|
20
20
|
* - The build-only `@rangojs/router:version` virtual and `@vitejs/plugin-rsc/rsc`
|
|
21
|
-
* (whose real body imports unresolvable
|
|
21
|
+
* plus `/rsc/server`, `/rsc/client` (whose real body imports unresolvable
|
|
22
|
+
* Vite virtuals) are stubbed.
|
|
22
23
|
* - Cloudflare apps additionally import the `cloudflare:workers` /
|
|
23
24
|
* `cloudflare:email` runtime virtuals; pass `{ preset: "cloudflare" }` to stub them.
|
|
24
25
|
*
|
|
@@ -126,7 +127,7 @@ export function rangoTestAliases(
|
|
|
126
127
|
replacement: here("src/testing/vitest-stubs/version.ts"),
|
|
127
128
|
},
|
|
128
129
|
{
|
|
129
|
-
find: /^@vitejs\/plugin-rsc\/rsc
|
|
130
|
+
find: /^@vitejs\/plugin-rsc\/rsc(\/(server|client))?$/,
|
|
130
131
|
replacement: here("src/testing/vitest-stubs/plugin-rsc.ts"),
|
|
131
132
|
},
|
|
132
133
|
];
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { Plugin } from "vite";
|
|
20
|
+
import type { ModuleExportMeta } from "@vitejs/plugin-rsc/transforms";
|
|
20
21
|
import path from "node:path";
|
|
21
22
|
import MagicString from "magic-string";
|
|
22
23
|
import { normalizePath, hashId } from "./expose-id-utils.js";
|
|
@@ -89,6 +90,13 @@ export function useCacheTransform(): Plugin {
|
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// plugin-rsc 0.5.34 `matchDirective` does `stmt.directive.match(...)`
|
|
94
|
+
// after `"directive" in node`. Vite/oxc parseAst now emits
|
|
95
|
+
// `directive: null` on ordinary ExpressionStatements, so a file that
|
|
96
|
+
// mixes a `"use cache"` function with a sibling handler whose first
|
|
97
|
+
// statement is an expression throws and the wrap is dropped.
|
|
98
|
+
stripNullDirectiveFields(ast);
|
|
99
|
+
|
|
92
100
|
const filePath = normalizePath(path.relative(projectRoot, id));
|
|
93
101
|
const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
|
|
94
102
|
|
|
@@ -101,6 +109,7 @@ export function useCacheTransform(): Plugin {
|
|
|
101
109
|
isBuild,
|
|
102
110
|
isLayoutOrTemplate,
|
|
103
111
|
transformWrapExport,
|
|
112
|
+
hasDirective,
|
|
104
113
|
);
|
|
105
114
|
}
|
|
106
115
|
|
|
@@ -131,6 +140,7 @@ function transformFileLevelUseCache(
|
|
|
131
140
|
isBuild: boolean,
|
|
132
141
|
isLayoutOrTemplate: boolean,
|
|
133
142
|
transformWrapExport: (typeof import("@vitejs/plugin-rsc/transforms"))["transformWrapExport"],
|
|
143
|
+
hasDirective: (typeof import("@vitejs/plugin-rsc/transforms"))["hasDirective"],
|
|
134
144
|
) {
|
|
135
145
|
const unconfirmedExports: string[] = [];
|
|
136
146
|
|
|
@@ -140,8 +150,18 @@ function transformFileLevelUseCache(
|
|
|
140
150
|
return `__rango_registerCachedFunction(${value}, ${JSON.stringify(funcId)}, "default")`;
|
|
141
151
|
},
|
|
142
152
|
rejectNonAsyncFunction: false,
|
|
143
|
-
filter: (name: string, meta:
|
|
153
|
+
filter: (name: string, meta: ModuleExportMeta) => {
|
|
144
154
|
if (name === "default" && isLayoutOrTemplate) return false;
|
|
155
|
+
// plugin-rsc 0.5.34 hoists mixed inline `"use server"` out of a
|
|
156
|
+
// file-level `"use cache"` module as `$$hoist_*` exports and rebinds the
|
|
157
|
+
// original name to `registerServerReference($$hoist_*, ...)`. Both are
|
|
158
|
+
// server references, not cached functions. The directive check covers
|
|
159
|
+
// the pre-hoist shape (this plugin seeing the source first).
|
|
160
|
+
if (name.startsWith("$$hoist_")) return false;
|
|
161
|
+
if (isHoistedServerReferenceRebind(meta.valueNode)) return false;
|
|
162
|
+
if (functionHasUseServerDirective(meta.valueNode, hasDirective)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
145
165
|
// isFunction is boolean | undefined: true = confirmed function, false =
|
|
146
166
|
// confirmed non-function, undefined = cannot tell statically (e.g. a
|
|
147
167
|
// factory/HOF initializer `const x = makeCached(fn)`). Deliberate policy:
|
|
@@ -250,6 +270,50 @@ function transformFunctionLevelUseCache(
|
|
|
250
270
|
}
|
|
251
271
|
}
|
|
252
272
|
|
|
273
|
+
function stripNullDirectiveFields(node: unknown): void {
|
|
274
|
+
if (!node || typeof node !== "object") return;
|
|
275
|
+
const rec = node as Record<string, unknown>;
|
|
276
|
+
if (rec.type === "ExpressionStatement" && typeof rec.directive !== "string") {
|
|
277
|
+
delete rec.directive;
|
|
278
|
+
}
|
|
279
|
+
for (const value of Object.values(rec)) {
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
for (const item of value) stripNullDirectiveFields(item);
|
|
282
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
283
|
+
stripNullDirectiveFields(value);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isHoistedServerReferenceRebind(
|
|
289
|
+
valueNode: ModuleExportMeta["valueNode"],
|
|
290
|
+
): boolean {
|
|
291
|
+
if (!valueNode || valueNode.type !== "CallExpression") return false;
|
|
292
|
+
const first = valueNode.arguments[0];
|
|
293
|
+
return (
|
|
294
|
+
first !== undefined &&
|
|
295
|
+
first.type === "Identifier" &&
|
|
296
|
+
first.name.startsWith("$$hoist_")
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function functionHasUseServerDirective(
|
|
301
|
+
valueNode: ModuleExportMeta["valueNode"],
|
|
302
|
+
hasDirective: (typeof import("@vitejs/plugin-rsc/transforms"))["hasDirective"],
|
|
303
|
+
): boolean {
|
|
304
|
+
if (!valueNode || !("body" in valueNode)) return false;
|
|
305
|
+
const { body } = valueNode;
|
|
306
|
+
if (!body || Array.isArray(body) || body.type !== "BlockStatement") {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
// plugin-rsc types valueNode with plain estree nodes but hasDirective with
|
|
310
|
+
// its oxc-flavored AST; the flavors differ only in position/extra fields.
|
|
311
|
+
return hasDirective(
|
|
312
|
+
body.body as Parameters<typeof hasDirective>[0],
|
|
313
|
+
"use server",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
253
317
|
function findFileLevelDirective(
|
|
254
318
|
ast: any,
|
|
255
319
|
): { start: number; end: number } | null {
|
|
@@ -63,18 +63,20 @@ function emitProgressiveChunkSize(value: number): string {
|
|
|
63
63
|
/**
|
|
64
64
|
* Generate the virtual SSR entry. `headScripts` mirrors the rango() plugin
|
|
65
65
|
* option: "preinit" (default) installs the client-reference preinit hook and
|
|
66
|
-
*
|
|
67
|
-
* "preload" omits the hook and
|
|
66
|
+
* threads `getClientEntryUrl` so Fizz emits `bootstrapModules`;
|
|
67
|
+
* "preload" omits the hook and uses the deprecated inline
|
|
68
|
+
* `loadBootstrapScriptContent` bootstrap.
|
|
68
69
|
*/
|
|
69
70
|
export function getVirtualEntrySSR(
|
|
70
71
|
headScripts: HeadScriptsOption = "preinit",
|
|
71
72
|
progressiveChunkSize?: number,
|
|
72
73
|
): string {
|
|
73
74
|
const preinit = headScripts !== "preload";
|
|
74
|
-
// The preload variant drops
|
|
75
|
-
// here so the template below stays a single
|
|
75
|
+
// The preload variant drops the preinit-only imports/install and swaps the
|
|
76
|
+
// bootstrap dep, all built here so the template below stays a single
|
|
77
|
+
// unconditional shape.
|
|
76
78
|
const depsImportNames = preinit
|
|
77
|
-
? "createFromReadableStream,\n setOnClientReference,"
|
|
79
|
+
? "createFromReadableStream,\n setOnClientReference,\n getClientEntryUrl,"
|
|
78
80
|
: "createFromReadableStream,";
|
|
79
81
|
const ssrImportNames = preinit ? "\n installClientReferencePreinit," : "";
|
|
80
82
|
const install = preinit
|
|
@@ -85,6 +87,10 @@ export function getVirtualEntrySSR(
|
|
|
85
87
|
installClientReferencePreinit(setOnClientReference);
|
|
86
88
|
`
|
|
87
89
|
: "";
|
|
90
|
+
const bootstrapDep = preinit
|
|
91
|
+
? "getClientEntryUrl,"
|
|
92
|
+
: `loadBootstrapScriptContent: () =>
|
|
93
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),`;
|
|
88
94
|
const hs = JSON.stringify(headScripts);
|
|
89
95
|
// Emitted into all three handlers: live SSR and shell capture consume it
|
|
90
96
|
// directly; the resume handler receives it for dep-shape uniformity (resume()
|
|
@@ -114,8 +120,7 @@ export const renderHTML = createSSRHandler({
|
|
|
114
120
|
renderToReadableStream,
|
|
115
121
|
injectRSCPayload,
|
|
116
122
|
headScripts: ${hs},${pcs}
|
|
117
|
-
|
|
118
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
123
|
+
${bootstrapDep}
|
|
119
124
|
});
|
|
120
125
|
|
|
121
126
|
export const captureShellHTML = createShellCaptureHandler({
|
|
@@ -125,8 +130,7 @@ export const captureShellHTML = createShellCaptureHandler({
|
|
|
125
130
|
prerender,
|
|
126
131
|
resume,
|
|
127
132
|
headScripts: ${hs},${pcs}
|
|
128
|
-
|
|
129
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
133
|
+
${bootstrapDep}
|
|
130
134
|
});
|
|
131
135
|
|
|
132
136
|
export const resumeShellHTML = createShellResumeHandler({
|
|
@@ -136,8 +140,7 @@ export const resumeShellHTML = createShellResumeHandler({
|
|
|
136
140
|
prerender,
|
|
137
141
|
resume,
|
|
138
142
|
headScripts: ${hs},${pcs}
|
|
139
|
-
|
|
140
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
143
|
+
${bootstrapDep}
|
|
141
144
|
});
|
|
142
145
|
`.trim();
|
|
143
146
|
}
|