@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
package/src/use-loader.tsx
CHANGED
|
@@ -88,6 +88,7 @@ function useLoaderInternal<T>(
|
|
|
88
88
|
const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
|
|
89
89
|
const [isLoading, setIsLoading] = useState(false);
|
|
90
90
|
const [error, setError] = useState<Error | null>(null);
|
|
91
|
+
const requestIdRef = useRef(0);
|
|
91
92
|
|
|
92
93
|
// Track context data changes to reset fetched data on navigation
|
|
93
94
|
const prevContextDataRef = useRef(contextData);
|
|
@@ -118,6 +119,7 @@ function useLoaderInternal<T>(
|
|
|
118
119
|
// Supports GET (data fetching) and POST/PUT/PATCH/DELETE (mutations).
|
|
119
120
|
const load = useCallback(
|
|
120
121
|
async (loadOptions?: LoadOptions): Promise<T> => {
|
|
122
|
+
const requestId = ++requestIdRef.current;
|
|
121
123
|
const loaderId = loaderIdRef.current;
|
|
122
124
|
// Verify the loader has $$id
|
|
123
125
|
if (!loaderId) {
|
|
@@ -130,7 +132,7 @@ function useLoaderInternal<T>(
|
|
|
130
132
|
setError(null);
|
|
131
133
|
|
|
132
134
|
try {
|
|
133
|
-
const url = new URL(window.location.
|
|
135
|
+
const url = new URL(window.location.href);
|
|
134
136
|
url.searchParams.set("_rsc_loader", loaderId);
|
|
135
137
|
|
|
136
138
|
const method = loadOptions?.method ?? "GET";
|
|
@@ -212,11 +214,15 @@ function useLoaderInternal<T>(
|
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
const result = payload.loaderResult;
|
|
215
|
-
|
|
217
|
+
if (requestId === requestIdRef.current) {
|
|
218
|
+
setFetchedData(result);
|
|
219
|
+
}
|
|
216
220
|
return result;
|
|
217
221
|
} catch (e) {
|
|
218
222
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
219
|
-
|
|
223
|
+
if (requestId === requestIdRef.current) {
|
|
224
|
+
setError(err);
|
|
225
|
+
}
|
|
220
226
|
if (throwOnError) {
|
|
221
227
|
throw err;
|
|
222
228
|
}
|
|
@@ -224,7 +230,9 @@ function useLoaderInternal<T>(
|
|
|
224
230
|
// successful value or undefined). Caller should check error state.
|
|
225
231
|
return dataRef.current as T;
|
|
226
232
|
} finally {
|
|
227
|
-
|
|
233
|
+
if (requestId === requestIdRef.current) {
|
|
234
|
+
setIsLoading(false);
|
|
235
|
+
}
|
|
228
236
|
}
|
|
229
237
|
},
|
|
230
238
|
[throwOnError],
|
|
@@ -91,7 +91,10 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
91
91
|
// and inject a manifest import into the RSC entry.
|
|
92
92
|
if (hasPrerenderData && existsSync(rscEntryPath)) {
|
|
93
93
|
const rscCode = readFileSync(rscEntryPath, "utf-8");
|
|
94
|
-
|
|
94
|
+
// Check for the specific injection marker, not just the variable name.
|
|
95
|
+
// The runtime code (prerender store) also references __PRERENDER_MANIFEST,
|
|
96
|
+
// so a broad string check would false-positive and skip injection.
|
|
97
|
+
if (!rscCode.includes("__prerender-manifest.js")) {
|
|
95
98
|
try {
|
|
96
99
|
const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
|
|
97
100
|
mkdirSync(assetsDir, { recursive: true });
|
|
@@ -155,17 +158,19 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
155
158
|
for (const [handlerId, { encoded, handles }] of Object.entries(
|
|
156
159
|
state.staticCollectedData!,
|
|
157
160
|
)) {
|
|
158
|
-
const contentHash = createHash("sha256")
|
|
159
|
-
.update(encoded)
|
|
160
|
-
.digest("hex")
|
|
161
|
-
.slice(0, 8);
|
|
162
|
-
const assetFileName = `__st-${contentHash}.js`;
|
|
163
|
-
const assetPath = resolve(assetsDir, assetFileName);
|
|
164
161
|
// Store both the Flight payload and handle data
|
|
165
162
|
const hasHandles = Object.keys(handles).length > 0;
|
|
166
163
|
const exportValue = hasHandles
|
|
167
164
|
? JSON.stringify({ encoded, handles })
|
|
168
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);
|
|
169
174
|
const assetCode = `export default ${exportValue};\n`;
|
|
170
175
|
writeFileSync(assetPath, assetCode);
|
|
171
176
|
totalBytes += Buffer.byteLength(assetCode);
|
|
@@ -100,12 +100,15 @@ export async function discoverRouters(
|
|
|
100
100
|
const buildMod = await rscEnv.runner.import("@rangojs/router/build");
|
|
101
101
|
const generateManifestFull = buildMod.generateManifestFull;
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
state
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
state.
|
|
103
|
+
// Build into local variables first. Only commit to state after the
|
|
104
|
+
// full pass succeeds, so a failed re-discovery preserves the last
|
|
105
|
+
// known-good state instead of leaving it partially wiped.
|
|
106
|
+
const newMergedRouteManifest: Record<string, string> = {};
|
|
107
|
+
const newMergedPrecomputedEntries: PrecomputedEntry[] = [];
|
|
108
|
+
const newPerRouterManifests: typeof state.perRouterManifests = [];
|
|
109
|
+
const newPerRouterManifestDataMap = new Map<string, any>();
|
|
110
|
+
const newPerRouterPrecomputedMap = new Map<string, PrecomputedEntry[]>();
|
|
111
|
+
const newPerRouterTrieMap = new Map<string, any>();
|
|
109
112
|
let mergedRouteAncestry: Record<string, string[]> = {};
|
|
110
113
|
let mergedRouteTrailingSlash: Record<string, string> = {};
|
|
111
114
|
|
|
@@ -128,7 +131,7 @@ export async function discoverRouters(
|
|
|
128
131
|
const dynamicRoutes = routeCount - staticRoutes;
|
|
129
132
|
|
|
130
133
|
// Merge into the combined manifest
|
|
131
|
-
Object.assign(
|
|
134
|
+
Object.assign(newMergedRouteManifest, manifest.routeManifest);
|
|
132
135
|
|
|
133
136
|
// Compute factory-only prefixes: dot-prefixed groups in the runtime
|
|
134
137
|
// manifest that the static parser cannot see. These are routes created
|
|
@@ -152,7 +155,7 @@ export async function discoverRouters(
|
|
|
152
155
|
if (factoryOnlyPrefixes.size === 0) factoryOnlyPrefixes = undefined;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
|
|
158
|
+
newPerRouterManifests.push({
|
|
156
159
|
id,
|
|
157
160
|
routeManifest: manifest.routeManifest,
|
|
158
161
|
routeSearchSchemas: manifest.routeSearchSchemas,
|
|
@@ -175,18 +178,18 @@ export async function discoverRouters(
|
|
|
175
178
|
flattenLeafEntries(
|
|
176
179
|
manifest.prefixTree,
|
|
177
180
|
manifest.routeManifest,
|
|
178
|
-
|
|
181
|
+
newMergedPrecomputedEntries,
|
|
179
182
|
);
|
|
180
183
|
|
|
181
184
|
// Store per-router manifest and precomputed entries for isolated virtual modules.
|
|
182
|
-
|
|
185
|
+
newPerRouterManifestDataMap.set(id, manifest.routeManifest);
|
|
183
186
|
const routerPrecomputed: PrecomputedEntry[] = [];
|
|
184
187
|
flattenLeafEntries(
|
|
185
188
|
manifest.prefixTree,
|
|
186
189
|
manifest.routeManifest,
|
|
187
190
|
routerPrecomputed,
|
|
188
191
|
);
|
|
189
|
-
|
|
192
|
+
newPerRouterPrecomputedMap.set(id, routerPrecomputed);
|
|
190
193
|
|
|
191
194
|
console.log(
|
|
192
195
|
`[rsc-router] Router "${id}" -> ${routeCount} routes ` +
|
|
@@ -214,10 +217,8 @@ export async function discoverRouters(
|
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
// Build route trie from merged manifest + ancestry
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
Object.keys(state.mergedRouteManifest).length > 0
|
|
220
|
-
) {
|
|
220
|
+
let newMergedRouteTrie: any = null;
|
|
221
|
+
if (Object.keys(newMergedRouteManifest).length > 0) {
|
|
221
222
|
const buildRouteTrie = buildMod.buildRouteTrie;
|
|
222
223
|
if (buildRouteTrie && mergedRouteAncestry) {
|
|
223
224
|
// Build routeToStaticPrefix from saved manifests
|
|
@@ -252,8 +253,8 @@ export async function discoverRouters(
|
|
|
252
253
|
}
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
newMergedRouteTrie = buildRouteTrie(
|
|
257
|
+
newMergedRouteManifest,
|
|
257
258
|
mergedRouteAncestry,
|
|
258
259
|
routeToStaticPrefix,
|
|
259
260
|
Object.keys(mergedRouteTrailingSlash).length > 0
|
|
@@ -305,11 +306,22 @@ export async function discoverRouters(
|
|
|
305
306
|
? manifest.responseTypeRoutes
|
|
306
307
|
: undefined,
|
|
307
308
|
);
|
|
308
|
-
|
|
309
|
+
newPerRouterTrieMap.set(id, perRouterTrie);
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
// Commit all local state to the shared discovery state atomically.
|
|
315
|
+
// This ensures a failed re-discovery (e.g. from a transient module
|
|
316
|
+
// evaluation error) preserves the last known-good state.
|
|
317
|
+
state.mergedRouteManifest = newMergedRouteManifest;
|
|
318
|
+
state.mergedPrecomputedEntries = newMergedPrecomputedEntries;
|
|
319
|
+
state.perRouterManifests = newPerRouterManifests;
|
|
320
|
+
state.perRouterManifestDataMap = newPerRouterManifestDataMap;
|
|
321
|
+
state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
|
|
322
|
+
state.perRouterTrieMap = newPerRouterTrieMap;
|
|
323
|
+
state.mergedRouteTrie = newMergedRouteTrie;
|
|
324
|
+
|
|
313
325
|
// Expand prerender routes and render static handlers (build mode only)
|
|
314
326
|
await expandPrerenderRoutes(state, rscEnv, registry, allManifests);
|
|
315
327
|
await renderStaticHandlers(state, rscEnv, registry);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { contextSet } from "../../context-var.js";
|
|
10
10
|
import {
|
|
11
11
|
encodePathParam,
|
|
12
|
-
|
|
12
|
+
substituteRouteParams,
|
|
13
13
|
runWithConcurrency,
|
|
14
14
|
groupByConcurrency,
|
|
15
15
|
notifyOnError,
|
|
@@ -33,6 +33,7 @@ export async function expandPrerenderRoutes(
|
|
|
33
33
|
routeName: string;
|
|
34
34
|
concurrency: number;
|
|
35
35
|
buildVars?: Record<string, any>;
|
|
36
|
+
isPassthroughRoute?: boolean;
|
|
36
37
|
};
|
|
37
38
|
const entries: PrerenderEntry[] = [];
|
|
38
39
|
|
|
@@ -44,19 +45,8 @@ export async function expandPrerenderRoutes(
|
|
|
44
45
|
const getParamsReverse = (name: string, params?: Record<string, string>) => {
|
|
45
46
|
const pattern = allRoutes[name];
|
|
46
47
|
if (!pattern) throw new Error(`Unknown route: "${name}"`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for (const [key, value] of Object.entries(params)) {
|
|
50
|
-
// Strip constraint syntax: :param(a|b) -> value
|
|
51
|
-
const escaped = escapeRegExp(key);
|
|
52
|
-
result = result.replace(
|
|
53
|
-
new RegExp(`:${escaped}(\\([^)]*\\))?`),
|
|
54
|
-
encodeURIComponent(value),
|
|
55
|
-
);
|
|
56
|
-
result = result.replace(`*${key}`, encodeURIComponent(value));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
48
|
+
if (!params) return pattern;
|
|
49
|
+
return substituteRouteParams(pattern, params);
|
|
60
50
|
};
|
|
61
51
|
|
|
62
52
|
for (const { manifest } of allManifests) {
|
|
@@ -65,6 +55,8 @@ export async function expandPrerenderRoutes(
|
|
|
65
55
|
for (const routeName of manifest.prerenderRoutes) {
|
|
66
56
|
const pattern = manifest.routeManifest[routeName];
|
|
67
57
|
if (!pattern) continue;
|
|
58
|
+
const def = defs[routeName];
|
|
59
|
+
const isPassthroughRoute = !!def?.options?.passthrough;
|
|
68
60
|
const hasDynamic = pattern.includes(":") || pattern.includes("*");
|
|
69
61
|
if (!hasDynamic) {
|
|
70
62
|
// Static route: use pattern directly (strip trailing slash for URL)
|
|
@@ -72,10 +64,10 @@ export async function expandPrerenderRoutes(
|
|
|
72
64
|
urlPath: pattern.replace(/\/$/, "") || "/",
|
|
73
65
|
routeName,
|
|
74
66
|
concurrency: 1,
|
|
67
|
+
isPassthroughRoute,
|
|
75
68
|
});
|
|
76
69
|
} else {
|
|
77
70
|
// Dynamic route: call getParams() to enumerate param combinations
|
|
78
|
-
const def = defs[routeName];
|
|
79
71
|
if (def?.getParams) {
|
|
80
72
|
try {
|
|
81
73
|
const buildVars: Record<string, any> = {};
|
|
@@ -92,19 +84,11 @@ export async function expandPrerenderRoutes(
|
|
|
92
84
|
Object.keys(buildVars).length > 0 ||
|
|
93
85
|
Object.getOwnPropertySymbols(buildVars).length > 0;
|
|
94
86
|
for (const params of paramsList) {
|
|
95
|
-
let url =
|
|
96
|
-
|
|
87
|
+
let url = substituteRouteParams(
|
|
88
|
+
pattern,
|
|
97
89
|
params as Record<string, string>,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// Strip constraint syntax: :param(a|b) -> value
|
|
101
|
-
const escaped = escapeRegExp(key);
|
|
102
|
-
url = url.replace(
|
|
103
|
-
new RegExp(`:${escaped}(\\([^)]*\\))?`),
|
|
104
|
-
encoded,
|
|
105
|
-
);
|
|
106
|
-
url = url.replace(`*${key}`, encoded);
|
|
107
|
-
}
|
|
90
|
+
encodePathParam,
|
|
91
|
+
);
|
|
108
92
|
// Anonymous wildcard fallback: use conventional keys if provided
|
|
109
93
|
if (url.includes("*")) {
|
|
110
94
|
const wildcardValue =
|
|
@@ -119,6 +103,7 @@ export async function expandPrerenderRoutes(
|
|
|
119
103
|
routeName,
|
|
120
104
|
concurrency,
|
|
121
105
|
...(hasBuildVars ? { buildVars } : {}),
|
|
106
|
+
isPassthroughRoute,
|
|
122
107
|
});
|
|
123
108
|
}
|
|
124
109
|
} catch (err: any) {
|
|
@@ -187,8 +172,20 @@ export async function expandPrerenderRoutes(
|
|
|
187
172
|
entry.urlPath,
|
|
188
173
|
{},
|
|
189
174
|
entry.buildVars,
|
|
175
|
+
entry.isPassthroughRoute,
|
|
190
176
|
);
|
|
191
177
|
if (!result) continue;
|
|
178
|
+
|
|
179
|
+
// Handler returned ctx.passthrough() — skip manifest entry
|
|
180
|
+
if (result.passthrough) {
|
|
181
|
+
const elapsed = (performance.now() - startUrl).toFixed(0);
|
|
182
|
+
console.log(
|
|
183
|
+
`[rsc-router] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
|
|
184
|
+
);
|
|
185
|
+
doneCount++;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
192
189
|
const paramHash = hashParams(result.params || {});
|
|
193
190
|
collectedData[`${result.routeName}/${paramHash}`] = {
|
|
194
191
|
segments: result.segments,
|
|
@@ -15,20 +15,20 @@ import {
|
|
|
15
15
|
} from "../../build/generate-route-types.js";
|
|
16
16
|
import type { DiscoveryState } from "./state.js";
|
|
17
17
|
import { markSelfGenWrite } from "./self-gen-tracking.js";
|
|
18
|
+
import { isAutoGeneratedRouteName } from "../../route-name.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Filter out auto-generated route names from a manifest.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* which never produces these names.
|
|
22
|
+
* Unnamed routes get "$path_"-prefixed names at runtime (see path-helper.ts).
|
|
23
|
+
* These should not appear in the typed gen file. User-defined names
|
|
24
|
+
* containing "$" (e.g. "$admin") are valid and preserved.
|
|
25
25
|
*/
|
|
26
26
|
function filterUserNamedRoutes(
|
|
27
27
|
manifest: Record<string, string>,
|
|
28
28
|
): Record<string, string> {
|
|
29
29
|
const filtered: Record<string, string> = {};
|
|
30
30
|
for (const [name, pattern] of Object.entries(manifest)) {
|
|
31
|
-
if (!name
|
|
31
|
+
if (!isAutoGeneratedRouteName(name)) {
|
|
32
32
|
filtered[name] = pattern;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -222,8 +222,8 @@ export function supplementGenFilesWithRuntimeRoutes(
|
|
|
222
222
|
};
|
|
223
223
|
|
|
224
224
|
for (const [name, pattern] of Object.entries(routeManifest)) {
|
|
225
|
-
// Skip
|
|
226
|
-
if (name
|
|
225
|
+
// Skip internal runtime-only names from unnamed routes/includes.
|
|
226
|
+
if (isAutoGeneratedRouteName(name)) continue;
|
|
227
227
|
const dotIdx = name.indexOf(".");
|
|
228
228
|
if (dotIdx <= 0) continue;
|
|
229
229
|
const prefix = name.substring(0, dotIdx + 1);
|
|
@@ -42,7 +42,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
42
42
|
const genPath = join(
|
|
43
43
|
routerDir,
|
|
44
44
|
`${routerBasename}.named-routes.gen.js`,
|
|
45
|
-
);
|
|
45
|
+
).replaceAll("\\", "/");
|
|
46
46
|
const varName = `_r${varIdx++}`;
|
|
47
47
|
genFileImports.push(
|
|
48
48
|
`import { NamedRoutes as ${varName} } from ${JSON.stringify(genPath)};`,
|
|
@@ -176,7 +176,10 @@ export function generatePerRouterModule(
|
|
|
176
176
|
/\.(tsx?|jsx?)$/,
|
|
177
177
|
"",
|
|
178
178
|
);
|
|
179
|
-
const genPath = join(
|
|
179
|
+
const genPath = join(
|
|
180
|
+
routerDir,
|
|
181
|
+
`${routerBasename}.named-routes.gen.js`,
|
|
182
|
+
).replaceAll("\\", "/");
|
|
180
183
|
lines.push(`import { NamedRoutes as _r } from ${JSON.stringify(genPath)};`);
|
|
181
184
|
lines.push(
|
|
182
185
|
`function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
-
import { relative
|
|
2
|
+
import { relative } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
|
|
5
5
|
// Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
|
|
@@ -33,11 +33,11 @@ export function computeProductionHash(
|
|
|
33
33
|
const absPath = decodeURIComponent(
|
|
34
34
|
refKey.slice(CLIENT_IN_SERVER_PKG_PROXY_PREFIX.length),
|
|
35
35
|
);
|
|
36
|
-
toHash =
|
|
36
|
+
toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
|
|
37
37
|
} else if (refKey.startsWith(FS_PREFIX)) {
|
|
38
38
|
// /@fs/abs/path.tsx -> hash(relative(root, "/abs/path.tsx"))
|
|
39
39
|
const absPath = refKey.slice(FS_PREFIX.length - 1); // keep leading /
|
|
40
|
-
toHash =
|
|
40
|
+
toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
|
|
41
41
|
} else if (refKey.startsWith("/")) {
|
|
42
42
|
// /src/Button.tsx -> hash("src/Button.tsx")
|
|
43
43
|
toHash = refKey.slice(1);
|
|
@@ -97,7 +97,7 @@ export function useCacheTransform(): Plugin {
|
|
|
97
97
|
|
|
98
98
|
// Check for function-level "use cache" / "use cache: profileName"
|
|
99
99
|
// (only if there's no file-level directive but code still contains the string)
|
|
100
|
-
|
|
100
|
+
const functionResult = transformFunctionLevelUseCache(
|
|
101
101
|
code,
|
|
102
102
|
ast,
|
|
103
103
|
filePath,
|
|
@@ -105,6 +105,13 @@ export function useCacheTransform(): Plugin {
|
|
|
105
105
|
isBuild,
|
|
106
106
|
transformHoistInlineDirective,
|
|
107
107
|
);
|
|
108
|
+
|
|
109
|
+
// Always check for near-miss directives, even when valid directives
|
|
110
|
+
// exist. A file may contain both valid and invalid "use cache" directives
|
|
111
|
+
// in different functions — the invalid ones should still warn.
|
|
112
|
+
warnOnNearMissDirectives(ast, id, this.warn.bind(this));
|
|
113
|
+
|
|
114
|
+
if (functionResult) return functionResult;
|
|
108
115
|
},
|
|
109
116
|
};
|
|
110
117
|
}
|
|
@@ -118,19 +125,38 @@ function transformFileLevelUseCache(
|
|
|
118
125
|
isLayoutOrTemplate: boolean,
|
|
119
126
|
transformWrapExport: (typeof import("@vitejs/plugin-rsc/transforms"))["transformWrapExport"],
|
|
120
127
|
) {
|
|
128
|
+
// Collect non-function exports to report after wrapping
|
|
129
|
+
const nonFunctionExports: string[] = [];
|
|
130
|
+
|
|
121
131
|
const { exportNames, output } = transformWrapExport(code, ast, {
|
|
122
132
|
runtime: (value: string, name: string) => {
|
|
123
133
|
const funcId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
|
|
124
134
|
return `__rango_registerCachedFunction(${value}, ${JSON.stringify(funcId)}, "default")`;
|
|
125
135
|
},
|
|
126
136
|
rejectNonAsyncFunction: false,
|
|
127
|
-
filter: (name: string) => {
|
|
137
|
+
filter: (name: string, meta: { isFunction?: boolean }) => {
|
|
128
138
|
// Skip default export of layout/template files (they receive children)
|
|
129
139
|
if (name === "default" && isLayoutOrTemplate) return false;
|
|
140
|
+
// Non-function exports cannot be wrapped with registerCachedFunction
|
|
141
|
+
if (meta.isFunction === false) {
|
|
142
|
+
nonFunctionExports.push(name);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
130
145
|
return true;
|
|
131
146
|
},
|
|
132
147
|
});
|
|
133
148
|
|
|
149
|
+
if (nonFunctionExports.length > 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`[rango:use-cache] File-level "use cache" in ${sourceId} cannot wrap ` +
|
|
152
|
+
`non-function export${nonFunctionExports.length > 1 ? "s" : ""}: ` +
|
|
153
|
+
`${nonFunctionExports.map((n) => `"${n}"`).join(", ")}. ` +
|
|
154
|
+
`Only function exports can be cached. Either remove "use cache" from ` +
|
|
155
|
+
`the file level and add it inside individual functions, or move the ` +
|
|
156
|
+
`non-function exports to a separate module.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
if (exportNames.length === 0) {
|
|
135
161
|
// Even if no exports were wrapped, strip the directive
|
|
136
162
|
const s = new MagicString(code);
|
|
@@ -180,7 +206,7 @@ function transformFunctionLevelUseCache(
|
|
|
180
206
|
) {
|
|
181
207
|
try {
|
|
182
208
|
const { output, names } = transformHoistInlineDirective(code, ast, {
|
|
183
|
-
directive: /^use cache(:\s
|
|
209
|
+
directive: /^use cache(:\s*[\w-]+)?$/,
|
|
184
210
|
runtime: (
|
|
185
211
|
value: string,
|
|
186
212
|
name: string,
|
|
@@ -233,3 +259,65 @@ function findFileLevelDirective(
|
|
|
233
259
|
}
|
|
234
260
|
return null;
|
|
235
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The valid directive regex (must stay in sync with transformFunctionLevelUseCache).
|
|
265
|
+
*/
|
|
266
|
+
const VALID_DIRECTIVE_RE = /^use cache(:\s*[\w-]+)?$/;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Regex for near-miss: starts with "use cache:" but has invalid tokens.
|
|
270
|
+
*/
|
|
271
|
+
const NEAR_MISS_RE = /^use cache:\s*.+$/;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Walk the AST looking for string literals that look like malformed
|
|
275
|
+
* "use cache" directives and emit a Vite warning for each.
|
|
276
|
+
*
|
|
277
|
+
* This catches cases like `"use cache: bad.name"` or `"use cache: "`
|
|
278
|
+
* that the transform regex silently ignores.
|
|
279
|
+
*/
|
|
280
|
+
function warnOnNearMissDirectives(
|
|
281
|
+
ast: any,
|
|
282
|
+
fileId: string,
|
|
283
|
+
warn: (message: string) => void,
|
|
284
|
+
): void {
|
|
285
|
+
const visit = (node: any) => {
|
|
286
|
+
if (!node || typeof node !== "object") return;
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
node.type === "ExpressionStatement" &&
|
|
290
|
+
node.expression?.type === "Literal" &&
|
|
291
|
+
typeof node.expression.value === "string"
|
|
292
|
+
) {
|
|
293
|
+
const value = node.expression.value;
|
|
294
|
+
if (
|
|
295
|
+
value.startsWith("use cache") &&
|
|
296
|
+
NEAR_MISS_RE.test(value) &&
|
|
297
|
+
!VALID_DIRECTIVE_RE.test(value)
|
|
298
|
+
) {
|
|
299
|
+
const profilePart = value.slice("use cache:".length).trim();
|
|
300
|
+
warn(
|
|
301
|
+
`[rango:use-cache] "${value}" in ${fileId} has an invalid profile name "${profilePart}". ` +
|
|
302
|
+
`Profile names must match [a-zA-Z0-9_-]+. This directive will be ignored.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Walk into function bodies where directives appear
|
|
308
|
+
for (const key of Object.keys(node)) {
|
|
309
|
+
const child = node[key];
|
|
310
|
+
if (Array.isArray(child)) {
|
|
311
|
+
for (const item of child) {
|
|
312
|
+
visit(item);
|
|
313
|
+
}
|
|
314
|
+
} else if (child && typeof child === "object" && child.type) {
|
|
315
|
+
visit(child);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const node of ast.body ?? []) {
|
|
321
|
+
visit(node);
|
|
322
|
+
}
|
|
323
|
+
}
|
package/src/vite/rango.ts
CHANGED
|
@@ -222,9 +222,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
222
222
|
const candidates = findRouterFiles(root, filter);
|
|
223
223
|
if (candidates.length === 1) {
|
|
224
224
|
const abs = candidates[0];
|
|
225
|
-
routerRef.path =
|
|
226
|
-
? "./" + abs.slice(root.length + 1)
|
|
227
|
-
|
|
225
|
+
routerRef.path = (
|
|
226
|
+
abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs
|
|
227
|
+
).replaceAll("\\", "/");
|
|
228
228
|
} else if (candidates.length > 1) {
|
|
229
229
|
const list = candidates
|
|
230
230
|
.map(
|