@rangojs/router 0.0.0-experimental.115 → 0.0.0-experimental.117
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/vite/index.js +148 -97
- package/package.json +1 -1
- package/skills/api-client/SKILL.md +211 -0
- package/skills/loader/SKILL.md +17 -17
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +61 -43
- package/skills/typesafety/SKILL.md +3 -3
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/browser/navigation-client.ts +56 -68
- package/src/browser/prefetch/cache.ts +58 -27
- package/src/browser/prefetch/fetch.ts +92 -33
- package/src/browser/response-adapter.ts +7 -1
- package/src/browser/rsc-router.tsx +5 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +28 -1
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +43 -0
- package/src/client.tsx +4 -23
- package/src/errors.ts +0 -3
- package/src/href-client.ts +7 -8
- package/src/index.rsc.ts +1 -2
- package/src/index.ts +1 -2
- package/src/router/find-match.ts +54 -6
- package/src/router/lazy-includes.ts +33 -14
- package/src/router/loader-resolution.ts +63 -34
- package/src/router/manifest.ts +19 -6
- package/src/router/pattern-matching.ts +15 -2
- package/src/router/router-interfaces.ts +11 -0
- package/src/router/trie-matching.ts +22 -3
- package/src/router.ts +21 -7
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +16 -13
- package/src/server/context.ts +32 -0
- package/src/server/request-context.ts +47 -9
- package/src/types/loader-types.ts +6 -3
- package/src/urls/index.ts +1 -2
- package/src/urls/type-extraction.ts +33 -24
- package/src/vite/discovery/discover-routers.ts +46 -29
- package/src/vite/discovery/state.ts +7 -0
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/rango.ts +32 -4
- package/src/vite/utils/client-chunks.ts +41 -7
- package/src/vite/utils/manifest-utils.ts +8 -75
- package/src/vite/utils/shared-utils.ts +58 -0
package/src/vite/rango.ts
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
onwarn,
|
|
24
24
|
getManualChunks,
|
|
25
25
|
} from "./utils/shared-utils.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
resolveClientChunks,
|
|
28
|
+
type ClientChunkContext,
|
|
29
|
+
} from "./utils/client-chunks.js";
|
|
27
30
|
import type { RangoOptions } from "./plugin-types.js";
|
|
28
31
|
import { printBanner, rangoVersion } from "./utils/banner.js";
|
|
29
32
|
import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
|
|
@@ -69,9 +72,17 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
69
72
|
// @vitejs/plugin-rsc in both presets. The built-in strategy only splits where it
|
|
70
73
|
// recognizes a route structure, so this default is a no-op for flat / host-split
|
|
71
74
|
// apps and never duplicates the shared runtime.
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
+
const clientChunksOption = resolvedOptions.clientChunks ?? true;
|
|
76
|
+
// Shared context the built-in strategy reads at build time: the production
|
|
77
|
+
// hashes of registered error/notFound fallback modules (-> app-fallback).
|
|
78
|
+
// Populated by the discovery plugin in buildStart, before the client build
|
|
79
|
+
// invokes the strategy. Only wired when the built-in strategy is active; a
|
|
80
|
+
// custom function owns its own grouping.
|
|
81
|
+
const useBuiltInClientChunks = clientChunksOption === true;
|
|
82
|
+
const clientChunkCtx: ClientChunkContext | undefined = useBuiltInClientChunks
|
|
83
|
+
? { fallbackRefs: new Set<string>() }
|
|
84
|
+
: undefined;
|
|
85
|
+
const clientChunks = resolveClientChunks(clientChunksOption, clientChunkCtx);
|
|
75
86
|
debugConfig?.("rango(%s) setup start", preset);
|
|
76
87
|
|
|
77
88
|
const plugins: PluginOption[] = [];
|
|
@@ -157,6 +168,14 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
157
168
|
client: {
|
|
158
169
|
build: {
|
|
159
170
|
rollupOptions: {
|
|
171
|
+
// FILE_NAME_CONFLICT (and any other client-build warning) is
|
|
172
|
+
// emitted by the CLIENT environment build, which consults THIS
|
|
173
|
+
// env's onwarn -- Vite 8's environment builds do NOT propagate
|
|
174
|
+
// the top-level build.rollupOptions.onwarn into the client env.
|
|
175
|
+
// Wire it here so the suppression runs where the conflicts
|
|
176
|
+
// originate (the top-level handler is invoked 0x for these; the
|
|
177
|
+
// client-env handler is invoked for all of them).
|
|
178
|
+
onwarn,
|
|
160
179
|
output: {
|
|
161
180
|
manualChunks: getManualChunks,
|
|
162
181
|
},
|
|
@@ -317,6 +336,14 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
317
336
|
client: {
|
|
318
337
|
build: {
|
|
319
338
|
rollupOptions: {
|
|
339
|
+
// FILE_NAME_CONFLICT (and any other client-build warning) is
|
|
340
|
+
// emitted by the CLIENT environment build, which consults THIS
|
|
341
|
+
// env's onwarn -- Vite 8's environment builds do NOT propagate
|
|
342
|
+
// the top-level build.rollupOptions.onwarn into the client env.
|
|
343
|
+
// Wire it here so the suppression runs where the conflicts
|
|
344
|
+
// originate (the top-level handler is invoked 0x for these; the
|
|
345
|
+
// client-env handler is invoked for all of them).
|
|
346
|
+
onwarn,
|
|
320
347
|
output: {
|
|
321
348
|
manualChunks: getManualChunks,
|
|
322
349
|
},
|
|
@@ -508,6 +535,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
508
535
|
enableBuildPrerender: prerenderEnabled,
|
|
509
536
|
buildEnv: options?.buildEnv,
|
|
510
537
|
preset,
|
|
538
|
+
clientChunkCtx,
|
|
511
539
|
}),
|
|
512
540
|
);
|
|
513
541
|
|
|
@@ -6,10 +6,28 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ClientChunkMeta, ClientChunks } from "../plugin-types.js";
|
|
8
8
|
import { createRangoDebugger, NS } from "../debug.js";
|
|
9
|
+
import { hashRefKey } from "../plugins/client-ref-hashing.js";
|
|
9
10
|
|
|
10
11
|
/** The callback shape @vitejs/plugin-rsc's `clientChunks` option accepts. */
|
|
11
12
|
export type RscClientChunksFn = (meta: ClientChunkMeta) => string | undefined;
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Build-time context the discovery pass populates and the built-in strategy
|
|
16
|
+
* reads. It refines how the catch-all (no route-root marker) modules are grouped
|
|
17
|
+
* without touching marker splits or the shared runtime:
|
|
18
|
+
*
|
|
19
|
+
* - `fallbackRefs`: production hashes of the `"use client"` modules a consumer
|
|
20
|
+
* registered as `errorBoundary`/`notFoundBoundary` fallbacks. Pulled into a
|
|
21
|
+
* dedicated `app-fallback` chunk so the error UI is not co-bundled with the
|
|
22
|
+
* very route code it exists to catch failures for (resilience), and so the
|
|
23
|
+
* chunk it would otherwise sit in gets named after a real module rather than
|
|
24
|
+
* the boundary. Populated by reading each fallback element's client-reference
|
|
25
|
+
* `$$id` during discovery (see discover-routers).
|
|
26
|
+
*/
|
|
27
|
+
export interface ClientChunkContext {
|
|
28
|
+
fallbackRefs: Set<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Opt-in observability for the built-in strategy. The route-root marker list is
|
|
15
33
|
* intentionally finite (see {@link ROUTE_ROOT_DIRS}); a consumer whose layout
|
|
@@ -101,18 +119,31 @@ const ROUTE_ROOT_DIRS = new Set([
|
|
|
101
119
|
* `app-components`), re-introducing cross-app leakage.
|
|
102
120
|
*
|
|
103
121
|
* Resolution order:
|
|
104
|
-
* 1.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
122
|
+
* 1. Shared runtime (React / router / node_modules) -> `undefined` (never split).
|
|
123
|
+
* 2. A registered error/notFound fallback (`ctx.fallbackRefs`) -> `app-fallback`,
|
|
124
|
+
* regardless of location, so the error UI is decoupled from the happy path.
|
|
125
|
+
* 3. A {@link ROUTE_ROOT_DIRS} marker with a directory after it -> key on that
|
|
126
|
+
* next segment (the route id), robust to any nesting depth.
|
|
127
|
+
* 4. Otherwise `undefined` (inherit the default `serverChunk` grouping).
|
|
108
128
|
*/
|
|
109
129
|
export function directoryClientChunks(
|
|
110
130
|
meta: ClientChunkMeta,
|
|
131
|
+
ctx?: ClientChunkContext,
|
|
111
132
|
): string | undefined {
|
|
112
133
|
if (isSharedRuntime(meta)) {
|
|
113
134
|
// React / router runtime / node_modules: always shared, expected, uninteresting.
|
|
114
135
|
return undefined;
|
|
115
136
|
}
|
|
137
|
+
// Registered error/notFound fallbacks -> a dedicated chunk. The error UI must
|
|
138
|
+
// not co-bundle with the code it catches failures for, and removing it lets the
|
|
139
|
+
// chunk it would otherwise anchor be named after a real module, not the boundary.
|
|
140
|
+
if (
|
|
141
|
+
ctx?.fallbackRefs.size &&
|
|
142
|
+
ctx.fallbackRefs.has(hashRefKey(meta.normalizedId))
|
|
143
|
+
) {
|
|
144
|
+
debugChunks?.("fallback %s -> app-fallback", meta.normalizedId);
|
|
145
|
+
return "app-fallback";
|
|
146
|
+
}
|
|
116
147
|
const segments = meta.normalizedId.split("/").filter(Boolean);
|
|
117
148
|
const dirCount = segments.length - 1; // exclude the filename
|
|
118
149
|
if (dirCount >= 1) {
|
|
@@ -144,13 +175,16 @@ export function directoryClientChunks(
|
|
|
144
175
|
* grouping.
|
|
145
176
|
*
|
|
146
177
|
* - `false` / `undefined` -> `undefined` (no override).
|
|
147
|
-
* - `true` -> the built-in {@link directoryClientChunks} strategy
|
|
148
|
-
*
|
|
178
|
+
* - `true` -> the built-in {@link directoryClientChunks} strategy,
|
|
179
|
+
* bound to the discovery-populated {@link ClientChunkContext} (fallback chunk).
|
|
180
|
+
* - function -> the user's function, used verbatim (full control; the
|
|
181
|
+
* fallback refinement does not apply — the consumer owns the grouping).
|
|
149
182
|
*/
|
|
150
183
|
export function resolveClientChunks(
|
|
151
184
|
option: ClientChunks | undefined,
|
|
185
|
+
ctx?: ClientChunkContext,
|
|
152
186
|
): RscClientChunksFn | undefined {
|
|
153
187
|
if (!option) return undefined;
|
|
154
|
-
if (option === true) return directoryClientChunks;
|
|
188
|
+
if (option === true) return (meta) => directoryClientChunks(meta, ctx);
|
|
155
189
|
return option;
|
|
156
190
|
}
|
|
@@ -1,78 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* include node's staticPrefix. That happens when a dynamic param collapses the
|
|
10
|
-
* staticPrefix of nested includes onto the parent's (e.g. `/m/:id/edit` -> sp
|
|
11
|
-
* `/m`): precomputing such a leaf under the collapsed prefix would let the
|
|
12
|
-
* ancestor's lazy entry claim a route it cannot register (the route is behind
|
|
13
|
-
* further nested lazy includes), producing a RouteNotFoundError at request time
|
|
14
|
-
* (issue #506). Those routes are resolved via the handler chain instead.
|
|
15
|
-
*/
|
|
16
|
-
export function flattenLeafEntries(
|
|
17
|
-
prefixTree: Record<string, any>,
|
|
18
|
-
routeManifest: Record<string, string>,
|
|
19
|
-
result: Array<{ staticPrefix: string; routes: Record<string, string> }>,
|
|
20
|
-
): void {
|
|
21
|
-
function visit(node: any, ancestorStaticPrefixes: Set<string>): void {
|
|
22
|
-
const children = node.children || {};
|
|
23
|
-
if (
|
|
24
|
-
Object.keys(children).length === 0 &&
|
|
25
|
-
node.routes &&
|
|
26
|
-
node.routes.length > 0
|
|
27
|
-
) {
|
|
28
|
-
// Leaf node. Skip if its staticPrefix collides with an ancestor include
|
|
29
|
-
// node's staticPrefix (dynamic-param collapse) — see doc comment above.
|
|
30
|
-
if (ancestorStaticPrefixes.has(node.staticPrefix)) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
// Collect its routes from the manifest
|
|
34
|
-
const routes: Record<string, string> = {};
|
|
35
|
-
for (const name of node.routes) {
|
|
36
|
-
if (name in routeManifest) {
|
|
37
|
-
routes[name] = routeManifest[name];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
result.push({ staticPrefix: node.staticPrefix, routes });
|
|
41
|
-
} else {
|
|
42
|
-
// Non-leaf: recurse into children, tracking this node's staticPrefix as
|
|
43
|
-
// an ancestor so a collapsed nested leaf below it is not over-claimed.
|
|
44
|
-
const nextAncestors = new Set(ancestorStaticPrefixes);
|
|
45
|
-
nextAncestors.add(node.staticPrefix);
|
|
46
|
-
for (const child of Object.values(children)) {
|
|
47
|
-
visit(child, nextAncestors);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
for (const node of Object.values(prefixTree)) {
|
|
52
|
-
visit(node, new Set());
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Walk prefix tree to map each route name to its scope's staticPrefix.
|
|
58
|
-
*/
|
|
59
|
-
export function buildRouteToStaticPrefix(
|
|
60
|
-
prefixTree: Record<string, any>,
|
|
61
|
-
result: Record<string, string>,
|
|
62
|
-
): void {
|
|
63
|
-
function visit(node: any): void {
|
|
64
|
-
const sp = node.staticPrefix || "";
|
|
65
|
-
for (const name of node.routes || []) {
|
|
66
|
-
result[name] = sp;
|
|
67
|
-
}
|
|
68
|
-
for (const child of Object.values(node.children || {})) {
|
|
69
|
-
visit(child);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
for (const node of Object.values(prefixTree)) {
|
|
73
|
-
visit(node);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
1
|
+
// Pure prefix-tree walks live in the build layer so runtime code can consume
|
|
2
|
+
// them without importing from vite/. Re-exported here for the vite-side
|
|
3
|
+
// callers (discover-routers, virtual-module-codegen) that already import them
|
|
4
|
+
// from this module.
|
|
5
|
+
export {
|
|
6
|
+
flattenLeafEntries,
|
|
7
|
+
buildRouteToStaticPrefix,
|
|
8
|
+
} from "../../build/prefix-tree-utils.js";
|
|
76
9
|
|
|
77
10
|
/**
|
|
78
11
|
* Wrap a value as `JSON.parse('...')` instead of a JS object literal.
|
|
@@ -116,6 +116,50 @@ export function createVirtualEntriesPlugin(
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Matches rollup's FILE_NAME_CONFLICT message and reports whether the colliding
|
|
120
|
+
// file is a content-hashed asset, e.g.
|
|
121
|
+
// The emitted file "assets/index-DlGNrvnU.css" overwrites a previously ...
|
|
122
|
+
// The emitted file "assets/inter-latin-Dx4kXJAl.woff2" overwrites a ...
|
|
123
|
+
// The match is UNANCHORED on purpose: by the time the warning reaches this user
|
|
124
|
+
// onwarn handler, Vite's logger has wrapped rollup's raw message with an ANSI
|
|
125
|
+
// color sequence and a "[CODE] " label, e.g.
|
|
126
|
+
// "[33m[FILE_NAME_CONFLICT] [0mThe emitted file \"...\" overwrites ..."
|
|
127
|
+
// A "^The emitted file" anchor sits behind that prefix and never matches; and
|
|
128
|
+
// Vite also strips the JSON.stringify quotes rollup puts around the filename, so
|
|
129
|
+
// the match is UNANCHORED and quote-OPTIONAL ("?...?"?). The non-whitespace
|
|
130
|
+
// capture stops at the space before "overwrites" (Vite's unquoted display form)
|
|
131
|
+
// or the closing quote (raw rollup form); either way it carries no ANSI.
|
|
132
|
+
// A content-hashed name ends with a "-" separator + a Vite content hash. The
|
|
133
|
+
// hash is a FIXED-LENGTH base64url run ([A-Za-z0-9_-], default 8), so it can
|
|
134
|
+
// itself contain "-"/"_": it CANNOT be located by splitting on the last "-"
|
|
135
|
+
// (that lands inside the hash whenever it carries a dash, e.g. "...-Cabi7G8-" ->
|
|
136
|
+
// "" or "...-CkhJZR-_" -> "_", which let those conflicts leak). Instead take the
|
|
137
|
+
// trailing HASH_LEN chars and require the "-" separator right before them. The
|
|
138
|
+
// hash must hold an uppercase letter or digit (a real hash is never an
|
|
139
|
+
// all-lowercase word), so stable names like "assets/manifest.json" or
|
|
140
|
+
// "assets/loading-skeleton.css" still surface as potential genuine overwrites.
|
|
141
|
+
function isContentHashedAssetConflict(message: string | undefined): boolean {
|
|
142
|
+
if (!message) return false;
|
|
143
|
+
const match =
|
|
144
|
+
/The emitted file "?([^"\s]+)"? overwrites a previously emitted file/.exec(
|
|
145
|
+
message,
|
|
146
|
+
);
|
|
147
|
+
if (!match) return false;
|
|
148
|
+
const fileName = match[1];
|
|
149
|
+
const base = fileName.slice(fileName.lastIndexOf("/") + 1);
|
|
150
|
+
const dot = base.lastIndexOf(".");
|
|
151
|
+
if (dot <= 0) return false;
|
|
152
|
+
const stem = base.slice(0, dot);
|
|
153
|
+
// HASH_LEN tracks Vite's default [hash] width; bump it if an app sets a custom
|
|
154
|
+
// assetFileNames hash length.
|
|
155
|
+
const HASH_LEN = 8;
|
|
156
|
+
if (stem.length < HASH_LEN + 1 || stem[stem.length - HASH_LEN - 1] !== "-") {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const hash = stem.slice(-HASH_LEN);
|
|
160
|
+
return /^[A-Za-z0-9_-]+$/.test(hash) && /[A-Z0-9]/.test(hash);
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
/**
|
|
120
164
|
* Rollup onwarn handler that suppresses known harmless warnings:
|
|
121
165
|
* - "use client" directives: handled by the RSC plugin, not relevant to Rollup
|
|
@@ -126,6 +170,14 @@ export function createVirtualEntriesPlugin(
|
|
|
126
170
|
* by the bundler, rather than the vite:reporter message handled below (Rollup/Vite 7 shape).
|
|
127
171
|
* - empty bundle: @vitejs/plugin-rsc scan build (step 1/5) produces an empty "index" chunk
|
|
128
172
|
* because the RSC entry is fully externalized during client-reference analysis
|
|
173
|
+
* - file name conflicts on content-hashed assets: @vitejs/plugin-rsc copies the rsc
|
|
174
|
+
* environment's imported CSS/assets into the client bundle (its assets-manifest
|
|
175
|
+
* generateBundle re-emits each via emitFile with an explicit content-hashed
|
|
176
|
+
* fileName). When the client bundle already produced that identical asset,
|
|
177
|
+
* rollup raises FILE_NAME_CONFLICT even though the bytes are identical (a
|
|
178
|
+
* content hash collision IS a content match). Only these are suppressed; a
|
|
179
|
+
* collision on a stable name still surfaces. No upstream fix as of
|
|
180
|
+
* @vitejs/plugin-rsc@0.5.27; remove when it skips the redundant emit.
|
|
129
181
|
*/
|
|
130
182
|
export function onwarn(
|
|
131
183
|
warning: Vite.Rollup.RollupLog,
|
|
@@ -139,6 +191,12 @@ export function onwarn(
|
|
|
139
191
|
) {
|
|
140
192
|
return;
|
|
141
193
|
}
|
|
194
|
+
if (
|
|
195
|
+
warning.code === "FILE_NAME_CONFLICT" &&
|
|
196
|
+
isContentHashedAssetConflict(warning.message)
|
|
197
|
+
) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
142
200
|
// @vitejs/plugin-rsc@0.5.14: rsc:virtual:vite-rsc/assets-manifest renderChunk
|
|
143
201
|
// returns { code } without map, causing Rollup to warn about incorrect sourcemaps.
|
|
144
202
|
// This is harmless (simple string replacement). Remove this suppression if a
|