@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379
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 +46 -12
- package/dist/bin/rango.js +109 -15
- package/dist/vite/index.js +323 -121
- package/package.json +15 -16
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/loader/SKILL.md +55 -15
- package/skills/prerender/SKILL.md +2 -2
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +3 -4
- package/skills/router-setup/SKILL.md +8 -3
- package/skills/typesafety/SKILL.md +25 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +95 -5
- package/src/browser/navigation-client.ts +97 -72
- package/src/browser/prefetch/cache.ts +112 -25
- package/src/browser/prefetch/fetch.ts +28 -30
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/react/Link.tsx +19 -7
- package/src/browser/rsc-router.tsx +11 -2
- package/src/browser/server-action-bridge.ts +448 -432
- package/src/browser/types.ts +24 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/router-processing.ts +125 -15
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +1 -46
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +5 -36
- package/src/index.ts +32 -66
- package/src/prerender/store.ts +56 -15
- package/src/route-definition/index.ts +0 -3
- package/src/router/handler-context.ts +30 -3
- package/src/router/loader-resolution.ts +1 -1
- package/src/router/match-api.ts +1 -1
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +53 -10
- package/src/router/middleware.ts +170 -81
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/router-interfaces.ts +14 -1
- package/src/router/router-options.ts +13 -8
- package/src/router/segment-resolution/fresh.ts +18 -0
- package/src/router/segment-resolution/helpers.ts +1 -1
- package/src/router/segment-resolution/revalidation.ts +22 -9
- package/src/router/trie-matching.ts +20 -2
- package/src/router.ts +29 -9
- package/src/rsc/handler.ts +106 -11
- package/src/rsc/index.ts +0 -20
- package/src/rsc/progressive-enhancement.ts +21 -8
- package/src/rsc/rsc-rendering.ts +30 -43
- package/src/rsc/server-action.ts +14 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +2 -0
- package/src/search-params.ts +16 -13
- package/src/server/context.ts +8 -2
- package/src/server/request-context.ts +38 -16
- package/src/server.ts +6 -0
- package/src/theme/index.ts +4 -13
- package/src/types/handler-context.ts +12 -16
- package/src/types/route-config.ts +17 -8
- package/src/types/segments.ts +0 -5
- package/src/vite/discovery/bundle-postprocess.ts +31 -56
- package/src/vite/discovery/discover-routers.ts +18 -4
- package/src/vite/discovery/prerender-collection.ts +34 -14
- package/src/vite/discovery/state.ts +4 -7
- package/src/vite/index.ts +4 -3
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/rango.ts +11 -0
- package/src/vite/router-discovery.ts +16 -0
- package/src/vite/utils/prerender-utils.ts +60 -0
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -228,9 +228,17 @@ export type HandlerContext<
|
|
|
228
228
|
*/
|
|
229
229
|
pathname: string;
|
|
230
230
|
/**
|
|
231
|
-
* The full URL object (with
|
|
231
|
+
* The full URL object (with internal `_rsc*` params stripped).
|
|
232
|
+
* Use this for application logic — routing, link generation, display.
|
|
232
233
|
*/
|
|
233
234
|
url: URL;
|
|
235
|
+
/**
|
|
236
|
+
* The original request URL with all parameters intact, including
|
|
237
|
+
* internal `_rsc*` transport params. Use `ctx.url` for application
|
|
238
|
+
* logic — this is only needed for advanced cases like debugging
|
|
239
|
+
* or custom cache keying.
|
|
240
|
+
*/
|
|
241
|
+
originalUrl: URL;
|
|
234
242
|
/**
|
|
235
243
|
* Platform bindings (DB, KV, secrets, etc.).
|
|
236
244
|
* Access resources like `ctx.env.DB`, `ctx.env.KV`.
|
|
@@ -267,21 +275,7 @@ export type HandlerContext<
|
|
|
267
275
|
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
268
276
|
} & (<K extends keyof DefaultVars>(key: K, value: DefaultVars[K]) => void);
|
|
269
277
|
/**
|
|
270
|
-
*
|
|
271
|
-
* Headers set here are merged into the final response.
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* ```typescript
|
|
275
|
-
* route("product", (ctx) => {
|
|
276
|
-
* ctx.res.headers.set("Cache-Control", "s-maxage=60");
|
|
277
|
-
* return <ProductPage />;
|
|
278
|
-
* });
|
|
279
|
-
* ```
|
|
280
|
-
*/
|
|
281
|
-
res: Response;
|
|
282
|
-
/**
|
|
283
|
-
* Shorthand for ctx.res.headers - response headers.
|
|
284
|
-
* Headers set here are merged into the final response.
|
|
278
|
+
* Response headers. Headers set here are merged into the final response.
|
|
285
279
|
*
|
|
286
280
|
* @example
|
|
287
281
|
* ```typescript
|
|
@@ -436,6 +430,8 @@ export type InternalHandlerContext<
|
|
|
436
430
|
TEnv = DefaultEnv,
|
|
437
431
|
TSearch extends SearchSchema = {},
|
|
438
432
|
> = HandlerContext<TParams, TEnv, TSearch> & {
|
|
433
|
+
/** @internal Stub response for collecting headers/cookies. */
|
|
434
|
+
res: Response;
|
|
439
435
|
/** Prerender-only control flow helper, attached when the runtime context supports it. */
|
|
440
436
|
passthrough?: () => unknown;
|
|
441
437
|
/** Current segment ID for handle data attribution. */
|
|
@@ -24,17 +24,26 @@ type ParseConstraint<T extends string> =
|
|
|
24
24
|
* - :param(a|b)? -> { name: "param", optional: true, type: "a" | "b" }
|
|
25
25
|
*/
|
|
26
26
|
type ExtractParamInfo<T extends string> =
|
|
27
|
-
// Optional + constrained: :param(a|b)?
|
|
28
|
-
T extends `${infer Name}(${infer Constraint})
|
|
27
|
+
// Optional + constrained (with optional suffix): :param(a|b)?suffix
|
|
28
|
+
T extends `${infer Name}(${infer Constraint})?${string}`
|
|
29
29
|
? { name: Name; optional: true; type: ParseConstraint<Constraint> }
|
|
30
|
-
: // Constrained
|
|
31
|
-
T extends `${infer Name}(${infer Constraint})`
|
|
30
|
+
: // Constrained (with optional suffix): :param(a|b)suffix
|
|
31
|
+
T extends `${infer Name}(${infer Constraint})${string}`
|
|
32
32
|
? { name: Name; optional: false; type: ParseConstraint<Constraint> }
|
|
33
|
-
: // Optional
|
|
34
|
-
T extends `${infer Name}
|
|
33
|
+
: // Optional (with optional suffix): :param?suffix
|
|
34
|
+
T extends `${infer Name}?${string}`
|
|
35
35
|
? { name: Name; optional: true; type: string }
|
|
36
|
-
: //
|
|
37
|
-
|
|
36
|
+
: // Param with dot-suffix: :param.html
|
|
37
|
+
T extends `${infer Name}.${string}`
|
|
38
|
+
? { name: Name; optional: false; type: string }
|
|
39
|
+
: // Param with dash-suffix: :param-slug
|
|
40
|
+
T extends `${infer Name}-${string}`
|
|
41
|
+
? { name: Name; optional: false; type: string }
|
|
42
|
+
: // Param with tilde-suffix: :param~v2
|
|
43
|
+
T extends `${infer Name}~${string}`
|
|
44
|
+
? { name: Name; optional: false; type: string }
|
|
45
|
+
: // Required: :param (no suffix)
|
|
46
|
+
{ name: T; optional: false; type: string };
|
|
38
47
|
|
|
39
48
|
/**
|
|
40
49
|
* Build param object from info
|
package/src/types/segments.ts
CHANGED
|
@@ -124,11 +124,6 @@ export interface MatchResult {
|
|
|
124
124
|
* Used by ctx.reverse() for local name resolution.
|
|
125
125
|
*/
|
|
126
126
|
routeName?: string;
|
|
127
|
-
/**
|
|
128
|
-
* Server-Timing header value (only present when debugPerformance is enabled)
|
|
129
|
-
* Can be added to response headers for DevTools integration
|
|
130
|
-
*/
|
|
131
|
-
serverTiming?: string;
|
|
132
127
|
/**
|
|
133
128
|
* State of named slots for this route match
|
|
134
129
|
* Key is slot name (e.g., "@modal"), value is slot state
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
11
10
|
import { evictHandlerCode } from "../utils/bundle-analysis.js";
|
|
11
|
+
import { copyStagedBuildAssets } from "../utils/prerender-utils.js";
|
|
12
12
|
import type { DiscoveryState } from "./state.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -17,11 +17,11 @@ import type { DiscoveryState } from "./state.js";
|
|
|
17
17
|
*/
|
|
18
18
|
export function postprocessBundle(state: DiscoveryState): void {
|
|
19
19
|
const hasPrerenderData =
|
|
20
|
-
state.
|
|
21
|
-
Object.keys(state.
|
|
20
|
+
state.prerenderManifestEntries &&
|
|
21
|
+
Object.keys(state.prerenderManifestEntries).length > 0;
|
|
22
22
|
const hasStaticData =
|
|
23
|
-
state.
|
|
24
|
-
Object.keys(state.
|
|
23
|
+
state.staticManifestEntries &&
|
|
24
|
+
Object.keys(state.staticManifestEntries).length > 0;
|
|
25
25
|
if (!hasPrerenderData && !hasStaticData) return;
|
|
26
26
|
|
|
27
27
|
// Find RSC entry (recorded in generateBundle, fallback to dist/rsc/index.js)
|
|
@@ -88,39 +88,30 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
88
88
|
state.staticHandlerChunkInfo = null;
|
|
89
89
|
|
|
90
90
|
// 2. Write prerender data as separate importable asset modules
|
|
91
|
-
// and inject a manifest
|
|
91
|
+
// and inject a lazy manifest loader into the RSC entry.
|
|
92
92
|
if (hasPrerenderData && existsSync(rscEntryPath)) {
|
|
93
93
|
const rscCode = readFileSync(rscEntryPath, "utf-8");
|
|
94
|
-
// Check for the specific injection marker
|
|
95
|
-
// The runtime code (prerender store) also references __PRERENDER_MANIFEST,
|
|
96
|
-
// so a broad string check would false-positive and skip injection.
|
|
94
|
+
// Check for the specific injection marker to avoid double-injection.
|
|
97
95
|
if (!rscCode.includes("__prerender-manifest.js")) {
|
|
98
96
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let totalBytes = 0;
|
|
97
|
+
let totalBytes = copyStagedBuildAssets(
|
|
98
|
+
state.projectRoot,
|
|
99
|
+
Object.values(state.prerenderManifestEntries!),
|
|
100
|
+
);
|
|
104
101
|
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
const manifestMap: Record<string, string> = {};
|
|
103
|
+
for (const [key, assetFileName] of Object.entries(
|
|
104
|
+
state.prerenderManifestEntries!,
|
|
107
105
|
)) {
|
|
108
|
-
|
|
109
|
-
const contentHash = createHash("sha256")
|
|
110
|
-
.update(entryJson)
|
|
111
|
-
.digest("hex")
|
|
112
|
-
.slice(0, 8);
|
|
113
|
-
const assetFileName = `__pr-${contentHash}.js`;
|
|
114
|
-
const assetPath = resolve(assetsDir, assetFileName);
|
|
115
|
-
const assetCode = `export default ${entryJson};\n`;
|
|
116
|
-
writeFileSync(assetPath, assetCode);
|
|
117
|
-
totalBytes += Buffer.byteLength(assetCode);
|
|
118
|
-
manifestEntries.push(
|
|
119
|
-
`${JSON.stringify(key)}:()=>import("./assets/${assetFileName}")`,
|
|
120
|
-
);
|
|
106
|
+
manifestMap[key] = `./assets/${assetFileName}`;
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
const manifestCode =
|
|
109
|
+
const manifestCode = [
|
|
110
|
+
`const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
|
|
111
|
+
`export function loadPrerenderAsset(s){return import(s)}`,
|
|
112
|
+
`export default m;`,
|
|
113
|
+
"",
|
|
114
|
+
].join("\n");
|
|
124
115
|
const manifestPath = resolve(
|
|
125
116
|
state.projectRoot,
|
|
126
117
|
"dist/rsc/__prerender-manifest.js",
|
|
@@ -128,12 +119,12 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
128
119
|
writeFileSync(manifestPath, manifestCode);
|
|
129
120
|
totalBytes += Buffer.byteLength(manifestCode);
|
|
130
121
|
|
|
131
|
-
const injection = `
|
|
122
|
+
const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");\n`;
|
|
132
123
|
writeFileSync(rscEntryPath, injection + rscCode);
|
|
133
124
|
|
|
134
125
|
const totalKB = (totalBytes / 1024).toFixed(1);
|
|
135
126
|
console.log(
|
|
136
|
-
`[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.
|
|
127
|
+
`[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
|
|
137
128
|
);
|
|
138
129
|
} catch (err: any) {
|
|
139
130
|
throw new Error(
|
|
@@ -149,31 +140,15 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
149
140
|
const rscCode = readFileSync(rscEntryPath, "utf-8");
|
|
150
141
|
if (!rscCode.includes("__STATIC_MANIFEST")) {
|
|
151
142
|
try {
|
|
152
|
-
const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
|
|
153
|
-
mkdirSync(assetsDir, { recursive: true });
|
|
154
|
-
|
|
155
143
|
const manifestEntries: string[] = [];
|
|
156
|
-
let totalBytes =
|
|
144
|
+
let totalBytes = copyStagedBuildAssets(
|
|
145
|
+
state.projectRoot,
|
|
146
|
+
Object.values(state.staticManifestEntries!),
|
|
147
|
+
);
|
|
157
148
|
|
|
158
|
-
for (const [handlerId,
|
|
159
|
-
state.
|
|
149
|
+
for (const [handlerId, assetFileName] of Object.entries(
|
|
150
|
+
state.staticManifestEntries!,
|
|
160
151
|
)) {
|
|
161
|
-
// Store both the Flight payload and handle data
|
|
162
|
-
const hasHandles = Object.keys(handles).length > 0;
|
|
163
|
-
const exportValue = hasHandles
|
|
164
|
-
? JSON.stringify({ encoded, handles })
|
|
165
|
-
: JSON.stringify(encoded);
|
|
166
|
-
// Hash the full payload that is written so distinct handle
|
|
167
|
-
// snapshots produce distinct asset filenames.
|
|
168
|
-
const contentHash = createHash("sha256")
|
|
169
|
-
.update(exportValue)
|
|
170
|
-
.digest("hex")
|
|
171
|
-
.slice(0, 8);
|
|
172
|
-
const assetFileName = `__st-${contentHash}.js`;
|
|
173
|
-
const assetPath = resolve(assetsDir, assetFileName);
|
|
174
|
-
const assetCode = `export default ${exportValue};\n`;
|
|
175
|
-
writeFileSync(assetPath, assetCode);
|
|
176
|
-
totalBytes += Buffer.byteLength(assetCode);
|
|
177
152
|
manifestEntries.push(
|
|
178
153
|
`${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`,
|
|
179
154
|
);
|
|
@@ -197,7 +172,7 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
197
172
|
|
|
198
173
|
const totalKB = (totalBytes / 1024).toFixed(1);
|
|
199
174
|
console.log(
|
|
200
|
-
`[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.
|
|
175
|
+
`[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
|
|
201
176
|
);
|
|
202
177
|
} catch (err: any) {
|
|
203
178
|
throw new Error(
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* router, and builds route tries for O(path_length) matching.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildCombinedRouteMapForRouterFile,
|
|
11
|
+
formatNestedRouterConflictError,
|
|
12
|
+
findNestedRouterConflict,
|
|
13
|
+
} from "../../build/generate-route-types.js";
|
|
10
14
|
import {
|
|
11
15
|
flattenLeafEntries,
|
|
12
16
|
buildRouteToStaticPrefix,
|
|
@@ -44,9 +48,8 @@ export async function discoverRouters(
|
|
|
44
48
|
// No RSC routers found directly. Check for host routers with lazy handlers
|
|
45
49
|
// that need to be resolved to trigger sub-app createRouter() calls.
|
|
46
50
|
try {
|
|
47
|
-
const hostMod = await rscEnv.runner.import("@rangojs/router/host");
|
|
48
51
|
const hostRegistry: Map<string, any> | undefined =
|
|
49
|
-
|
|
52
|
+
serverMod.HostRouterRegistry;
|
|
50
53
|
|
|
51
54
|
if (hostRegistry && hostRegistry.size > 0) {
|
|
52
55
|
console.log(
|
|
@@ -85,7 +88,7 @@ export async function discoverRouters(
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
} catch {
|
|
88
|
-
//
|
|
91
|
+
// Host-router discovery is best-effort; skip if unavailable
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
// If still no routers after host router resolution, fail
|
|
@@ -100,6 +103,17 @@ export async function discoverRouters(
|
|
|
100
103
|
const buildMod = await rscEnv.runner.import("@rangojs/router/build");
|
|
101
104
|
const generateManifestFull = buildMod.generateManifestFull;
|
|
102
105
|
|
|
106
|
+
const nestedRouterConflict = findNestedRouterConflict(
|
|
107
|
+
[...registry.values()]
|
|
108
|
+
.map((router) => router.__sourceFile)
|
|
109
|
+
.filter(
|
|
110
|
+
(sourceFile): sourceFile is string => typeof sourceFile === "string",
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
if (nestedRouterConflict) {
|
|
114
|
+
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
115
|
+
}
|
|
116
|
+
|
|
103
117
|
// Build into local variables first. Only commit to state after the
|
|
104
118
|
// full pass succeeds, so a failed re-discovery preserves the last
|
|
105
119
|
// known-good state instead of leaving it partially wiped.
|
|
@@ -13,12 +13,14 @@ import {
|
|
|
13
13
|
runWithConcurrency,
|
|
14
14
|
groupByConcurrency,
|
|
15
15
|
notifyOnError,
|
|
16
|
+
stageBuildAssetModule,
|
|
16
17
|
} from "../utils/prerender-utils.js";
|
|
17
18
|
import type { DiscoveryState } from "./state.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Expand prerender routes into concrete URLs and render them via the
|
|
21
|
-
* RSC runner.
|
|
22
|
+
* RSC runner. Stages asset modules and stores key-to-file entries in
|
|
23
|
+
* state.prerenderManifestEntries.
|
|
22
24
|
*/
|
|
23
25
|
export async function expandPrerenderRoutes(
|
|
24
26
|
state: DiscoveryState,
|
|
@@ -150,7 +152,7 @@ export async function expandPrerenderRoutes(
|
|
|
150
152
|
|
|
151
153
|
const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
|
|
152
154
|
|
|
153
|
-
const
|
|
155
|
+
const manifestEntries: Record<string, string> = {};
|
|
154
156
|
let doneCount = 0;
|
|
155
157
|
let skipCount = 0;
|
|
156
158
|
const startTotal = performance.now();
|
|
@@ -187,18 +189,30 @@ export async function expandPrerenderRoutes(
|
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
const paramHash = hashParams(result.params || {});
|
|
190
|
-
|
|
192
|
+
const mainKey = `${result.routeName}/${paramHash}`;
|
|
193
|
+
const mainValue = JSON.stringify({
|
|
191
194
|
segments: result.segments,
|
|
192
195
|
handles: result.handles,
|
|
193
|
-
};
|
|
196
|
+
});
|
|
197
|
+
manifestEntries[mainKey] = stageBuildAssetModule(
|
|
198
|
+
state.projectRoot,
|
|
199
|
+
"__pr",
|
|
200
|
+
mainValue,
|
|
201
|
+
);
|
|
194
202
|
if (result.interceptSegments?.length) {
|
|
195
|
-
|
|
203
|
+
const interceptKey = `${result.routeName}/${paramHash}/i`;
|
|
204
|
+
const interceptValue = JSON.stringify({
|
|
196
205
|
segments: [...result.segments, ...result.interceptSegments],
|
|
197
206
|
handles: {
|
|
198
207
|
...result.handles,
|
|
199
208
|
...(result.interceptHandles || {}),
|
|
200
209
|
},
|
|
201
|
-
};
|
|
210
|
+
});
|
|
211
|
+
manifestEntries[interceptKey] = stageBuildAssetModule(
|
|
212
|
+
state.projectRoot,
|
|
213
|
+
"__pr",
|
|
214
|
+
interceptValue,
|
|
215
|
+
);
|
|
202
216
|
}
|
|
203
217
|
const elapsed = (performance.now() - startUrl).toFixed(0);
|
|
204
218
|
console.log(
|
|
@@ -244,7 +258,7 @@ export async function expandPrerenderRoutes(
|
|
|
244
258
|
|
|
245
259
|
const totalElapsed = (performance.now() - startTotal).toFixed(0);
|
|
246
260
|
if (doneCount > 0) {
|
|
247
|
-
state.
|
|
261
|
+
state.prerenderManifestEntries = manifestEntries;
|
|
248
262
|
}
|
|
249
263
|
const parts = [`${doneCount} done`];
|
|
250
264
|
if (skipCount > 0) parts.push(`${skipCount} skipped`);
|
|
@@ -256,7 +270,8 @@ export async function expandPrerenderRoutes(
|
|
|
256
270
|
/**
|
|
257
271
|
* Render Static handlers at build time. Each Static handler is called
|
|
258
272
|
* with a synthetic BuildContext and its output is RSC-serialized.
|
|
259
|
-
*
|
|
273
|
+
* Stages asset modules and stores handlerId-to-file entries in
|
|
274
|
+
* state.staticManifestEntries.
|
|
260
275
|
*/
|
|
261
276
|
export async function renderStaticHandlers(
|
|
262
277
|
state: DiscoveryState,
|
|
@@ -270,10 +285,7 @@ export async function renderStaticHandlers(
|
|
|
270
285
|
)
|
|
271
286
|
return;
|
|
272
287
|
|
|
273
|
-
const
|
|
274
|
-
string,
|
|
275
|
-
{ encoded: string; handles: Record<string, unknown[]> }
|
|
276
|
-
> = {};
|
|
288
|
+
const manifestEntries: Record<string, string> = {};
|
|
277
289
|
let staticDone = 0;
|
|
278
290
|
let staticSkip = 0;
|
|
279
291
|
let totalStaticCount = 0;
|
|
@@ -316,7 +328,15 @@ export async function renderStaticHandlers(
|
|
|
316
328
|
(def as any).$$routePrefix,
|
|
317
329
|
);
|
|
318
330
|
if (result) {
|
|
319
|
-
|
|
331
|
+
const hasHandles = Object.keys(result.handles).length > 0;
|
|
332
|
+
const exportValue = hasHandles
|
|
333
|
+
? JSON.stringify(result)
|
|
334
|
+
: JSON.stringify(result.encoded);
|
|
335
|
+
manifestEntries[def.$$id] = stageBuildAssetModule(
|
|
336
|
+
state.projectRoot,
|
|
337
|
+
"__st",
|
|
338
|
+
exportValue,
|
|
339
|
+
);
|
|
320
340
|
const elapsed = (performance.now() - startHandler).toFixed(0);
|
|
321
341
|
console.log(
|
|
322
342
|
`[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
|
|
@@ -355,7 +375,7 @@ export async function renderStaticHandlers(
|
|
|
355
375
|
|
|
356
376
|
const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
|
|
357
377
|
if (staticDone > 0) {
|
|
358
|
-
state.
|
|
378
|
+
state.staticManifestEntries = manifestEntries;
|
|
359
379
|
}
|
|
360
380
|
const staticParts = [`${staticDone} done`];
|
|
361
381
|
if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
|
|
@@ -56,11 +56,8 @@ export interface DiscoveryState {
|
|
|
56
56
|
perRouterPrecomputedMap: Map<string, PrecomputedEntry[]>;
|
|
57
57
|
perRouterManifestDataMap: Map<string, Record<string, string>>;
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
string,
|
|
62
|
-
{ encoded: string; handles: Record<string, unknown[]> }
|
|
63
|
-
> | null;
|
|
59
|
+
prerenderManifestEntries: Record<string, string> | null;
|
|
60
|
+
staticManifestEntries: Record<string, string> | null;
|
|
64
61
|
handlerChunkInfo: ChunkInfo | null;
|
|
65
62
|
staticHandlerChunkInfo: ChunkInfo | null;
|
|
66
63
|
rscEntryFileName: string | null;
|
|
@@ -96,8 +93,8 @@ export function createDiscoveryState(
|
|
|
96
93
|
perRouterPrecomputedMap: new Map(),
|
|
97
94
|
perRouterManifestDataMap: new Map(),
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
prerenderManifestEntries: null,
|
|
97
|
+
staticManifestEntries: null,
|
|
101
98
|
handlerChunkInfo: null,
|
|
102
99
|
staticHandlerChunkInfo: null,
|
|
103
100
|
rscEntryFileName: null,
|
package/src/vite/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public API for @rangojs/router/vite
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* consumed via direct imports within the package.
|
|
4
|
+
* Exports: rango() plugin factory, poke() dev utility plugin,
|
|
5
|
+
* and related option types. All other utilities are internal implementation
|
|
6
|
+
* details consumed via direct imports within the package.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { rango } from "./rango.js";
|
|
10
|
+
export { poke } from "./plugins/refresh-cmd.js";
|
|
10
11
|
|
|
11
12
|
export type {
|
|
12
13
|
RangoNodeOptions,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
const CLIENT_IN_SERVER_PROXY_PREFIX =
|
|
4
|
+
"virtual:vite-rsc/client-in-server-package-proxy/";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract the bare package name from an absolute node_modules path.
|
|
8
|
+
* Handles scoped packages (@org/name) and nested node_modules.
|
|
9
|
+
* Returns null if the path doesn't contain a valid package reference.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This is a lossy transformation. It maps a specific submodule path
|
|
12
|
+
* (e.g., pkg/internal/context.js) to the package root (pkg). The load()
|
|
13
|
+
* hook then re-exports via the bare specifier, which resolves to the
|
|
14
|
+
* package entry point. This works for packages that barrel-export their
|
|
15
|
+
* "use client" symbols from the root, which covers the common case
|
|
16
|
+
* (component libraries like @mantine/core, @chakra-ui/react, etc.).
|
|
17
|
+
* Packages whose client symbols are only available from deep subpaths
|
|
18
|
+
* (not re-exported from the root) would lose those symbols after the
|
|
19
|
+
* rewrite. A more precise approach would resolve through the package's
|
|
20
|
+
* exports map to find the correct entry point, but that adds significant
|
|
21
|
+
* complexity for a rare edge case.
|
|
22
|
+
* See: https://github.com/cloudflare/vinext/pull/413
|
|
23
|
+
*/
|
|
24
|
+
export function extractPackageName(absolutePath: string): string | null {
|
|
25
|
+
// Find the last /node_modules/ segment (handles nested node_modules)
|
|
26
|
+
const marker = "/node_modules/";
|
|
27
|
+
const idx = absolutePath.lastIndexOf(marker);
|
|
28
|
+
if (idx === -1) return null;
|
|
29
|
+
|
|
30
|
+
const afterModules = absolutePath.slice(idx + marker.length);
|
|
31
|
+
|
|
32
|
+
if (afterModules.startsWith("@")) {
|
|
33
|
+
// Scoped package: @org/name
|
|
34
|
+
const parts = afterModules.split("/");
|
|
35
|
+
if (parts.length < 2 || !parts[1]) return null;
|
|
36
|
+
return `${parts[0]}/${parts[1]}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Unscoped package: name
|
|
40
|
+
const name = afterModules.split("/")[0];
|
|
41
|
+
return name || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Vite plugin that deduplicates client references from third-party packages
|
|
46
|
+
* in dev mode.
|
|
47
|
+
*
|
|
48
|
+
* When @vitejs/plugin-rsc encounters a "use client" submodule inside a
|
|
49
|
+
* package imported from a server component, it creates a
|
|
50
|
+
* client-in-server-package-proxy virtual module that re-exports from the
|
|
51
|
+
* absolute file path. In the client environment, this absolute path bypasses
|
|
52
|
+
* Vite's pre-bundling, while direct client imports of the same package go
|
|
53
|
+
* through .vite/deps/. Two separate module instances are created, breaking
|
|
54
|
+
* React contexts (createContext runs twice, provider/consumer mismatch).
|
|
55
|
+
*
|
|
56
|
+
* This plugin intercepts absolute node_modules imports from proxy modules
|
|
57
|
+
* in the client environment and rewrites them to bare specifier imports
|
|
58
|
+
* that go through pre-bundling, ensuring a single module instance.
|
|
59
|
+
*
|
|
60
|
+
* Dev-only: production builds use the SSR manifest which handles module
|
|
61
|
+
* identity correctly.
|
|
62
|
+
*/
|
|
63
|
+
export function clientRefDedup(): Plugin {
|
|
64
|
+
let clientExclude: string[] = [];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
name: "@rangojs/router:client-ref-dedup",
|
|
68
|
+
enforce: "pre",
|
|
69
|
+
apply: "serve",
|
|
70
|
+
|
|
71
|
+
configResolved(config: ResolvedConfig) {
|
|
72
|
+
// Respect user's optimizeDeps.exclude — if a package is explicitly
|
|
73
|
+
// excluded from pre-bundling, we shouldn't redirect it there.
|
|
74
|
+
const clientEnv = config.environments?.["client"];
|
|
75
|
+
clientExclude =
|
|
76
|
+
clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
resolveId(source, importer, options) {
|
|
80
|
+
// Only intercept in the client environment
|
|
81
|
+
if (this.environment?.name !== "client") return;
|
|
82
|
+
|
|
83
|
+
// Only handle imports from client-in-server-package-proxy virtual modules
|
|
84
|
+
if (!importer?.includes(CLIENT_IN_SERVER_PROXY_PREFIX)) return;
|
|
85
|
+
|
|
86
|
+
// Only handle absolute node_modules paths
|
|
87
|
+
if (!source.includes("/node_modules/")) return;
|
|
88
|
+
|
|
89
|
+
// Must have an importer
|
|
90
|
+
if (!importer) return;
|
|
91
|
+
|
|
92
|
+
const packageName = extractPackageName(source);
|
|
93
|
+
if (!packageName) return;
|
|
94
|
+
|
|
95
|
+
// Don't redirect packages that are excluded from optimization
|
|
96
|
+
if (clientExclude.includes(packageName)) return;
|
|
97
|
+
|
|
98
|
+
// Return a virtual module that re-exports via bare specifier
|
|
99
|
+
return `\0rango:dedup/${packageName}`;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
load(id) {
|
|
103
|
+
if (!id.startsWith("\0rango:dedup/")) return;
|
|
104
|
+
|
|
105
|
+
const packageName = id.slice("\0rango:dedup/".length);
|
|
106
|
+
|
|
107
|
+
// Re-export via bare specifier so Vite routes through pre-bundling
|
|
108
|
+
return [
|
|
109
|
+
`export * from ${JSON.stringify(packageName)};`,
|
|
110
|
+
`import * as __all__ from ${JSON.stringify(packageName)};`,
|
|
111
|
+
`export default __all__.default;`,
|
|
112
|
+
].join("\n");
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin that triggers a full browser reload when Ctrl+R is pressed
|
|
5
|
+
* in the terminal running the dev server.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { poke } from "@rangojs/router/vite";
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* plugins: [rango(), poke()],
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function poke(): Plugin {
|
|
17
|
+
return {
|
|
18
|
+
name: "vite-plugin-poke",
|
|
19
|
+
apply: "serve",
|
|
20
|
+
|
|
21
|
+
configureServer(server) {
|
|
22
|
+
const stdin = process.stdin;
|
|
23
|
+
|
|
24
|
+
// Raw mode delivers individual keystrokes as immediate single-byte
|
|
25
|
+
// events instead of waiting for Enter (cooked/line-buffered mode).
|
|
26
|
+
// Without it, Ctrl+R (0x12) is never delivered as a discrete byte.
|
|
27
|
+
// When stdin is a pipe (CI, spawned process) setRawMode is unavailable
|
|
28
|
+
// but data already arrives unbuffered, so the isTTY guard suffices.
|
|
29
|
+
const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
|
|
30
|
+
if (stdin.isTTY) {
|
|
31
|
+
stdin.setRawMode(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const onData = (data: Buffer) => {
|
|
35
|
+
if (data.length !== 1) return;
|
|
36
|
+
|
|
37
|
+
// Ctrl+C (0x03) — defensive fallback. This plugin enables raw mode
|
|
38
|
+
// before Vite's internal stdin handler is registered (user plugins
|
|
39
|
+
// run first), so there is a brief window where Ctrl+C would be
|
|
40
|
+
// swallowed. Re-emit SIGINT so the process exits as expected.
|
|
41
|
+
if (data[0] === 0x03) {
|
|
42
|
+
process.emit("SIGINT", "SIGINT");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ctrl+R = 0x12 in raw mode
|
|
47
|
+
if (data[0] === 0x12) {
|
|
48
|
+
server.hot.send({ type: "full-reload", path: "*" });
|
|
49
|
+
server.config.logger.info(" browser reload (ctrl+r)", {
|
|
50
|
+
timestamp: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
stdin.on("data", onData);
|
|
56
|
+
|
|
57
|
+
server.httpServer?.on("close", () => {
|
|
58
|
+
stdin.off("data", onData);
|
|
59
|
+
if (stdin.isTTY && previousRawMode !== null) {
|
|
60
|
+
stdin.setRawMode(previousRawMode);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/vite/rango.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
exposeRouterId,
|
|
8
8
|
} from "./plugins/expose-internal-ids.js";
|
|
9
9
|
import { useCacheTransform } from "./plugins/use-cache-transform.js";
|
|
10
|
+
import { clientRefDedup } from "./plugins/client-ref-dedup.js";
|
|
10
11
|
import { VIRTUAL_IDS } from "./plugins/virtual-entries.js";
|
|
11
12
|
import {
|
|
12
13
|
getExcludeDeps,
|
|
@@ -200,6 +201,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
200
201
|
serverHandler: false,
|
|
201
202
|
}) as PluginOption,
|
|
202
203
|
);
|
|
204
|
+
|
|
205
|
+
// Deduplicate client references from third-party packages in dev mode.
|
|
206
|
+
// Prevents module duplication when server components import "use client"
|
|
207
|
+
// packages that are also imported directly by client components.
|
|
208
|
+
plugins.push(clientRefDedup());
|
|
203
209
|
} else {
|
|
204
210
|
// Node preset: full RSC plugin integration
|
|
205
211
|
const nodeOptions = resolvedOptions as RangoNodeOptions;
|
|
@@ -393,6 +399,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
393
399
|
}) as PluginOption,
|
|
394
400
|
);
|
|
395
401
|
}
|
|
402
|
+
|
|
403
|
+
// Deduplicate client references from third-party packages in dev mode.
|
|
404
|
+
// Prevents module duplication when server components import "use client"
|
|
405
|
+
// packages that are also imported directly by client components.
|
|
406
|
+
plugins.push(clientRefDedup());
|
|
396
407
|
}
|
|
397
408
|
|
|
398
409
|
// Fix HMR for "use client" components.
|