@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2138 -841
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +15 -9
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/migrate-react-router/SKILL.md +5 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +12 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +33 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +816 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +65 -9
- package/src/browser/navigation-client.ts +45 -25
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +52 -26
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +114 -38
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +2 -1
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +16 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -39
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +253 -265
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +43 -15
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -41
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +3 -3
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +122 -56
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +332 -0
- package/src/testing/flight.entry.ts +46 -0
- package/src/testing/flight.ts +224 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +267 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +188 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +270 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/segments.ts +35 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
import type { UrlPatterns } from "../urls.js";
|
|
12
12
|
import type { AllUseItems } from "../route-types.js";
|
|
13
13
|
import { extractStaticPrefix } from "../router/pattern-matching.js";
|
|
14
|
-
import {
|
|
14
|
+
import { RangoContext, runWithPrefixes } from "../server/context.js";
|
|
15
15
|
import type { EntryData, TrackedInclude } from "../server/context.js";
|
|
16
16
|
import type { TrailingSlashMode } from "../types.js";
|
|
17
17
|
import { createRouteHelpers } from "../route-definition.js";
|
|
18
18
|
import MapRootLayout from "../server/root-layout.js";
|
|
19
|
+
import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Node in the prefix tree
|
|
@@ -57,6 +58,26 @@ export interface GeneratedManifest {
|
|
|
57
58
|
* Build prefix tree node by running the patterns with proper context.
|
|
58
59
|
* Uses a visited set to detect circular includes and prevent infinite recursion.
|
|
59
60
|
*/
|
|
61
|
+
// Merge tracked nested includes into `target`. Multiple includes can share a
|
|
62
|
+
// fullPrefix (e.g. include("/", a), include("/", b)) — concat their routes and
|
|
63
|
+
// Object.assign children rather than overwrite.
|
|
64
|
+
function mergeIncludeNodes(
|
|
65
|
+
target: Record<string, PrefixTreeNode>,
|
|
66
|
+
includes: TrackedInclude[],
|
|
67
|
+
buildChild: (include: TrackedInclude) => PrefixTreeNode,
|
|
68
|
+
): void {
|
|
69
|
+
for (const include of includes) {
|
|
70
|
+
const node = buildChild(include);
|
|
71
|
+
const existing = target[include.fullPrefix];
|
|
72
|
+
if (existing) {
|
|
73
|
+
existing.routes.push(...node.routes);
|
|
74
|
+
Object.assign(existing.children, node.children);
|
|
75
|
+
} else {
|
|
76
|
+
target[include.fullPrefix] = node;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
function buildPrefixTreeNode(
|
|
61
82
|
urlPrefix: string,
|
|
62
83
|
namePrefix: string | undefined,
|
|
@@ -93,7 +114,7 @@ function buildPrefixTreeNode(
|
|
|
93
114
|
const searchSchemasMap = new Map<string, Record<string, string>>();
|
|
94
115
|
const trackedIncludes: TrackedInclude[] = [];
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
RangoContext.run(
|
|
97
118
|
{
|
|
98
119
|
manifest,
|
|
99
120
|
patterns: patternsMap,
|
|
@@ -166,13 +187,9 @@ function buildPrefixTreeNode(
|
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
// Build children from tracked nested includes.
|
|
170
|
-
// Multiple includes can share the same fullPrefix (e.g., include("/", patternsA),
|
|
171
|
-
// include("/", patternsB)). Merge their routes instead of overwriting.
|
|
172
190
|
const children: Record<string, PrefixTreeNode> = {};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const childNode = buildPrefixTreeNode(
|
|
191
|
+
mergeIncludeNodes(children, trackedIncludes, (include) =>
|
|
192
|
+
buildPrefixTreeNode(
|
|
176
193
|
include.fullPrefix,
|
|
177
194
|
include.namePrefix,
|
|
178
195
|
include.patterns as UrlPatterns<any>,
|
|
@@ -186,16 +203,8 @@ function buildPrefixTreeNode(
|
|
|
186
203
|
passthroughRoutes,
|
|
187
204
|
responseTypeRoutes,
|
|
188
205
|
routeSearchSchemas,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
const existing = children[include.fullPrefix];
|
|
192
|
-
if (existing) {
|
|
193
|
-
existing.routes.push(...childNode.routes);
|
|
194
|
-
Object.assign(existing.children, childNode.children);
|
|
195
|
-
} else {
|
|
196
|
-
children[include.fullPrefix] = childNode;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
206
|
+
),
|
|
207
|
+
);
|
|
199
208
|
|
|
200
209
|
// Remove from visited so sibling branches can reuse the same patterns
|
|
201
210
|
// without false circular-include detection. Only ancestors in the current
|
|
@@ -282,7 +291,17 @@ export function generateManifest<TEnv>(
|
|
|
282
291
|
export function generateManifestFull<TEnv>(
|
|
283
292
|
urlpatterns: UrlPatterns<TEnv, any>,
|
|
284
293
|
mountIndex: number = 0,
|
|
285
|
-
options?: {
|
|
294
|
+
options?: {
|
|
295
|
+
urlPrefix?: string;
|
|
296
|
+
/**
|
|
297
|
+
* Called once per `"use client"` component registered as an
|
|
298
|
+
* errorBoundary/notFoundBoundary fallback, with its client-reference key
|
|
299
|
+
* (`$$id`). Lets the build collect fallback module ids for dedicated
|
|
300
|
+
* chunking without exposing the otherwise-discarded EntryData tree. The
|
|
301
|
+
* EntryData map built below is local; this is the only seam that surfaces it.
|
|
302
|
+
*/
|
|
303
|
+
collectClientFallbackRef?: (refKey: string) => void;
|
|
304
|
+
},
|
|
286
305
|
): FullManifest {
|
|
287
306
|
const routeManifest: Record<string, string> = {};
|
|
288
307
|
const routeAncestry: Record<string, string[]> = {};
|
|
@@ -296,7 +315,7 @@ export function generateManifestFull<TEnv>(
|
|
|
296
315
|
const searchSchemasMap = new Map<string, Record<string, string>>();
|
|
297
316
|
const trackedIncludes: TrackedInclude[] = [];
|
|
298
317
|
|
|
299
|
-
|
|
318
|
+
RangoContext.run(
|
|
300
319
|
{
|
|
301
320
|
manifest,
|
|
302
321
|
patterns: patternsMap,
|
|
@@ -320,6 +339,22 @@ export function generateManifestFull<TEnv>(
|
|
|
320
339
|
},
|
|
321
340
|
);
|
|
322
341
|
|
|
342
|
+
// Surface the "use client" components registered as error/notFound fallbacks
|
|
343
|
+
// (route-tree errorBoundary()/notFoundBoundary() helpers, stored on EntryData).
|
|
344
|
+
// The boundary may be a handler function and/or wrap the client boundary in
|
|
345
|
+
// server providers, so walk the whole tree (see collectFallbackClientRefs).
|
|
346
|
+
if (options?.collectClientFallbackRef) {
|
|
347
|
+
const report = options.collectClientFallbackRef;
|
|
348
|
+
const collect = (boundary: unknown[] | undefined) => {
|
|
349
|
+
for (const item of boundary ?? [])
|
|
350
|
+
collectFallbackClientRefs(item, report);
|
|
351
|
+
};
|
|
352
|
+
for (const entry of manifest.values()) {
|
|
353
|
+
collect(entry.errorBoundary);
|
|
354
|
+
collect(entry.notFoundBoundary);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
323
358
|
// Collect root-level routes and trailing slash config
|
|
324
359
|
const routeTrailingSlash: Record<string, string> = {};
|
|
325
360
|
for (const [name, pattern] of patternsMap.entries()) {
|
|
@@ -356,12 +391,10 @@ export function generateManifestFull<TEnv>(
|
|
|
356
391
|
}
|
|
357
392
|
}
|
|
358
393
|
|
|
359
|
-
//
|
|
360
|
-
// Multiple includes can share the same fullPrefix (e.g., include("/", patternsA),
|
|
361
|
-
// include("/", patternsB)). Merge their routes instead of overwriting.
|
|
394
|
+
// Shared visited set for cycle detection across all root-level includes.
|
|
362
395
|
const visited = new Set<unknown>();
|
|
363
|
-
|
|
364
|
-
|
|
396
|
+
mergeIncludeNodes(prefixTree, trackedIncludes, (include) =>
|
|
397
|
+
buildPrefixTreeNode(
|
|
365
398
|
include.fullPrefix,
|
|
366
399
|
include.namePrefix,
|
|
367
400
|
include.patterns as UrlPatterns<any>,
|
|
@@ -375,16 +408,8 @@ export function generateManifestFull<TEnv>(
|
|
|
375
408
|
passthroughRoutes,
|
|
376
409
|
responseTypeRoutes,
|
|
377
410
|
routeSearchSchemas,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
const existing = prefixTree[include.fullPrefix];
|
|
381
|
-
if (existing) {
|
|
382
|
-
existing.routes.push(...node.routes);
|
|
383
|
-
Object.assign(existing.children, node.children);
|
|
384
|
-
} else {
|
|
385
|
-
prefixTree[include.fullPrefix] = node;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
411
|
+
),
|
|
412
|
+
);
|
|
388
413
|
|
|
389
414
|
return {
|
|
390
415
|
prefixTree,
|
package/src/build/index.ts
CHANGED
package/src/build/route-trie.ts
CHANGED
|
@@ -20,7 +20,8 @@ export interface TrieLeaf {
|
|
|
20
20
|
sp: string;
|
|
21
21
|
/** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
|
|
22
22
|
a: string[];
|
|
23
|
-
/** Optional param names
|
|
23
|
+
/** Optional param names declared on the route. Absent params are
|
|
24
|
+
* omitted from the matched params record (read as `undefined`). */
|
|
24
25
|
op?: string[];
|
|
25
26
|
/** Constraint validation: paramName -> allowed values */
|
|
26
27
|
cv?: Record<string, string[]>;
|
|
@@ -23,7 +23,7 @@ export function generatePerModuleTypesSource(
|
|
|
23
23
|
const valid = routes.filter(({ name }) => {
|
|
24
24
|
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
25
25
|
console.warn(
|
|
26
|
-
`[
|
|
26
|
+
`[rango] Skipping route with invalid name: ${JSON.stringify(name)}`,
|
|
27
27
|
);
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
@@ -42,7 +42,7 @@ export function generatePerModuleTypesSource(
|
|
|
42
42
|
for (const { name, pattern, params, search } of valid) {
|
|
43
43
|
if (deduped.has(name)) {
|
|
44
44
|
console.warn(
|
|
45
|
-
`[
|
|
45
|
+
`[rango] Duplicate route name "${name}" — keeping first definition`,
|
|
46
46
|
);
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
@@ -59,7 +59,7 @@ export function generatePerModuleTypesSource(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Generates a .ts file that augments
|
|
62
|
+
* Generates a .ts file that augments Rango.GeneratedRouteMap
|
|
63
63
|
* with route name -> pattern mappings. This enables Handler<"routeName">
|
|
64
64
|
* without circular references since the file has no imports from the app.
|
|
65
65
|
*/
|
|
@@ -94,7 +94,7 @@ ${objectBody}
|
|
|
94
94
|
} as const;
|
|
95
95
|
|
|
96
96
|
declare global {
|
|
97
|
-
namespace
|
|
97
|
+
namespace Rango {
|
|
98
98
|
interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -376,7 +376,7 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
376
376
|
const realPath = resolve(filePath);
|
|
377
377
|
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
378
378
|
if (visited.has(key)) {
|
|
379
|
-
console.warn(`[
|
|
379
|
+
console.warn(`[rango] Circular include detected, skipping: ${key}`);
|
|
380
380
|
return { routes: {}, searchSchemas: {} };
|
|
381
381
|
}
|
|
382
382
|
visited.add(key);
|
|
@@ -97,7 +97,10 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
97
97
|
routes = extractRoutesFromSource(source);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
// Match .ts/.tsx/.js/.jsx (same as router-processing.ts / router-transform.ts).
|
|
101
|
+
// Without the jsx? branch a .jsx/.js source produced genPath === filePath,
|
|
102
|
+
// overwriting the source file instead of writing a sibling .gen.ts.
|
|
103
|
+
const genPath = filePath.replace(/\.(tsx?|jsx?)$/, ".gen.ts");
|
|
101
104
|
|
|
102
105
|
// When a urls() variable was found but static resolution yields zero
|
|
103
106
|
// routes, write an empty placeholder so generated imports stay
|
|
@@ -106,7 +109,7 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
106
109
|
if (varNames.length > 0 && !existsSync(genPath)) {
|
|
107
110
|
writeFileSync(genPath, generatePerModuleTypesSource([]));
|
|
108
111
|
console.log(
|
|
109
|
-
`[
|
|
112
|
+
`[rango] Generated route types (placeholder) -> ${genPath}`,
|
|
110
113
|
);
|
|
111
114
|
}
|
|
112
115
|
return;
|
|
@@ -118,11 +121,11 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
118
121
|
: null;
|
|
119
122
|
if (existing !== genSource) {
|
|
120
123
|
writeFileSync(genPath, genSource);
|
|
121
|
-
console.log(`[
|
|
124
|
+
console.log(`[rango] Generated route types -> ${genPath}`);
|
|
122
125
|
}
|
|
123
126
|
} catch (err) {
|
|
124
127
|
console.warn(
|
|
125
|
-
`[
|
|
128
|
+
`[rango] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
|
|
126
129
|
);
|
|
127
130
|
}
|
|
128
131
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import ts from "typescript";
|
|
16
16
|
import { generateRouteTypesSource } from "./codegen.js";
|
|
17
17
|
import type { ScanFilter } from "./scan-filter.js";
|
|
18
|
+
import { firstCodeMatchIndex } from "./source-scan.js";
|
|
18
19
|
import {
|
|
19
20
|
resolveImportedVariable,
|
|
20
21
|
resolveImportPath,
|
|
@@ -38,6 +39,8 @@ function countPublicRouteEntries(source: string): number {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
|
|
42
|
+
// Global variant for the code-region scan (firstCodeMatchIndex sets lastIndex).
|
|
43
|
+
const ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
|
|
41
44
|
|
|
42
45
|
function isRoutableSourceFile(name: string): boolean {
|
|
43
46
|
return (
|
|
@@ -61,7 +64,7 @@ function findRouterFilesRecursive(
|
|
|
61
64
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
62
65
|
} catch (err) {
|
|
63
66
|
console.warn(
|
|
64
|
-
`[
|
|
67
|
+
`[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
65
68
|
);
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
@@ -90,7 +93,17 @@ function findRouterFilesRecursive(
|
|
|
90
93
|
|
|
91
94
|
try {
|
|
92
95
|
const source = readFileSync(fullPath, "utf-8");
|
|
93
|
-
|
|
96
|
+
// Fast path: most files contain no `createRouter(` at all, so the cheap
|
|
97
|
+
// raw regex short-circuits before the code-region scan. Only a file that
|
|
98
|
+
// mentions the token (real call OR a comment/string mention) is rescanned
|
|
99
|
+
// over code regions — allocation-free, never building a stripped copy —
|
|
100
|
+
// so a mention inside a comment or string is not mistaken for a real
|
|
101
|
+
// router file (which previously triggered a spurious "Multiple routers
|
|
102
|
+
// found" error).
|
|
103
|
+
if (
|
|
104
|
+
ROUTER_CALL_PATTERN.test(source) &&
|
|
105
|
+
firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0
|
|
106
|
+
) {
|
|
94
107
|
routerFilesInDir.push(fullPath);
|
|
95
108
|
}
|
|
96
109
|
} catch {
|
|
@@ -142,7 +155,7 @@ export function findNestedRouterConflict(
|
|
|
142
155
|
|
|
143
156
|
export function formatNestedRouterConflictError(
|
|
144
157
|
conflict: { ancestor: string; nested: string },
|
|
145
|
-
prefix = "[
|
|
158
|
+
prefix = "[rango]",
|
|
146
159
|
): string {
|
|
147
160
|
return (
|
|
148
161
|
`${prefix} Nested router roots are not supported.\n` +
|
|
@@ -339,6 +352,36 @@ function applyBasenameToRoutes(
|
|
|
339
352
|
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
340
353
|
}
|
|
341
354
|
|
|
355
|
+
// Filesystem path of the generated route-types file for a router source file.
|
|
356
|
+
// Native separators — matches the self-gen-tracking Map key the watcher compares.
|
|
357
|
+
export function genFileTsPath(sourceFile: string): string {
|
|
358
|
+
const base = pathBasename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
|
|
359
|
+
return join(dirname(sourceFile), `${base}.named-routes.gen.ts`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Search schemas for the gen file: prefer the runtime manifest's; when it omits
|
|
363
|
+
// them (some module-runner flows) fall back to static parsing filtered to the
|
|
364
|
+
// public route-name set. Returns the runtime value unchanged otherwise.
|
|
365
|
+
export function resolveSearchSchemas(
|
|
366
|
+
publicRouteNames: string[],
|
|
367
|
+
runtimeSchemas: Record<string, Record<string, string>> | undefined,
|
|
368
|
+
sourceFile: string,
|
|
369
|
+
): Record<string, Record<string, string>> | undefined {
|
|
370
|
+
if (runtimeSchemas && Object.keys(runtimeSchemas).length > 0) {
|
|
371
|
+
return runtimeSchemas;
|
|
372
|
+
}
|
|
373
|
+
const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
|
|
374
|
+
if (Object.keys(staticParsed.searchSchemas).length === 0) {
|
|
375
|
+
return runtimeSchemas;
|
|
376
|
+
}
|
|
377
|
+
const filtered: Record<string, Record<string, string>> = {};
|
|
378
|
+
for (const name of publicRouteNames) {
|
|
379
|
+
const schema = staticParsed.searchSchemas[name];
|
|
380
|
+
if (schema) filtered[name] = schema;
|
|
381
|
+
}
|
|
382
|
+
return Object.keys(filtered).length > 0 ? filtered : runtimeSchemas;
|
|
383
|
+
}
|
|
384
|
+
|
|
342
385
|
/**
|
|
343
386
|
* Resolve routes and search schemas from a router source file by following the
|
|
344
387
|
* variable passed to `.routes(...)` or `urls: ...` in createRouter options,
|
|
@@ -528,7 +571,10 @@ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
|
528
571
|
export function writeCombinedRouteTypes(
|
|
529
572
|
root: string,
|
|
530
573
|
knownRouterFiles?: string[],
|
|
531
|
-
opts?: {
|
|
574
|
+
opts?: {
|
|
575
|
+
preserveIfLarger?: boolean;
|
|
576
|
+
onWrite?: (outPath: string, content: string) => void;
|
|
577
|
+
},
|
|
532
578
|
): void {
|
|
533
579
|
// Delete old combined named-routes.gen.ts if it exists (stale from older versions)
|
|
534
580
|
try {
|
|
@@ -536,7 +582,7 @@ export function writeCombinedRouteTypes(
|
|
|
536
582
|
if (existsSync(oldCombinedPath)) {
|
|
537
583
|
unlinkSync(oldCombinedPath);
|
|
538
584
|
console.log(
|
|
539
|
-
`[
|
|
585
|
+
`[rango] Removed stale combined route types: ${oldCombinedPath}`,
|
|
540
586
|
);
|
|
541
587
|
}
|
|
542
588
|
} catch {}
|
|
@@ -566,14 +612,7 @@ export function writeCombinedRouteTypes(
|
|
|
566
612
|
if (!extractUrlsFromRouter(routerSource)) continue;
|
|
567
613
|
}
|
|
568
614
|
|
|
569
|
-
const
|
|
570
|
-
/\.(tsx?|jsx?)$/,
|
|
571
|
-
"",
|
|
572
|
-
);
|
|
573
|
-
const outPath = join(
|
|
574
|
-
dirname(routerFilePath),
|
|
575
|
-
`${routerBasename}.named-routes.gen.ts`,
|
|
576
|
-
);
|
|
615
|
+
const outPath = genFileTsPath(routerFilePath);
|
|
577
616
|
const existing = existsSync(outPath)
|
|
578
617
|
? readFileSync(outPath, "utf-8")
|
|
579
618
|
: null;
|
|
@@ -584,6 +623,7 @@ export function writeCombinedRouteTypes(
|
|
|
584
623
|
if (Object.keys(result.routes).length === 0) {
|
|
585
624
|
if (!existing) {
|
|
586
625
|
const emptySource = generateRouteTypesSource({});
|
|
626
|
+
opts?.onWrite?.(outPath, emptySource);
|
|
587
627
|
writeFileSync(outPath, emptySource);
|
|
588
628
|
}
|
|
589
629
|
continue;
|
|
@@ -609,9 +649,10 @@ export function writeCombinedRouteTypes(
|
|
|
609
649
|
continue;
|
|
610
650
|
}
|
|
611
651
|
}
|
|
652
|
+
opts?.onWrite?.(outPath, source);
|
|
612
653
|
writeFileSync(outPath, source);
|
|
613
654
|
console.log(
|
|
614
|
-
`[
|
|
655
|
+
`[rango] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
|
|
615
656
|
);
|
|
616
657
|
}
|
|
617
658
|
}
|
|
@@ -54,7 +54,7 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
54
54
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
55
|
} catch (err) {
|
|
56
56
|
console.warn(
|
|
57
|
-
`[
|
|
57
|
+
`[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
58
58
|
);
|
|
59
59
|
return results;
|
|
60
60
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Allocation-light, linear-time source scanning for the build-time scanners.
|
|
2
|
+
//
|
|
3
|
+
// The router-file scanner, the HMR relevance check, and the unsupported-shape
|
|
4
|
+
// warning all need to know whether a token like `createRouter(` / `createLoader(`
|
|
5
|
+
// appears in REAL code versus inside a comment or string literal. Rather than
|
|
6
|
+
// build a full comment/string-stripped copy of the source (which on a large
|
|
7
|
+
// file allocates an O(n) string plus, naively, a per-char array), these helpers
|
|
8
|
+
// run the regex over the whole source ONCE (the engine sweeps left-to-right,
|
|
9
|
+
// O(n)) and classify each match's offset with a forward, O(1)-memory cursor that
|
|
10
|
+
// advances monotonically across the source.
|
|
11
|
+
//
|
|
12
|
+
// Time: O(n) — one native regex sweep plus one forward classification pass.
|
|
13
|
+
// Memory: O(1) for the boolean check; O(#matches) for the index list. No
|
|
14
|
+
// stripped copy and no per-char array are ever materialized.
|
|
15
|
+
//
|
|
16
|
+
// Pragmatic scanner, not a full tokenizer: regex literals are not special-cased
|
|
17
|
+
// (a target token inside one is implausible) and template interpolations are
|
|
18
|
+
// treated as opaque string content. One intentional consequence: a token whose
|
|
19
|
+
// match would only complete by treating an interleaved comment as whitespace
|
|
20
|
+
// (e.g. `createRouter /* x */ (`) is not detected — real calls never interleave
|
|
21
|
+
// a comment between the callee and its arguments.
|
|
22
|
+
|
|
23
|
+
// JS line terminators end a `//` comment: LF, CR, LS (U+2028), PS (U+2029).
|
|
24
|
+
function isLineTerminator(ch: string): boolean {
|
|
25
|
+
const c = ch.charCodeAt(0);
|
|
26
|
+
// LF, CR, LS (U+2028), PS (U+2029)
|
|
27
|
+
return c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a classifier that answers "is offset `q` in code (not a comment or
|
|
32
|
+
* string)?" for STRICTLY INCREASING `q`. The internal cursor only moves forward,
|
|
33
|
+
* so a full left-to-right sequence of queries costs O(n) total with O(1) memory.
|
|
34
|
+
*/
|
|
35
|
+
function makeCodeClassifier(code: string): (q: number) => boolean {
|
|
36
|
+
const n = code.length;
|
|
37
|
+
let i = 0; // forward cursor: everything before `i` is already classified
|
|
38
|
+
let skipStart = -1; // last detected comment/string region (cache)
|
|
39
|
+
let skipEnd = -1;
|
|
40
|
+
|
|
41
|
+
return (q: number): boolean => {
|
|
42
|
+
if (q >= skipStart && q < skipEnd) return false; // q in the cached region
|
|
43
|
+
while (i < n && i <= q) {
|
|
44
|
+
const c = code[i];
|
|
45
|
+
const d = i + 1 < n ? code[i + 1] : "";
|
|
46
|
+
let end = -1;
|
|
47
|
+
if (c === "/" && d === "/") {
|
|
48
|
+
let j = i + 2;
|
|
49
|
+
while (j < n && !isLineTerminator(code[j])) j++;
|
|
50
|
+
end = j;
|
|
51
|
+
} else if (c === "/" && d === "*") {
|
|
52
|
+
let j = i + 2;
|
|
53
|
+
while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
|
|
54
|
+
end = Math.min(n, j + 2);
|
|
55
|
+
} else if (c === '"' || c === "'" || c === "`") {
|
|
56
|
+
let j = i + 1;
|
|
57
|
+
while (j < n) {
|
|
58
|
+
if (code[j] === "\\") {
|
|
59
|
+
j += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (code[j] === c) {
|
|
63
|
+
j++;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
j++;
|
|
67
|
+
}
|
|
68
|
+
end = j;
|
|
69
|
+
}
|
|
70
|
+
if (end >= 0) {
|
|
71
|
+
// Comment/string region [i, end). `q >= i` here (loop condition).
|
|
72
|
+
if (q < end) {
|
|
73
|
+
skipStart = i;
|
|
74
|
+
skipEnd = end;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
i = end;
|
|
78
|
+
} else {
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true; // reached q in code mode
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Index of the first match of `pattern` that occurs in code (not in a comment
|
|
88
|
+
* or string), or -1. `pattern` MUST be a global (`/g`) regex. Single native
|
|
89
|
+
* regex sweep with early-exit; O(1) extra memory.
|
|
90
|
+
*/
|
|
91
|
+
export function firstCodeMatchIndex(code: string, pattern: RegExp): number {
|
|
92
|
+
const inCode = makeCodeClassifier(code);
|
|
93
|
+
pattern.lastIndex = 0;
|
|
94
|
+
let m: RegExpExecArray | null;
|
|
95
|
+
while ((m = pattern.exec(code)) !== null) {
|
|
96
|
+
if (inCode(m.index)) return m.index;
|
|
97
|
+
if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
|
|
98
|
+
}
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Byte offsets of every match of `pattern` that occurs in code (not in a
|
|
104
|
+
* comment or string). `pattern` MUST be a global (`/g`) regex. Each offset is
|
|
105
|
+
* the match start — the same byte offset a raw `pattern.exec` reports. O(n)
|
|
106
|
+
* time, O(#matches) memory.
|
|
107
|
+
*/
|
|
108
|
+
export function codeMatchIndices(code: string, pattern: RegExp): number[] {
|
|
109
|
+
const inCode = makeCodeClassifier(code);
|
|
110
|
+
const indices: number[] = [];
|
|
111
|
+
pattern.lastIndex = 0;
|
|
112
|
+
let m: RegExpExecArray | null;
|
|
113
|
+
while ((m = pattern.exec(code)) !== null) {
|
|
114
|
+
if (inCode(m.index)) indices.push(m.index);
|
|
115
|
+
if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
|
|
116
|
+
}
|
|
117
|
+
return indices;
|
|
118
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
generateRouteTypesSource,
|
|
5
|
-
|
|
5
|
+
genFileTsPath,
|
|
6
|
+
resolveSearchSchemas,
|
|
6
7
|
} from "./generate-route-types.ts";
|
|
7
8
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
8
9
|
|
|
@@ -175,25 +176,13 @@ export async function discoverAndWriteRouteTypes(
|
|
|
175
176
|
);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const filtered: Record<string, Record<string, string>> = {};
|
|
184
|
-
for (const name of Object.keys(routeManifest)) {
|
|
185
|
-
const schema = staticParsed.searchSchemas[name];
|
|
186
|
-
if (schema) filtered[name] = schema;
|
|
187
|
-
}
|
|
188
|
-
if (Object.keys(filtered).length > 0) {
|
|
189
|
-
routeSearchSchemas = filtered;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
179
|
+
routeSearchSchemas = resolveSearchSchemas(
|
|
180
|
+
Object.keys(routeManifest),
|
|
181
|
+
routeSearchSchemas,
|
|
182
|
+
sourceFile,
|
|
183
|
+
);
|
|
193
184
|
|
|
194
|
-
const
|
|
195
|
-
const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
|
|
196
|
-
const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
|
|
185
|
+
const outPath = genFileTsPath(sourceFile);
|
|
197
186
|
|
|
198
187
|
const source = generateRouteTypesSource(
|
|
199
188
|
routeManifest,
|