@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1fa245e2
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/AGENTS.md +4 -0
- package/README.md +76 -18
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +558 -319
- package/package.json +16 -15
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +19 -13
- package/src/browser/navigation-client.ts +115 -58
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +80 -15
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +38 -23
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +53 -9
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +134 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +36 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +2 -56
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +73 -25
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +79 -23
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +122 -10
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +88 -16
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +4 -6
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +183 -20
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +412 -297
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +59 -6
- package/src/rsc/handler.ts +460 -368
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +8 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +140 -14
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +144 -18
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +137 -33
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +14 -1
- package/src/vite/discovery/state.ts +13 -6
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +163 -211
- package/src/vite/router-discovery.ts +153 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +18 -0
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -45,7 +45,9 @@ function isRoutableSourceFile(name: string): boolean {
|
|
|
45
45
|
name.endsWith(".tsx") ||
|
|
46
46
|
name.endsWith(".js") ||
|
|
47
47
|
name.endsWith(".jsx")) &&
|
|
48
|
-
!name.includes(".gen.")
|
|
48
|
+
!name.includes(".gen.") &&
|
|
49
|
+
!name.includes(".test.") &&
|
|
50
|
+
!name.includes(".spec.")
|
|
49
51
|
);
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -70,7 +72,15 @@ function findRouterFilesRecursive(
|
|
|
70
72
|
for (const entry of entries) {
|
|
71
73
|
const fullPath = join(dir, entry.name);
|
|
72
74
|
if (entry.isDirectory()) {
|
|
73
|
-
if (
|
|
75
|
+
if (
|
|
76
|
+
entry.name === "node_modules" ||
|
|
77
|
+
entry.name === "dist" ||
|
|
78
|
+
entry.name === "coverage" ||
|
|
79
|
+
entry.name === "__tests__" ||
|
|
80
|
+
entry.name === "__mocks__" ||
|
|
81
|
+
entry.name.startsWith(".")
|
|
82
|
+
)
|
|
83
|
+
continue;
|
|
74
84
|
childDirs.push(fullPath);
|
|
75
85
|
continue;
|
|
76
86
|
}
|
|
@@ -147,13 +157,26 @@ export function formatNestedRouterConflictError(
|
|
|
147
157
|
// ---------------------------------------------------------------------------
|
|
148
158
|
|
|
149
159
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
160
|
+
* Result of extracting URL patterns from a router file.
|
|
161
|
+
* - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
|
|
162
|
+
* - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
|
|
163
|
+
*/
|
|
164
|
+
export type UrlsExtractionResult =
|
|
165
|
+
| { kind: "variable"; name: string }
|
|
166
|
+
| { kind: "inline"; block: string };
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract the url patterns from a router file using AST.
|
|
170
|
+
* Detects four patterns:
|
|
152
171
|
* 1. createRouter(...).routes(variableName)
|
|
153
172
|
* 2. createRouter({ urls: variableName, ... })
|
|
154
|
-
*
|
|
173
|
+
* 3. createRouter(...).routes(({ path, ... }) => [...])
|
|
174
|
+
* 4. createRouter({ urls: ({ path, ... }) => [...], ... })
|
|
175
|
+
* Returns either a variable name or an inline code block.
|
|
155
176
|
*/
|
|
156
|
-
export function
|
|
177
|
+
export function extractUrlsFromRouter(
|
|
178
|
+
code: string,
|
|
179
|
+
): UrlsExtractionResult | null {
|
|
157
180
|
const sourceFile = ts.createSourceFile(
|
|
158
181
|
"router.tsx",
|
|
159
182
|
code,
|
|
@@ -161,7 +184,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
161
184
|
true,
|
|
162
185
|
ts.ScriptKind.TSX,
|
|
163
186
|
);
|
|
164
|
-
let result:
|
|
187
|
+
let result: UrlsExtractionResult | null = null;
|
|
165
188
|
|
|
166
189
|
function isCreateRouterCall(node: ts.Node): boolean {
|
|
167
190
|
if (!ts.isCallExpression(node)) return false;
|
|
@@ -169,44 +192,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
169
192
|
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
170
193
|
}
|
|
171
194
|
|
|
195
|
+
/** Check if a node is an arrow/function expression (inline builder). */
|
|
196
|
+
function isInlineBuilder(node: ts.Node): boolean {
|
|
197
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Check if a .routes() call chains from createRouter(). */
|
|
201
|
+
function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
|
|
202
|
+
if (
|
|
203
|
+
!ts.isPropertyAccessExpression(node.expression) ||
|
|
204
|
+
node.expression.name.text !== "routes"
|
|
205
|
+
)
|
|
206
|
+
return false;
|
|
207
|
+
let inner: ts.Expression = node.expression.expression;
|
|
208
|
+
while (
|
|
209
|
+
ts.isCallExpression(inner) &&
|
|
210
|
+
ts.isPropertyAccessExpression(inner.expression)
|
|
211
|
+
) {
|
|
212
|
+
inner = inner.expression.expression;
|
|
213
|
+
}
|
|
214
|
+
return isCreateRouterCall(inner);
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
function visit(node: ts.Node) {
|
|
173
218
|
if (result) return;
|
|
174
219
|
|
|
175
|
-
// Pattern 1: createRouter(...).routes(variableName)
|
|
176
|
-
// The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
|
|
220
|
+
// Pattern 1 & 3: createRouter(...).routes(variableName | builder)
|
|
177
221
|
if (
|
|
178
222
|
ts.isCallExpression(node) &&
|
|
179
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
180
|
-
node.expression.name.text === "routes" &&
|
|
181
223
|
node.arguments.length >= 1 &&
|
|
182
|
-
|
|
224
|
+
isRoutesOnCreateRouter(node)
|
|
183
225
|
) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
ts.isPropertyAccessExpression(inner.expression)
|
|
190
|
-
) {
|
|
191
|
-
inner = inner.expression.expression;
|
|
192
|
-
}
|
|
193
|
-
if (isCreateRouterCall(inner)) {
|
|
194
|
-
result = (node.arguments[0] as ts.Identifier).text;
|
|
195
|
-
return;
|
|
226
|
+
const arg = node.arguments[0];
|
|
227
|
+
if (ts.isIdentifier(arg)) {
|
|
228
|
+
result = { kind: "variable", name: arg.text };
|
|
229
|
+
} else if (isInlineBuilder(arg)) {
|
|
230
|
+
result = { kind: "inline", block: arg.getText(sourceFile) };
|
|
196
231
|
}
|
|
232
|
+
return;
|
|
197
233
|
}
|
|
198
234
|
|
|
199
|
-
// Pattern 2: createRouter({ urls: variableName, ... })
|
|
235
|
+
// Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
|
|
200
236
|
if (isCreateRouterCall(node)) {
|
|
201
237
|
const callExpr = node as ts.CallExpression;
|
|
202
|
-
for (const
|
|
238
|
+
for (const callArg of callExpr.arguments) {
|
|
239
|
+
if (ts.isObjectLiteralExpression(callArg)) {
|
|
240
|
+
for (const prop of callArg.properties) {
|
|
241
|
+
if (
|
|
242
|
+
ts.isPropertyAssignment(prop) &&
|
|
243
|
+
ts.isIdentifier(prop.name) &&
|
|
244
|
+
prop.name.text === "urls"
|
|
245
|
+
) {
|
|
246
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
247
|
+
result = { kind: "variable", name: prop.initializer.text };
|
|
248
|
+
} else if (isInlineBuilder(prop.initializer)) {
|
|
249
|
+
result = {
|
|
250
|
+
kind: "inline",
|
|
251
|
+
block: prop.initializer.getText(sourceFile),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ts.forEachChild(node, visit);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
visit(sourceFile);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract the `basename` string literal from createRouter({ basename: "..." }).
|
|
270
|
+
* Returns the basename value or undefined if not present.
|
|
271
|
+
*/
|
|
272
|
+
export function extractBasenameFromRouter(code: string): string | undefined {
|
|
273
|
+
const sourceFile = ts.createSourceFile(
|
|
274
|
+
"router.tsx",
|
|
275
|
+
code,
|
|
276
|
+
ts.ScriptTarget.Latest,
|
|
277
|
+
true,
|
|
278
|
+
ts.ScriptKind.TSX,
|
|
279
|
+
);
|
|
280
|
+
let result: string | undefined;
|
|
281
|
+
|
|
282
|
+
function visit(node: ts.Node) {
|
|
283
|
+
if (result !== undefined) return;
|
|
284
|
+
if (
|
|
285
|
+
ts.isCallExpression(node) &&
|
|
286
|
+
ts.isIdentifier(node.expression) &&
|
|
287
|
+
node.expression.text === "createRouter"
|
|
288
|
+
) {
|
|
289
|
+
for (const arg of node.arguments) {
|
|
203
290
|
if (ts.isObjectLiteralExpression(arg)) {
|
|
204
291
|
for (const prop of arg.properties) {
|
|
205
292
|
if (
|
|
206
293
|
ts.isPropertyAssignment(prop) &&
|
|
207
294
|
ts.isIdentifier(prop.name) &&
|
|
208
|
-
prop.name.text === "
|
|
209
|
-
ts.
|
|
295
|
+
prop.name.text === "basename" &&
|
|
296
|
+
ts.isStringLiteral(prop.initializer)
|
|
210
297
|
) {
|
|
211
298
|
result = prop.initializer.text;
|
|
212
299
|
return;
|
|
@@ -215,7 +302,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
215
302
|
}
|
|
216
303
|
}
|
|
217
304
|
}
|
|
218
|
-
|
|
219
305
|
ts.forEachChild(node, visit);
|
|
220
306
|
}
|
|
221
307
|
|
|
@@ -223,9 +309,40 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
223
309
|
return result;
|
|
224
310
|
}
|
|
225
311
|
|
|
312
|
+
/** @deprecated Use extractUrlsFromRouter instead */
|
|
313
|
+
export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
314
|
+
const result = extractUrlsFromRouter(code);
|
|
315
|
+
return result?.kind === "variable" ? result.name : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Apply a basename prefix to all route patterns in a result set. */
|
|
319
|
+
function applyBasenameToRoutes(
|
|
320
|
+
result: {
|
|
321
|
+
routes: Record<string, string>;
|
|
322
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
323
|
+
},
|
|
324
|
+
basename: string,
|
|
325
|
+
): {
|
|
326
|
+
routes: Record<string, string>;
|
|
327
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
328
|
+
} {
|
|
329
|
+
const prefixed: Record<string, string> = {};
|
|
330
|
+
for (const [name, pattern] of Object.entries(result.routes)) {
|
|
331
|
+
if (pattern === "/") {
|
|
332
|
+
prefixed[name] = basename;
|
|
333
|
+
} else if (basename.endsWith("/") && pattern.startsWith("/")) {
|
|
334
|
+
prefixed[name] = basename + pattern.slice(1);
|
|
335
|
+
} else {
|
|
336
|
+
prefixed[name] = basename + pattern;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
340
|
+
}
|
|
341
|
+
|
|
226
342
|
/**
|
|
227
343
|
* Resolve routes and search schemas from a router source file by following the
|
|
228
|
-
* variable passed to `.routes(...)` or `urls: ...` in createRouter options
|
|
344
|
+
* variable passed to `.routes(...)` or `urls: ...` in createRouter options,
|
|
345
|
+
* or by parsing an inline builder function directly.
|
|
229
346
|
*/
|
|
230
347
|
export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
231
348
|
routes: Record<string, string>;
|
|
@@ -238,21 +355,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
238
355
|
return { routes: {}, searchSchemas: {} };
|
|
239
356
|
}
|
|
240
357
|
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
358
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
359
|
+
if (!extraction) {
|
|
243
360
|
return { routes: {}, searchSchemas: {} };
|
|
244
361
|
}
|
|
245
362
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
363
|
+
// Detect basename from createRouter({ basename: "..." })
|
|
364
|
+
const rawBasename = extractBasenameFromRouter(routerSource);
|
|
365
|
+
const basename = rawBasename
|
|
366
|
+
? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
|
|
367
|
+
: undefined;
|
|
368
|
+
|
|
369
|
+
let result: {
|
|
370
|
+
routes: Record<string, string>;
|
|
371
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Inline builder: extract routes directly from the function body
|
|
375
|
+
if (extraction.kind === "inline") {
|
|
376
|
+
result = buildCombinedRouteMapWithSearch(
|
|
377
|
+
routerFilePath,
|
|
378
|
+
undefined,
|
|
379
|
+
undefined,
|
|
380
|
+
undefined,
|
|
381
|
+
extraction.block,
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
// Variable reference: follow imports or same-file declaration
|
|
385
|
+
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
386
|
+
if (imported) {
|
|
387
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
388
|
+
if (!targetFile) {
|
|
389
|
+
return { routes: {}, searchSchemas: {} };
|
|
390
|
+
}
|
|
391
|
+
result = buildCombinedRouteMapWithSearch(
|
|
392
|
+
targetFile,
|
|
393
|
+
imported.exportedName,
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
|
|
251
397
|
}
|
|
252
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
253
398
|
}
|
|
254
399
|
|
|
255
|
-
|
|
400
|
+
// Apply basename prefix to all extracted route patterns
|
|
401
|
+
if (basename) {
|
|
402
|
+
result = applyBasenameToRoutes(result, basename);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
256
406
|
}
|
|
257
407
|
|
|
258
408
|
// ---------------------------------------------------------------------------
|
|
@@ -275,12 +425,26 @@ export function detectUnresolvableIncludes(
|
|
|
275
425
|
return [];
|
|
276
426
|
}
|
|
277
427
|
|
|
278
|
-
// Extract the urls
|
|
279
|
-
const
|
|
280
|
-
if (!
|
|
428
|
+
// Extract the urls source from the router file
|
|
429
|
+
const extraction = extractUrlsFromRouter(source);
|
|
430
|
+
if (!extraction) return [];
|
|
431
|
+
|
|
432
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
433
|
+
|
|
434
|
+
if (extraction.kind === "inline") {
|
|
435
|
+
// Inline builder: parse directly
|
|
436
|
+
buildCombinedRouteMapWithSearch(
|
|
437
|
+
realPath,
|
|
438
|
+
undefined,
|
|
439
|
+
new Set(),
|
|
440
|
+
diagnostics,
|
|
441
|
+
extraction.block,
|
|
442
|
+
);
|
|
443
|
+
return diagnostics;
|
|
444
|
+
}
|
|
281
445
|
|
|
282
|
-
//
|
|
283
|
-
const imported = resolveImportedVariable(source,
|
|
446
|
+
// Variable reference: resolve where it comes from
|
|
447
|
+
const imported = resolveImportedVariable(source, extraction.name);
|
|
284
448
|
let targetFile: string;
|
|
285
449
|
let exportedName: string | undefined;
|
|
286
450
|
|
|
@@ -302,10 +466,9 @@ export function detectUnresolvableIncludes(
|
|
|
302
466
|
} else {
|
|
303
467
|
// Same-file urls() definition
|
|
304
468
|
targetFile = realPath;
|
|
305
|
-
exportedName =
|
|
469
|
+
exportedName = extraction.name;
|
|
306
470
|
}
|
|
307
471
|
|
|
308
|
-
const diagnostics: UnresolvableInclude[] = [];
|
|
309
472
|
buildCombinedRouteMapWithSearch(
|
|
310
473
|
targetFile,
|
|
311
474
|
exportedName,
|
|
@@ -387,34 +550,20 @@ export function writeCombinedRouteTypes(
|
|
|
387
550
|
}
|
|
388
551
|
|
|
389
552
|
for (const routerFilePath of routerFilePaths) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
407
|
-
if (imported) {
|
|
408
|
-
// Variable is imported from another module
|
|
409
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
410
|
-
if (!targetFile) continue;
|
|
411
|
-
result = buildCombinedRouteMapWithSearch(
|
|
412
|
-
targetFile,
|
|
413
|
-
imported.exportedName,
|
|
414
|
-
);
|
|
415
|
-
} else {
|
|
416
|
-
// Variable is defined in the same file
|
|
417
|
-
result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
553
|
+
const result = buildCombinedRouteMapForRouterFile(routerFilePath);
|
|
554
|
+
if (
|
|
555
|
+
Object.keys(result.routes).length === 0 &&
|
|
556
|
+
Object.keys(result.searchSchemas).length === 0
|
|
557
|
+
) {
|
|
558
|
+
// Check if the file even has a createRouter call — if not, skip entirely.
|
|
559
|
+
// If it does, fall through to write an empty placeholder below.
|
|
560
|
+
let routerSource: string;
|
|
561
|
+
try {
|
|
562
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (!extractUrlsFromRouter(routerSource)) continue;
|
|
418
567
|
}
|
|
419
568
|
|
|
420
569
|
const routerBasename = pathBasename(routerFilePath).replace(
|
|
@@ -61,7 +61,14 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
61
61
|
for (const entry of entries) {
|
|
62
62
|
const fullPath = join(dir, entry.name);
|
|
63
63
|
if (entry.isDirectory()) {
|
|
64
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
entry.name === "node_modules" ||
|
|
66
|
+
entry.name.startsWith(".") ||
|
|
67
|
+
entry.name === "dist" ||
|
|
68
|
+
entry.name === "build" ||
|
|
69
|
+
entry.name === "coverage"
|
|
70
|
+
)
|
|
71
|
+
continue;
|
|
65
72
|
results.push(...findTsFiles(fullPath, filter));
|
|
66
73
|
} else if (
|
|
67
74
|
(entry.name.endsWith(".ts") ||
|
|
@@ -214,11 +214,21 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
214
214
|
bgStopCapture = c.stop;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// Stamp tainted
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
217
|
+
// Stamp tainted ARGS only — not requestCtx. The args stamp guards
|
|
218
|
+
// direct ctx method calls (ctx.set, ctx.header, ctx.onResponse, etc.)
|
|
219
|
+
// which is sufficient for correctness.
|
|
220
|
+
//
|
|
221
|
+
// We intentionally skip stamping requestCtx here because:
|
|
222
|
+
// 1. runBackground starts the async task synchronously (before the
|
|
223
|
+
// first await), so stampCacheExec would pollute the shared
|
|
224
|
+
// requestCtx while the foreground pipeline is still running.
|
|
225
|
+
// This causes assertNotInsideCacheExec to fire when cache-store
|
|
226
|
+
// later calls requestCtx.onResponse().
|
|
227
|
+
// 2. requestCtx methods are closure-bound to the original ctx, so
|
|
228
|
+
// neither Object.create() nor a proxy can isolate the stamp.
|
|
229
|
+
// 3. The foreground miss path already stamps requestCtx and catches
|
|
230
|
+
// cookies()/headers() misuse on first execution. The background
|
|
231
|
+
// re-runs the same function with the same request.
|
|
222
232
|
const bgTaintedArgs: unknown[] = [];
|
|
223
233
|
for (const arg of args) {
|
|
224
234
|
if (isTainted(arg)) {
|
|
@@ -226,9 +236,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
226
236
|
bgTaintedArgs.push(arg);
|
|
227
237
|
}
|
|
228
238
|
}
|
|
229
|
-
if (requestCtx) {
|
|
230
|
-
stampCacheExec(requestCtx as object);
|
|
231
|
-
}
|
|
232
239
|
|
|
233
240
|
try {
|
|
234
241
|
const freshResult = await fn.apply(this, args);
|
|
@@ -249,9 +256,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
249
256
|
for (const arg of bgTaintedArgs) {
|
|
250
257
|
unstampCacheExec(arg as object);
|
|
251
258
|
}
|
|
252
|
-
if (requestCtx) {
|
|
253
|
-
unstampCacheExec(requestCtx as object);
|
|
254
|
-
}
|
|
255
259
|
// Restore original handle store
|
|
256
260
|
if (originalHandleStore && requestCtx) {
|
|
257
261
|
requestCtx._handleStore = originalHandleStore;
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -73,7 +73,7 @@ function getDefaultRouteCacheKey(
|
|
|
73
73
|
isIntercept?: boolean,
|
|
74
74
|
): string {
|
|
75
75
|
const ctx = getRequestContext();
|
|
76
|
-
const isPartial = ctx?.
|
|
76
|
+
const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
|
|
77
77
|
const searchParams = ctx?.url.searchParams;
|
|
78
78
|
const host = ctx?.url.host ?? "localhost";
|
|
79
79
|
|
|
@@ -326,24 +326,61 @@ export class CacheScope {
|
|
|
326
326
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
327
327
|
|
|
328
328
|
// Check if this is a partial request (navigation) vs document request
|
|
329
|
-
const isPartial = requestCtx.
|
|
329
|
+
const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
|
|
330
|
+
|
|
331
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
332
|
+
debugCacheLog(
|
|
333
|
+
`[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
330
336
|
|
|
331
337
|
requestCtx.waitUntil(async () => {
|
|
338
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
339
|
+
debugCacheLog(
|
|
340
|
+
`[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
332
344
|
await handleStore.settled;
|
|
333
345
|
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
347
|
+
debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// For document requests: only cache if layout segments have components
|
|
351
|
+
// (complete render). Parallel and route segments may legitimately have
|
|
352
|
+
// null components — UI-less @meta parallels return null, and void route
|
|
353
|
+
// handlers produce null when the UI lives in parallel slots/layouts.
|
|
354
|
+
// Partial requests always allow null components (client already has them).
|
|
336
355
|
if (!isPartial) {
|
|
337
|
-
const
|
|
338
|
-
(s) => s.component
|
|
356
|
+
const hasIncompleteLayouts = nonLoaderSegments.some(
|
|
357
|
+
(s) => s.component === null && s.type === "layout",
|
|
339
358
|
);
|
|
340
|
-
if (
|
|
359
|
+
if (hasIncompleteLayouts) {
|
|
360
|
+
const nullSegments = nonLoaderSegments
|
|
361
|
+
.filter((s) => s.component === null && s.type === "layout")
|
|
362
|
+
.map((s) => s.id);
|
|
363
|
+
const error = new Error(
|
|
364
|
+
`[CacheScope] Cache write skipped: layout segments have null components ` +
|
|
365
|
+
`(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
|
|
366
|
+
`layout handlers must return JSX for document requests to be cacheable.`,
|
|
367
|
+
);
|
|
368
|
+
error.name = "CacheScopeInvariantError";
|
|
369
|
+
console.error(error.message);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
341
372
|
}
|
|
342
373
|
|
|
343
374
|
// Collect handle data for non-loader segments only
|
|
344
375
|
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
345
376
|
|
|
346
377
|
try {
|
|
378
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
379
|
+
debugCacheLog(
|
|
380
|
+
`[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
347
384
|
// Serialize non-loader segments only
|
|
348
385
|
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
349
386
|
|
|
@@ -353,6 +390,10 @@ export class CacheScope {
|
|
|
353
390
|
expiresAt: Date.now() + ttl * 1000,
|
|
354
391
|
};
|
|
355
392
|
|
|
393
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
394
|
+
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
356
397
|
await store.set(key, data, ttl, swr);
|
|
357
398
|
|
|
358
399
|
if (INTERNAL_RANGO_DEBUG) {
|