@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 +8 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- 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 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- 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 +96 -68
- 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 +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Collect the `"use client"` client-reference keys reachable from an error /
|
|
2
|
+
// notFound boundary registration, for routing them into the dedicated
|
|
3
|
+
// `app-fallback` chunk (see vite/utils/client-chunks.ts).
|
|
4
|
+
//
|
|
5
|
+
// A boundary registration is not always a bare client element. The common,
|
|
6
|
+
// load-bearing pattern wraps the client boundary in providers a thrown handler
|
|
7
|
+
// needs (the layout that would normally supply them did not mount):
|
|
8
|
+
//
|
|
9
|
+
// defaultErrorBoundary: ({ error }) => (
|
|
10
|
+
// <FallbackIntl locales={...}>
|
|
11
|
+
// <ThemedError error={error} /> // <- the real "use client" boundary
|
|
12
|
+
// </FallbackIntl>
|
|
13
|
+
// )
|
|
14
|
+
//
|
|
15
|
+
// So the value may be (a) a handler FUNCTION returning a tree, or (b) an element
|
|
16
|
+
// tree with the client boundary nested below server wrappers. We:
|
|
17
|
+
// 1. If it's a function, CALL it with synthetic props to get the returned tree.
|
|
18
|
+
// This only constructs JSX — the inner components are element `type`s, never
|
|
19
|
+
// invoked — so no hooks run. Guarded: a boundary that needs a real render
|
|
20
|
+
// context (request globals, etc.) throws and is skipped (graceful: it simply
|
|
21
|
+
// stays on the default grouping, as before).
|
|
22
|
+
// 2. Walk the resulting tree and report every element whose `.type` is a
|
|
23
|
+
// plugin-rsc client reference.
|
|
24
|
+
//
|
|
25
|
+
// Limit: a boundary that *conditionally* renders different client components based
|
|
26
|
+
// on the runtime error cannot be resolved statically — only the branch taken with
|
|
27
|
+
// the synthetic error is seen. Such cases fall back to the default chunk; the
|
|
28
|
+
// custom `clientChunks` function is the escape hatch.
|
|
29
|
+
|
|
30
|
+
const CLIENT_REF = Symbol.for("react.client.reference");
|
|
31
|
+
const MAX_DEPTH = 40;
|
|
32
|
+
|
|
33
|
+
// Synthetic props covering the error-boundary (`{ error, reset }`) and notFound
|
|
34
|
+
// (`{ pathname }`) handler shapes. The handler destructures what it needs.
|
|
35
|
+
const SYNTHETIC_PROPS = {
|
|
36
|
+
error: new Error("rango: build-time fallback-chunk discovery"),
|
|
37
|
+
reset: () => {},
|
|
38
|
+
pathname: "/",
|
|
39
|
+
info: { componentStack: "" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface MaybeElement {
|
|
43
|
+
type?: { $$typeof?: symbol; $$id?: string };
|
|
44
|
+
props?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isReactNodeLike(v: unknown): boolean {
|
|
48
|
+
return (
|
|
49
|
+
Array.isArray(v) ||
|
|
50
|
+
(typeof v === "object" && v !== null && "$$typeof" in (v as object))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walkElementTree(
|
|
55
|
+
node: unknown,
|
|
56
|
+
report: (refKey: string) => void,
|
|
57
|
+
depth: number,
|
|
58
|
+
): void {
|
|
59
|
+
if (node == null || depth > MAX_DEPTH) return;
|
|
60
|
+
if (Array.isArray(node)) {
|
|
61
|
+
for (const child of node) walkElementTree(child, report, depth + 1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof node !== "object") return;
|
|
65
|
+
|
|
66
|
+
const el = node as MaybeElement;
|
|
67
|
+
const type = el.type;
|
|
68
|
+
if (type?.$$typeof === CLIENT_REF && typeof type.$$id === "string") {
|
|
69
|
+
// $$id is `<referenceKey>#<exportName>` in build mode — keep the referenceKey.
|
|
70
|
+
report(type.$$id.split("#")[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = el.props;
|
|
74
|
+
if (props && typeof props === "object") {
|
|
75
|
+
// Children are always nodes; other props are followed only when they look
|
|
76
|
+
// like React nodes (slots/icons), never arbitrary data objects.
|
|
77
|
+
walkElementTree(props.children, report, depth + 1);
|
|
78
|
+
for (const key in props) {
|
|
79
|
+
if (key === "children") continue;
|
|
80
|
+
const value = props[key];
|
|
81
|
+
if (isReactNodeLike(value)) walkElementTree(value, report, depth + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Report every `"use client"` client-reference key reachable from a single
|
|
88
|
+
* error/notFound boundary registration (handler function or element tree).
|
|
89
|
+
*/
|
|
90
|
+
export function collectFallbackClientRefs(
|
|
91
|
+
boundary: unknown,
|
|
92
|
+
report: (refKey: string) => void,
|
|
93
|
+
): void {
|
|
94
|
+
try {
|
|
95
|
+
let node = boundary;
|
|
96
|
+
if (typeof node === "function") {
|
|
97
|
+
node = (node as (props: unknown) => unknown)(SYNTHETIC_PROPS);
|
|
98
|
+
}
|
|
99
|
+
walkElementTree(node, report, 0);
|
|
100
|
+
} catch {
|
|
101
|
+
// The boundary needs a real render context (request globals, hooks at the
|
|
102
|
+
// top level) or its tree has hostile getters. Its client refs can't be
|
|
103
|
+
// resolved statically — skip. It stays on the default grouping (no
|
|
104
|
+
// regression vs. not collecting), and the custom clientChunks fn is the
|
|
105
|
+
// escape hatch for such cases.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -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,
|
|
@@ -24,7 +24,6 @@ export {
|
|
|
24
24
|
extractIncludesWithDiagnostics,
|
|
25
25
|
} from "./route-types/include-resolution.js";
|
|
26
26
|
export {
|
|
27
|
-
extractUrlsVariableFromRouter,
|
|
28
27
|
extractUrlsFromRouter,
|
|
29
28
|
extractBasenameFromRouter,
|
|
30
29
|
type UrlsExtractionResult,
|
|
@@ -35,5 +34,7 @@ export {
|
|
|
35
34
|
formatNestedRouterConflictError,
|
|
36
35
|
findRouterFiles,
|
|
37
36
|
writeCombinedRouteTypes,
|
|
37
|
+
genFileTsPath,
|
|
38
|
+
resolveSearchSchemas,
|
|
38
39
|
} from "./route-types/router-processing.js";
|
|
39
40
|
export { findUrlsVariableNames } from "./route-types/per-module-writer.js";
|
package/src/build/index.ts
CHANGED
|
@@ -22,8 +22,14 @@ export {
|
|
|
22
22
|
type GeneratedManifest,
|
|
23
23
|
} from "./generate-manifest.js";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// buildRouteTrie / buildPerRouterTrie / collectFallbackClientRefs and the
|
|
26
|
+
// TrieNode/TrieLeaf types are NOT exported here: they are build-pipeline
|
|
27
|
+
// internals, not public API. Their only build-time consumer (the Vite
|
|
28
|
+
// discovery pass) imports them directly from source via a relative path
|
|
29
|
+
// (vite/discovery/discover-routers.ts), and the runtime RSC realm likewise
|
|
30
|
+
// imports route-trie.js directly (rsc/manifest-init.ts). Keeping them off the
|
|
31
|
+
// public ./build surface (#569 decision 6) means consumers can't mistake them
|
|
32
|
+
// for intended API. generateManifest* / route-types / hashParams stay public.
|
|
27
33
|
export {
|
|
28
34
|
writePerModuleRouteTypes,
|
|
29
35
|
extractRoutesFromSource,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure prefix-tree walks shared by the build/discovery layer and the runtime
|
|
3
|
+
* trie builder. Kept in `build/` (not `vite/utils`) so runtime code
|
|
4
|
+
* (rsc/manifest-init via build/route-trie) can consume them without importing
|
|
5
|
+
* from the vite layer. `vite/utils/manifest-utils` re-exports them so existing
|
|
6
|
+
* vite-side imports stay unchanged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Flatten prefix tree leaf nodes into precomputed route entries.
|
|
11
|
+
* Leaf nodes have no children (no nested includes), so their routes can be
|
|
12
|
+
* used directly by evaluateLazyEntry() without running the handler.
|
|
13
|
+
* Non-leaf nodes are skipped because they have nested lazy includes that
|
|
14
|
+
* require the handler to run for discovery.
|
|
15
|
+
*
|
|
16
|
+
* A leaf is also skipped when its staticPrefix collides with an ancestor
|
|
17
|
+
* include node's staticPrefix. That happens when a dynamic param collapses the
|
|
18
|
+
* staticPrefix of nested includes onto the parent's (e.g. `/m/:id/edit` -> sp
|
|
19
|
+
* `/m`): precomputing such a leaf under the collapsed prefix would let the
|
|
20
|
+
* ancestor's lazy entry claim a route it cannot register (the route is behind
|
|
21
|
+
* further nested lazy includes), producing a RouteNotFoundError at request time
|
|
22
|
+
* (issue #506). Those routes are resolved via the handler chain instead.
|
|
23
|
+
*/
|
|
24
|
+
export function flattenLeafEntries(
|
|
25
|
+
prefixTree: Record<string, any>,
|
|
26
|
+
routeManifest: Record<string, string>,
|
|
27
|
+
result: Array<{ staticPrefix: string; routes: Record<string, string> }>,
|
|
28
|
+
): void {
|
|
29
|
+
function visit(node: any, ancestorStaticPrefixes: Set<string>): void {
|
|
30
|
+
const children = node.children || {};
|
|
31
|
+
if (
|
|
32
|
+
Object.keys(children).length === 0 &&
|
|
33
|
+
node.routes &&
|
|
34
|
+
node.routes.length > 0
|
|
35
|
+
) {
|
|
36
|
+
// Leaf node. Skip if its staticPrefix collides with an ancestor include
|
|
37
|
+
// node's staticPrefix (dynamic-param collapse) — see doc comment above.
|
|
38
|
+
if (ancestorStaticPrefixes.has(node.staticPrefix)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Collect its routes from the manifest
|
|
42
|
+
const routes: Record<string, string> = {};
|
|
43
|
+
for (const name of node.routes) {
|
|
44
|
+
if (name in routeManifest) {
|
|
45
|
+
routes[name] = routeManifest[name];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
result.push({ staticPrefix: node.staticPrefix, routes });
|
|
49
|
+
} else {
|
|
50
|
+
// Non-leaf: recurse into children, tracking this node's staticPrefix as
|
|
51
|
+
// an ancestor so a collapsed nested leaf below it is not over-claimed.
|
|
52
|
+
const nextAncestors = new Set(ancestorStaticPrefixes);
|
|
53
|
+
nextAncestors.add(node.staticPrefix);
|
|
54
|
+
for (const child of Object.values(children)) {
|
|
55
|
+
visit(child, nextAncestors);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const node of Object.values(prefixTree)) {
|
|
60
|
+
visit(node, new Set());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the staticPrefix -> routes lookup the runtime shortcut consumes from a
|
|
66
|
+
* flat precomputed-entry array.
|
|
67
|
+
*
|
|
68
|
+
* A staticPrefix owned by MORE THAN ONE leaf include cannot be collapsed to a
|
|
69
|
+
* single routes object: `new Map(entries.map(e => [e.staticPrefix, e.routes]))`
|
|
70
|
+
* is last-wins, so one include's routes are silently dropped and mis-assigned
|
|
71
|
+
* to whichever entry evaluates first. Two distinct includes legitimately share a
|
|
72
|
+
* staticPrefix when a dynamic param collapses their literal prefixes onto the
|
|
73
|
+
* same value (e.g. `include("/shop/:cat", ...)` and a nested
|
|
74
|
+
* `include("/shop/:brand", ...)` both extract "/shop/"). Merging them is also
|
|
75
|
+
* wrong — assigning the merged set to the first matching entry makes findMatch
|
|
76
|
+
* pick the wrong handler for routes belonging to the other include, which then
|
|
77
|
+
* fails its `Store.manifest.has(routeKey)` invariant at render (500 on a valid
|
|
78
|
+
* route, dev/prod identical).
|
|
79
|
+
*
|
|
80
|
+
* So any shared staticPrefix is OMITTED from the shortcut entirely. Those
|
|
81
|
+
* includes fall through to the handler path in evaluateLazyEntry(), which is the
|
|
82
|
+
* ground truth (identical to pre-precomputed behavior). The shortcut is purely an
|
|
83
|
+
* optimization, so dropping a prefix can only cost a handler run, never change a
|
|
84
|
+
* result.
|
|
85
|
+
*/
|
|
86
|
+
export function buildPrecomputedByPrefix(
|
|
87
|
+
entries: Array<{ staticPrefix: string; routes: Record<string, string> }>,
|
|
88
|
+
): Map<string, Record<string, string>> {
|
|
89
|
+
const byPrefix = new Map<string, Record<string, string>>();
|
|
90
|
+
const shared = new Set<string>();
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
if (byPrefix.has(e.staticPrefix)) {
|
|
93
|
+
shared.add(e.staticPrefix);
|
|
94
|
+
} else {
|
|
95
|
+
byPrefix.set(e.staticPrefix, e.routes);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const sp of shared) {
|
|
99
|
+
byPrefix.delete(sp);
|
|
100
|
+
}
|
|
101
|
+
return byPrefix;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Walk prefix tree to map each route name to its scope's staticPrefix.
|
|
106
|
+
*/
|
|
107
|
+
export function buildRouteToStaticPrefix(
|
|
108
|
+
prefixTree: Record<string, any>,
|
|
109
|
+
result: Record<string, string>,
|
|
110
|
+
): void {
|
|
111
|
+
function visit(node: any): void {
|
|
112
|
+
const sp = node.staticPrefix || "";
|
|
113
|
+
for (const name of node.routes || []) {
|
|
114
|
+
result[name] = sp;
|
|
115
|
+
}
|
|
116
|
+
for (const child of Object.values(node.children || {})) {
|
|
117
|
+
visit(child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const node of Object.values(prefixTree)) {
|
|
121
|
+
visit(node);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/build/route-trie.ts
CHANGED
|
@@ -10,9 +10,24 @@ import {
|
|
|
10
10
|
parsePattern,
|
|
11
11
|
type ParsedSegment,
|
|
12
12
|
} from "../router/pattern-matching.js";
|
|
13
|
+
import { buildRouteToStaticPrefix } from "./prefix-tree-utils.js";
|
|
14
|
+
import type { FullManifest } from "./generate-manifest.js";
|
|
13
15
|
|
|
14
16
|
// -- Trie data structures (compact keys for JSON serialization) --
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* A response-type variant folded into a primary leaf's negotiate list. `pa` is
|
|
20
|
+
* the variant's own positional param-name array, carried so the runtime can
|
|
21
|
+
* re-key the matched params under the variant's names when it wins negotiation
|
|
22
|
+
* (the trie match extracts params under the PRIMARY leaf's pa). Omitted when the
|
|
23
|
+
* variant has no params; absent/identical pa means no re-key is needed.
|
|
24
|
+
*/
|
|
25
|
+
export interface NegotiateVariant {
|
|
26
|
+
routeKey: string;
|
|
27
|
+
responseType: string;
|
|
28
|
+
pa?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export interface TrieLeaf {
|
|
17
32
|
/** Route name (e.g., "site.l1_500") */
|
|
18
33
|
n: string;
|
|
@@ -20,8 +35,6 @@ export interface TrieLeaf {
|
|
|
20
35
|
sp: string;
|
|
21
36
|
/** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
|
|
22
37
|
a: string[];
|
|
23
|
-
/** Optional param names (absent params get empty string value) */
|
|
24
|
-
op?: string[];
|
|
25
38
|
/** Constraint validation: paramName -> allowed values */
|
|
26
39
|
cv?: Record<string, string[]>;
|
|
27
40
|
/** Ordered param names for this route (positional) */
|
|
@@ -35,7 +48,7 @@ export interface TrieLeaf {
|
|
|
35
48
|
/** Response type for non-RSC routes (json, text, image, any) */
|
|
36
49
|
rt?: string;
|
|
37
50
|
/** Negotiate variants: response-type routes sharing this path */
|
|
38
|
-
nv?:
|
|
51
|
+
nv?: NegotiateVariant[];
|
|
39
52
|
/** RSC-first: RSC route was defined before response-type variants */
|
|
40
53
|
rf?: true;
|
|
41
54
|
}
|
|
@@ -60,6 +73,9 @@ export interface TrieNode {
|
|
|
60
73
|
* @param routeAncestry - Map of route name to ancestry shortCodes
|
|
61
74
|
* @param routeToStaticPrefix - Map of route name to its entry's staticPrefix
|
|
62
75
|
* @param routeTrailingSlash - Optional map of route name to trailing slash mode
|
|
76
|
+
* @param prerenderRouteNames - Optional set of prerendered route names (sets leaf.pr)
|
|
77
|
+
* @param passthroughRouteNames - Optional set of passthrough route names (sets leaf.pt)
|
|
78
|
+
* @param responseTypeRoutes - Optional map of route name to response type (sets leaf.rt)
|
|
63
79
|
*/
|
|
64
80
|
export function buildRouteTrie(
|
|
65
81
|
routeManifest: Record<string, string>,
|
|
@@ -94,9 +110,88 @@ export function buildRouteTrie(
|
|
|
94
110
|
});
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
sortSuffixParams(root);
|
|
97
114
|
return root;
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Sort every node's suffix-param map (`node.xp`) by descending suffix length so
|
|
119
|
+
* the matcher tries the most specific suffix first. Overlapping suffixes like
|
|
120
|
+
* `.min.js` and `.js` must resolve by specificity, not route declaration order:
|
|
121
|
+
* a request for `/app.min.js` should match `:file.min.js`, not `:file.js`.
|
|
122
|
+
*
|
|
123
|
+
* This started as a bug — `walkTrie` iterates `node.xp` in object order and
|
|
124
|
+
* returns the first suffix the segment ends with, so the winner depended on
|
|
125
|
+
* which route was declared first. Sorting at build time fixes it allocation-free
|
|
126
|
+
* on the match hot path: the serialized production trie preserves this key order
|
|
127
|
+
* through JSON.parse, so dev (per-request rebuild) and production match
|
|
128
|
+
* identically. Array.prototype.sort is stable (ES2019+), so equal-length
|
|
129
|
+
* suffixes keep their declaration order — the router's existing tiebreak.
|
|
130
|
+
*/
|
|
131
|
+
function sortSuffixParams(node: TrieNode): void {
|
|
132
|
+
if (node.xp) {
|
|
133
|
+
const sorted: Record<string, { n: string; c: TrieNode }> = {};
|
|
134
|
+
for (const suffix of Object.keys(node.xp).sort(
|
|
135
|
+
(a, b) => b.length - a.length,
|
|
136
|
+
)) {
|
|
137
|
+
sorted[suffix] = node.xp[suffix];
|
|
138
|
+
}
|
|
139
|
+
node.xp = sorted;
|
|
140
|
+
for (const child of Object.values(node.xp)) {
|
|
141
|
+
sortSuffixParams(child.c);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (node.s) {
|
|
145
|
+
for (const child of Object.values(node.s)) {
|
|
146
|
+
sortSuffixParams(child);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (node.p) {
|
|
150
|
+
sortSuffixParams(node.p.c);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a per-router trie from a generated manifest. This is the single
|
|
156
|
+
* construction path shared by build/discovery (discover-routers.ts, serialized
|
|
157
|
+
* into the production chunk) and the dev/HMR runtime rebuild
|
|
158
|
+
* (rsc/manifest-init.ts). Keeping one code path is what guarantees the dev
|
|
159
|
+
* runtime trie and the production serialized trie are byte-for-byte identical
|
|
160
|
+
* (modulo `leaf.a` ancestry, which embeds the mount index and is debug-only).
|
|
161
|
+
*
|
|
162
|
+
* Returns null when the manifest has no route ancestry (no routes), matching
|
|
163
|
+
* the prior guard at both call sites.
|
|
164
|
+
*/
|
|
165
|
+
export function buildPerRouterTrie(manifest: FullManifest): TrieNode | null {
|
|
166
|
+
const ancestry = manifest._routeAncestry;
|
|
167
|
+
if (!ancestry || Object.keys(ancestry).length === 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Seed every route to the root static prefix (""), then override with each
|
|
172
|
+
// route's include() scope prefix from the prefix tree so the trie returns the
|
|
173
|
+
// correct `sp` for lazy-entry lookup in find-match.
|
|
174
|
+
const routeToStaticPrefix: Record<string, string> = {};
|
|
175
|
+
for (const name of Object.keys(manifest.routeManifest)) {
|
|
176
|
+
routeToStaticPrefix[name] = "";
|
|
177
|
+
}
|
|
178
|
+
if (manifest.prefixTree) {
|
|
179
|
+
buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return buildRouteTrie(
|
|
183
|
+
manifest.routeManifest,
|
|
184
|
+
ancestry,
|
|
185
|
+
routeToStaticPrefix,
|
|
186
|
+
manifest.routeTrailingSlash,
|
|
187
|
+
manifest.prerenderRoutes ? new Set(manifest.prerenderRoutes) : undefined,
|
|
188
|
+
manifest.passthroughRoutes
|
|
189
|
+
? new Set(manifest.passthroughRoutes)
|
|
190
|
+
: undefined,
|
|
191
|
+
manifest.responseTypeRoutes,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
100
195
|
/**
|
|
101
196
|
* Insert a route into the trie. Optional params expand into two branches at
|
|
102
197
|
* registration time (skip-first, then present), so each terminal lives at the
|
|
@@ -111,18 +206,14 @@ function insertRoute(
|
|
|
111
206
|
node: TrieNode,
|
|
112
207
|
segments: ParsedSegment[],
|
|
113
208
|
index: number,
|
|
114
|
-
leaf: Omit<TrieLeaf, "
|
|
209
|
+
leaf: Omit<TrieLeaf, "cv" | "pa">,
|
|
115
210
|
): void {
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
const optionalParams: string[] = [];
|
|
211
|
+
// cv (full constraint map) is route-level and identical on every terminal,
|
|
212
|
+
// so compute it once on the shared base.
|
|
119
213
|
const constraints: Record<string, string[]> = {};
|
|
120
214
|
|
|
121
215
|
for (const seg of segments) {
|
|
122
216
|
if (seg.type === "param") {
|
|
123
|
-
if (seg.optional) {
|
|
124
|
-
optionalParams.push(seg.value);
|
|
125
|
-
}
|
|
126
217
|
if (seg.constraint) {
|
|
127
218
|
constraints[seg.value] = seg.constraint;
|
|
128
219
|
}
|
|
@@ -131,7 +222,6 @@ function insertRoute(
|
|
|
131
222
|
|
|
132
223
|
const leafBase: Omit<TrieLeaf, "pa"> = {
|
|
133
224
|
...leaf,
|
|
134
|
-
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
135
225
|
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
136
226
|
};
|
|
137
227
|
|
|
@@ -180,6 +270,19 @@ export function extractAncestryFromTrie(
|
|
|
180
270
|
* appended to the nv (negotiate variants) array.
|
|
181
271
|
* Multiple response types on the same path are supported (json + text + xml).
|
|
182
272
|
*/
|
|
273
|
+
/**
|
|
274
|
+
* Build a negotiate-variant entry from a leaf being folded into another leaf's
|
|
275
|
+
* nv list. Carries the variant's positional param names (`pa`) so the runtime
|
|
276
|
+
* can re-key matched params under the variant's names; omitted when the variant
|
|
277
|
+
* has none (the common case where primary and variant share the same names is a
|
|
278
|
+
* no-op re-key regardless).
|
|
279
|
+
*/
|
|
280
|
+
function toVariant(leaf: TrieLeaf, responseType: string): NegotiateVariant {
|
|
281
|
+
return leaf.pa
|
|
282
|
+
? { routeKey: leaf.n, responseType, pa: leaf.pa }
|
|
283
|
+
: { routeKey: leaf.n, responseType };
|
|
284
|
+
}
|
|
285
|
+
|
|
183
286
|
function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
|
|
184
287
|
if (!existing) return leaf;
|
|
185
288
|
|
|
@@ -187,7 +290,7 @@ function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
|
|
|
187
290
|
// Both are response-type: preserve old as variant
|
|
188
291
|
const merged = leaf;
|
|
189
292
|
merged.nv = existing.nv || [];
|
|
190
|
-
merged.nv.push(
|
|
293
|
+
merged.nv.push(toVariant(existing, existing.rt));
|
|
191
294
|
return merged;
|
|
192
295
|
}
|
|
193
296
|
if (leaf.rt && !existing.rt) {
|
|
@@ -197,7 +300,7 @@ function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
|
|
|
197
300
|
existing.nv = [];
|
|
198
301
|
existing.rf = true;
|
|
199
302
|
}
|
|
200
|
-
existing.nv.push(
|
|
303
|
+
existing.nv.push(toVariant(leaf, leaf.rt));
|
|
201
304
|
return existing;
|
|
202
305
|
}
|
|
203
306
|
if (!leaf.rt && existing.rt) {
|
|
@@ -205,7 +308,7 @@ function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
|
|
|
205
308
|
// RSC was defined second (response-type was already the existing leaf)
|
|
206
309
|
if (!leaf.nv) leaf.nv = [];
|
|
207
310
|
if (existing.nv) leaf.nv.push(...existing.nv);
|
|
208
|
-
leaf.nv.push(
|
|
311
|
+
leaf.nv.push(toVariant(existing, existing.rt));
|
|
209
312
|
// rf intentionally not set — RSC came after response-type variants
|
|
210
313
|
return leaf;
|
|
211
314
|
}
|
|
@@ -15,19 +15,26 @@ import { extractParamsFromPattern } from "./param-extraction.js";
|
|
|
15
15
|
* the pattern, name, params, and optional search schema from each.
|
|
16
16
|
* Skips unnamed paths (no { name: "..." }).
|
|
17
17
|
*/
|
|
18
|
-
export function extractRoutesFromSource(
|
|
18
|
+
export function extractRoutesFromSource(
|
|
19
|
+
code: string,
|
|
20
|
+
sourceFileArg?: ts.SourceFile,
|
|
21
|
+
): Array<{
|
|
19
22
|
name: string;
|
|
20
23
|
pattern: string;
|
|
21
24
|
params?: Record<string, string>;
|
|
22
25
|
search?: Record<string, string>;
|
|
23
26
|
}> {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Reuse a caller-provided SourceFile (parsed once per scan) when given;
|
|
28
|
+
// otherwise parse the block here. The walk does not mutate the tree.
|
|
29
|
+
const sourceFile =
|
|
30
|
+
sourceFileArg ??
|
|
31
|
+
ts.createSourceFile(
|
|
32
|
+
"input.tsx",
|
|
33
|
+
code,
|
|
34
|
+
ts.ScriptTarget.Latest,
|
|
35
|
+
true,
|
|
36
|
+
ts.ScriptKind.TSX,
|
|
37
|
+
);
|
|
31
38
|
const routes: Array<{
|
|
32
39
|
name: string;
|
|
33
40
|
pattern: string;
|