@rangojs/router 0.0.0-experimental.66 → 0.0.0-experimental.66cdebe3
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/README.md +112 -17
- package/dist/vite/index.js +1462 -422
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +54 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +45 -0
- package/skills/layout/SKILL.md +24 -0
- package/skills/links/SKILL.md +234 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +68 -0
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +48 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +9 -1
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +151 -9
- package/src/browser/navigation-client.ts +64 -13
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +58 -12
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +95 -44
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +17 -4
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/scroll-restoration.ts +69 -28
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +52 -25
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +87 -175
- package/src/href-client.ts +4 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +44 -9
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +62 -36
- package/src/route-definition/dsl-helpers.ts +175 -23
- package/src/route-definition/helpers-types.ts +63 -14
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-types.ts +7 -0
- package/src/router/handler-context.ts +21 -38
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/cache-lookup.ts +2 -1
- package/src/router/match-result.ts +101 -4
- package/src/router/middleware-types.ts +14 -25
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +13 -0
- package/src/router/segment-resolution/revalidation.ts +135 -101
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +16 -8
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/server-action.ts +4 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +71 -70
- package/src/server/context.ts +26 -3
- package/src/server/request-context.ts +10 -42
- package/src/ssr/index.tsx +5 -1
- package/src/types/handler-context.ts +12 -39
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +30 -4
- package/src/urls/response-types.ts +2 -10
- package/src/use-loader.tsx +4 -1
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +172 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +545 -304
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +558 -53
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
stageBuildAssetModule,
|
|
17
17
|
} from "../utils/prerender-utils.js";
|
|
18
18
|
import type { DiscoveryState } from "./state.js";
|
|
19
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
20
|
+
|
|
21
|
+
const debug = createRangoDebugger(NS.prerender);
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Expand prerender routes into concrete URLs and render them via the
|
|
@@ -30,6 +33,12 @@ export async function expandPrerenderRoutes(
|
|
|
30
33
|
): Promise<void> {
|
|
31
34
|
if (!state.opts?.enableBuildPrerender || !state.isBuildMode) return;
|
|
32
35
|
|
|
36
|
+
const overallStart = debug ? performance.now() : 0;
|
|
37
|
+
debug?.(
|
|
38
|
+
"expandPrerenderRoutes: start (%d router manifest(s))",
|
|
39
|
+
allManifests.length,
|
|
40
|
+
);
|
|
41
|
+
|
|
33
42
|
type PrerenderEntry = {
|
|
34
43
|
urlPath: string;
|
|
35
44
|
routeName: string;
|
|
@@ -51,106 +60,160 @@ export async function expandPrerenderRoutes(
|
|
|
51
60
|
return substituteRouteParams(pattern, params);
|
|
52
61
|
};
|
|
53
62
|
|
|
63
|
+
let resolvedRoutes = 0;
|
|
64
|
+
let totalDynamic = 0;
|
|
65
|
+
|
|
66
|
+
// Count dynamic routes upfront for progress reporting
|
|
54
67
|
for (const { manifest } of allManifests) {
|
|
55
68
|
if (!manifest.prerenderRoutes) continue;
|
|
56
|
-
const defs = manifest._prerenderDefs || {};
|
|
57
|
-
const passthroughSet = new Set(manifest.passthroughRoutes || []);
|
|
58
69
|
for (const routeName of manifest.prerenderRoutes) {
|
|
59
70
|
const pattern = manifest.routeManifest[routeName];
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
71
|
+
if (pattern && (pattern.includes(":") || pattern.includes("*"))) {
|
|
72
|
+
totalDynamic++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Periodic progress log so long getParams() calls don't look stalled
|
|
78
|
+
const paramsStart = performance.now();
|
|
79
|
+
const progressInterval =
|
|
80
|
+
totalDynamic > 0
|
|
81
|
+
? setInterval(() => {
|
|
82
|
+
const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
|
|
83
|
+
console.log(
|
|
84
|
+
`[rsc-router] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
|
|
85
|
+
);
|
|
86
|
+
}, 5000)
|
|
87
|
+
: undefined;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
for (const { manifest } of allManifests) {
|
|
91
|
+
if (!manifest.prerenderRoutes) continue;
|
|
92
|
+
const defs = manifest._prerenderDefs || {};
|
|
93
|
+
const passthroughSet = new Set(manifest.passthroughRoutes || []);
|
|
94
|
+
for (const routeName of manifest.prerenderRoutes) {
|
|
95
|
+
const pattern = manifest.routeManifest[routeName];
|
|
96
|
+
if (!pattern) continue;
|
|
97
|
+
const def = defs[routeName];
|
|
98
|
+
const isPassthroughRoute = passthroughSet.has(routeName);
|
|
99
|
+
const hasDynamic = pattern.includes(":") || pattern.includes("*");
|
|
100
|
+
if (!hasDynamic) {
|
|
101
|
+
// Static route: use pattern directly (strip trailing slash for URL)
|
|
102
|
+
entries.push({
|
|
103
|
+
urlPath: pattern.replace(/\/$/, "") || "/",
|
|
104
|
+
routeName,
|
|
105
|
+
concurrency: 1,
|
|
106
|
+
isPassthroughRoute,
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
// Dynamic route: call getParams() to enumerate param combinations
|
|
110
|
+
if (def?.getParams) {
|
|
111
|
+
const getParamsStart = debug ? performance.now() : 0;
|
|
112
|
+
try {
|
|
113
|
+
const buildVars: Record<string, any> = {};
|
|
114
|
+
const buildEnv = state.resolvedBuildEnv;
|
|
115
|
+
const getParamsCtx = {
|
|
116
|
+
build: true as const,
|
|
117
|
+
dev: !state.isBuildMode,
|
|
118
|
+
set: ((keyOrVar: any, value: any) => {
|
|
119
|
+
contextSet(buildVars, keyOrVar, value);
|
|
120
|
+
}) as any,
|
|
121
|
+
reverse: getParamsReverse,
|
|
122
|
+
get env() {
|
|
123
|
+
if (buildEnv !== undefined) return buildEnv;
|
|
124
|
+
throw new Error(
|
|
125
|
+
"[rsc-router] ctx.env is not available during build-time getParams(). " +
|
|
126
|
+
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const paramsList = await def.getParams(getParamsCtx);
|
|
131
|
+
debug?.(
|
|
132
|
+
"getParams %s -> %d params (%sms)",
|
|
133
|
+
routeName,
|
|
134
|
+
paramsList.length,
|
|
135
|
+
(performance.now() - getParamsStart).toFixed(1),
|
|
103
136
|
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
const concurrency = def.options?.concurrency ?? 1;
|
|
138
|
+
const hasBuildVars =
|
|
139
|
+
Object.keys(buildVars).length > 0 ||
|
|
140
|
+
Object.getOwnPropertySymbols(buildVars).length > 0;
|
|
141
|
+
for (const params of paramsList) {
|
|
142
|
+
let url = substituteRouteParams(
|
|
143
|
+
pattern,
|
|
144
|
+
params as Record<string, string>,
|
|
145
|
+
encodePathParam,
|
|
146
|
+
);
|
|
147
|
+
// Anonymous wildcard fallback: use conventional keys if provided
|
|
148
|
+
if (url.includes("*")) {
|
|
149
|
+
const wildcardValue =
|
|
150
|
+
(params as Record<string, string>)["*"] ??
|
|
151
|
+
(params as Record<string, string>).splat;
|
|
152
|
+
if (wildcardValue !== undefined) {
|
|
153
|
+
url = url.replace(
|
|
154
|
+
/\*[^/]*$/,
|
|
155
|
+
encodePathParam(wildcardValue),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
111
158
|
}
|
|
159
|
+
entries.push({
|
|
160
|
+
urlPath: url.replace(/\/$/, "") || "/",
|
|
161
|
+
routeName,
|
|
162
|
+
concurrency,
|
|
163
|
+
...(hasBuildVars ? { buildVars } : {}),
|
|
164
|
+
isPassthroughRoute,
|
|
165
|
+
});
|
|
112
166
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
resolvedRoutes++;
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
resolvedRoutes++;
|
|
170
|
+
// Skip in getParams() skips the entire route
|
|
171
|
+
if (err.name === "Skip") {
|
|
172
|
+
console.log(
|
|
173
|
+
`[rsc-router] SKIP route "${routeName}" - ${err.message}`,
|
|
174
|
+
);
|
|
175
|
+
notifyOnError(
|
|
176
|
+
registry,
|
|
177
|
+
err,
|
|
178
|
+
"prerender",
|
|
179
|
+
routeName,
|
|
180
|
+
undefined,
|
|
181
|
+
true,
|
|
182
|
+
);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Regular error: fail the build
|
|
186
|
+
console.error(
|
|
187
|
+
`[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
|
|
134
188
|
);
|
|
135
|
-
|
|
189
|
+
notifyOnError(registry, err, "prerender", routeName);
|
|
190
|
+
throw err;
|
|
136
191
|
}
|
|
137
|
-
|
|
138
|
-
console.
|
|
139
|
-
`[rsc-router]
|
|
192
|
+
} else {
|
|
193
|
+
console.warn(
|
|
194
|
+
`[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
|
|
140
195
|
);
|
|
141
|
-
notifyOnError(registry, err, "prerender", routeName);
|
|
142
|
-
throw err;
|
|
143
196
|
}
|
|
144
|
-
} else {
|
|
145
|
-
console.warn(
|
|
146
|
-
`[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
|
|
147
|
-
);
|
|
148
197
|
}
|
|
149
198
|
}
|
|
150
199
|
}
|
|
200
|
+
} finally {
|
|
201
|
+
if (progressInterval) {
|
|
202
|
+
clearInterval(progressInterval);
|
|
203
|
+
const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
|
|
204
|
+
console.log(
|
|
205
|
+
`[rsc-router] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
151
208
|
}
|
|
152
209
|
|
|
153
|
-
if (entries.length === 0)
|
|
210
|
+
if (entries.length === 0) {
|
|
211
|
+
debug?.(
|
|
212
|
+
"no prerender entries (done in %sms)",
|
|
213
|
+
(performance.now() - overallStart).toFixed(1),
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
154
217
|
|
|
155
218
|
// Determine the max concurrency for the log header
|
|
156
219
|
const maxConcurrency = Math.max(...entries.map((e) => e.concurrency));
|
|
@@ -159,6 +222,11 @@ export async function expandPrerenderRoutes(
|
|
|
159
222
|
console.log(
|
|
160
223
|
`[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
|
|
161
224
|
);
|
|
225
|
+
debug?.(
|
|
226
|
+
"prerender loop: %d entries, max concurrency %d",
|
|
227
|
+
entries.length,
|
|
228
|
+
maxConcurrency,
|
|
229
|
+
);
|
|
162
230
|
|
|
163
231
|
const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
|
|
164
232
|
|
|
@@ -276,6 +344,13 @@ export async function expandPrerenderRoutes(
|
|
|
276
344
|
console.log(
|
|
277
345
|
`[rsc-router] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
|
|
278
346
|
);
|
|
347
|
+
debug?.(
|
|
348
|
+
"expandPrerenderRoutes done: %d done, %d skipped, %sms (overall %sms)",
|
|
349
|
+
doneCount,
|
|
350
|
+
skipCount,
|
|
351
|
+
totalElapsed,
|
|
352
|
+
(performance.now() - overallStart).toFixed(1),
|
|
353
|
+
);
|
|
279
354
|
}
|
|
280
355
|
|
|
281
356
|
/**
|
|
@@ -296,6 +371,12 @@ export async function renderStaticHandlers(
|
|
|
296
371
|
)
|
|
297
372
|
return;
|
|
298
373
|
|
|
374
|
+
const overallStart = debug ? performance.now() : 0;
|
|
375
|
+
debug?.(
|
|
376
|
+
"renderStaticHandlers: start (%d static module(s))",
|
|
377
|
+
state.resolvedStaticModules.size,
|
|
378
|
+
);
|
|
379
|
+
|
|
299
380
|
const manifestEntries: Record<string, string> = {};
|
|
300
381
|
let staticDone = 0;
|
|
301
382
|
let staticSkip = 0;
|
|
@@ -395,4 +476,11 @@ export async function renderStaticHandlers(
|
|
|
395
476
|
console.log(
|
|
396
477
|
`[rsc-router] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
|
|
397
478
|
);
|
|
479
|
+
debug?.(
|
|
480
|
+
"renderStaticHandlers done: %d done, %d skipped, %sms (overall %sms)",
|
|
481
|
+
staticDone,
|
|
482
|
+
staticSkip,
|
|
483
|
+
totalStaticElapsed,
|
|
484
|
+
(performance.now() - overallStart).toFixed(1),
|
|
485
|
+
);
|
|
398
486
|
}
|
|
@@ -22,6 +22,32 @@ export function markSelfGenWrite(
|
|
|
22
22
|
export function consumeSelfGenWrite(
|
|
23
23
|
state: DiscoveryState,
|
|
24
24
|
filePath: string,
|
|
25
|
+
): boolean {
|
|
26
|
+
return checkSelfGenWrite(state, filePath, true);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Non-consuming variant. Used by the `handleHotUpdate` plugin hook to
|
|
31
|
+
* suppress vite's HMR cascade for our own gen-file writes WITHOUT
|
|
32
|
+
* consuming the entry — `consumeSelfGenWrite` (called later from the
|
|
33
|
+
* chokidar `change` handler in `handleRouteFileChange`) still needs to
|
|
34
|
+
* see and consume the same entry to short-circuit our regen path.
|
|
35
|
+
*
|
|
36
|
+
* Both hooks fire for the same file change event:
|
|
37
|
+
* - `handleHotUpdate` runs first (vite's HMR pipeline).
|
|
38
|
+
* - chokidar `change` callback runs after (filesystem watcher).
|
|
39
|
+
*/
|
|
40
|
+
export function peekSelfGenWrite(
|
|
41
|
+
state: DiscoveryState,
|
|
42
|
+
filePath: string,
|
|
43
|
+
): boolean {
|
|
44
|
+
return checkSelfGenWrite(state, filePath, false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkSelfGenWrite(
|
|
48
|
+
state: DiscoveryState,
|
|
49
|
+
filePath: string,
|
|
50
|
+
consume: boolean,
|
|
25
51
|
): boolean {
|
|
26
52
|
const info = state.selfWrittenGenFiles.get(filePath);
|
|
27
53
|
if (!info) return false;
|
|
@@ -33,7 +59,7 @@ export function consumeSelfGenWrite(
|
|
|
33
59
|
const current = readFileSync(filePath, "utf-8");
|
|
34
60
|
const currentHash = createHash("sha256").update(current).digest("hex");
|
|
35
61
|
if (currentHash === info.hash) {
|
|
36
|
-
state.selfWrittenGenFiles.delete(filePath);
|
|
62
|
+
if (consume) state.selfWrittenGenFiles.delete(filePath);
|
|
37
63
|
return true;
|
|
38
64
|
}
|
|
39
65
|
// Hash mismatch: file was changed externally. Keep the entry so a
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
const debug = createRangoDebugger(NS.transform);
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
|
|
@@ -21,6 +24,7 @@ export function createCjsToEsmPlugin(): Plugin {
|
|
|
21
24
|
? "./cjs/react-server-dom-webpack-client.browser.production.js"
|
|
22
25
|
: "./cjs/react-server-dom-webpack-client.browser.development.js";
|
|
23
26
|
|
|
27
|
+
debug?.("cjs-to-esm entry redirect %s", id);
|
|
24
28
|
return {
|
|
25
29
|
code: `export * from "${cjsFile}";`,
|
|
26
30
|
map: null,
|
|
@@ -81,6 +85,7 @@ export function createCjsToEsmPlugin(): Plugin {
|
|
|
81
85
|
// Reconstruct with license at the top
|
|
82
86
|
transformed = license + "\n" + transformed;
|
|
83
87
|
|
|
88
|
+
debug?.("cjs-to-esm body rewrite %s", id);
|
|
84
89
|
return {
|
|
85
90
|
code: transformed,
|
|
86
91
|
map: null,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
const debug = createRangoDebugger(NS.transform);
|
|
2
5
|
|
|
3
6
|
const CLIENT_IN_SERVER_PROXY_PREFIX =
|
|
4
7
|
"virtual:vite-rsc/client-in-server-package-proxy/";
|
|
@@ -62,6 +65,7 @@ export function extractPackageName(absolutePath: string): string | null {
|
|
|
62
65
|
*/
|
|
63
66
|
export function clientRefDedup(): Plugin {
|
|
64
67
|
let clientExclude: string[] = [];
|
|
68
|
+
const dedupedPackages = new Set<string>();
|
|
65
69
|
|
|
66
70
|
return {
|
|
67
71
|
name: "@rangojs/router:client-ref-dedup",
|
|
@@ -76,6 +80,16 @@ export function clientRefDedup(): Plugin {
|
|
|
76
80
|
clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
|
|
77
81
|
},
|
|
78
82
|
|
|
83
|
+
buildEnd() {
|
|
84
|
+
if (debug && dedupedPackages.size > 0) {
|
|
85
|
+
debug(
|
|
86
|
+
"client-ref-dedup: redirected %d package(s) (%s)",
|
|
87
|
+
dedupedPackages.size,
|
|
88
|
+
[...dedupedPackages].join(","),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
79
93
|
resolveId(source, importer, options) {
|
|
80
94
|
// Only intercept in the client environment
|
|
81
95
|
if (this.environment?.name !== "client") return;
|
|
@@ -95,6 +109,8 @@ export function clientRefDedup(): Plugin {
|
|
|
95
109
|
// Don't redirect packages that are excluded from optimization
|
|
96
110
|
if (clientExclude.includes(packageName)) return;
|
|
97
111
|
|
|
112
|
+
if (debug) dedupedPackages.add(packageName);
|
|
113
|
+
|
|
98
114
|
// Return a virtual module that re-exports via bare specifier
|
|
99
115
|
return `\0rango:dedup/${packageName}`;
|
|
100
116
|
},
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import { relative } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { createRangoDebugger, createCounter, NS } from "../debug.js";
|
|
5
|
+
|
|
6
|
+
const debug = createRangoDebugger(NS.transform);
|
|
4
7
|
|
|
5
8
|
// Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
|
|
6
9
|
const CLIENT_PKG_PROXY_PREFIX =
|
|
@@ -89,6 +92,7 @@ export function transformClientRefs(
|
|
|
89
92
|
* regex replacement of Flight payloads.
|
|
90
93
|
*/
|
|
91
94
|
export function hashClientRefs(projectRoot: string): Plugin {
|
|
95
|
+
const counter = createCounter(debug, "hash-client-refs");
|
|
92
96
|
return {
|
|
93
97
|
name: "@rangojs/router:hash-client-refs",
|
|
94
98
|
// Run after the RSC plugin's transform (default enforce is normal)
|
|
@@ -96,10 +100,18 @@ export function hashClientRefs(projectRoot: string): Plugin {
|
|
|
96
100
|
applyToEnvironment(env) {
|
|
97
101
|
return env.name === "rsc";
|
|
98
102
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
buildEnd() {
|
|
104
|
+
counter?.flush();
|
|
105
|
+
},
|
|
106
|
+
transform(code, id) {
|
|
107
|
+
const start = counter ? performance.now() : 0;
|
|
108
|
+
try {
|
|
109
|
+
const result = transformClientRefs(code, projectRoot);
|
|
110
|
+
if (result === null) return;
|
|
111
|
+
return { code: result, map: null };
|
|
112
|
+
} finally {
|
|
113
|
+
counter?.record(id, performance.now() - start);
|
|
114
|
+
}
|
|
103
115
|
},
|
|
104
116
|
};
|
|
105
117
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface LoaderResolveContext {
|
|
2
|
+
parentURL?: string;
|
|
3
|
+
conditions?: readonly string[];
|
|
4
|
+
importAttributes?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LoaderResolveResult {
|
|
8
|
+
shortCircuit?: boolean;
|
|
9
|
+
url: string;
|
|
10
|
+
format?: "module" | "commonjs" | "json" | "wasm" | null;
|
|
11
|
+
importAttributes?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type NextResolve = (
|
|
15
|
+
specifier: string,
|
|
16
|
+
context?: LoaderResolveContext,
|
|
17
|
+
) => Promise<LoaderResolveResult>;
|
|
18
|
+
|
|
19
|
+
export function resolve(
|
|
20
|
+
specifier: string,
|
|
21
|
+
context: LoaderResolveContext,
|
|
22
|
+
nextResolve: NextResolve,
|
|
23
|
+
): Promise<LoaderResolveResult>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Node ESM loader hook that resolves `cloudflare:*` imports to the same
|
|
2
|
+
// stub ESM the Vite transform produces for rewritten specifiers.
|
|
3
|
+
//
|
|
4
|
+
// Why both? The Vite transform (cloudflare-protocol-stub.ts) catches
|
|
5
|
+
// imports in modules that flow through Vite's plugin pipeline — covers
|
|
6
|
+
// user source and any node_modules package Vite fetches and transforms.
|
|
7
|
+
// But Vite/Rollup externalize certain packages (e.g. `partyserver`,
|
|
8
|
+
// which has `import { DurableObject, env } from "cloudflare:workers"`
|
|
9
|
+
// at its top level, and similar "workerd-native" libraries). Externalized
|
|
10
|
+
// modules bypass the transform: Rollup hands their resolution to Node's
|
|
11
|
+
// native ESM loader, which rejects URL-scheme specifiers. This loader
|
|
12
|
+
// hook registers via `module.register()` from `createTempRscServer` and
|
|
13
|
+
// intercepts `cloudflare:*` at Node's resolve layer — before the default
|
|
14
|
+
// loader throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
15
|
+
//
|
|
16
|
+
// Lifecycle: the hook runs in a dedicated worker thread (Node ESM loader
|
|
17
|
+
// architecture) with its own globalThis. It cannot see the main thread's
|
|
18
|
+
// `__rango_build_env__` bridge, so the `env` export here is always `{}`.
|
|
19
|
+
// That's fine in practice — externalized libraries don't typically touch
|
|
20
|
+
// `env` at module top level; they read it at request time in workerd
|
|
21
|
+
// where the real module exists. Build-time prerender handlers in user
|
|
22
|
+
// source DO read `env`, but they flow through the Vite transform (which
|
|
23
|
+
// does bridge `env` from `getPlatformProxy()`), not through this loader.
|
|
24
|
+
//
|
|
25
|
+
// Keep STUBS in sync with cloudflare-protocol-stub.ts — both paths need
|
|
26
|
+
// to hand out the same base classes.
|
|
27
|
+
|
|
28
|
+
const CF_PREFIX = "cloudflare:";
|
|
29
|
+
|
|
30
|
+
const STUBS = {
|
|
31
|
+
"cloudflare:workers": `
|
|
32
|
+
export class DurableObject { constructor(_ctx, _env) {} }
|
|
33
|
+
export class WorkerEntrypoint { constructor(_ctx, _env) {} }
|
|
34
|
+
export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
|
|
35
|
+
export class RpcTarget {}
|
|
36
|
+
export const env = {};
|
|
37
|
+
export default {};
|
|
38
|
+
`,
|
|
39
|
+
"cloudflare:email": `
|
|
40
|
+
export class EmailMessage { constructor(_from, _to, _raw) {} }
|
|
41
|
+
export default {};
|
|
42
|
+
`,
|
|
43
|
+
"cloudflare:sockets": `
|
|
44
|
+
export function connect() { return {}; }
|
|
45
|
+
export default {};
|
|
46
|
+
`,
|
|
47
|
+
"cloudflare:workflows": `
|
|
48
|
+
export class NonRetryableError extends Error {
|
|
49
|
+
constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
|
|
50
|
+
}
|
|
51
|
+
export default {};
|
|
52
|
+
`,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Policy: unknown `cloudflare:*` specifiers resolve permissively to an
|
|
56
|
+
// empty default export rather than throwing. Same reasoning as
|
|
57
|
+
// cloudflare-protocol-stub.ts's FALLBACK_STUB — we prioritize
|
|
58
|
+
// dependency-graph resilience over strict validation, because third-party
|
|
59
|
+
// packages can pull `cloudflare:*` modules we haven't curated.
|
|
60
|
+
const FALLBACK_STUB = `export default {};\n`;
|
|
61
|
+
|
|
62
|
+
function dataUrlFor(specifier) {
|
|
63
|
+
const body = STUBS[specifier] ?? FALLBACK_STUB;
|
|
64
|
+
return "data:text/javascript;base64," + Buffer.from(body).toString("base64");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
68
|
+
if (specifier.startsWith(CF_PREFIX)) {
|
|
69
|
+
return {
|
|
70
|
+
shortCircuit: true,
|
|
71
|
+
url: dataUrlFor(specifier),
|
|
72
|
+
format: "module",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return nextResolve(specifier, context);
|
|
76
|
+
}
|