@rangojs/router 0.0.0-experimental.2 → 0.0.0-experimental.20dbba0c
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 +9 -0
- package/README.md +972 -4
- package/dist/__internal.d.ts +83 -0
- package/dist/__internal.d.ts.map +1 -0
- package/dist/__internal.js +19 -0
- package/dist/__internal.js.map +1 -0
- package/dist/__mocks__/version.d.ts +7 -0
- package/dist/__mocks__/version.d.ts.map +1 -0
- package/{src/__mocks__/version.ts → dist/__mocks__/version.js} +1 -0
- package/dist/__mocks__/version.js.map +1 -0
- package/dist/__tests__/client-href.test.d.ts +2 -0
- package/dist/__tests__/client-href.test.d.ts.map +1 -0
- package/dist/__tests__/client-href.test.js +74 -0
- package/dist/__tests__/client-href.test.js.map +1 -0
- package/dist/__tests__/component-utils.test.d.ts +2 -0
- package/dist/__tests__/component-utils.test.d.ts.map +1 -0
- package/dist/__tests__/component-utils.test.js +51 -0
- package/dist/__tests__/component-utils.test.js.map +1 -0
- package/dist/__tests__/event-controller.test.d.ts +2 -0
- package/dist/__tests__/event-controller.test.d.ts.map +1 -0
- package/dist/__tests__/event-controller.test.js +538 -0
- package/dist/__tests__/event-controller.test.js.map +1 -0
- package/dist/__tests__/helpers/route-tree.d.ts +118 -0
- package/dist/__tests__/helpers/route-tree.d.ts.map +1 -0
- package/dist/__tests__/helpers/route-tree.js +374 -0
- package/dist/__tests__/helpers/route-tree.js.map +1 -0
- package/dist/__tests__/match-result.test.d.ts +2 -0
- package/dist/__tests__/match-result.test.d.ts.map +1 -0
- package/dist/__tests__/match-result.test.js +154 -0
- package/dist/__tests__/match-result.test.js.map +1 -0
- package/dist/__tests__/navigation-store.test.d.ts +2 -0
- package/dist/__tests__/navigation-store.test.d.ts.map +1 -0
- package/dist/__tests__/navigation-store.test.js +440 -0
- package/dist/__tests__/navigation-store.test.js.map +1 -0
- package/dist/__tests__/partial-update.test.d.ts +2 -0
- package/dist/__tests__/partial-update.test.d.ts.map +1 -0
- package/dist/__tests__/partial-update.test.js +1009 -0
- package/dist/__tests__/partial-update.test.js.map +1 -0
- package/dist/__tests__/reverse-types.test.d.ts +8 -0
- package/dist/__tests__/reverse-types.test.d.ts.map +1 -0
- package/dist/__tests__/reverse-types.test.js +656 -0
- package/dist/__tests__/reverse-types.test.js.map +1 -0
- package/dist/__tests__/route-definition.test.d.ts +2 -0
- package/dist/__tests__/route-definition.test.d.ts.map +1 -0
- package/dist/__tests__/route-definition.test.js +55 -0
- package/dist/__tests__/route-definition.test.js.map +1 -0
- package/dist/__tests__/router-helpers.test.d.ts +2 -0
- package/dist/__tests__/router-helpers.test.d.ts.map +1 -0
- package/dist/__tests__/router-helpers.test.js +377 -0
- package/dist/__tests__/router-helpers.test.js.map +1 -0
- package/dist/__tests__/router-integration-2.test.d.ts +2 -0
- package/dist/__tests__/router-integration-2.test.d.ts.map +1 -0
- package/dist/__tests__/router-integration-2.test.js +426 -0
- package/dist/__tests__/router-integration-2.test.js.map +1 -0
- package/dist/__tests__/router-integration.test.d.ts +2 -0
- package/dist/__tests__/router-integration.test.d.ts.map +1 -0
- package/dist/__tests__/router-integration.test.js +1051 -0
- package/dist/__tests__/router-integration.test.js.map +1 -0
- package/dist/__tests__/search-params.test.d.ts +5 -0
- package/dist/__tests__/search-params.test.d.ts.map +1 -0
- package/dist/__tests__/search-params.test.js +306 -0
- package/dist/__tests__/search-params.test.js.map +1 -0
- package/dist/__tests__/segment-system.test.d.ts +2 -0
- package/dist/__tests__/segment-system.test.d.ts.map +1 -0
- package/dist/__tests__/segment-system.test.js +627 -0
- package/dist/__tests__/segment-system.test.js.map +1 -0
- package/dist/__tests__/static-handler-types.test.d.ts +8 -0
- package/dist/__tests__/static-handler-types.test.d.ts.map +1 -0
- package/dist/__tests__/static-handler-types.test.js +63 -0
- package/dist/__tests__/static-handler-types.test.js.map +1 -0
- package/dist/__tests__/urls.test.d.ts +2 -0
- package/dist/__tests__/urls.test.d.ts.map +1 -0
- package/dist/__tests__/urls.test.js +421 -0
- package/dist/__tests__/urls.test.js.map +1 -0
- package/dist/__tests__/use-mount.test.d.ts +2 -0
- package/dist/__tests__/use-mount.test.d.ts.map +1 -0
- package/dist/__tests__/use-mount.test.js +35 -0
- package/dist/__tests__/use-mount.test.js.map +1 -0
- package/dist/bin/rango.d.ts +2 -0
- package/dist/bin/rango.d.ts.map +1 -0
- package/dist/bin/rango.js +1689 -0
- package/dist/bin/rango.js.map +1 -0
- package/dist/browser/event-controller.d.ts +191 -0
- package/dist/browser/event-controller.d.ts.map +1 -0
- package/dist/browser/event-controller.js +559 -0
- package/dist/browser/event-controller.js.map +1 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/link-interceptor.d.ts +38 -0
- package/dist/browser/link-interceptor.d.ts.map +1 -0
- package/dist/browser/link-interceptor.js +99 -0
- package/dist/browser/link-interceptor.js.map +1 -0
- package/dist/browser/logging.d.ts +10 -0
- package/dist/browser/logging.d.ts.map +1 -0
- package/dist/browser/logging.js +29 -0
- package/dist/browser/logging.js.map +1 -0
- package/dist/browser/lru-cache.d.ts +17 -0
- package/dist/browser/lru-cache.d.ts.map +1 -0
- package/dist/browser/lru-cache.js +50 -0
- package/dist/browser/lru-cache.js.map +1 -0
- package/dist/browser/merge-segment-loaders.d.ts +39 -0
- package/dist/browser/merge-segment-loaders.d.ts.map +1 -0
- package/dist/browser/merge-segment-loaders.js +102 -0
- package/dist/browser/merge-segment-loaders.js.map +1 -0
- package/dist/browser/navigation-bridge.d.ts +102 -0
- package/dist/browser/navigation-bridge.d.ts.map +1 -0
- package/dist/browser/navigation-bridge.js +708 -0
- package/dist/browser/navigation-bridge.js.map +1 -0
- package/dist/browser/navigation-client.d.ts +25 -0
- package/dist/browser/navigation-client.d.ts.map +1 -0
- package/dist/browser/navigation-client.js +157 -0
- package/dist/browser/navigation-client.js.map +1 -0
- package/dist/browser/navigation-store.d.ts +101 -0
- package/dist/browser/navigation-store.d.ts.map +1 -0
- package/dist/browser/navigation-store.js +625 -0
- package/dist/browser/navigation-store.js.map +1 -0
- package/dist/browser/partial-update.d.ts +75 -0
- package/dist/browser/partial-update.d.ts.map +1 -0
- package/dist/browser/partial-update.js +426 -0
- package/dist/browser/partial-update.js.map +1 -0
- package/dist/browser/react/Link.d.ts +86 -0
- package/dist/browser/react/Link.d.ts.map +1 -0
- package/dist/browser/react/Link.js +128 -0
- package/dist/browser/react/Link.js.map +1 -0
- package/dist/browser/react/NavigationProvider.d.ts +63 -0
- package/dist/browser/react/NavigationProvider.d.ts.map +1 -0
- package/dist/browser/react/NavigationProvider.js +216 -0
- package/dist/browser/react/NavigationProvider.js.map +1 -0
- package/dist/browser/react/ScrollRestoration.d.ts +75 -0
- package/dist/browser/react/ScrollRestoration.d.ts.map +1 -0
- package/dist/browser/react/ScrollRestoration.js +57 -0
- package/dist/browser/react/ScrollRestoration.js.map +1 -0
- package/dist/browser/react/context.d.ts +46 -0
- package/dist/browser/react/context.d.ts.map +1 -0
- package/dist/browser/react/context.js +10 -0
- package/dist/browser/react/context.js.map +1 -0
- package/dist/browser/react/index.d.ts +11 -0
- package/dist/browser/react/index.d.ts.map +1 -0
- package/dist/browser/react/index.js +22 -0
- package/dist/browser/react/index.js.map +1 -0
- package/dist/browser/react/location-state-shared.d.ts +63 -0
- package/dist/browser/react/location-state-shared.d.ts.map +1 -0
- package/dist/browser/react/location-state-shared.js +81 -0
- package/dist/browser/react/location-state-shared.js.map +1 -0
- package/dist/browser/react/location-state.d.ts +23 -0
- package/dist/browser/react/location-state.d.ts.map +1 -0
- package/dist/browser/react/location-state.js +29 -0
- package/dist/browser/react/location-state.js.map +1 -0
- package/dist/browser/react/mount-context.d.ts +24 -0
- package/dist/browser/react/mount-context.d.ts.map +1 -0
- package/dist/browser/react/mount-context.js +24 -0
- package/dist/browser/react/mount-context.js.map +1 -0
- package/dist/browser/react/use-action.d.ts +64 -0
- package/dist/browser/react/use-action.d.ts.map +1 -0
- package/dist/browser/react/use-action.js +134 -0
- package/dist/browser/react/use-action.js.map +1 -0
- package/dist/browser/react/use-client-cache.d.ts +41 -0
- package/dist/browser/react/use-client-cache.d.ts.map +1 -0
- package/dist/browser/react/use-client-cache.js +39 -0
- package/dist/browser/react/use-client-cache.js.map +1 -0
- package/dist/browser/react/use-handle.d.ts +31 -0
- package/dist/browser/react/use-handle.d.ts.map +1 -0
- package/dist/browser/react/use-handle.js +144 -0
- package/dist/browser/react/use-handle.js.map +1 -0
- package/dist/browser/react/use-href.d.ts +33 -0
- package/dist/browser/react/use-href.d.ts.map +1 -0
- package/dist/browser/react/use-href.js +39 -0
- package/dist/browser/react/use-href.js.map +1 -0
- package/dist/browser/react/use-link-status.d.ts +37 -0
- package/dist/browser/react/use-link-status.d.ts.map +1 -0
- package/dist/browser/react/use-link-status.js +99 -0
- package/dist/browser/react/use-link-status.js.map +1 -0
- package/dist/browser/react/use-mount.d.ts +25 -0
- package/dist/browser/react/use-mount.d.ts.map +1 -0
- package/dist/browser/react/use-mount.js +30 -0
- package/dist/browser/react/use-mount.js.map +1 -0
- package/dist/browser/react/use-navigation.d.ts +27 -0
- package/dist/browser/react/use-navigation.d.ts.map +1 -0
- package/dist/browser/react/use-navigation.js +87 -0
- package/dist/browser/react/use-navigation.js.map +1 -0
- package/dist/browser/react/use-segments.d.ts +38 -0
- package/dist/browser/react/use-segments.d.ts.map +1 -0
- package/dist/browser/react/use-segments.js +130 -0
- package/dist/browser/react/use-segments.js.map +1 -0
- package/dist/browser/request-controller.d.ts +26 -0
- package/dist/browser/request-controller.d.ts.map +1 -0
- package/dist/browser/request-controller.js +147 -0
- package/dist/browser/request-controller.js.map +1 -0
- package/dist/browser/rsc-router.d.ts +129 -0
- package/dist/browser/rsc-router.d.ts.map +1 -0
- package/dist/browser/rsc-router.js +195 -0
- package/dist/browser/rsc-router.js.map +1 -0
- package/dist/browser/scroll-restoration.d.ts +93 -0
- package/dist/browser/scroll-restoration.d.ts.map +1 -0
- package/dist/browser/scroll-restoration.js +321 -0
- package/dist/browser/scroll-restoration.js.map +1 -0
- package/dist/browser/segment-structure-assert.d.ts +17 -0
- package/dist/browser/segment-structure-assert.d.ts.map +1 -0
- package/dist/browser/segment-structure-assert.js +59 -0
- package/dist/browser/segment-structure-assert.js.map +1 -0
- package/dist/browser/server-action-bridge.d.ts +26 -0
- package/dist/browser/server-action-bridge.d.ts.map +1 -0
- package/dist/browser/server-action-bridge.js +668 -0
- package/dist/browser/server-action-bridge.js.map +1 -0
- package/dist/browser/shallow.d.ts +12 -0
- package/dist/browser/shallow.d.ts.map +1 -0
- package/dist/browser/shallow.js +34 -0
- package/dist/browser/shallow.js.map +1 -0
- package/dist/browser/types.d.ts +369 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/build/__tests__/generate-cli.test.d.ts +2 -0
- package/dist/build/__tests__/generate-cli.test.d.ts.map +1 -0
- package/dist/build/__tests__/generate-cli.test.js +237 -0
- package/dist/build/__tests__/generate-cli.test.js.map +1 -0
- package/dist/build/__tests__/generate-manifest.test.d.ts +2 -0
- package/dist/build/__tests__/generate-manifest.test.d.ts.map +1 -0
- package/dist/build/__tests__/generate-manifest.test.js +119 -0
- package/dist/build/__tests__/generate-manifest.test.js.map +1 -0
- package/dist/build/__tests__/generate-route-types.test.d.ts +2 -0
- package/dist/build/__tests__/generate-route-types.test.d.ts.map +1 -0
- package/dist/build/__tests__/generate-route-types.test.js +620 -0
- package/dist/build/__tests__/generate-route-types.test.js.map +1 -0
- package/dist/build/__tests__/per-router-manifest.test.d.ts +2 -0
- package/dist/build/__tests__/per-router-manifest.test.d.ts.map +1 -0
- package/dist/build/__tests__/per-router-manifest.test.js +308 -0
- package/dist/build/__tests__/per-router-manifest.test.js.map +1 -0
- package/dist/build/generate-manifest.d.ts +81 -0
- package/dist/build/generate-manifest.d.ts.map +1 -0
- package/dist/build/generate-manifest.js +276 -0
- package/dist/build/generate-manifest.js.map +1 -0
- package/dist/build/generate-route-types.d.ts +115 -0
- package/dist/build/generate-route-types.d.ts.map +1 -0
- package/dist/build/generate-route-types.js +740 -0
- package/dist/build/generate-route-types.js.map +1 -0
- package/dist/build/index.d.ts +21 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +21 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/route-trie.d.ts +71 -0
- package/dist/build/route-trie.d.ts.map +1 -0
- package/dist/build/route-trie.js +175 -0
- package/dist/build/route-trie.js.map +1 -0
- package/dist/cache/__tests__/cache-scope.test.d.ts +2 -0
- package/dist/cache/__tests__/cache-scope.test.d.ts.map +1 -0
- package/dist/cache/__tests__/cache-scope.test.js +208 -0
- package/dist/cache/__tests__/cache-scope.test.js.map +1 -0
- package/dist/cache/__tests__/document-cache.test.d.ts +2 -0
- package/dist/cache/__tests__/document-cache.test.d.ts.map +1 -0
- package/dist/cache/__tests__/document-cache.test.js +345 -0
- package/dist/cache/__tests__/document-cache.test.js.map +1 -0
- package/dist/cache/__tests__/memory-segment-store.test.d.ts +2 -0
- package/dist/cache/__tests__/memory-segment-store.test.d.ts.map +1 -0
- package/dist/cache/__tests__/memory-segment-store.test.js +425 -0
- package/dist/cache/__tests__/memory-segment-store.test.js.map +1 -0
- package/dist/cache/__tests__/memory-store.test.d.ts +2 -0
- package/dist/cache/__tests__/memory-store.test.d.ts.map +1 -0
- package/dist/cache/__tests__/memory-store.test.js +367 -0
- package/dist/cache/__tests__/memory-store.test.js.map +1 -0
- package/dist/cache/cache-scope.d.ts +102 -0
- package/dist/cache/cache-scope.d.ts.map +1 -0
- package/dist/cache/cache-scope.js +440 -0
- package/dist/cache/cache-scope.js.map +1 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts +2 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts.map +1 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.js +330 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.js.map +1 -0
- package/dist/cache/cf/cf-cache-store.d.ts +165 -0
- package/dist/cache/cf/cf-cache-store.d.ts.map +1 -0
- package/dist/cache/cf/cf-cache-store.js +242 -0
- package/dist/cache/cf/cf-cache-store.js.map +1 -0
- package/dist/cache/cf/index.d.ts +14 -0
- package/dist/cache/cf/index.d.ts.map +1 -0
- package/dist/cache/cf/index.js +17 -0
- package/dist/cache/cf/index.js.map +1 -0
- package/dist/cache/document-cache.d.ts +64 -0
- package/dist/cache/document-cache.d.ts.map +1 -0
- package/dist/cache/document-cache.js +228 -0
- package/dist/cache/document-cache.js.map +1 -0
- package/dist/cache/index.d.ts +19 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +21 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/memory-segment-store.d.ts +110 -0
- package/dist/cache/memory-segment-store.d.ts.map +1 -0
- package/dist/cache/memory-segment-store.js +117 -0
- package/dist/cache/memory-segment-store.js.map +1 -0
- package/dist/cache/memory-store.d.ts +41 -0
- package/dist/cache/memory-store.d.ts.map +1 -0
- package/dist/cache/memory-store.js +191 -0
- package/dist/cache/memory-store.js.map +1 -0
- package/dist/cache/types.d.ts +317 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +12 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/client.d.ts +248 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +367 -0
- package/dist/client.js.map +1 -0
- package/dist/client.rsc.d.ts +26 -0
- package/dist/client.rsc.d.ts.map +1 -0
- package/dist/client.rsc.js +46 -0
- package/dist/client.rsc.js.map +1 -0
- package/dist/component-utils.d.ts +36 -0
- package/dist/component-utils.d.ts.map +1 -0
- package/dist/component-utils.js +61 -0
- package/dist/component-utils.js.map +1 -0
- package/dist/components/DefaultDocument.d.ts +13 -0
- package/dist/components/DefaultDocument.d.ts.map +1 -0
- package/dist/components/DefaultDocument.js +15 -0
- package/dist/components/DefaultDocument.js.map +1 -0
- package/dist/debug.d.ts +58 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +157 -0
- package/dist/debug.js.map +1 -0
- package/dist/default-error-boundary.d.ts +11 -0
- package/dist/default-error-boundary.d.ts.map +1 -0
- package/dist/default-error-boundary.js +45 -0
- package/dist/default-error-boundary.js.map +1 -0
- package/dist/deps/browser.d.ts +2 -0
- package/dist/deps/browser.d.ts.map +1 -0
- package/dist/deps/browser.js +3 -0
- package/dist/deps/browser.js.map +1 -0
- package/dist/deps/html-stream-client.d.ts +2 -0
- package/dist/deps/html-stream-client.d.ts.map +1 -0
- package/dist/deps/html-stream-client.js +3 -0
- package/dist/deps/html-stream-client.js.map +1 -0
- package/dist/deps/html-stream-server.d.ts +2 -0
- package/dist/deps/html-stream-server.d.ts.map +1 -0
- package/dist/deps/html-stream-server.js +3 -0
- package/dist/deps/html-stream-server.js.map +1 -0
- package/dist/deps/rsc.d.ts +2 -0
- package/dist/deps/rsc.d.ts.map +1 -0
- package/dist/deps/rsc.js +4 -0
- package/dist/deps/rsc.js.map +1 -0
- package/dist/deps/ssr.d.ts +2 -0
- package/dist/deps/ssr.d.ts.map +1 -0
- package/dist/deps/ssr.js +3 -0
- package/dist/deps/ssr.js.map +1 -0
- package/dist/errors.d.ts +174 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +241 -0
- package/dist/errors.js.map +1 -0
- package/dist/handle.d.ts +78 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +82 -0
- package/dist/handle.js.map +1 -0
- package/dist/handles/MetaTags.d.ts +14 -0
- package/dist/handles/MetaTags.d.ts.map +1 -0
- package/dist/handles/MetaTags.js +136 -0
- package/dist/handles/MetaTags.js.map +1 -0
- package/dist/handles/index.d.ts +6 -0
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/index.js +6 -0
- package/dist/handles/index.js.map +1 -0
- package/dist/handles/meta.d.ts +39 -0
- package/dist/handles/meta.d.ts.map +1 -0
- package/dist/handles/meta.js +202 -0
- package/dist/handles/meta.js.map +1 -0
- package/dist/host/__tests__/errors.test.d.ts +2 -0
- package/dist/host/__tests__/errors.test.d.ts.map +1 -0
- package/dist/host/__tests__/errors.test.js +76 -0
- package/dist/host/__tests__/errors.test.js.map +1 -0
- package/dist/host/__tests__/pattern-comprehensive.test.d.ts +2 -0
- package/dist/host/__tests__/pattern-comprehensive.test.d.ts.map +1 -0
- package/dist/host/__tests__/pattern-comprehensive.test.js +732 -0
- package/dist/host/__tests__/pattern-comprehensive.test.js.map +1 -0
- package/dist/host/__tests__/pattern-matcher.test.d.ts +2 -0
- package/dist/host/__tests__/pattern-matcher.test.d.ts.map +1 -0
- package/dist/host/__tests__/pattern-matcher.test.js +251 -0
- package/dist/host/__tests__/pattern-matcher.test.js.map +1 -0
- package/dist/host/__tests__/router.test.d.ts +2 -0
- package/dist/host/__tests__/router.test.d.ts.map +1 -0
- package/dist/host/__tests__/router.test.js +241 -0
- package/dist/host/__tests__/router.test.js.map +1 -0
- package/dist/host/__tests__/testing.test.d.ts +2 -0
- package/dist/host/__tests__/testing.test.d.ts.map +1 -0
- package/dist/host/__tests__/testing.test.js +64 -0
- package/dist/host/__tests__/testing.test.js.map +1 -0
- package/dist/host/__tests__/utils.test.d.ts +2 -0
- package/dist/host/__tests__/utils.test.d.ts.map +1 -0
- package/dist/host/__tests__/utils.test.js +29 -0
- package/dist/host/__tests__/utils.test.js.map +1 -0
- package/dist/host/cookie-handler.d.ts +34 -0
- package/dist/host/cookie-handler.d.ts.map +1 -0
- package/dist/host/cookie-handler.js +124 -0
- package/dist/host/cookie-handler.js.map +1 -0
- package/dist/host/errors.d.ts +56 -0
- package/dist/host/errors.d.ts.map +1 -0
- package/dist/host/errors.js +79 -0
- package/dist/host/errors.js.map +1 -0
- package/dist/host/index.d.ts +29 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +32 -0
- package/dist/host/index.js.map +1 -0
- package/dist/host/pattern-matcher.d.ts +36 -0
- package/dist/host/pattern-matcher.d.ts.map +1 -0
- package/dist/host/pattern-matcher.js +172 -0
- package/dist/host/pattern-matcher.js.map +1 -0
- package/dist/host/router.d.ts +26 -0
- package/dist/host/router.d.ts.map +1 -0
- package/dist/host/router.js +218 -0
- package/dist/host/router.js.map +1 -0
- package/dist/host/testing.d.ts +36 -0
- package/dist/host/testing.d.ts.map +1 -0
- package/dist/host/testing.js +55 -0
- package/dist/host/testing.js.map +1 -0
- package/dist/host/types.d.ts +115 -0
- package/dist/host/types.d.ts.map +1 -0
- package/dist/host/types.js +7 -0
- package/dist/host/types.js.map +1 -0
- package/dist/host/utils.d.ts +21 -0
- package/dist/host/utils.d.ts.map +1 -0
- package/dist/host/utils.js +23 -0
- package/dist/host/utils.js.map +1 -0
- package/dist/href-client.d.ts +131 -0
- package/dist/href-client.d.ts.map +1 -0
- package/dist/href-client.js +64 -0
- package/dist/href-client.js.map +1 -0
- package/{src/href-context.ts → dist/href-context.d.ts} +7 -11
- package/dist/href-context.d.ts.map +1 -0
- package/dist/href-context.js +21 -0
- package/dist/href-context.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/index.rsc.d.ts +32 -0
- package/dist/index.rsc.d.ts.map +1 -0
- package/dist/index.rsc.js +40 -0
- package/dist/index.rsc.js.map +1 -0
- package/dist/internal-debug.d.ts +2 -0
- package/dist/internal-debug.d.ts.map +1 -0
- package/dist/internal-debug.js +5 -0
- package/dist/internal-debug.js.map +1 -0
- package/dist/loader.d.ts +14 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +20 -0
- package/dist/loader.js.map +1 -0
- package/dist/loader.rsc.d.ts +19 -0
- package/dist/loader.rsc.d.ts.map +1 -0
- package/dist/loader.rsc.js +99 -0
- package/dist/loader.rsc.js.map +1 -0
- package/dist/network-error-thrower.d.ts +17 -0
- package/dist/network-error-thrower.d.ts.map +1 -0
- package/dist/network-error-thrower.js +14 -0
- package/dist/network-error-thrower.js.map +1 -0
- package/dist/outlet-context.d.ts +13 -0
- package/dist/outlet-context.d.ts.map +1 -0
- package/dist/outlet-context.js +3 -0
- package/dist/outlet-context.js.map +1 -0
- package/dist/prerender/__tests__/param-hash.test.d.ts +2 -0
- package/dist/prerender/__tests__/param-hash.test.d.ts.map +1 -0
- package/dist/prerender/__tests__/param-hash.test.js +148 -0
- package/dist/prerender/__tests__/param-hash.test.js.map +1 -0
- package/dist/prerender/param-hash.d.ts +16 -0
- package/dist/prerender/param-hash.d.ts.map +1 -0
- package/dist/prerender/param-hash.js +36 -0
- package/dist/prerender/param-hash.js.map +1 -0
- package/dist/prerender/store.d.ts +38 -0
- package/dist/prerender/store.d.ts.map +1 -0
- package/dist/prerender/store.js +61 -0
- package/dist/prerender/store.js.map +1 -0
- package/dist/prerender.d.ts +66 -0
- package/dist/prerender.d.ts.map +1 -0
- package/dist/prerender.js +57 -0
- package/dist/prerender.js.map +1 -0
- package/dist/reverse.d.ts +196 -0
- package/dist/reverse.d.ts.map +1 -0
- package/dist/reverse.js +78 -0
- package/dist/reverse.js.map +1 -0
- package/dist/root-error-boundary.d.ts +33 -0
- package/dist/root-error-boundary.d.ts.map +1 -0
- package/dist/root-error-boundary.js +165 -0
- package/dist/root-error-boundary.js.map +1 -0
- package/dist/route-content-wrapper.d.ts +46 -0
- package/dist/route-content-wrapper.d.ts.map +1 -0
- package/dist/route-content-wrapper.js +77 -0
- package/dist/route-content-wrapper.js.map +1 -0
- package/dist/route-definition.d.ts +421 -0
- package/dist/route-definition.d.ts.map +1 -0
- package/dist/route-definition.js +868 -0
- package/dist/route-definition.js.map +1 -0
- package/dist/route-map-builder.d.ts +155 -0
- package/dist/route-map-builder.d.ts.map +1 -0
- package/dist/route-map-builder.js +237 -0
- package/dist/route-map-builder.js.map +1 -0
- package/dist/route-types.d.ts +165 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +7 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router/__tests__/handler-context.test.d.ts +2 -0
- package/dist/router/__tests__/handler-context.test.d.ts.map +1 -0
- package/dist/router/__tests__/handler-context.test.js +65 -0
- package/dist/router/__tests__/handler-context.test.js.map +1 -0
- package/dist/router/__tests__/loader-cycle-detection.test.d.ts +2 -0
- package/dist/router/__tests__/loader-cycle-detection.test.d.ts.map +1 -0
- package/dist/router/__tests__/loader-cycle-detection.test.js +221 -0
- package/dist/router/__tests__/loader-cycle-detection.test.js.map +1 -0
- package/dist/router/__tests__/match-context.test.d.ts +2 -0
- package/dist/router/__tests__/match-context.test.d.ts.map +1 -0
- package/dist/router/__tests__/match-context.test.js +92 -0
- package/dist/router/__tests__/match-context.test.js.map +1 -0
- package/dist/router/__tests__/match-pipelines.test.d.ts +2 -0
- package/dist/router/__tests__/match-pipelines.test.d.ts.map +1 -0
- package/dist/router/__tests__/match-pipelines.test.js +417 -0
- package/dist/router/__tests__/match-pipelines.test.js.map +1 -0
- package/dist/router/__tests__/match-result.test.d.ts +2 -0
- package/dist/router/__tests__/match-result.test.d.ts.map +1 -0
- package/dist/router/__tests__/match-result.test.js +457 -0
- package/dist/router/__tests__/match-result.test.js.map +1 -0
- package/dist/router/__tests__/on-error.test.d.ts +2 -0
- package/dist/router/__tests__/on-error.test.d.ts.map +1 -0
- package/dist/router/__tests__/on-error.test.js +678 -0
- package/dist/router/__tests__/on-error.test.js.map +1 -0
- package/dist/router/__tests__/pattern-matching.test.d.ts +2 -0
- package/dist/router/__tests__/pattern-matching.test.d.ts.map +1 -0
- package/dist/router/__tests__/pattern-matching.test.js +629 -0
- package/dist/router/__tests__/pattern-matching.test.js.map +1 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts +2 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts.map +1 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.js +155 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.js.map +1 -0
- package/dist/router/error-handling.d.ts +77 -0
- package/dist/router/error-handling.d.ts.map +1 -0
- package/dist/router/error-handling.js +202 -0
- package/dist/router/error-handling.js.map +1 -0
- package/dist/router/handler-context.d.ts +20 -0
- package/dist/router/handler-context.d.ts.map +1 -0
- package/dist/router/handler-context.js +198 -0
- package/dist/router/handler-context.js.map +1 -0
- package/dist/router/intercept-resolution.d.ts +66 -0
- package/dist/router/intercept-resolution.d.ts.map +1 -0
- package/dist/router/intercept-resolution.js +246 -0
- package/dist/router/intercept-resolution.js.map +1 -0
- package/dist/router/loader-resolution.d.ts +64 -0
- package/dist/router/loader-resolution.d.ts.map +1 -0
- package/dist/router/loader-resolution.js +284 -0
- package/dist/router/loader-resolution.js.map +1 -0
- package/dist/router/logging.d.ts +15 -0
- package/dist/router/logging.d.ts.map +1 -0
- package/dist/router/logging.js +99 -0
- package/dist/router/logging.js.map +1 -0
- package/dist/router/manifest.d.ts +22 -0
- package/dist/router/manifest.d.ts.map +1 -0
- package/dist/router/manifest.js +181 -0
- package/dist/router/manifest.js.map +1 -0
- package/dist/router/match-api.d.ts +35 -0
- package/dist/router/match-api.d.ts.map +1 -0
- package/dist/router/match-api.js +406 -0
- package/dist/router/match-api.js.map +1 -0
- package/dist/router/match-context.d.ts +206 -0
- package/dist/router/match-context.d.ts.map +1 -0
- package/dist/router/match-context.js +17 -0
- package/dist/router/match-context.js.map +1 -0
- package/dist/router/match-middleware/background-revalidation.d.ts +127 -0
- package/dist/router/match-middleware/background-revalidation.d.ts.map +1 -0
- package/dist/router/match-middleware/background-revalidation.js +75 -0
- package/dist/router/match-middleware/background-revalidation.js.map +1 -0
- package/dist/router/match-middleware/cache-lookup.d.ts +112 -0
- package/dist/router/match-middleware/cache-lookup.d.ts.map +1 -0
- package/dist/router/match-middleware/cache-lookup.js +257 -0
- package/dist/router/match-middleware/cache-lookup.js.map +1 -0
- package/dist/router/match-middleware/cache-store.d.ts +113 -0
- package/dist/router/match-middleware/cache-store.d.ts.map +1 -0
- package/dist/router/match-middleware/cache-store.js +108 -0
- package/dist/router/match-middleware/cache-store.js.map +1 -0
- package/dist/router/match-middleware/index.d.ts +81 -0
- package/dist/router/match-middleware/index.d.ts.map +1 -0
- package/dist/router/match-middleware/index.js +80 -0
- package/dist/router/match-middleware/index.js.map +1 -0
- package/dist/router/match-middleware/intercept-resolution.d.ts +117 -0
- package/dist/router/match-middleware/intercept-resolution.d.ts.map +1 -0
- package/dist/router/match-middleware/intercept-resolution.js +134 -0
- package/dist/router/match-middleware/intercept-resolution.js.map +1 -0
- package/dist/router/match-middleware/segment-resolution.d.ts +99 -0
- package/dist/router/match-middleware/segment-resolution.d.ts.map +1 -0
- package/dist/router/match-middleware/segment-resolution.js +53 -0
- package/dist/router/match-middleware/segment-resolution.js.map +1 -0
- package/dist/router/match-pipelines.d.ts +147 -0
- package/dist/router/match-pipelines.d.ts.map +1 -0
- package/dist/router/match-pipelines.js +82 -0
- package/dist/router/match-pipelines.js.map +1 -0
- package/dist/router/match-result.d.ts +126 -0
- package/dist/router/match-result.d.ts.map +1 -0
- package/dist/router/match-result.js +93 -0
- package/dist/router/match-result.js.map +1 -0
- package/dist/router/metrics.d.ts +20 -0
- package/dist/router/metrics.d.ts.map +1 -0
- package/dist/router/metrics.js +47 -0
- package/dist/router/metrics.js.map +1 -0
- package/dist/router/middleware.d.ts +249 -0
- package/dist/router/middleware.d.ts.map +1 -0
- package/dist/router/middleware.js +434 -0
- package/dist/router/middleware.js.map +1 -0
- package/dist/router/middleware.test.d.ts +2 -0
- package/dist/router/middleware.test.d.ts.map +1 -0
- package/dist/router/middleware.test.js +816 -0
- package/dist/router/middleware.test.js.map +1 -0
- package/dist/router/pattern-matching.d.ts +149 -0
- package/dist/router/pattern-matching.d.ts.map +1 -0
- package/dist/router/pattern-matching.js +349 -0
- package/dist/router/pattern-matching.js.map +1 -0
- package/dist/router/revalidation.d.ts +44 -0
- package/dist/router/revalidation.d.ts.map +1 -0
- package/dist/router/revalidation.js +147 -0
- package/dist/router/revalidation.js.map +1 -0
- package/dist/router/router-context.d.ts +135 -0
- package/dist/router/router-context.d.ts.map +1 -0
- package/dist/router/router-context.js +36 -0
- package/dist/router/router-context.js.map +1 -0
- package/dist/router/segment-resolution.d.ts +127 -0
- package/dist/router/segment-resolution.d.ts.map +1 -0
- package/dist/router/segment-resolution.js +919 -0
- package/dist/router/segment-resolution.js.map +1 -0
- package/dist/router/trie-matching.d.ts +40 -0
- package/dist/router/trie-matching.d.ts.map +1 -0
- package/dist/router/trie-matching.js +127 -0
- package/dist/router/trie-matching.js.map +1 -0
- package/dist/router/types.d.ts +136 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.js +7 -0
- package/dist/router/types.js.map +1 -0
- package/dist/router.d.ts +753 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.gen.d.ts +6 -0
- package/dist/router.gen.d.ts.map +1 -0
- package/dist/router.gen.js +6 -0
- package/dist/router.gen.js.map +1 -0
- package/dist/router.js +1304 -0
- package/dist/router.js.map +1 -0
- package/dist/rsc/__tests__/helpers.test.d.ts +2 -0
- package/dist/rsc/__tests__/helpers.test.d.ts.map +1 -0
- package/dist/rsc/__tests__/helpers.test.js +140 -0
- package/dist/rsc/__tests__/helpers.test.js.map +1 -0
- package/dist/rsc/handler.d.ts +45 -0
- package/dist/rsc/handler.d.ts.map +1 -0
- package/dist/rsc/handler.js +1172 -0
- package/dist/rsc/handler.js.map +1 -0
- package/dist/rsc/helpers.d.ts +16 -0
- package/dist/rsc/helpers.d.ts.map +1 -0
- package/dist/rsc/helpers.js +55 -0
- package/dist/rsc/helpers.js.map +1 -0
- package/dist/rsc/index.d.ts +22 -0
- package/dist/rsc/index.d.ts.map +1 -0
- package/dist/rsc/index.js +23 -0
- package/dist/rsc/index.js.map +1 -0
- package/dist/rsc/nonce.d.ts +9 -0
- package/dist/rsc/nonce.d.ts.map +1 -0
- package/dist/rsc/nonce.js +18 -0
- package/dist/rsc/nonce.js.map +1 -0
- package/dist/rsc/types.d.ts +206 -0
- package/dist/rsc/types.d.ts.map +1 -0
- package/dist/rsc/types.js +8 -0
- package/dist/rsc/types.js.map +1 -0
- package/dist/search-params.d.ts +103 -0
- package/dist/search-params.d.ts.map +1 -0
- package/dist/search-params.js +74 -0
- package/dist/search-params.js.map +1 -0
- package/dist/segment-system.d.ts +75 -0
- package/dist/segment-system.d.ts.map +1 -0
- package/dist/segment-system.js +336 -0
- package/dist/segment-system.js.map +1 -0
- package/dist/server/context.d.ts +245 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +197 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/fetchable-loader-store.d.ts +18 -0
- package/dist/server/fetchable-loader-store.d.ts.map +1 -0
- package/dist/server/fetchable-loader-store.js +18 -0
- package/dist/server/fetchable-loader-store.js.map +1 -0
- package/dist/server/handle-store.d.ts +85 -0
- package/dist/server/handle-store.d.ts.map +1 -0
- package/dist/server/handle-store.js +142 -0
- package/dist/server/handle-store.js.map +1 -0
- package/dist/server/loader-registry.d.ts +55 -0
- package/dist/server/loader-registry.d.ts.map +1 -0
- package/dist/server/loader-registry.js +132 -0
- package/dist/server/loader-registry.js.map +1 -0
- package/dist/server/request-context.d.ts +226 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +290 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/root-layout.d.ts +4 -0
- package/dist/server/root-layout.d.ts.map +1 -0
- package/dist/server/root-layout.js +5 -0
- package/dist/server/root-layout.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +20 -0
- package/dist/server.js.map +1 -0
- package/dist/ssr/__tests__/ssr-handler.test.d.ts +2 -0
- package/dist/ssr/__tests__/ssr-handler.test.d.ts.map +1 -0
- package/dist/ssr/__tests__/ssr-handler.test.js +132 -0
- package/dist/ssr/__tests__/ssr-handler.test.js.map +1 -0
- package/dist/ssr/index.d.ts +98 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/index.js +158 -0
- package/dist/ssr/index.js.map +1 -0
- package/dist/static-handler.d.ts +50 -0
- package/dist/static-handler.d.ts.map +1 -0
- package/dist/static-handler.gen.d.ts +5 -0
- package/dist/static-handler.gen.d.ts.map +1 -0
- package/dist/static-handler.gen.js +5 -0
- package/dist/static-handler.gen.js.map +1 -0
- package/dist/static-handler.js +29 -0
- package/dist/static-handler.js.map +1 -0
- package/dist/theme/ThemeProvider.d.ts +20 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +240 -0
- package/dist/theme/ThemeProvider.js.map +1 -0
- package/dist/theme/ThemeScript.d.ts +48 -0
- package/dist/theme/ThemeScript.d.ts.map +1 -0
- package/dist/theme/ThemeScript.js +13 -0
- package/dist/theme/ThemeScript.js.map +1 -0
- package/dist/theme/__tests__/theme.test.d.ts +2 -0
- package/dist/theme/__tests__/theme.test.d.ts.map +1 -0
- package/dist/theme/__tests__/theme.test.js +103 -0
- package/dist/theme/__tests__/theme.test.js.map +1 -0
- package/dist/theme/constants.d.ts +29 -0
- package/dist/theme/constants.d.ts.map +1 -0
- package/dist/theme/constants.js +48 -0
- package/dist/theme/constants.js.map +1 -0
- package/dist/theme/index.d.ts +31 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +36 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/theme-context.d.ts +40 -0
- package/dist/theme/theme-context.d.ts.map +1 -0
- package/dist/theme/theme-context.js +60 -0
- package/dist/theme/theme-context.js.map +1 -0
- package/dist/theme/theme-script.d.ts +27 -0
- package/dist/theme/theme-script.d.ts.map +1 -0
- package/dist/theme/theme-script.js +147 -0
- package/dist/theme/theme-script.js.map +1 -0
- package/dist/theme/types.d.ts +163 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +11 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/theme/use-theme.d.ts +12 -0
- package/dist/theme/use-theme.d.ts.map +1 -0
- package/dist/theme/use-theme.js +40 -0
- package/dist/theme/use-theme.js.map +1 -0
- package/dist/types.d.ts +1479 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/urls.d.ts +441 -0
- package/dist/urls.d.ts.map +1 -0
- package/dist/urls.gen.d.ts +8 -0
- package/dist/urls.gen.d.ts.map +1 -0
- package/dist/urls.gen.js +8 -0
- package/dist/urls.gen.js.map +1 -0
- package/dist/urls.js +443 -0
- package/dist/urls.js.map +1 -0
- package/dist/use-loader.d.ts +127 -0
- package/dist/use-loader.d.ts.map +1 -0
- package/dist/use-loader.js +237 -0
- package/dist/use-loader.js.map +1 -0
- package/dist/vite/__tests__/ast-handler-extract.test.d.ts +2 -0
- package/dist/vite/__tests__/ast-handler-extract.test.d.ts.map +1 -0
- package/dist/vite/__tests__/ast-handler-extract.test.js +294 -0
- package/dist/vite/__tests__/ast-handler-extract.test.js.map +1 -0
- package/dist/vite/__tests__/expose-id-utils.test.d.ts +2 -0
- package/dist/vite/__tests__/expose-id-utils.test.d.ts.map +1 -0
- package/dist/vite/__tests__/expose-id-utils.test.js +224 -0
- package/dist/vite/__tests__/expose-id-utils.test.js.map +1 -0
- package/dist/vite/__tests__/expose-internal-ids.test.d.ts +2 -0
- package/dist/vite/__tests__/expose-internal-ids.test.d.ts.map +1 -0
- package/dist/vite/__tests__/expose-internal-ids.test.js +647 -0
- package/dist/vite/__tests__/expose-internal-ids.test.js.map +1 -0
- package/dist/vite/__tests__/expose-router-id.test.d.ts +2 -0
- package/dist/vite/__tests__/expose-router-id.test.d.ts.map +1 -0
- package/dist/vite/__tests__/expose-router-id.test.js +39 -0
- package/dist/vite/__tests__/expose-router-id.test.js.map +1 -0
- package/dist/vite/ast-handler-extract.d.ts +49 -0
- package/dist/vite/ast-handler-extract.d.ts.map +1 -0
- package/dist/vite/ast-handler-extract.js +249 -0
- package/dist/vite/ast-handler-extract.js.map +1 -0
- package/dist/vite/expose-action-id.d.ts +19 -0
- package/dist/vite/expose-action-id.d.ts.map +1 -0
- package/dist/vite/expose-action-id.js +250 -0
- package/dist/vite/expose-action-id.js.map +1 -0
- package/dist/vite/expose-id-utils.d.ts +69 -0
- package/dist/vite/expose-id-utils.d.ts.map +1 -0
- package/dist/vite/expose-id-utils.js +289 -0
- package/dist/vite/expose-id-utils.js.map +1 -0
- package/dist/vite/expose-internal-ids.d.ts +22 -0
- package/dist/vite/expose-internal-ids.d.ts.map +1 -0
- package/dist/vite/expose-internal-ids.js +886 -0
- package/dist/vite/expose-internal-ids.js.map +1 -0
- package/dist/vite/index.d.ts +149 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +5320 -834
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/index.named-routes.gen.ts +103 -0
- package/dist/vite/package-resolution.d.ts +43 -0
- package/dist/vite/package-resolution.d.ts.map +1 -0
- package/dist/vite/package-resolution.js +112 -0
- package/dist/vite/package-resolution.js.map +1 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/dist/vite/virtual-entries.d.ts +25 -0
- package/dist/vite/virtual-entries.d.ts.map +1 -0
- package/{src/vite/virtual-entries.ts → dist/vite/virtual-entries.js} +18 -17
- package/dist/vite/virtual-entries.js.map +1 -0
- package/package.json +92 -55
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +173 -231
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +88 -58
- package/skills/fonts/SKILL.md +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +420 -67
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +243 -202
- package/skills/layout/SKILL.md +262 -146
- package/skills/links/SKILL.md +313 -0
- package/skills/loader/SKILL.md +525 -251
- package/skills/middleware/SKILL.md +248 -320
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +393 -158
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +120 -0
- package/skills/response-routes/SKILL.md +419 -0
- package/skills/route/SKILL.md +388 -89
- package/skills/router-setup/SKILL.md +388 -267
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +36 -11
- package/skills/typesafety/SKILL.md +456 -173
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +383 -557
- package/src/browser/navigation-client.ts +229 -68
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +391 -304
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +258 -74
- package/src/browser/react/NavigationProvider.tsx +219 -33
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -95
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +49 -80
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +251 -70
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +510 -588
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +176 -58
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +291 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +76 -121
- package/src/client.rsc.tsx +12 -15
- package/src/client.tsx +142 -308
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +6 -2
- package/src/context-var.ts +156 -0
- package/src/debug.ts +243 -0
- package/src/errors.ts +108 -2
- package/src/handle.ts +74 -19
- package/src/handles/MetaTags.tsx +76 -23
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +32 -15
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +141 -47
- package/src/index.rsc.ts +187 -28
- package/src/index.ts +271 -38
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +355 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1371
- package/src/route-map-builder.ts +247 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +88 -9
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +457 -88
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +360 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +187 -43
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +6 -4
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -36
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +200 -0
- package/src/router/middleware.ts +359 -333
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +375 -33
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +45 -23
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +245 -0
- package/src/router/types.ts +79 -4
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +750 -3572
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +934 -801
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +393 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +253 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +358 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +48 -14
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +165 -81
- package/src/server/context.ts +384 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +117 -20
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +36 -131
- package/src/ssr/index.tsx +160 -25
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +11 -4
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +5 -31
- package/src/theme/theme-script.ts +21 -18
- package/src/theme/types.ts +1 -1
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +759 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1561
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -726
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +18 -785
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- 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/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +483 -0
- package/src/vite/router-discovery.ts +977 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +58 -29
- package/src/vite/utils/prerender-utils.ts +221 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -7
- package/src/__tests__/component-utils.test.ts +0 -76
- package/src/__tests__/route-definition.test.ts +0 -63
- package/src/__tests__/urls.test.tsx +0 -436
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/__tests__/document-cache.test.ts +0 -522
- package/src/cache/__tests__/memory-segment-store.test.ts +0 -487
- package/src/cache/__tests__/memory-store.test.ts +0 -484
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +0 -428
- package/src/cache/memory-store.ts +0 -253
- package/src/href.ts +0 -177
- package/src/route-utils.ts +0 -89
- package/src/router/__tests__/match-context.test.ts +0 -104
- package/src/router/__tests__/match-pipelines.test.ts +0 -537
- package/src/router/__tests__/match-result.test.ts +0 -566
- package/src/router/__tests__/on-error.test.ts +0 -935
- package/src/router/__tests__/pattern-matching.test.ts +0 -577
- package/src/router/middleware.test.ts +0 -1355
- package/src/rsc/__tests__/helpers.test.ts +0 -175
- package/src/server/__tests__/request-context.test.ts +0 -171
- package/src/ssr/__tests__/ssr-handler.test.tsx +0 -188
- package/src/theme/__tests__/theme.test.ts +0 -120
- package/src/vite/__tests__/expose-loader-id.test.ts +0 -117
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/router.ts
CHANGED
|
@@ -1,741 +1,140 @@
|
|
|
1
|
-
import type { ComponentType } from "react";
|
|
2
1
|
import { type ReactNode } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { createCacheScope } from "./cache/cache-scope.js";
|
|
3
|
+
import {
|
|
4
|
+
setCacheProfiles,
|
|
5
|
+
resolveCacheProfiles,
|
|
6
|
+
} from "./cache/profile-registry.js";
|
|
7
|
+
import { isCachedFunction } from "./cache/taint.js";
|
|
5
8
|
import { assertClientComponent } from "./component-utils.js";
|
|
6
9
|
import { DefaultDocument } from "./components/DefaultDocument.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
DataNotFoundError,
|
|
10
|
-
RouteNotFoundError,
|
|
11
|
-
invariant,
|
|
12
|
-
sanitizeError,
|
|
13
|
-
} from "./errors";
|
|
10
|
+
import type { SerializedManifest } from "./debug.js";
|
|
11
|
+
import { createReverse, type ReverseFunction } from "./reverse.js";
|
|
14
12
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
createRouteHelpers,
|
|
22
|
-
type RouteHelpers,
|
|
23
|
-
type RouteHandlers,
|
|
24
|
-
} from "./route-definition.js";
|
|
13
|
+
registerRouteMap,
|
|
14
|
+
getPrecomputedEntries,
|
|
15
|
+
getRouterManifest,
|
|
16
|
+
getRouterPrecomputedEntries,
|
|
17
|
+
ensureRouterManifest,
|
|
18
|
+
} from "./route-map-builder.js";
|
|
25
19
|
import MapRootLayout from "./server/root-layout.js";
|
|
26
20
|
import type { AllUseItems } from "./route-types.js";
|
|
27
21
|
import type { UrlPatterns } from "./urls.js";
|
|
22
|
+
import type { UrlBuilder } from "./urls/pattern-types.js";
|
|
23
|
+
import { urls } from "./urls.js";
|
|
28
24
|
import {
|
|
29
|
-
EntryData,
|
|
30
|
-
InterceptEntry,
|
|
31
|
-
InterceptSelectorContext,
|
|
32
|
-
LoaderEntry,
|
|
25
|
+
type EntryData,
|
|
33
26
|
getContext,
|
|
34
27
|
RSCRouterContext,
|
|
28
|
+
type MetricsStore,
|
|
35
29
|
} from "./server/context";
|
|
36
30
|
import { createHandleStore, type HandleStore } from "./server/handle-store.js";
|
|
37
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
getRequestContext,
|
|
33
|
+
_getRequestContext,
|
|
34
|
+
} from "./server/request-context.js";
|
|
38
35
|
import type {
|
|
39
|
-
ErrorBoundaryHandler,
|
|
40
|
-
ErrorInfo,
|
|
41
36
|
ErrorPhase,
|
|
42
37
|
HandlerContext,
|
|
43
38
|
LoaderDataResult,
|
|
44
|
-
MatchResult,
|
|
45
|
-
NotFoundBoundaryHandler,
|
|
46
|
-
OnErrorCallback,
|
|
47
|
-
OnErrorContext,
|
|
48
39
|
ResolvedRouteMap,
|
|
49
|
-
ResolvedSegment,
|
|
50
|
-
RouteDefinition,
|
|
51
40
|
RouteEntry,
|
|
52
|
-
ShouldRevalidateFn,
|
|
53
41
|
TrailingSlashMode,
|
|
54
42
|
} from "./types";
|
|
55
43
|
|
|
56
44
|
// Extracted router utilities
|
|
57
45
|
import {
|
|
58
46
|
createErrorInfo,
|
|
59
|
-
createErrorSegment,
|
|
60
|
-
createNotFoundInfo,
|
|
61
|
-
createNotFoundSegment,
|
|
62
47
|
findNearestErrorBoundary as findErrorBoundary,
|
|
63
48
|
findNearestNotFoundBoundary as findNotFoundBoundary,
|
|
64
49
|
invokeOnError,
|
|
65
50
|
} from "./router/error-handling.js";
|
|
51
|
+
|
|
52
|
+
// Extracted module factories
|
|
53
|
+
import { createSegmentWrappers } from "./router/segment-wrappers.js";
|
|
54
|
+
import { createMatchHandlers } from "./router/match-handlers.js";
|
|
55
|
+
import { buildDebugManifest } from "./router/debug-manifest.js";
|
|
56
|
+
|
|
57
|
+
import type { SegmentResolutionDeps, MatchApiDeps } from "./router/types.js";
|
|
66
58
|
import { createHandlerContext } from "./router/handler-context.js";
|
|
67
59
|
import {
|
|
68
|
-
revalidate,
|
|
69
60
|
setupLoaderAccess,
|
|
70
61
|
setupLoaderAccessSilent,
|
|
71
62
|
wrapLoaderWithErrorHandling,
|
|
72
63
|
} from "./router/loader-resolution.js";
|
|
73
64
|
import { loadManifest } from "./router/manifest.js";
|
|
65
|
+
import { createMetricsStore } from "./router/metrics.js";
|
|
74
66
|
import {
|
|
75
|
-
createMetricsStore,
|
|
76
|
-
generateServerTiming,
|
|
77
|
-
logMetrics,
|
|
78
|
-
} from "./router/metrics.js";
|
|
79
|
-
import {
|
|
80
|
-
collectRouteMiddleware,
|
|
81
|
-
executeInterceptMiddleware,
|
|
82
67
|
parsePattern,
|
|
83
68
|
type MiddlewareEntry,
|
|
84
69
|
type MiddlewareFn,
|
|
85
70
|
} from "./router/middleware.js";
|
|
86
71
|
import {
|
|
87
|
-
|
|
72
|
+
extractStaticPrefix,
|
|
88
73
|
traverseBack,
|
|
89
74
|
} from "./router/pattern-matching.js";
|
|
75
|
+
import { resolveSink, safeEmit, getRequestId } from "./router/telemetry.js";
|
|
90
76
|
import { evaluateRevalidation } from "./router/revalidation.js";
|
|
91
77
|
import {
|
|
92
78
|
type RouterContext,
|
|
93
79
|
runWithRouterContext,
|
|
94
80
|
} from "./router/router-context.js";
|
|
95
|
-
import {
|
|
96
|
-
type ActionContext,
|
|
97
|
-
type MatchContext,
|
|
98
|
-
type MatchPipelineState,
|
|
99
|
-
createPipelineState,
|
|
100
|
-
} from "./router/match-context.js";
|
|
101
|
-
import { createMatchPartialPipeline } from "./router/match-pipelines.js";
|
|
102
|
-
import { collectMatchResult } from "./router/match-result.js";
|
|
103
81
|
import { resolveThemeConfig } from "./theme/constants.js";
|
|
82
|
+
import { resolveTimeouts } from "./router/timeout.js";
|
|
104
83
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*/
|
|
108
|
-
export interface RootLayoutProps {
|
|
109
|
-
children: ReactNode;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Router configuration options
|
|
114
|
-
*/
|
|
115
|
-
export interface RSCRouterOptions<TEnv = any> {
|
|
116
|
-
/**
|
|
117
|
-
* Enable performance metrics collection
|
|
118
|
-
* When enabled, metrics are output to console and available via Server-Timing header
|
|
119
|
-
*/
|
|
120
|
-
debugPerformance?: boolean;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Document component that wraps the entire application.
|
|
124
|
-
*
|
|
125
|
-
* This component provides the HTML structure for your app and wraps
|
|
126
|
-
* both normal route content AND error states, preventing the app shell
|
|
127
|
-
* from unmounting during errors (avoids FOUC).
|
|
128
|
-
*
|
|
129
|
-
* Must be a client component ("use client") that accepts { children }.
|
|
130
|
-
*
|
|
131
|
-
* If not provided, a default document with basic HTML structure is used:
|
|
132
|
-
* `<html><head><meta charset/viewport></head><body>{children}</body></html>`
|
|
133
|
-
*
|
|
134
|
-
* @example
|
|
135
|
-
* ```typescript
|
|
136
|
-
* // components/Document.tsx
|
|
137
|
-
* "use client";
|
|
138
|
-
* export function Document({ children }: { children: ReactNode }) {
|
|
139
|
-
* return (
|
|
140
|
-
* <html lang="en">
|
|
141
|
-
* <head>
|
|
142
|
-
* <link rel="stylesheet" href="/styles.css" />
|
|
143
|
-
* </head>
|
|
144
|
-
* <body>
|
|
145
|
-
* <nav>...</nav>
|
|
146
|
-
* {children}
|
|
147
|
-
* </body>
|
|
148
|
-
* </html>
|
|
149
|
-
* );
|
|
150
|
-
* }
|
|
151
|
-
*
|
|
152
|
-
* // router.tsx
|
|
153
|
-
* const router = createRSCRouter<AppEnv>({
|
|
154
|
-
* document: Document,
|
|
155
|
-
* });
|
|
156
|
-
* ```
|
|
157
|
-
*/
|
|
158
|
-
document?: ComponentType<RootLayoutProps>;
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Default error boundary fallback used when no error boundary is defined in the route tree
|
|
162
|
-
* If not provided, errors will propagate and crash the request
|
|
163
|
-
*/
|
|
164
|
-
defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Default not-found boundary fallback used when no notFoundBoundary is defined in the route tree
|
|
168
|
-
* If not provided, DataNotFoundError will be treated as a regular error
|
|
169
|
-
*/
|
|
170
|
-
defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler;
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Component to render when no route matches the requested URL.
|
|
174
|
-
*
|
|
175
|
-
* This is rendered within your document/app shell with a 404 status code.
|
|
176
|
-
* Use this for a custom 404 page that maintains your app's look and feel.
|
|
177
|
-
*
|
|
178
|
-
* If not provided, a default "Page not found" component is rendered.
|
|
179
|
-
*
|
|
180
|
-
* Can be a static ReactNode or a function receiving the pathname.
|
|
181
|
-
*
|
|
182
|
-
* @example
|
|
183
|
-
* ```typescript
|
|
184
|
-
* // Simple static component
|
|
185
|
-
* const router = createRSCRouter<AppEnv>({
|
|
186
|
-
* document: AppShell,
|
|
187
|
-
* notFound: <NotFound404 />,
|
|
188
|
-
* });
|
|
189
|
-
*
|
|
190
|
-
* // Dynamic component with pathname
|
|
191
|
-
* const router = createRSCRouter<AppEnv>({
|
|
192
|
-
* document: AppShell,
|
|
193
|
-
* notFound: ({ pathname }) => (
|
|
194
|
-
* <div>
|
|
195
|
-
* <h1>404 - Not Found</h1>
|
|
196
|
-
* <p>No page exists at {pathname}</p>
|
|
197
|
-
* <a href="/">Go home</a>
|
|
198
|
-
* </div>
|
|
199
|
-
* ),
|
|
200
|
-
* });
|
|
201
|
-
* ```
|
|
202
|
-
*/
|
|
203
|
-
notFound?: ReactNode | ((props: { pathname: string }) => ReactNode);
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Callback invoked when an error occurs during request handling.
|
|
207
|
-
*
|
|
208
|
-
* This callback is for notification/logging purposes - it cannot modify
|
|
209
|
-
* the error handling flow. Use errorBoundary() in route definitions to
|
|
210
|
-
* customize error UI.
|
|
211
|
-
*
|
|
212
|
-
* The callback receives comprehensive context about the error including:
|
|
213
|
-
* - The error itself
|
|
214
|
-
* - Phase where it occurred (routing, middleware, loader, handler, etc.)
|
|
215
|
-
* - Request info (URL, method, params)
|
|
216
|
-
* - Route info (routeKey, segmentId)
|
|
217
|
-
* - Environment/bindings
|
|
218
|
-
* - Duration from request start
|
|
219
|
-
*
|
|
220
|
-
* @example
|
|
221
|
-
* ```typescript
|
|
222
|
-
* const router = createRSCRouter<AppEnv>({
|
|
223
|
-
* onError: (context) => {
|
|
224
|
-
* // Send to error tracking service
|
|
225
|
-
* Sentry.captureException(context.error, {
|
|
226
|
-
* tags: {
|
|
227
|
-
* phase: context.phase,
|
|
228
|
-
* route: context.routeKey,
|
|
229
|
-
* },
|
|
230
|
-
* extra: {
|
|
231
|
-
* url: context.url.toString(),
|
|
232
|
-
* params: context.params,
|
|
233
|
-
* duration: context.duration,
|
|
234
|
-
* },
|
|
235
|
-
* });
|
|
236
|
-
* },
|
|
237
|
-
* });
|
|
238
|
-
* ```
|
|
239
|
-
*/
|
|
240
|
-
onError?: OnErrorCallback<TEnv>;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Cache store for segment caching.
|
|
244
|
-
*
|
|
245
|
-
* When provided, enables route-level caching via cache() boundaries.
|
|
246
|
-
* The store handles persistence (memory, KV, Redis, etc.).
|
|
247
|
-
*
|
|
248
|
-
* Can be a static config or a function receiving env for runtime bindings.
|
|
249
|
-
* Can be overridden in createRSCHandler.
|
|
250
|
-
*
|
|
251
|
-
* @example Static config
|
|
252
|
-
* ```typescript
|
|
253
|
-
* import { MemorySegmentCacheStore } from "rsc-router/rsc";
|
|
254
|
-
*
|
|
255
|
-
* const router = createRSCRouter({
|
|
256
|
-
* cache: {
|
|
257
|
-
* store: new MemorySegmentCacheStore({ defaults: { ttl: 60 } }),
|
|
258
|
-
* },
|
|
259
|
-
* });
|
|
260
|
-
* ```
|
|
261
|
-
*
|
|
262
|
-
* @example Dynamic config with env
|
|
263
|
-
* ```typescript
|
|
264
|
-
* const router = createRSCRouter<AppEnv>({
|
|
265
|
-
* cache: (env) => ({
|
|
266
|
-
* store: new KVSegmentCacheStore(env.Bindings.MY_CACHE),
|
|
267
|
-
* }),
|
|
268
|
-
* });
|
|
269
|
-
* ```
|
|
270
|
-
*/
|
|
271
|
-
cache?:
|
|
272
|
-
| { store: SegmentCacheStore; enabled?: boolean }
|
|
273
|
-
| ((env: TEnv) => { store: SegmentCacheStore; enabled?: boolean });
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Theme configuration for automatic theme management.
|
|
277
|
-
*
|
|
278
|
-
* When provided, enables:
|
|
279
|
-
* - ctx.theme and ctx.setTheme() in route handlers
|
|
280
|
-
* - useTheme() hook for client components
|
|
281
|
-
* - FOUC prevention via inline script in MetaTags
|
|
282
|
-
* - Automatic ThemeProvider wrapping in NavigationProvider
|
|
283
|
-
*
|
|
284
|
-
* @example
|
|
285
|
-
* ```typescript
|
|
286
|
-
* const router = createRSCRouter<AppEnv>({
|
|
287
|
-
* theme: {
|
|
288
|
-
* defaultTheme: "system",
|
|
289
|
-
* themes: ["light", "dark"],
|
|
290
|
-
* }
|
|
291
|
-
* });
|
|
292
|
-
*
|
|
293
|
-
* // In route handler:
|
|
294
|
-
* route("settings", (ctx) => {
|
|
295
|
-
* const theme = ctx.theme; // "light" | "dark" | "system"
|
|
296
|
-
* ctx.setTheme("dark"); // Sets cookie
|
|
297
|
-
* return <SettingsPage />;
|
|
298
|
-
* });
|
|
299
|
-
*
|
|
300
|
-
* // In client component:
|
|
301
|
-
* import { useTheme } from "@rangojs/router/theme";
|
|
302
|
-
*
|
|
303
|
-
* function ThemeToggle() {
|
|
304
|
-
* const { theme, setTheme, themes } = useTheme();
|
|
305
|
-
* return <select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
306
|
-
* {themes.map(t => <option key={t}>{t}</option>)}
|
|
307
|
-
* </select>;
|
|
308
|
-
* }
|
|
309
|
-
* ```
|
|
310
|
-
*
|
|
311
|
-
* Use `theme: true` to enable with all defaults.
|
|
312
|
-
*/
|
|
313
|
-
theme?: import("./theme/types.js").ThemeConfig | true;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Type-level detection of conflicting route keys.
|
|
318
|
-
* Extracts keys that exist in both TExisting and TNew but with different URL patterns.
|
|
319
|
-
* Returns `never` if no conflicts exist.
|
|
320
|
-
*
|
|
321
|
-
* @example
|
|
322
|
-
* ```typescript
|
|
323
|
-
* ConflictingKeys<{ a: "/a" }, { a: "/b" }> // "a" (conflict - same key, different URLs)
|
|
324
|
-
* ConflictingKeys<{ a: "/a" }, { a: "/a" }> // never (no conflict - same key and URL)
|
|
325
|
-
* ConflictingKeys<{ a: "/a" }, { b: "/b" }> // never (no conflict - different keys)
|
|
326
|
-
* ```
|
|
327
|
-
*/
|
|
328
|
-
type ConflictingKeys<
|
|
329
|
-
TExisting extends Record<string, string>,
|
|
330
|
-
TNew extends Record<string, string>,
|
|
331
|
-
> = {
|
|
332
|
-
[K in keyof TExisting & keyof TNew]: TExisting[K] extends TNew[K]
|
|
333
|
-
? TNew[K] extends TExisting[K]
|
|
334
|
-
? never // Same value, no conflict
|
|
335
|
-
: K // Different values, conflict
|
|
336
|
-
: K; // Different values, conflict
|
|
337
|
-
}[keyof TExisting & keyof TNew];
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Error type returned when route keys conflict.
|
|
341
|
-
* Methods require an impossible `never` parameter so TypeScript errors at the call site.
|
|
342
|
-
*/
|
|
343
|
-
type RouteConflictError<TConflicts extends string> = {
|
|
344
|
-
__error: `Route key conflict! Key "${TConflicts}" already exists with a different URL pattern.`;
|
|
345
|
-
hint: "Route keys must be globally unique. Use prefixed names like 'blog.index' instead of 'index'.";
|
|
346
|
-
conflictingKeys: TConflicts;
|
|
347
|
-
// These methods require `never` so calling them produces an error at the call site
|
|
348
|
-
routes: (
|
|
349
|
-
__conflict: `Fix route key conflict: "${TConflicts}" is already defined with a different URL pattern`
|
|
350
|
-
) => never;
|
|
351
|
-
map: (
|
|
352
|
-
__conflict: `Fix route key conflict: "${TConflicts}" is already defined with a different URL pattern`
|
|
353
|
-
) => never;
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Simplified route helpers for inline route definitions.
|
|
358
|
-
* Uses TRoutes (Record<string, string>) instead of RouteDefinition.
|
|
359
|
-
*
|
|
360
|
-
* Note: Some helpers use `any` for context types as a trade-off for simpler usage.
|
|
361
|
-
* The main type safety is in the `route` helper which enforces valid route names.
|
|
362
|
-
* For full type safety, use the standard map() API with separate handler files.
|
|
363
|
-
*/
|
|
364
|
-
type InlineRouteHelpers<
|
|
365
|
-
TRoutes extends Record<string, string>,
|
|
366
|
-
TEnv,
|
|
367
|
-
> = {
|
|
368
|
-
/**
|
|
369
|
-
* Define a route handler for a specific route pattern
|
|
370
|
-
*/
|
|
371
|
-
route: <K extends keyof TRoutes & string>(
|
|
372
|
-
name: K,
|
|
373
|
-
handler:
|
|
374
|
-
| ((ctx: HandlerContext<{}, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
375
|
-
| ReactNode
|
|
376
|
-
) => AllUseItems;
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Define a layout that wraps child routes
|
|
380
|
-
*/
|
|
381
|
-
layout: (
|
|
382
|
-
component: ReactNode | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>),
|
|
383
|
-
use?: () => AllUseItems[]
|
|
384
|
-
) => AllUseItems;
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Define parallel routes
|
|
388
|
-
*/
|
|
389
|
-
parallel: (
|
|
390
|
-
slots: Record<`@${string}`, ReactNode | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)>,
|
|
391
|
-
use?: () => AllUseItems[]
|
|
392
|
-
) => AllUseItems;
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Define route middleware
|
|
396
|
-
*/
|
|
397
|
-
middleware: (fn: (ctx: any, next: () => Promise<void>) => Promise<void>) => AllUseItems;
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Define revalidation handlers
|
|
401
|
-
*/
|
|
402
|
-
revalidate: (fn: (ctx: any) => boolean | Promise<boolean>) => AllUseItems;
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Define data loaders
|
|
406
|
-
*/
|
|
407
|
-
loader: (loader: any, use?: () => AllUseItems[]) => AllUseItems;
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Define loading states
|
|
411
|
-
*/
|
|
412
|
-
loading: (component: ReactNode) => AllUseItems;
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Define error boundaries
|
|
416
|
-
*/
|
|
417
|
-
errorBoundary: (
|
|
418
|
-
handler: ReactNode | ((props: { error: Error }) => ReactNode)
|
|
419
|
-
) => AllUseItems;
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Define not found boundaries
|
|
423
|
-
*/
|
|
424
|
-
notFoundBoundary: (
|
|
425
|
-
handler: ReactNode | ((props: { pathname: string }) => ReactNode)
|
|
426
|
-
) => AllUseItems;
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Define intercept routes
|
|
430
|
-
*/
|
|
431
|
-
intercept: (
|
|
432
|
-
name: string,
|
|
433
|
-
handler: ReactNode | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>),
|
|
434
|
-
use?: () => AllUseItems[]
|
|
435
|
-
) => AllUseItems;
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Define when conditions for intercepts
|
|
439
|
-
*/
|
|
440
|
-
when: (condition: (ctx: any) => boolean | Promise<boolean>) => AllUseItems;
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Define cache configuration
|
|
444
|
-
*/
|
|
445
|
-
cache: (config: { ttl?: number; swr?: number } | false, use?: () => AllUseItems[]) => AllUseItems;
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Router builder for chaining .use() and .map()
|
|
450
|
-
* TRoutes accumulates all registered route types through the chain
|
|
451
|
-
* TLocalRoutes contains the routes for the current .routes() call (for inline handler typing)
|
|
452
|
-
*/
|
|
453
|
-
interface RouteBuilder<
|
|
454
|
-
T extends RouteDefinition,
|
|
455
|
-
TEnv,
|
|
456
|
-
TRoutes extends Record<string, string>,
|
|
457
|
-
TLocalRoutes extends Record<string, string> = Record<string, string>,
|
|
458
|
-
> {
|
|
459
|
-
/**
|
|
460
|
-
* Add middleware scoped to this mount
|
|
461
|
-
* Called between .routes() and .map()
|
|
462
|
-
*
|
|
463
|
-
* @example
|
|
464
|
-
* ```typescript
|
|
465
|
-
* .routes("/admin", adminRoutes)
|
|
466
|
-
* .use(authMiddleware) // All of /admin/*
|
|
467
|
-
* .use("/danger/*", superAuth) // Only /admin/danger/*
|
|
468
|
-
* .map(() => import("./admin"))
|
|
469
|
-
* ```
|
|
470
|
-
*/
|
|
471
|
-
use(
|
|
472
|
-
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
473
|
-
middleware?: MiddlewareFn<TEnv>
|
|
474
|
-
): RouteBuilder<T, TEnv, TRoutes, TLocalRoutes>;
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Map routes to handlers
|
|
478
|
-
*
|
|
479
|
-
* Supports two patterns:
|
|
480
|
-
*
|
|
481
|
-
* 1. Lazy loading (code-split):
|
|
482
|
-
* ```typescript
|
|
483
|
-
* .routes(homeRoutes)
|
|
484
|
-
* .map(() => import("./handlers/home"))
|
|
485
|
-
* ```
|
|
486
|
-
*
|
|
487
|
-
* 2. Inline definition:
|
|
488
|
-
* ```typescript
|
|
489
|
-
* .routes({ index: "/", about: "/about" })
|
|
490
|
-
* .map(({ route }) => [
|
|
491
|
-
* route("index", () => <HomePage />),
|
|
492
|
-
* route("about", () => <AboutPage />),
|
|
493
|
-
* ])
|
|
494
|
-
* ```
|
|
495
|
-
*/
|
|
496
|
-
// Inline definition overload - handler receives helpers (must be first for correct inference)
|
|
497
|
-
// Uses TLocalRoutes so route names don't need the prefix
|
|
498
|
-
map<H extends (helpers: InlineRouteHelpers<TLocalRoutes, TEnv>) => Array<AllUseItems>>(
|
|
499
|
-
handler: H
|
|
500
|
-
): RSCRouter<TEnv, TRoutes>;
|
|
501
|
-
// Lazy loading overload - verifies imported handlers match route definition
|
|
502
|
-
map(
|
|
503
|
-
handler: () =>
|
|
504
|
-
| Array<AllUseItems>
|
|
505
|
-
| Promise<{ default: RouteHandlers<TLocalRoutes> }>
|
|
506
|
-
| Promise<RouteHandlers<TLocalRoutes>>
|
|
507
|
-
): RSCRouter<TEnv, TRoutes>;
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Accumulated route map for typeof extraction
|
|
511
|
-
* Used for module augmentation: `type AppRoutes = typeof _router.routeMap`
|
|
512
|
-
*/
|
|
513
|
-
readonly routeMap: TRoutes;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* RSC Router interface
|
|
518
|
-
* TRoutes accumulates all registered route types through the builder chain
|
|
519
|
-
*/
|
|
520
|
-
export interface RSCRouter<
|
|
521
|
-
TEnv = any,
|
|
522
|
-
TRoutes extends Record<string, string> = Record<string, string>,
|
|
523
|
-
> {
|
|
524
|
-
/**
|
|
525
|
-
* Register routes with a prefix
|
|
526
|
-
* Route keys stay unchanged, only URL patterns get the prefix applied.
|
|
527
|
-
* This enables composable route modules that work regardless of mount point.
|
|
528
|
-
*
|
|
529
|
-
* @throws Compile-time error if route keys conflict with previously registered routes
|
|
530
|
-
*/
|
|
531
|
-
routes<const TPrefix extends string, const T extends Record<string, string>>(
|
|
532
|
-
prefix: TPrefix,
|
|
533
|
-
routes: T
|
|
534
|
-
): ConflictingKeys<TRoutes, PrefixRoutePatterns<T, TPrefix>> extends never
|
|
535
|
-
? RouteBuilder<
|
|
536
|
-
RouteDefinition,
|
|
537
|
-
TEnv,
|
|
538
|
-
TRoutes & PrefixRoutePatterns<T, TPrefix>,
|
|
539
|
-
T
|
|
540
|
-
>
|
|
541
|
-
: RouteConflictError<ConflictingKeys<TRoutes, PrefixRoutePatterns<T, TPrefix>> & string>;
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Register routes without a prefix
|
|
545
|
-
* Route types are accumulated through the chain
|
|
546
|
-
*
|
|
547
|
-
* @throws Compile-time error if route keys conflict with previously registered routes
|
|
548
|
-
*/
|
|
549
|
-
routes<const T extends Record<string, string>>(
|
|
550
|
-
routes: T
|
|
551
|
-
): ConflictingKeys<TRoutes, T> extends never
|
|
552
|
-
? RouteBuilder<RouteDefinition, TEnv, TRoutes & T, T>
|
|
553
|
-
: RouteConflictError<ConflictingKeys<TRoutes, T> & string>;
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Register routes using Django-style URL patterns
|
|
557
|
-
* This is the new API for @rangojs/router - call once with urls() result
|
|
558
|
-
*
|
|
559
|
-
* @example
|
|
560
|
-
* ```typescript
|
|
561
|
-
* createRSCRouter({})
|
|
562
|
-
* .routes(urlpatterns) // Single call with urls() result
|
|
563
|
-
* ```
|
|
564
|
-
*/
|
|
565
|
-
routes<T extends UrlPatterns<TEnv, any>>(
|
|
566
|
-
patterns: T
|
|
567
|
-
): RSCRouter<
|
|
568
|
-
TEnv,
|
|
569
|
-
TRoutes & (T extends UrlPatterns<any, infer R> ? R : Record<string, string>)
|
|
570
|
-
>;
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Add global middleware that runs on all routes
|
|
574
|
-
* Position matters: middleware before any .routes() is global
|
|
575
|
-
*
|
|
576
|
-
* @example
|
|
577
|
-
* ```typescript
|
|
578
|
-
* createRSCRouter({ document: RootLayout })
|
|
579
|
-
* .use(loggerMiddleware) // All routes
|
|
580
|
-
* .use("/api/*", rateLimiter) // Pattern match
|
|
581
|
-
* .routes(homeRoutes)
|
|
582
|
-
* .map(() => import("./home"))
|
|
583
|
-
* ```
|
|
584
|
-
*/
|
|
585
|
-
use(
|
|
586
|
-
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
587
|
-
middleware?: MiddlewareFn<TEnv>
|
|
588
|
-
): RSCRouter<TEnv, TRoutes>;
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Type-safe URL builder for registered routes
|
|
592
|
-
* Types are inferred from the accumulated route registrations
|
|
593
|
-
* Route keys stay unchanged regardless of mount prefix.
|
|
594
|
-
*
|
|
595
|
-
* @example
|
|
596
|
-
* ```typescript
|
|
597
|
-
* // Given: .routes("/shop", { cart: "/cart", detail: "/product/:slug" })
|
|
598
|
-
* router.href("cart"); // "/shop/cart"
|
|
599
|
-
* router.href("detail", { slug: "widget" }); // "/shop/product/widget"
|
|
600
|
-
* ```
|
|
601
|
-
*/
|
|
602
|
-
href: HrefFunction<TRoutes>;
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Accumulated route map for typeof extraction
|
|
606
|
-
* Used for module augmentation: `type AppRoutes = typeof _router.routeMap`
|
|
607
|
-
*
|
|
608
|
-
* @example
|
|
609
|
-
* ```typescript
|
|
610
|
-
* const _router = createRSCRouter<AppEnv>()
|
|
611
|
-
* .routes(homeRoutes).map(() => import('./home'))
|
|
612
|
-
* .routes('/shop', shopRoutes).map(() => import('./shop'));
|
|
613
|
-
*
|
|
614
|
-
* type AppRoutes = typeof _router.routeMap;
|
|
615
|
-
*
|
|
616
|
-
* declare global {
|
|
617
|
-
* namespace RSCRouter {
|
|
618
|
-
* interface RegisteredRoutes extends AppRoutes {}
|
|
619
|
-
* }
|
|
620
|
-
* }
|
|
621
|
-
* ```
|
|
622
|
-
*/
|
|
623
|
-
readonly routeMap: TRoutes;
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Root layout component that wraps the entire application
|
|
627
|
-
* Access this to pass to renderSegments
|
|
628
|
-
*/
|
|
629
|
-
readonly rootLayout?: ComponentType<RootLayoutProps>;
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Error callback for monitoring/alerting
|
|
633
|
-
* Called when errors occur in loaders, actions, or routes
|
|
634
|
-
*/
|
|
635
|
-
readonly onError?: RSCRouterOptions<TEnv>["onError"];
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Cache configuration (for internal use by RSC handler)
|
|
639
|
-
*/
|
|
640
|
-
readonly cache?: RSCRouterOptions<TEnv>["cache"];
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Not found component to render when no route matches (for internal use by RSC handler)
|
|
644
|
-
*/
|
|
645
|
-
readonly notFound?: RSCRouterOptions<TEnv>["notFound"];
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Resolved theme configuration (null if theme not enabled)
|
|
649
|
-
* Used by NavigationProvider to include ThemeProvider and by MetaTags to render theme script
|
|
650
|
-
*/
|
|
651
|
-
readonly themeConfig: import("./theme/types.js").ResolvedThemeConfig | null;
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* App-level middleware entries (for internal use by RSC handler)
|
|
655
|
-
* These wrap the entire request/response cycle
|
|
656
|
-
*/
|
|
657
|
-
readonly middleware: MiddlewareEntry<TEnv>[];
|
|
658
|
-
|
|
659
|
-
match(request: Request, context: TEnv): Promise<MatchResult>;
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Preview match - returns route middleware without segment resolution
|
|
663
|
-
* Used by RSC handler to execute route middleware before full matching
|
|
664
|
-
*/
|
|
665
|
-
previewMatch(
|
|
666
|
-
request: Request,
|
|
667
|
-
context: TEnv
|
|
668
|
-
): Promise<{
|
|
669
|
-
routeMiddleware?: Array<{
|
|
670
|
-
handler: import("./router/middleware.js").MiddlewareFn;
|
|
671
|
-
params: Record<string, string>;
|
|
672
|
-
}>;
|
|
673
|
-
} | null>;
|
|
674
|
-
|
|
675
|
-
matchPartial(
|
|
676
|
-
request: Request,
|
|
677
|
-
context: TEnv,
|
|
678
|
-
actionContext?: {
|
|
679
|
-
actionId?: string;
|
|
680
|
-
actionUrl?: URL;
|
|
681
|
-
actionResult?: any;
|
|
682
|
-
formData?: FormData;
|
|
683
|
-
}
|
|
684
|
-
): Promise<MatchResult | null>;
|
|
84
|
+
// Extracted content negotiation utilities
|
|
85
|
+
import { flattenNamedRoutes } from "./router/content-negotiation.js";
|
|
685
86
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
context: TEnv,
|
|
702
|
-
error: unknown,
|
|
703
|
-
segmentType?: ErrorInfo["segmentType"]
|
|
704
|
-
): Promise<MatchResult | null>;
|
|
705
|
-
}
|
|
87
|
+
// Extracted router types and registry
|
|
88
|
+
import {
|
|
89
|
+
RSC_ROUTER_BRAND,
|
|
90
|
+
RouterRegistry,
|
|
91
|
+
nextRouterAutoId,
|
|
92
|
+
} from "./router/router-registry.js";
|
|
93
|
+
import type {
|
|
94
|
+
RSCRouterOptions,
|
|
95
|
+
RootLayoutProps,
|
|
96
|
+
} from "./router/router-options.js";
|
|
97
|
+
import type {
|
|
98
|
+
RSCRouter,
|
|
99
|
+
RSCRouterInternal,
|
|
100
|
+
RouterRequestInput,
|
|
101
|
+
} from "./router/router-interfaces.js";
|
|
706
102
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
export function
|
|
736
|
-
options: RSCRouterOptions<TEnv> = {}
|
|
103
|
+
// Extracted closure functions
|
|
104
|
+
import {
|
|
105
|
+
findLazyIncludes,
|
|
106
|
+
evaluateLazyEntry as _evaluateLazyEntry,
|
|
107
|
+
type LazyEvalDeps,
|
|
108
|
+
} from "./router/lazy-includes.js";
|
|
109
|
+
import { createFindMatch } from "./router/find-match.js";
|
|
110
|
+
import {
|
|
111
|
+
matchForPrerender as _matchForPrerender,
|
|
112
|
+
renderStaticSegment as _renderStaticSegment,
|
|
113
|
+
} from "./router/prerender-match.js";
|
|
114
|
+
|
|
115
|
+
// Re-export public types and values from extracted modules
|
|
116
|
+
export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
|
|
117
|
+
export type {
|
|
118
|
+
RSCRouterOptions,
|
|
119
|
+
RootLayoutProps,
|
|
120
|
+
SSRStreamMode,
|
|
121
|
+
SSROptions,
|
|
122
|
+
ResolveStreamingContext,
|
|
123
|
+
} from "./router/router-options.js";
|
|
124
|
+
export type {
|
|
125
|
+
RSCRouter,
|
|
126
|
+
RSCRouterInternal,
|
|
127
|
+
RouterRequestInput,
|
|
128
|
+
} from "./router/router-interfaces.js";
|
|
129
|
+
export { toInternal } from "./router/router-interfaces.js";
|
|
130
|
+
|
|
131
|
+
export function createRouter<TEnv = any>(
|
|
132
|
+
options: RSCRouterOptions<TEnv> = {},
|
|
737
133
|
): RSCRouter<TEnv, {}> {
|
|
738
134
|
const {
|
|
135
|
+
id: userProvidedId,
|
|
136
|
+
$$id: injectedId,
|
|
137
|
+
basename: basenameOption,
|
|
739
138
|
debugPerformance = false,
|
|
740
139
|
document: documentOption,
|
|
741
140
|
defaultErrorBoundary,
|
|
@@ -743,23 +142,116 @@ export function createRSCRouter<TEnv = any>(
|
|
|
743
142
|
notFound,
|
|
744
143
|
onError,
|
|
745
144
|
cache,
|
|
145
|
+
cacheProfiles: cacheProfilesOption,
|
|
746
146
|
theme: themeOption,
|
|
147
|
+
urls: urlsOption,
|
|
148
|
+
$$routeNames: staticRouteNames,
|
|
149
|
+
$$sourceFile: injectedSourceFile,
|
|
150
|
+
nonce,
|
|
151
|
+
version,
|
|
152
|
+
prefetchCacheTTL: prefetchCacheTTLOption,
|
|
153
|
+
warmup: warmupOption,
|
|
154
|
+
allowDebugManifest: allowDebugManifestOption = false,
|
|
155
|
+
telemetry: telemetrySink,
|
|
156
|
+
ssr: ssrOption,
|
|
157
|
+
timeout: timeoutShorthand,
|
|
158
|
+
timeouts: timeoutsOption,
|
|
159
|
+
onTimeout,
|
|
160
|
+
originCheck: originCheckOption,
|
|
747
161
|
} = options;
|
|
748
162
|
|
|
163
|
+
// Normalize basename: ensure leading slash, strip trailing slash.
|
|
164
|
+
// A bare "/" is equivalent to no basename.
|
|
165
|
+
const basename =
|
|
166
|
+
basenameOption && basenameOption.replace(/^\/+|\/+$/g, "")
|
|
167
|
+
? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
|
|
168
|
+
: undefined;
|
|
169
|
+
|
|
170
|
+
// Resolve telemetry sink (no-op when not configured)
|
|
171
|
+
const telemetry = resolveSink(telemetrySink);
|
|
172
|
+
|
|
173
|
+
// Resolve cache profiles: merge user config with guaranteed default profile.
|
|
174
|
+
// This resolved map is both stored on the router (for per-request context)
|
|
175
|
+
// and written to the global registry (for DSL-time cache("profileName")).
|
|
176
|
+
const resolvedCacheProfiles = resolveCacheProfiles(cacheProfilesOption);
|
|
177
|
+
setCacheProfiles(resolvedCacheProfiles);
|
|
178
|
+
|
|
179
|
+
// Source file: prefer Vite-injected path (zero cost), fall back to
|
|
180
|
+
// stack trace parsing for non-Vite environments (e.g. tests).
|
|
181
|
+
let __sourceFile: string | undefined = injectedSourceFile;
|
|
182
|
+
if (!__sourceFile) {
|
|
183
|
+
try {
|
|
184
|
+
const stack = new Error().stack;
|
|
185
|
+
if (stack) {
|
|
186
|
+
const lines = stack.split("\n");
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
const match = line.match(/\((.+?\.(ts|tsx|js|jsx)):\d+:\d+\)/);
|
|
189
|
+
if (
|
|
190
|
+
match &&
|
|
191
|
+
!match[1].endsWith("/router.ts") &&
|
|
192
|
+
!match[1].includes("@rangojs/router") &&
|
|
193
|
+
!match[1].includes("node_modules")
|
|
194
|
+
) {
|
|
195
|
+
__sourceFile = match[1].startsWith("file:")
|
|
196
|
+
? match[1].slice(5)
|
|
197
|
+
: match[1];
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Router ID priority: explicit id > Vite-injected $$id > counter fallback.
|
|
206
|
+
// $$id is a hash of filename+line injected by the Vite transform at compile
|
|
207
|
+
// time, so it's stable across build/runtime regardless of module evaluation
|
|
208
|
+
// order (unlike the counter which depends on import order).
|
|
209
|
+
const routerId =
|
|
210
|
+
userProvidedId ?? injectedId ?? `router_${nextRouterAutoId()}`;
|
|
211
|
+
|
|
212
|
+
// Resolve prefetch cache TTL (default: 300 seconds / 5 minutes)
|
|
213
|
+
// Clamp to a non-negative integer for valid Cache-Control max-age.
|
|
214
|
+
const rawTTL =
|
|
215
|
+
prefetchCacheTTLOption !== undefined ? prefetchCacheTTLOption : 300;
|
|
216
|
+
const prefetchCacheTTLSeconds =
|
|
217
|
+
rawTTL === false ? 0 : Math.max(0, Math.floor(rawTTL));
|
|
218
|
+
const prefetchCacheTTL = prefetchCacheTTLSeconds * 1000;
|
|
219
|
+
const prefetchCacheControl: string | false =
|
|
220
|
+
prefetchCacheTTLSeconds === 0
|
|
221
|
+
? false
|
|
222
|
+
: `private, max-age=${prefetchCacheTTLSeconds}`;
|
|
223
|
+
|
|
224
|
+
// Resolve warmup enabled flag (default: true)
|
|
225
|
+
const warmupEnabled = warmupOption !== false;
|
|
226
|
+
|
|
749
227
|
// Resolve theme config (null if theme not enabled)
|
|
750
228
|
const resolvedThemeConfig = themeOption
|
|
751
229
|
? resolveThemeConfig(themeOption)
|
|
752
230
|
: null;
|
|
753
231
|
|
|
232
|
+
// Resolve timeout config (merge shorthand + structured)
|
|
233
|
+
const resolvedTimeouts = resolveTimeouts(timeoutShorthand, timeoutsOption);
|
|
234
|
+
|
|
754
235
|
/**
|
|
755
236
|
* Wrapper for invokeOnError that binds the router's onError callback.
|
|
756
237
|
* Uses the shared utility from router/error-handling.ts for consistent behavior.
|
|
238
|
+
*
|
|
239
|
+
* Deduplicates via per-request WeakSet stored on the ALS request context.
|
|
240
|
+
* A closure-level WeakSet would silently swallow errors if the same object
|
|
241
|
+
* instance is thrown across separate requests (e.g. a singleton error).
|
|
757
242
|
*/
|
|
758
243
|
function callOnError(
|
|
759
244
|
error: unknown,
|
|
760
245
|
phase: ErrorPhase,
|
|
761
|
-
context: Parameters<typeof invokeOnError<TEnv>>[3]
|
|
246
|
+
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
762
247
|
): void {
|
|
248
|
+
if (error != null && typeof error === "object") {
|
|
249
|
+
const reportedErrors = _getRequestContext()?._reportedErrors;
|
|
250
|
+
if (reportedErrors) {
|
|
251
|
+
if (reportedErrors.has(error)) return;
|
|
252
|
+
reportedErrors.add(error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
763
255
|
invokeOnError(onError, error, phase, context, "Router");
|
|
764
256
|
}
|
|
765
257
|
|
|
@@ -773,8 +265,8 @@ export function createRSCRouter<TEnv = any>(
|
|
|
773
265
|
const routesEntries: RouteEntry<TEnv>[] = [];
|
|
774
266
|
let mountIndex = 0;
|
|
775
267
|
|
|
776
|
-
//
|
|
777
|
-
let
|
|
268
|
+
// Store reference to urlpatterns for runtime manifest generation
|
|
269
|
+
let storedUrlPatterns: UrlPatterns<TEnv, any> | null = null;
|
|
778
270
|
|
|
779
271
|
// Global middleware storage
|
|
780
272
|
const globalMiddleware: MiddlewareEntry<TEnv>[] = [];
|
|
@@ -783,7 +275,7 @@ export function createRSCRouter<TEnv = any>(
|
|
|
783
275
|
function addMiddleware(
|
|
784
276
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
785
277
|
middleware?: MiddlewareFn<TEnv>,
|
|
786
|
-
mountPrefix: string | null = null
|
|
278
|
+
mountPrefix: string | null = null,
|
|
787
279
|
): void {
|
|
788
280
|
let pattern: string | null = null;
|
|
789
281
|
let handler: MiddlewareFn<TEnv>;
|
|
@@ -793,7 +285,7 @@ export function createRSCRouter<TEnv = any>(
|
|
|
793
285
|
pattern = patternOrMiddleware;
|
|
794
286
|
if (!middleware) {
|
|
795
287
|
throw new Error(
|
|
796
|
-
"Middleware function required when pattern is provided"
|
|
288
|
+
"Middleware function required when pattern is provided",
|
|
797
289
|
);
|
|
798
290
|
}
|
|
799
291
|
handler = middleware;
|
|
@@ -802,6 +294,18 @@ export function createRSCRouter<TEnv = any>(
|
|
|
802
294
|
handler = patternOrMiddleware;
|
|
803
295
|
}
|
|
804
296
|
|
|
297
|
+
// Prevent "use cache" functions from being used as middleware.
|
|
298
|
+
// They return data/JSX and do not call next() — silently accepting
|
|
299
|
+
// them would be a confusing no-op.
|
|
300
|
+
if (isCachedFunction(handler)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`A "use cache" function cannot be used as middleware. ` +
|
|
303
|
+
`Cached functions return data and do not participate in the ` +
|
|
304
|
+
`middleware chain. Remove the "use cache" directive or use a ` +
|
|
305
|
+
`regular middleware function instead.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
805
309
|
// If mount-scoped, prepend mount prefix to pattern
|
|
806
310
|
let fullPattern = pattern;
|
|
807
311
|
if (mountPrefix && pattern) {
|
|
@@ -831,11 +335,55 @@ export function createRSCRouter<TEnv = any>(
|
|
|
831
335
|
});
|
|
832
336
|
}
|
|
833
337
|
|
|
834
|
-
// Track all registered routes with their prefixes for
|
|
835
|
-
|
|
338
|
+
// Track all registered routes with their prefixes for reverse().
|
|
339
|
+
// Seed from injected NamedRoutes so reverse() works at module load time
|
|
340
|
+
// for routes that come from lazy includes.
|
|
341
|
+
const mergedRouteMap: Record<string, string> =
|
|
342
|
+
flattenNamedRoutes(staticRouteNames);
|
|
343
|
+
|
|
344
|
+
// Track names that came from the static seed so we can silently overwrite
|
|
345
|
+
// them during routes() registration. The gen file may be stale during HMR,
|
|
346
|
+
// so conflicts between seeded and runtime-registered values are expected.
|
|
347
|
+
const seededNames = new Set(Object.keys(mergedRouteMap));
|
|
348
|
+
|
|
349
|
+
// Lazy precomputed entries lookup: rebuilt when per-router data arrives.
|
|
350
|
+
// In production multi-router setups, per-router data is loaded lazily via
|
|
351
|
+
// ensureRouterManifest(). At createRouter() time the data isn't available yet,
|
|
352
|
+
// so we defer building the Map until first use and invalidate when the
|
|
353
|
+
// per-router source changes.
|
|
354
|
+
let precomputedByPrefix: Map<string, Record<string, string>> | null = null;
|
|
355
|
+
let precomputedSource:
|
|
356
|
+
| Array<{ staticPrefix: string; routes: Record<string, string> }>
|
|
357
|
+
| null
|
|
358
|
+
| undefined;
|
|
359
|
+
|
|
360
|
+
function getPrecomputedByPrefix(): Map<
|
|
361
|
+
string,
|
|
362
|
+
Record<string, string>
|
|
363
|
+
> | null {
|
|
364
|
+
const current =
|
|
365
|
+
getRouterPrecomputedEntries(routerId) ?? getPrecomputedEntries();
|
|
366
|
+
if (current !== precomputedSource) {
|
|
367
|
+
precomputedSource = current;
|
|
368
|
+
precomputedByPrefix = current
|
|
369
|
+
? new Map(current.map((e) => [e.staticPrefix, e.routes]))
|
|
370
|
+
: null;
|
|
371
|
+
}
|
|
372
|
+
return precomputedByPrefix;
|
|
373
|
+
}
|
|
836
374
|
|
|
837
|
-
// Wrapper to pass debugPerformance to external createMetricsStore
|
|
838
|
-
|
|
375
|
+
// Wrapper to pass debugPerformance to external createMetricsStore.
|
|
376
|
+
// Also checks per-request flag set by ctx.debugPerformance() in middleware.
|
|
377
|
+
const getMetricsStore = () => {
|
|
378
|
+
const reqCtx = _getRequestContext();
|
|
379
|
+
const enabled = debugPerformance || !!reqCtx?._debugPerformance;
|
|
380
|
+
if (!enabled) return undefined;
|
|
381
|
+
if (!reqCtx) {
|
|
382
|
+
return createMetricsStore(true);
|
|
383
|
+
}
|
|
384
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
385
|
+
return reqCtx._metricsStore;
|
|
386
|
+
};
|
|
839
387
|
|
|
840
388
|
// Wrapper to pass defaults to error/notFound boundary finders
|
|
841
389
|
const findNearestErrorBoundary = (entry: EntryData | null) =>
|
|
@@ -846,17 +394,46 @@ export function createRSCRouter<TEnv = any>(
|
|
|
846
394
|
|
|
847
395
|
// Helper to get handleStore from request context
|
|
848
396
|
const getHandleStore = (): HandleStore | undefined => {
|
|
849
|
-
return
|
|
397
|
+
return _getRequestContext()?._handleStore;
|
|
850
398
|
};
|
|
851
399
|
|
|
852
|
-
// Track a pending handler promise (non-blocking)
|
|
853
|
-
|
|
400
|
+
// Track a pending handler promise (non-blocking).
|
|
401
|
+
// Attaches a side-effect .catch() to report streaming handler errors to onError
|
|
402
|
+
// without altering the rejection chain (React's streaming error boundary still handles it).
|
|
403
|
+
const trackHandler = <T>(
|
|
404
|
+
promise: Promise<T>,
|
|
405
|
+
errorContext?: {
|
|
406
|
+
segmentId?: string;
|
|
407
|
+
segmentType?: string;
|
|
408
|
+
},
|
|
409
|
+
): Promise<T> => {
|
|
854
410
|
const store = getHandleStore();
|
|
855
|
-
|
|
411
|
+
const tracked = store ? store.track(promise) : promise;
|
|
412
|
+
|
|
413
|
+
// Report streaming handler errors to onError as a side-effect.
|
|
414
|
+
// The rejection still propagates to the RSC stream for client error boundaries.
|
|
415
|
+
// Captures request context eagerly (closure) so the catch handler has full context.
|
|
416
|
+
const reqCtx = _getRequestContext();
|
|
417
|
+
if (reqCtx && onError) {
|
|
418
|
+
tracked.catch((error) => {
|
|
419
|
+
callOnError(error, "handler", {
|
|
420
|
+
request: reqCtx.request,
|
|
421
|
+
url: reqCtx.url,
|
|
422
|
+
routeKey: reqCtx._routeName,
|
|
423
|
+
params: reqCtx.params as Record<string, string>,
|
|
424
|
+
env: reqCtx.env as TEnv,
|
|
425
|
+
segmentId: errorContext?.segmentId,
|
|
426
|
+
segmentType: errorContext?.segmentType as any,
|
|
427
|
+
handledByBoundary: true,
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return tracked;
|
|
856
433
|
};
|
|
857
434
|
|
|
858
435
|
// Wrapper for wrapLoaderWithErrorHandling that uses router's error boundary finder
|
|
859
|
-
// Includes onError callback for loader error notification
|
|
436
|
+
// Includes onError callback for loader error notification and telemetry emission.
|
|
860
437
|
function wrapLoaderPromise<T>(
|
|
861
438
|
promise: Promise<T>,
|
|
862
439
|
entry: EntryData,
|
|
@@ -870,9 +447,27 @@ export function createRSCRouter<TEnv = any>(
|
|
|
870
447
|
env?: TEnv;
|
|
871
448
|
isPartial?: boolean;
|
|
872
449
|
requestStartTime?: number;
|
|
873
|
-
}
|
|
450
|
+
},
|
|
874
451
|
): Promise<LoaderDataResult<T>> {
|
|
875
|
-
|
|
452
|
+
const loaderStart = telemetrySink ? performance.now() : 0;
|
|
453
|
+
const loaderRequestId = telemetrySink
|
|
454
|
+
? errorContext?.request
|
|
455
|
+
? getRequestId(errorContext.request)
|
|
456
|
+
: undefined
|
|
457
|
+
: undefined;
|
|
458
|
+
if (telemetrySink) {
|
|
459
|
+
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
460
|
+
safeEmit(telemetry, {
|
|
461
|
+
type: "loader.start",
|
|
462
|
+
timestamp: loaderStart,
|
|
463
|
+
requestId: loaderRequestId,
|
|
464
|
+
segmentId,
|
|
465
|
+
loaderName,
|
|
466
|
+
pathname,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const result = wrapLoaderWithErrorHandling(
|
|
876
471
|
promise,
|
|
877
472
|
entry,
|
|
878
473
|
segmentId,
|
|
@@ -895,2660 +490,103 @@ export function createRSCRouter<TEnv = any>(
|
|
|
895
490
|
handledByBoundary: ctx.handledByBoundary,
|
|
896
491
|
requestStartTime: errorContext.requestStartTime,
|
|
897
492
|
});
|
|
493
|
+
if (telemetrySink) {
|
|
494
|
+
const errorObj =
|
|
495
|
+
error instanceof Error ? error : new Error(String(error));
|
|
496
|
+
safeEmit(telemetry, {
|
|
497
|
+
type: "loader.error",
|
|
498
|
+
timestamp: performance.now(),
|
|
499
|
+
requestId: loaderRequestId,
|
|
500
|
+
segmentId: ctx.segmentId,
|
|
501
|
+
loaderName: ctx.loaderName,
|
|
502
|
+
pathname,
|
|
503
|
+
error: errorObj,
|
|
504
|
+
handledByBoundary: ctx.handledByBoundary,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
898
507
|
}
|
|
899
|
-
: undefined
|
|
508
|
+
: undefined,
|
|
900
509
|
);
|
|
901
|
-
}
|
|
902
510
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
belongsToRoute: boolean,
|
|
920
|
-
shortCodeOverride?: string
|
|
921
|
-
): Promise<ResolvedSegment[]> {
|
|
922
|
-
const loaderEntries = entry.loader ?? [];
|
|
923
|
-
if (loaderEntries.length === 0) return [];
|
|
924
|
-
|
|
925
|
-
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
926
|
-
|
|
927
|
-
// Check if entry has loading property (cache entries don't)
|
|
928
|
-
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
929
|
-
const loadingDisabled = hasLoading && entry.loading === false;
|
|
930
|
-
|
|
931
|
-
// Trigger all loaders in parallel via ctx.use() (memoized, so safe to call multiple times)
|
|
932
|
-
// Don't await - wrap promises with error handling for deferred client-side resolution
|
|
933
|
-
return Promise.all(
|
|
934
|
-
loaderEntries.map(async ({ loader }, i) => {
|
|
935
|
-
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
936
|
-
return {
|
|
937
|
-
id: segmentId,
|
|
938
|
-
namespace: entry.id,
|
|
939
|
-
type: "loader" as const,
|
|
940
|
-
index: i,
|
|
941
|
-
component: null, // Loaders don't render directly
|
|
942
|
-
params: ctx.params,
|
|
943
|
-
loaderId: loader.$$id,
|
|
944
|
-
loaderData: await wrapLoaderPromise(
|
|
945
|
-
loadingDisabled ? await ctx.use(loader) : ctx.use(loader),
|
|
946
|
-
entry,
|
|
947
|
-
segmentId,
|
|
948
|
-
ctx.pathname
|
|
949
|
-
),
|
|
950
|
-
belongsToRoute,
|
|
951
|
-
};
|
|
952
|
-
})
|
|
953
|
-
);
|
|
954
|
-
}
|
|
511
|
+
// Emit loader.end after the promise settles (fire-and-forget)
|
|
512
|
+
if (telemetrySink) {
|
|
513
|
+
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
514
|
+
result.then((r) => {
|
|
515
|
+
safeEmit(telemetry, {
|
|
516
|
+
type: "loader.end",
|
|
517
|
+
timestamp: performance.now(),
|
|
518
|
+
requestId: loaderRequestId,
|
|
519
|
+
segmentId,
|
|
520
|
+
loaderName,
|
|
521
|
+
pathname,
|
|
522
|
+
durationMs: performance.now() - loaderStart,
|
|
523
|
+
ok: r.ok,
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
}
|
|
955
527
|
|
|
956
|
-
|
|
957
|
-
* Result of resolving loaders with revalidation
|
|
958
|
-
* Contains both segments to render and all matched segment IDs
|
|
959
|
-
*/
|
|
960
|
-
interface LoaderRevalidationResult {
|
|
961
|
-
segments: ResolvedSegment[];
|
|
962
|
-
matchedIds: string[];
|
|
528
|
+
return result;
|
|
963
529
|
}
|
|
964
530
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
async function resolveLoadersWithRevalidation(
|
|
976
|
-
entry: EntryData,
|
|
977
|
-
ctx: HandlerContext<any, TEnv>,
|
|
978
|
-
belongsToRoute: boolean,
|
|
979
|
-
clientSegmentIds: Set<string>,
|
|
980
|
-
prevParams: Record<string, string>,
|
|
981
|
-
request: Request,
|
|
982
|
-
prevUrl: URL,
|
|
983
|
-
nextUrl: URL,
|
|
984
|
-
routeKey: string,
|
|
985
|
-
actionContext?: {
|
|
986
|
-
actionId?: string;
|
|
987
|
-
actionUrl?: URL;
|
|
988
|
-
actionResult?: any;
|
|
989
|
-
formData?: FormData;
|
|
990
|
-
},
|
|
991
|
-
shortCodeOverride?: string,
|
|
992
|
-
stale?: boolean
|
|
993
|
-
): Promise<LoaderRevalidationResult> {
|
|
994
|
-
const loaderEntries = entry.loader ?? [];
|
|
995
|
-
if (loaderEntries.length === 0) return { segments: [], matchedIds: [] };
|
|
996
|
-
|
|
997
|
-
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
998
|
-
|
|
999
|
-
// Build segment IDs and matchedIds upfront
|
|
1000
|
-
const loaderMeta = loaderEntries.map(
|
|
1001
|
-
({ loader, revalidate: loaderRevalidateFns }, i) => ({
|
|
1002
|
-
loader,
|
|
1003
|
-
loaderRevalidateFns,
|
|
1004
|
-
segmentId: `${shortCode}D${i}.${loader.$$id}`,
|
|
1005
|
-
index: i,
|
|
1006
|
-
})
|
|
1007
|
-
);
|
|
531
|
+
// Dependencies object for extracted segment resolution functions.
|
|
532
|
+
// Captures closure-bound helpers from createRouter.
|
|
533
|
+
const segmentDeps: SegmentResolutionDeps<TEnv> = {
|
|
534
|
+
wrapLoaderPromise,
|
|
535
|
+
trackHandler,
|
|
536
|
+
findNearestErrorBoundary,
|
|
537
|
+
findNearestNotFoundBoundary,
|
|
538
|
+
notFoundComponent: notFound,
|
|
539
|
+
callOnError,
|
|
540
|
+
};
|
|
1008
541
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
const dummySegment: ResolvedSegment = {
|
|
1022
|
-
id: segmentId,
|
|
1023
|
-
namespace: entry.id,
|
|
1024
|
-
type: "loader",
|
|
1025
|
-
index,
|
|
1026
|
-
component: null,
|
|
1027
|
-
params: ctx.params,
|
|
1028
|
-
loaderId: loader.$$id,
|
|
1029
|
-
belongsToRoute,
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
|
-
// Evaluate loader's revalidation functions
|
|
1033
|
-
return await evaluateRevalidation({
|
|
1034
|
-
segment: dummySegment,
|
|
1035
|
-
prevParams,
|
|
1036
|
-
getPrevSegment: null,
|
|
1037
|
-
request,
|
|
1038
|
-
prevUrl,
|
|
1039
|
-
nextUrl,
|
|
1040
|
-
revalidations: loaderRevalidateFns.map((fn, j) => ({
|
|
1041
|
-
name: `loader-revalidate${j}`,
|
|
1042
|
-
fn,
|
|
1043
|
-
})),
|
|
1044
|
-
routeKey,
|
|
1045
|
-
context: ctx,
|
|
1046
|
-
actionContext,
|
|
1047
|
-
stale,
|
|
1048
|
-
});
|
|
1049
|
-
},
|
|
1050
|
-
async () => true,
|
|
1051
|
-
() => false
|
|
1052
|
-
);
|
|
1053
|
-
return { shouldRun, loader, segmentId, index };
|
|
1054
|
-
}
|
|
1055
|
-
)
|
|
1056
|
-
);
|
|
542
|
+
// Match API dependencies
|
|
543
|
+
const matchApiDeps: MatchApiDeps<TEnv> = {
|
|
544
|
+
findMatch: (pathname: string, ms?: any) => findMatch(pathname, ms),
|
|
545
|
+
getMetricsStore,
|
|
546
|
+
findInterceptForRoute: (routeKey, parentEntry, selectorContext, isAction) =>
|
|
547
|
+
findInterceptForRoute(routeKey, parentEntry, selectorContext, isAction),
|
|
548
|
+
callOnError,
|
|
549
|
+
findNearestErrorBoundary,
|
|
550
|
+
// Use per-router manifest when available, otherwise the static named map
|
|
551
|
+
// seeded into mergedRouteMap at router creation.
|
|
552
|
+
getRouteMap: () => getRouterManifest(routerId) ?? mergedRouteMap,
|
|
553
|
+
};
|
|
1057
554
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
);
|
|
555
|
+
// Create segment resolution wrappers bound to segmentDeps
|
|
556
|
+
const {
|
|
557
|
+
resolveAllSegments,
|
|
558
|
+
resolveLoadersOnly,
|
|
559
|
+
resolveLoadersOnlyWithRevalidation,
|
|
560
|
+
buildEntryRevalidateMap,
|
|
561
|
+
resolveAllSegmentsWithRevalidation,
|
|
562
|
+
findInterceptForRoute,
|
|
563
|
+
resolveInterceptEntry,
|
|
564
|
+
resolveInterceptLoadersOnly,
|
|
565
|
+
} = createSegmentWrappers<TEnv>(segmentDeps);
|
|
566
|
+
|
|
567
|
+
// Lazy evaluation deps — captures closure state for extracted evaluateLazyEntry
|
|
568
|
+
const lazyEvalDeps: LazyEvalDeps<TEnv> = {
|
|
569
|
+
routesEntries,
|
|
570
|
+
mergedRouteMap,
|
|
571
|
+
nextMountIndex: () => mountIndex++,
|
|
572
|
+
getPrecomputedByPrefix,
|
|
573
|
+
routerId,
|
|
574
|
+
};
|
|
1079
575
|
|
|
1080
|
-
|
|
576
|
+
function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
|
|
577
|
+
_evaluateLazyEntry(entry, lazyEvalDeps);
|
|
1081
578
|
}
|
|
1082
|
-
/**
|
|
1083
|
-
* Resolve segments from EntryData
|
|
1084
|
-
* Executes middlewares, loaders, parallels, and handlers in correct order
|
|
1085
|
-
* Returns array: [main segment, ...orphan layout segments]
|
|
1086
|
-
*/
|
|
1087
|
-
async function resolveSegment(
|
|
1088
|
-
entry: EntryData,
|
|
1089
|
-
routeKey: string,
|
|
1090
|
-
params: Record<string, string>,
|
|
1091
|
-
context: HandlerContext<any, TEnv>,
|
|
1092
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
1093
|
-
isRouteEntry: boolean = false
|
|
1094
|
-
): Promise<ResolvedSegment[]> {
|
|
1095
|
-
const segments: ResolvedSegment[] = [];
|
|
1096
|
-
|
|
1097
|
-
if (entry.type === "layout" || entry.type === "cache") {
|
|
1098
|
-
// Layout/Cache execution order:
|
|
1099
|
-
// 1. Loaders → 2. Parallels (emit segments) → 3. Handler (emit segment) → 4. Orphan Layouts
|
|
1100
|
-
// Note: Middleware is now collected and executed at the top level (coreRequestHandler)
|
|
1101
|
-
|
|
1102
|
-
// Step 1: Run layout loaders
|
|
1103
|
-
const loaderSegments = await resolveLoaders(
|
|
1104
|
-
entry,
|
|
1105
|
-
context,
|
|
1106
|
-
false // Parent chain layouts don't belong to specific route
|
|
1107
|
-
);
|
|
1108
|
-
segments.push(...loaderSegments);
|
|
1109
|
-
|
|
1110
|
-
// Step 3: Process and emit layout parallel segments
|
|
1111
|
-
for (const parallelEntry of entry.parallel) {
|
|
1112
|
-
const parallelSegments = await resolveParallelEntry(
|
|
1113
|
-
parallelEntry,
|
|
1114
|
-
params,
|
|
1115
|
-
context,
|
|
1116
|
-
false, // Parent chain parallels don't belong to specific route
|
|
1117
|
-
entry.shortCode // Pass parent layout's shortCode for segment ID association
|
|
1118
|
-
);
|
|
1119
|
-
segments.push(...parallelSegments);
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// Step 4: Execute layout handler and emit layout segment
|
|
1123
|
-
// Set current segment ID for handle data attribution
|
|
1124
|
-
context._currentSegmentId = entry.shortCode;
|
|
1125
|
-
const component =
|
|
1126
|
-
typeof entry.handler === "function"
|
|
1127
|
-
? await entry.handler(context)
|
|
1128
|
-
: entry.handler;
|
|
1129
|
-
|
|
1130
|
-
segments.push({
|
|
1131
|
-
id: entry.shortCode,
|
|
1132
|
-
namespace: entry.id,
|
|
1133
|
-
type: "layout", // Cache entries also emit "layout" type segments
|
|
1134
|
-
index: 0,
|
|
1135
|
-
component,
|
|
1136
|
-
loading: entry.loading === false ? null : entry.loading,
|
|
1137
|
-
params,
|
|
1138
|
-
belongsToRoute: false, // Parent chain layouts/cache don't belong to specific route
|
|
1139
|
-
layoutName: entry.id,
|
|
1140
|
-
});
|
|
1141
579
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
loaderPromises,
|
|
1149
|
-
false // Parent chain layouts don't belong to specific route
|
|
1150
|
-
);
|
|
1151
|
-
segments.push(...orphanSegments);
|
|
1152
|
-
}
|
|
1153
|
-
} else if (entry.type === "route") {
|
|
1154
|
-
// Route execution order:
|
|
1155
|
-
// 1. Route Loader → 2. Orphan Layouts → 3. Route Parallels (emit segments) → 4. Route Handler (emit segment)
|
|
1156
|
-
// Note: Route middleware is now collected and executed at the top level (coreRequestHandler)
|
|
1157
|
-
|
|
1158
|
-
// Step 1: Run route loaders
|
|
1159
|
-
const loaderSegments = await resolveLoaders(
|
|
1160
|
-
entry,
|
|
1161
|
-
context,
|
|
1162
|
-
true // Route loaders belong to the route
|
|
1163
|
-
);
|
|
1164
|
-
segments.push(...loaderSegments);
|
|
1165
|
-
|
|
1166
|
-
// Step 3: Process orphan layouts first
|
|
1167
|
-
for (const orphan of entry.layout) {
|
|
1168
|
-
const orphanSegments = await resolveOrphanLayout(
|
|
1169
|
-
orphan,
|
|
1170
|
-
params,
|
|
1171
|
-
context,
|
|
1172
|
-
loaderPromises,
|
|
1173
|
-
true // Route's orphan layouts belong to the route
|
|
1174
|
-
);
|
|
1175
|
-
segments.push(...orphanSegments);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// Step 4: Process and emit route parallel segments
|
|
1179
|
-
for (const parallelEntry of entry.parallel) {
|
|
1180
|
-
const parallelSegments = await resolveParallelEntry(
|
|
1181
|
-
parallelEntry,
|
|
1182
|
-
params,
|
|
1183
|
-
context,
|
|
1184
|
-
true, // Route's parallels belong to the route
|
|
1185
|
-
entry.shortCode // Pass parent route's shortCode for segment ID association
|
|
1186
|
-
);
|
|
1187
|
-
segments.push(...parallelSegments);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Step 5: Execute route handler and emit route segment
|
|
1191
|
-
// If loading is defined, wrap in Suspense for RSC streaming
|
|
1192
|
-
// This allows the fallback to be sent immediately while content streams in
|
|
1193
|
-
// Set current segment ID for handle data attribution
|
|
1194
|
-
context._currentSegmentId = entry.shortCode;
|
|
1195
|
-
let component: ReactNode | Promise<ReactNode>;
|
|
1196
|
-
if (entry.loading) {
|
|
1197
|
-
const result = entry.handler(context);
|
|
1198
|
-
component = result instanceof Promise ? trackHandler(result) : result;
|
|
1199
|
-
} else {
|
|
1200
|
-
component = await entry.handler(context);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
segments.push({
|
|
1204
|
-
id: entry.shortCode,
|
|
1205
|
-
namespace: entry.id,
|
|
1206
|
-
type: "route",
|
|
1207
|
-
index: 0,
|
|
1208
|
-
component,
|
|
1209
|
-
loading: entry.loading === false ? null : entry.loading,
|
|
1210
|
-
params,
|
|
1211
|
-
belongsToRoute: true, // Route always belongs to itself
|
|
1212
|
-
});
|
|
1213
|
-
} else {
|
|
1214
|
-
throw new Error(`Unknown entry type: ${(entry as any).type}`);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
return segments;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
/**
|
|
1221
|
-
* Helper: Resolve orphan layout with its middlewares, loaders, and parallels
|
|
1222
|
-
* Also handles cache entries in the layout array (structural boundaries)
|
|
1223
|
-
*/
|
|
1224
|
-
async function resolveOrphanLayout(
|
|
1225
|
-
orphan: EntryData,
|
|
1226
|
-
params: Record<string, string>,
|
|
1227
|
-
context: HandlerContext<any, TEnv>,
|
|
1228
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
1229
|
-
belongsToRoute: boolean
|
|
1230
|
-
): Promise<ResolvedSegment[]> {
|
|
1231
|
-
// Orphans must be layouts or cache entries
|
|
1232
|
-
invariant(
|
|
1233
|
-
orphan.type === "layout" || orphan.type === "cache",
|
|
1234
|
-
`Expected orphan to be a layout or cache, got: ${orphan.type}`
|
|
1235
|
-
);
|
|
1236
|
-
|
|
1237
|
-
// Orphan Loader → Orphan Parallels → Orphan Handler
|
|
1238
|
-
// Note: Orphan middleware is now collected and executed at the top level (coreRequestHandler)
|
|
1239
|
-
|
|
1240
|
-
// Step 1: Run orphan loaders
|
|
1241
|
-
const loaderSegments = await resolveLoaders(
|
|
1242
|
-
orphan,
|
|
1243
|
-
context,
|
|
1244
|
-
belongsToRoute
|
|
1245
|
-
);
|
|
1246
|
-
|
|
1247
|
-
// Step 3: Process and emit orphan parallel segments
|
|
1248
|
-
const segments: ResolvedSegment[] = [...loaderSegments];
|
|
1249
|
-
for (const parallelEntry of orphan.parallel) {
|
|
1250
|
-
const parallelSegments = await resolveParallelEntry(
|
|
1251
|
-
parallelEntry,
|
|
1252
|
-
params,
|
|
1253
|
-
context,
|
|
1254
|
-
belongsToRoute,
|
|
1255
|
-
orphan.shortCode // Pass parent orphan layout's shortCode for segment ID association
|
|
1256
|
-
);
|
|
1257
|
-
segments.push(...parallelSegments);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Step 4: Execute orphan handler and emit layout segment
|
|
1261
|
-
const component =
|
|
1262
|
-
typeof orphan.handler === "function"
|
|
1263
|
-
? await orphan.handler(context)
|
|
1264
|
-
: orphan.handler;
|
|
1265
|
-
|
|
1266
|
-
segments.push({
|
|
1267
|
-
id: orphan.shortCode,
|
|
1268
|
-
namespace: orphan.id,
|
|
1269
|
-
type: "layout",
|
|
1270
|
-
index: 0,
|
|
1271
|
-
component,
|
|
1272
|
-
params,
|
|
1273
|
-
belongsToRoute,
|
|
1274
|
-
layoutName: orphan.id,
|
|
1275
|
-
loading: orphan.loading === false ? null : orphan.loading,
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
return segments;
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* Check if an intercept's when conditions are satisfied
|
|
1283
|
-
* All when() functions must return true for the intercept to activate.
|
|
1284
|
-
* If no when() conditions are defined, the intercept always activates.
|
|
1285
|
-
*
|
|
1286
|
-
* IMPORTANT: During action revalidation, when() is NOT evaluated.
|
|
1287
|
-
* The intercept was already activated during navigation, and we preserve
|
|
1288
|
-
* that state to avoid accidentally closing modals after actions.
|
|
1289
|
-
*/
|
|
1290
|
-
function evaluateInterceptWhen(
|
|
1291
|
-
intercept: InterceptEntry,
|
|
1292
|
-
selectorContext: InterceptSelectorContext | null,
|
|
1293
|
-
isAction: boolean
|
|
1294
|
-
): boolean {
|
|
1295
|
-
// During action revalidation, skip when() evaluation - preserve current state
|
|
1296
|
-
// The intercept was already activated during navigation
|
|
1297
|
-
if (isAction) {
|
|
1298
|
-
return true;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// If no when conditions, always intercept (backwards compatible)
|
|
1302
|
-
if (!intercept.when || intercept.when.length === 0) {
|
|
1303
|
-
return true;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
// If no selector context provided, can't evaluate - skip intercept
|
|
1307
|
-
if (!selectorContext) {
|
|
1308
|
-
return false;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// All when conditions must return true (AND logic)
|
|
1312
|
-
return intercept.when.every((fn) => fn(selectorContext));
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
/**
|
|
1316
|
-
* Find an intercept for the target route by walking up the entry chain
|
|
1317
|
-
* Returns the first (innermost) matching intercept along with the entry that defines it
|
|
1318
|
-
*
|
|
1319
|
-
* Intercepts are "lazy parallels" that only activate during soft navigation.
|
|
1320
|
-
* They render alternative content in a named slot (like @modal) instead of the
|
|
1321
|
-
* route's normal handler.
|
|
1322
|
-
*
|
|
1323
|
-
* @param targetRouteKey - The route key to find an intercept for (e.g., "card")
|
|
1324
|
-
* @param fromEntry - Starting entry to walk up from (usually the route entry)
|
|
1325
|
-
* @param selectorContext - Navigation context for evaluating when() conditions
|
|
1326
|
-
* @param isAction - Whether this is an action revalidation (skips when() evaluation)
|
|
1327
|
-
* @returns The matching intercept and its defining entry, or null if none found
|
|
1328
|
-
*/
|
|
1329
|
-
function findInterceptForRoute(
|
|
1330
|
-
targetRouteKey: string,
|
|
1331
|
-
fromEntry: EntryData | null,
|
|
1332
|
-
selectorContext: InterceptSelectorContext | null = null,
|
|
1333
|
-
isAction: boolean = false
|
|
1334
|
-
): { intercept: InterceptEntry; entry: EntryData } | null {
|
|
1335
|
-
let current: EntryData | null = fromEntry;
|
|
1336
|
-
|
|
1337
|
-
while (current) {
|
|
1338
|
-
// Check if this entry has intercepts defined
|
|
1339
|
-
if (current.intercept && current.intercept.length > 0) {
|
|
1340
|
-
// Find intercept matching the target route name and when conditions
|
|
1341
|
-
for (const intercept of current.intercept) {
|
|
1342
|
-
if (
|
|
1343
|
-
intercept.routeName === targetRouteKey &&
|
|
1344
|
-
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
1345
|
-
) {
|
|
1346
|
-
return { intercept, entry: current };
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Also check sibling layouts for intercepts
|
|
1352
|
-
// Intercepts are defined as siblings in the route tree - e.g., an intercept
|
|
1353
|
-
// like (.)card/[cardId] is placed alongside the parent route's layouts
|
|
1354
|
-
if (current.layout && current.layout.length > 0) {
|
|
1355
|
-
for (const siblingLayout of current.layout) {
|
|
1356
|
-
if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
|
|
1357
|
-
for (const intercept of siblingLayout.intercept) {
|
|
1358
|
-
if (
|
|
1359
|
-
intercept.routeName === targetRouteKey &&
|
|
1360
|
-
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
1361
|
-
) {
|
|
1362
|
-
return { intercept, entry: siblingLayout };
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
current = current.parent;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
return null;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
/**
|
|
1376
|
-
* Resolve an intercept entry and emit segment with the slot name
|
|
1377
|
-
* Similar to parallel entry resolution but for intercept handlers.
|
|
1378
|
-
*
|
|
1379
|
-
* Intercepts can have their own middleware, loaders, revalidate, and loading.
|
|
1380
|
-
* The handler is rendered in the named slot (e.g., @modal).
|
|
1381
|
-
*
|
|
1382
|
-
* @param interceptEntry - The intercept definition
|
|
1383
|
-
* @param parentEntry - The entry that defines the intercept (for shortCode)
|
|
1384
|
-
* @param params - URL parameters
|
|
1385
|
-
* @param context - Handler context
|
|
1386
|
-
* @param belongsToRoute - Whether this intercept belongs to the matched route
|
|
1387
|
-
* @param revalidationContext - Optional revalidation context for partial updates
|
|
1388
|
-
*/
|
|
1389
|
-
async function resolveInterceptEntry(
|
|
1390
|
-
interceptEntry: InterceptEntry,
|
|
1391
|
-
parentEntry: EntryData,
|
|
1392
|
-
params: Record<string, string>,
|
|
1393
|
-
context: HandlerContext<any, TEnv>,
|
|
1394
|
-
belongsToRoute: boolean = true,
|
|
1395
|
-
revalidationContext?: {
|
|
1396
|
-
clientSegmentIds: Set<string>;
|
|
1397
|
-
prevParams: Record<string, string>;
|
|
1398
|
-
request: Request;
|
|
1399
|
-
prevUrl: URL;
|
|
1400
|
-
nextUrl: URL;
|
|
1401
|
-
routeKey: string;
|
|
1402
|
-
actionContext?: {
|
|
1403
|
-
actionId?: string;
|
|
1404
|
-
actionUrl?: URL;
|
|
1405
|
-
actionResult?: any;
|
|
1406
|
-
formData?: FormData;
|
|
1407
|
-
};
|
|
1408
|
-
stale?: boolean;
|
|
1409
|
-
}
|
|
1410
|
-
): Promise<ResolvedSegment[]> {
|
|
1411
|
-
const segments: ResolvedSegment[] = [];
|
|
1412
|
-
|
|
1413
|
-
// Step 1: Execute intercept middleware
|
|
1414
|
-
if (interceptEntry.middleware.length > 0) {
|
|
1415
|
-
// Get stubResponse from request context for header/cookie collection
|
|
1416
|
-
const requestCtx = getRequestContext();
|
|
1417
|
-
if (!requestCtx?.res) {
|
|
1418
|
-
throw new Error(
|
|
1419
|
-
"Request context with stubResponse is required for intercept middleware"
|
|
1420
|
-
);
|
|
1421
|
-
}
|
|
1422
|
-
const middlewareResponse = await executeInterceptMiddleware(
|
|
1423
|
-
interceptEntry.middleware,
|
|
1424
|
-
context.request,
|
|
1425
|
-
context.env,
|
|
1426
|
-
params,
|
|
1427
|
-
context.var as Record<string, any>,
|
|
1428
|
-
requestCtx.res
|
|
1429
|
-
);
|
|
1430
|
-
if (middlewareResponse) throw middlewareResponse;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// Step 2: Collect intercept loaders as promises (with revalidation check)
|
|
1434
|
-
// These will be attached directly to the intercept segment for streaming
|
|
1435
|
-
const loaderPromises: Promise<any>[] = [];
|
|
1436
|
-
const loaderIds: string[] = [];
|
|
1437
|
-
|
|
1438
|
-
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
1439
|
-
const { loader, revalidate: loaderRevalidateFns } =
|
|
1440
|
-
interceptEntry.loader[i];
|
|
1441
|
-
const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
|
|
1442
|
-
|
|
1443
|
-
// Check revalidation if context provided (partial updates)
|
|
1444
|
-
if (revalidationContext) {
|
|
1445
|
-
const {
|
|
1446
|
-
clientSegmentIds,
|
|
1447
|
-
prevParams,
|
|
1448
|
-
request,
|
|
1449
|
-
prevUrl,
|
|
1450
|
-
nextUrl,
|
|
1451
|
-
routeKey,
|
|
1452
|
-
actionContext,
|
|
1453
|
-
stale,
|
|
1454
|
-
} = revalidationContext;
|
|
1455
|
-
|
|
1456
|
-
// Check if client has the parent intercept segment (loaders are embedded, not separate segments)
|
|
1457
|
-
const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
1458
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
1459
|
-
// Create dummy segment for evaluation
|
|
1460
|
-
const dummySegment: ResolvedSegment = {
|
|
1461
|
-
id: segmentId,
|
|
1462
|
-
namespace: `intercept:${interceptEntry.routeName}`,
|
|
1463
|
-
type: "loader",
|
|
1464
|
-
index: i,
|
|
1465
|
-
component: null,
|
|
1466
|
-
params,
|
|
1467
|
-
loaderId: loader.$$id,
|
|
1468
|
-
belongsToRoute,
|
|
1469
|
-
};
|
|
1470
|
-
|
|
1471
|
-
const shouldRevalidate = await evaluateRevalidation({
|
|
1472
|
-
segment: dummySegment,
|
|
1473
|
-
prevParams,
|
|
1474
|
-
getPrevSegment: null,
|
|
1475
|
-
request,
|
|
1476
|
-
prevUrl,
|
|
1477
|
-
nextUrl,
|
|
1478
|
-
revalidations: loaderRevalidateFns.map((fn, j) => ({
|
|
1479
|
-
name: `intercept-loader-revalidate${j}`,
|
|
1480
|
-
fn,
|
|
1481
|
-
})),
|
|
1482
|
-
routeKey,
|
|
1483
|
-
context,
|
|
1484
|
-
actionContext,
|
|
1485
|
-
stale,
|
|
1486
|
-
});
|
|
1487
|
-
|
|
1488
|
-
if (!shouldRevalidate) {
|
|
1489
|
-
console.log(
|
|
1490
|
-
`[Router] Intercept loader ${loader.$$id} skipped (revalidation=false)`
|
|
1491
|
-
);
|
|
1492
|
-
continue;
|
|
1493
|
-
}
|
|
1494
|
-
console.log(
|
|
1495
|
-
`[Router] Intercept loader ${loader.$$id} revalidating (stale=${stale})`
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
loaderIds.push(loader.$$id);
|
|
1501
|
-
loaderPromises.push(
|
|
1502
|
-
wrapLoaderPromise(
|
|
1503
|
-
context.use(loader),
|
|
1504
|
-
parentEntry,
|
|
1505
|
-
segmentId,
|
|
1506
|
-
context.pathname
|
|
1507
|
-
)
|
|
1508
|
-
);
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// Step 3: Execute intercept handler and prepare component
|
|
1512
|
-
// Get handler result - don't await if we have loading (enables streaming)
|
|
1513
|
-
const handlerResult =
|
|
1514
|
-
typeof interceptEntry.handler === "function"
|
|
1515
|
-
? interceptEntry.handler(context)
|
|
1516
|
-
: interceptEntry.handler;
|
|
1517
|
-
|
|
1518
|
-
// Step 4: Prepare layout element (if defined)
|
|
1519
|
-
// Layout will be applied in segment-system, not here
|
|
1520
|
-
let layoutElement: ReactNode | undefined;
|
|
1521
|
-
if (interceptEntry.layout) {
|
|
1522
|
-
layoutElement =
|
|
1523
|
-
typeof interceptEntry.layout === "function"
|
|
1524
|
-
? await interceptEntry.layout(context)
|
|
1525
|
-
: interceptEntry.layout;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// Determine if we should await the handler result and loaders
|
|
1529
|
-
// If we have loading, DON'T await - let Suspense handle streaming
|
|
1530
|
-
let component: ReactNode | Promise<ReactNode>;
|
|
1531
|
-
let loaderDataPromise: Promise<any[]> | any[] | undefined;
|
|
1532
|
-
|
|
1533
|
-
if (interceptEntry.loading && loaderPromises.length > 0) {
|
|
1534
|
-
// Has loading skeleton - keep everything as Promises for streaming
|
|
1535
|
-
// Don't track intercept handlers - they're parallels and shouldn't block handle data
|
|
1536
|
-
component =
|
|
1537
|
-
handlerResult instanceof Promise
|
|
1538
|
-
? handlerResult
|
|
1539
|
-
: Promise.resolve(handlerResult);
|
|
1540
|
-
loaderDataPromise = Promise.all(loaderPromises);
|
|
1541
|
-
} else if (loaderPromises.length > 0) {
|
|
1542
|
-
// No loading skeleton - await loaders and component
|
|
1543
|
-
loaderDataPromise = await Promise.all(loaderPromises);
|
|
1544
|
-
component =
|
|
1545
|
-
handlerResult instanceof Promise ? await handlerResult : handlerResult;
|
|
1546
|
-
} else {
|
|
1547
|
-
// No loaders - don't track intercept handlers (they're parallels)
|
|
1548
|
-
component =
|
|
1549
|
-
interceptEntry.loading && handlerResult instanceof Promise
|
|
1550
|
-
? handlerResult
|
|
1551
|
-
: handlerResult instanceof Promise
|
|
1552
|
-
? await handlerResult
|
|
1553
|
-
: handlerResult;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
const interceptSegment = {
|
|
1557
|
-
id: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
|
|
1558
|
-
namespace: `intercept:${interceptEntry.routeName}`,
|
|
1559
|
-
type: "parallel" as const,
|
|
1560
|
-
index: 0,
|
|
1561
|
-
component,
|
|
1562
|
-
loading: interceptEntry.loading === false ? null : interceptEntry.loading,
|
|
1563
|
-
layout: layoutElement,
|
|
1564
|
-
params,
|
|
1565
|
-
slot: interceptEntry.slotName,
|
|
1566
|
-
belongsToRoute,
|
|
1567
|
-
parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
|
|
1568
|
-
// Attach loader info directly to segment for streaming
|
|
1569
|
-
loaderDataPromise,
|
|
1570
|
-
loaderIds: loaderIds.length > 0 ? loaderIds : undefined,
|
|
1571
|
-
};
|
|
1572
|
-
segments.push(interceptSegment);
|
|
1573
|
-
|
|
1574
|
-
return segments;
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
/**
|
|
1578
|
-
* Helper: Resolve only the loaders for a cached intercept segment.
|
|
1579
|
-
* Used on intercept cache hit to get fresh loader data while keeping cached component/layout.
|
|
1580
|
-
* Returns the fresh loaderDataPromise and loaderIds, or null if no loaders need resolution.
|
|
1581
|
-
*/
|
|
1582
|
-
async function resolveInterceptLoadersOnly(
|
|
1583
|
-
interceptEntry: InterceptEntry,
|
|
1584
|
-
parentEntry: EntryData,
|
|
1585
|
-
params: Record<string, string>,
|
|
1586
|
-
context: HandlerContext<any, TEnv>,
|
|
1587
|
-
belongsToRoute: boolean = true,
|
|
1588
|
-
revalidationContext: {
|
|
1589
|
-
clientSegmentIds: Set<string>;
|
|
1590
|
-
prevParams: Record<string, string>;
|
|
1591
|
-
request: Request;
|
|
1592
|
-
prevUrl: URL;
|
|
1593
|
-
nextUrl: URL;
|
|
1594
|
-
routeKey: string;
|
|
1595
|
-
actionContext?: {
|
|
1596
|
-
actionId?: string;
|
|
1597
|
-
actionUrl?: URL;
|
|
1598
|
-
actionResult?: any;
|
|
1599
|
-
formData?: FormData;
|
|
1600
|
-
};
|
|
1601
|
-
stale?: boolean;
|
|
1602
|
-
}
|
|
1603
|
-
): Promise<{
|
|
1604
|
-
loaderDataPromise: Promise<any[]> | any[];
|
|
1605
|
-
loaderIds: string[];
|
|
1606
|
-
} | null> {
|
|
1607
|
-
if (interceptEntry.loader.length === 0) {
|
|
1608
|
-
return null;
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
const loaderPromises: Promise<any>[] = [];
|
|
1612
|
-
const loaderIds: string[] = [];
|
|
1613
|
-
|
|
1614
|
-
const {
|
|
1615
|
-
clientSegmentIds,
|
|
1616
|
-
prevParams,
|
|
1617
|
-
request,
|
|
1618
|
-
prevUrl,
|
|
1619
|
-
nextUrl,
|
|
1620
|
-
routeKey,
|
|
1621
|
-
actionContext,
|
|
1622
|
-
stale,
|
|
1623
|
-
} = revalidationContext;
|
|
1624
|
-
|
|
1625
|
-
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
1626
|
-
const { loader, revalidate: loaderRevalidateFns } =
|
|
1627
|
-
interceptEntry.loader[i];
|
|
1628
|
-
const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
|
|
1629
|
-
|
|
1630
|
-
// Check if client has the parent intercept segment (loaders are embedded, not separate segments)
|
|
1631
|
-
const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
1632
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
1633
|
-
// Create dummy segment for evaluation
|
|
1634
|
-
const dummySegment: ResolvedSegment = {
|
|
1635
|
-
id: segmentId,
|
|
1636
|
-
namespace: `intercept:${interceptEntry.routeName}`,
|
|
1637
|
-
type: "loader",
|
|
1638
|
-
index: i,
|
|
1639
|
-
component: null,
|
|
1640
|
-
params,
|
|
1641
|
-
loaderId: loader.$$id,
|
|
1642
|
-
belongsToRoute,
|
|
1643
|
-
};
|
|
1644
|
-
|
|
1645
|
-
const shouldRevalidate = await evaluateRevalidation({
|
|
1646
|
-
segment: dummySegment,
|
|
1647
|
-
prevParams,
|
|
1648
|
-
getPrevSegment: null,
|
|
1649
|
-
request,
|
|
1650
|
-
prevUrl,
|
|
1651
|
-
nextUrl,
|
|
1652
|
-
revalidations: loaderRevalidateFns.map((fn, j) => ({
|
|
1653
|
-
name: `intercept-loader-revalidate${j}`,
|
|
1654
|
-
fn,
|
|
1655
|
-
})),
|
|
1656
|
-
routeKey,
|
|
1657
|
-
context,
|
|
1658
|
-
actionContext,
|
|
1659
|
-
stale,
|
|
1660
|
-
});
|
|
1661
|
-
|
|
1662
|
-
if (!shouldRevalidate) {
|
|
1663
|
-
console.log(
|
|
1664
|
-
`[Router] Intercept loader ${loader.$$id} skipped (cache hit, revalidation=false)`
|
|
1665
|
-
);
|
|
1666
|
-
continue;
|
|
1667
|
-
}
|
|
1668
|
-
console.log(
|
|
1669
|
-
`[Router] Intercept loader ${loader.$$id} revalidating on cache hit (stale=${stale})`
|
|
1670
|
-
);
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
loaderIds.push(loader.$$id);
|
|
1674
|
-
loaderPromises.push(
|
|
1675
|
-
wrapLoaderPromise(
|
|
1676
|
-
context.use(loader),
|
|
1677
|
-
parentEntry,
|
|
1678
|
-
segmentId,
|
|
1679
|
-
context.pathname
|
|
1680
|
-
)
|
|
1681
|
-
);
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
if (loaderPromises.length === 0) {
|
|
1685
|
-
return null;
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
// If intercept has loading skeleton, keep as Promise for streaming
|
|
1689
|
-
// Otherwise await immediately
|
|
1690
|
-
const loaderDataPromise =
|
|
1691
|
-
interceptEntry.loading !== undefined
|
|
1692
|
-
? Promise.all(loaderPromises)
|
|
1693
|
-
: await Promise.all(loaderPromises);
|
|
1694
|
-
|
|
1695
|
-
return { loaderDataPromise, loaderIds };
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
/**
|
|
1699
|
-
* Helper: Resolve parallel EntryData with its loaders and slot handlers
|
|
1700
|
-
* Parallels now have their own loaders, revalidate functions, and loading components
|
|
1701
|
-
*
|
|
1702
|
-
* @param parentShortCode - The shortCode of the parent layout/route that owns this parallel.
|
|
1703
|
-
* Used for segment IDs so the segment tree can correctly associate parallels with their parent.
|
|
1704
|
-
*/
|
|
1705
|
-
async function resolveParallelEntry(
|
|
1706
|
-
parallelEntry: EntryData,
|
|
1707
|
-
params: Record<string, string>,
|
|
1708
|
-
context: HandlerContext<any, TEnv>,
|
|
1709
|
-
belongsToRoute: boolean,
|
|
1710
|
-
parentShortCode: string
|
|
1711
|
-
): Promise<ResolvedSegment[]> {
|
|
1712
|
-
invariant(
|
|
1713
|
-
parallelEntry.type === "parallel",
|
|
1714
|
-
`Expected parallel entry, got: ${parallelEntry.type}`
|
|
1715
|
-
);
|
|
1716
|
-
|
|
1717
|
-
const segments: ResolvedSegment[] = [];
|
|
1718
|
-
|
|
1719
|
-
// Step 1: Execute each slot handler first (they trigger loaders via ctx.use())
|
|
1720
|
-
// Handlers are NOT awaited if loading is defined - this keeps Promises pending for Suspense
|
|
1721
|
-
const slots = parallelEntry.handler as Record<
|
|
1722
|
-
`@${string}`,
|
|
1723
|
-
| ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
1724
|
-
| ReactNode
|
|
1725
|
-
>;
|
|
1726
|
-
|
|
1727
|
-
for (const [slot, handler] of Object.entries(slots)) {
|
|
1728
|
-
// If loading is defined, don't await the handler (stream with Suspense)
|
|
1729
|
-
// Don't track parallel handlers - they shouldn't block handle data
|
|
1730
|
-
let component: ReactNode | Promise<ReactNode>;
|
|
1731
|
-
if (parallelEntry.loading) {
|
|
1732
|
-
const result =
|
|
1733
|
-
typeof handler === "function" ? handler(context) : handler;
|
|
1734
|
-
component = result;
|
|
1735
|
-
} else {
|
|
1736
|
-
component =
|
|
1737
|
-
typeof handler === "function" ? await handler(context) : handler;
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
// Use parent's shortCode so segment tree correctly associates this parallel with its parent
|
|
1741
|
-
segments.push({
|
|
1742
|
-
id: `${parentShortCode}.${slot}`,
|
|
1743
|
-
namespace: parallelEntry.id,
|
|
1744
|
-
type: "parallel",
|
|
1745
|
-
index: 0,
|
|
1746
|
-
component,
|
|
1747
|
-
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
1748
|
-
params,
|
|
1749
|
-
slot,
|
|
1750
|
-
belongsToRoute,
|
|
1751
|
-
parallelName: `${parallelEntry.id}.${slot}`,
|
|
1752
|
-
});
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
// Step 2: Resolve loaders AFTER handlers have run
|
|
1756
|
-
// If loading is defined, do NOT await loaders - this keeps handler Promises pending for Suspense
|
|
1757
|
-
// Loader data flows through component props (via ctx.use() in handler)
|
|
1758
|
-
// If no loading, await loaders to create segments for useLoader() support
|
|
1759
|
-
if (!parallelEntry.loading) {
|
|
1760
|
-
const loaderSegments = await resolveLoaders(
|
|
1761
|
-
parallelEntry,
|
|
1762
|
-
context,
|
|
1763
|
-
belongsToRoute,
|
|
1764
|
-
parentShortCode
|
|
1765
|
-
);
|
|
1766
|
-
segments.push(...loaderSegments);
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
return segments;
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
/**
|
|
1773
|
-
* Wrapper that adds error boundary handling to segment resolution
|
|
1774
|
-
* Catches errors during execution and returns error segments if an error boundary exists
|
|
1775
|
-
*
|
|
1776
|
-
* @param entry - The entry to resolve
|
|
1777
|
-
* @param routeKey - Route key for context
|
|
1778
|
-
* @param params - URL parameters
|
|
1779
|
-
* @param context - Handler context
|
|
1780
|
-
* @param loaderPromises - Shared loader promise map
|
|
1781
|
-
* @param resolveFn - The actual resolution function to call
|
|
1782
|
-
* @param errorContext - Additional context for onError callback
|
|
1783
|
-
* @returns Segments from successful resolution, or an error segment if error boundary caught
|
|
1784
|
-
* @throws If error occurs and no error boundary is defined
|
|
1785
|
-
*/
|
|
1786
|
-
async function resolveWithErrorHandling(
|
|
1787
|
-
entry: EntryData,
|
|
1788
|
-
routeKey: string,
|
|
1789
|
-
params: Record<string, string>,
|
|
1790
|
-
context: HandlerContext<any, TEnv>,
|
|
1791
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
1792
|
-
resolveFn: () => Promise<ResolvedSegment[]>,
|
|
1793
|
-
errorContext?: {
|
|
1794
|
-
env?: TEnv;
|
|
1795
|
-
isPartial?: boolean;
|
|
1796
|
-
requestStartTime?: number;
|
|
1797
|
-
}
|
|
1798
|
-
): Promise<ResolvedSegment[]> {
|
|
1799
|
-
try {
|
|
1800
|
-
return await resolveFn();
|
|
1801
|
-
} catch (error) {
|
|
1802
|
-
// Don't catch Response objects (middleware short-circuit)
|
|
1803
|
-
if (error instanceof Response) {
|
|
1804
|
-
throw error;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// Handle DataNotFoundError separately - look for notFoundBoundary first
|
|
1808
|
-
if (error instanceof DataNotFoundError) {
|
|
1809
|
-
const notFoundFallback = findNearestNotFoundBoundary(entry);
|
|
1810
|
-
|
|
1811
|
-
if (notFoundFallback) {
|
|
1812
|
-
// Create notFound info
|
|
1813
|
-
const notFoundInfo = createNotFoundInfo(
|
|
1814
|
-
error,
|
|
1815
|
-
entry.shortCode,
|
|
1816
|
-
entry.type,
|
|
1817
|
-
context.pathname
|
|
1818
|
-
);
|
|
1819
|
-
|
|
1820
|
-
// Invoke onError with notFound context
|
|
1821
|
-
callOnError(error, "handler", {
|
|
1822
|
-
request: context.request,
|
|
1823
|
-
url: context.url,
|
|
1824
|
-
routeKey,
|
|
1825
|
-
params,
|
|
1826
|
-
segmentId: entry.shortCode,
|
|
1827
|
-
segmentType: entry.type as any,
|
|
1828
|
-
env: errorContext?.env,
|
|
1829
|
-
isPartial: errorContext?.isPartial,
|
|
1830
|
-
handledByBoundary: true,
|
|
1831
|
-
metadata: { notFound: true, message: notFoundInfo.message },
|
|
1832
|
-
requestStartTime: errorContext?.requestStartTime,
|
|
1833
|
-
});
|
|
1834
|
-
|
|
1835
|
-
console.log(
|
|
1836
|
-
`[Router] NotFound caught by notFoundBoundary in ${entry.shortCode}:`,
|
|
1837
|
-
notFoundInfo.message
|
|
1838
|
-
);
|
|
1839
|
-
|
|
1840
|
-
// Set response status to 404 for notFound
|
|
1841
|
-
const reqCtx = getRequestContext();
|
|
1842
|
-
if (reqCtx) {
|
|
1843
|
-
reqCtx.res = new Response(null, { status: 404, headers: reqCtx.res.headers });
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
// Create and return notFound segment
|
|
1847
|
-
const notFoundSegment = createNotFoundSegment(
|
|
1848
|
-
notFoundInfo,
|
|
1849
|
-
notFoundFallback,
|
|
1850
|
-
entry,
|
|
1851
|
-
params
|
|
1852
|
-
);
|
|
1853
|
-
return [notFoundSegment];
|
|
1854
|
-
}
|
|
1855
|
-
// If no notFoundBoundary, fall through to error boundary handling
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
// Find nearest error boundary
|
|
1859
|
-
const fallback = findNearestErrorBoundary(entry);
|
|
1860
|
-
|
|
1861
|
-
// Determine segment type for error info
|
|
1862
|
-
const segmentType: ErrorInfo["segmentType"] = entry.type;
|
|
1863
|
-
|
|
1864
|
-
// Create error info
|
|
1865
|
-
const errorInfo = createErrorInfo(error, entry.shortCode, segmentType);
|
|
1866
|
-
|
|
1867
|
-
// Use default fallback if no error boundary found
|
|
1868
|
-
const effectiveFallback = fallback ?? DefaultErrorFallback;
|
|
1869
|
-
|
|
1870
|
-
// Invoke onError callback
|
|
1871
|
-
callOnError(error, "handler", {
|
|
1872
|
-
request: context.request,
|
|
1873
|
-
url: context.url,
|
|
1874
|
-
routeKey,
|
|
1875
|
-
params,
|
|
1876
|
-
segmentId: entry.shortCode,
|
|
1877
|
-
segmentType: entry.type as any,
|
|
1878
|
-
env: errorContext?.env,
|
|
1879
|
-
isPartial: errorContext?.isPartial,
|
|
1880
|
-
handledByBoundary: !!fallback,
|
|
1881
|
-
requestStartTime: errorContext?.requestStartTime,
|
|
1882
|
-
});
|
|
1883
|
-
|
|
1884
|
-
console.log(
|
|
1885
|
-
`[Router] Error caught by ${fallback ? "error boundary" : "default fallback"} in ${entry.shortCode}:`,
|
|
1886
|
-
errorInfo.message
|
|
1887
|
-
);
|
|
1888
|
-
|
|
1889
|
-
// Set response status to 500 for error
|
|
1890
|
-
{
|
|
1891
|
-
const reqCtx = getRequestContext();
|
|
1892
|
-
if (reqCtx) {
|
|
1893
|
-
reqCtx.res = new Response(null, { status: 500, headers: reqCtx.res.headers });
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
// Create and return error segment
|
|
1898
|
-
const errorSegment = createErrorSegment(
|
|
1899
|
-
errorInfo,
|
|
1900
|
-
effectiveFallback,
|
|
1901
|
-
entry,
|
|
1902
|
-
params
|
|
1903
|
-
);
|
|
1904
|
-
return [errorSegment];
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
/**
|
|
1909
|
-
* Resolve all segments for a route (used for single-cache-per-request pattern)
|
|
1910
|
-
* Loops through all entries and resolves them with error handling
|
|
1911
|
-
*/
|
|
1912
|
-
async function resolveAllSegments(
|
|
1913
|
-
entries: EntryData[],
|
|
1914
|
-
routeKey: string,
|
|
1915
|
-
params: Record<string, string>,
|
|
1916
|
-
context: HandlerContext<any, TEnv>,
|
|
1917
|
-
loaderPromises: Map<string, Promise<any>>
|
|
1918
|
-
): Promise<ResolvedSegment[]> {
|
|
1919
|
-
const allSegments: ResolvedSegment[] = [];
|
|
1920
|
-
|
|
1921
|
-
for (const entry of entries) {
|
|
1922
|
-
const resolvedSegments = await resolveWithErrorHandling(
|
|
1923
|
-
entry,
|
|
1924
|
-
routeKey,
|
|
1925
|
-
params,
|
|
1926
|
-
context,
|
|
1927
|
-
loaderPromises,
|
|
1928
|
-
() => resolveSegment(entry, routeKey, params, context, loaderPromises)
|
|
1929
|
-
);
|
|
1930
|
-
allSegments.push(...resolvedSegments);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
return allSegments;
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
/**
|
|
1937
|
-
* Resolve only loader segments for all entries (used when serving cached non-loader segments)
|
|
1938
|
-
* Loaders are always fresh by default, so we resolve them even on cache hit
|
|
1939
|
-
*/
|
|
1940
|
-
async function resolveLoadersOnly(
|
|
1941
|
-
entries: EntryData[],
|
|
1942
|
-
context: HandlerContext<any, TEnv>
|
|
1943
|
-
): Promise<ResolvedSegment[]> {
|
|
1944
|
-
const loaderSegments: ResolvedSegment[] = [];
|
|
1945
|
-
|
|
1946
|
-
for (const entry of entries) {
|
|
1947
|
-
const belongsToRoute = entry.type === "route";
|
|
1948
|
-
const segments = await resolveLoaders(entry, context, belongsToRoute);
|
|
1949
|
-
loaderSegments.push(...segments);
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
return loaderSegments;
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
/**
|
|
1956
|
-
* Resolve only loader segments for all entries with revalidation logic (for matchPartial cache hit)
|
|
1957
|
-
* Loaders are always fresh by default, so we resolve them even on cache hit
|
|
1958
|
-
*/
|
|
1959
|
-
async function resolveLoadersOnlyWithRevalidation(
|
|
1960
|
-
entries: EntryData[],
|
|
1961
|
-
context: HandlerContext<any, TEnv>,
|
|
1962
|
-
clientSegmentIds: Set<string>,
|
|
1963
|
-
prevParams: Record<string, string>,
|
|
1964
|
-
request: Request,
|
|
1965
|
-
prevUrl: URL,
|
|
1966
|
-
nextUrl: URL,
|
|
1967
|
-
routeKey: string,
|
|
1968
|
-
actionContext?: ActionContext
|
|
1969
|
-
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
1970
|
-
const allLoaderSegments: ResolvedSegment[] = [];
|
|
1971
|
-
const allMatchedIds: string[] = [];
|
|
1972
|
-
|
|
1973
|
-
for (const entry of entries) {
|
|
1974
|
-
const belongsToRoute = entry.type === "route";
|
|
1975
|
-
const { segments, matchedIds } = await resolveLoadersWithRevalidation(
|
|
1976
|
-
entry,
|
|
1977
|
-
context,
|
|
1978
|
-
belongsToRoute,
|
|
1979
|
-
clientSegmentIds,
|
|
1980
|
-
prevParams,
|
|
1981
|
-
request,
|
|
1982
|
-
prevUrl,
|
|
1983
|
-
nextUrl,
|
|
1984
|
-
routeKey,
|
|
1985
|
-
actionContext
|
|
1986
|
-
);
|
|
1987
|
-
allLoaderSegments.push(...segments);
|
|
1988
|
-
allMatchedIds.push(...matchedIds);
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
return { segments: allLoaderSegments, matchedIds: allMatchedIds };
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
/**
|
|
1995
|
-
* Build a map of segment shortCode → entry with revalidate functions
|
|
1996
|
-
* Used to look up revalidation rules for cached segments
|
|
1997
|
-
*/
|
|
1998
|
-
function buildEntryRevalidateMap(
|
|
1999
|
-
entries: EntryData[]
|
|
2000
|
-
): Map<string, { entry: EntryData; revalidate: ShouldRevalidateFn<any, any>[] }> {
|
|
2001
|
-
const map = new Map<string, { entry: EntryData; revalidate: ShouldRevalidateFn<any, any>[] }>();
|
|
2002
|
-
|
|
2003
|
-
function processEntry(entry: EntryData, parentShortCode?: string) {
|
|
2004
|
-
// Map main entry
|
|
2005
|
-
map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
|
|
2006
|
-
|
|
2007
|
-
// Process nested parallels - they use parallelEntry.shortCode.slotName as ID
|
|
2008
|
-
if (entry.type !== "parallel") {
|
|
2009
|
-
for (const parallelEntry of entry.parallel) {
|
|
2010
|
-
if (parallelEntry.type === "parallel") {
|
|
2011
|
-
// Parallel handlers are Record<slotName, handler>
|
|
2012
|
-
const slots = Object.keys(parallelEntry.handler) as `@${string}`[];
|
|
2013
|
-
for (const slot of slots) {
|
|
2014
|
-
// Segment ID uses parallelEntry.shortCode, not parent entry.shortCode
|
|
2015
|
-
const parallelId = `${parallelEntry.shortCode}.${slot}`;
|
|
2016
|
-
map.set(parallelId, { entry: parallelEntry, revalidate: parallelEntry.revalidate });
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
// Recursively process nested layouts
|
|
2023
|
-
for (const layoutEntry of entry.layout) {
|
|
2024
|
-
processEntry(layoutEntry);
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
for (const entry of entries) {
|
|
2029
|
-
processEntry(entry);
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
return map;
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
/**
|
|
2036
|
-
* Resolve all segments for a route with revalidation logic (for matchPartial)
|
|
2037
|
-
* Used for single-cache-per-request pattern in partial/navigation requests
|
|
2038
|
-
*/
|
|
2039
|
-
async function resolveAllSegmentsWithRevalidation(
|
|
2040
|
-
entries: EntryData[],
|
|
2041
|
-
routeKey: string,
|
|
2042
|
-
params: Record<string, string>,
|
|
2043
|
-
context: HandlerContext<any, TEnv>,
|
|
2044
|
-
clientSegmentSet: Set<string>,
|
|
2045
|
-
prevParams: Record<string, string>,
|
|
2046
|
-
request: Request,
|
|
2047
|
-
prevUrl: URL,
|
|
2048
|
-
nextUrl: URL,
|
|
2049
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
2050
|
-
actionContext: ActionContext | undefined,
|
|
2051
|
-
interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
|
|
2052
|
-
localRouteName: string,
|
|
2053
|
-
pathname: string
|
|
2054
|
-
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
2055
|
-
const allSegments: ResolvedSegment[] = [];
|
|
2056
|
-
const matchedIds: string[] = [];
|
|
2057
|
-
|
|
2058
|
-
for (const entry of entries) {
|
|
2059
|
-
// When intercepting, skip route entries - intercept replaces route handler
|
|
2060
|
-
if (entry.type === "route" && interceptResult) {
|
|
2061
|
-
console.log(
|
|
2062
|
-
`[Router.matchPartial] Intercepting "${localRouteName}" - skipping route handler`
|
|
2063
|
-
);
|
|
2064
|
-
matchedIds.push(entry.shortCode);
|
|
2065
|
-
continue;
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
// Resolve entry with revalidation logic
|
|
2069
|
-
const nonParallelEntry = entry as Exclude<
|
|
2070
|
-
EntryData,
|
|
2071
|
-
{ type: "parallel" }
|
|
2072
|
-
>;
|
|
2073
|
-
const resolved = await resolveWithRevalidationErrorHandling(
|
|
2074
|
-
nonParallelEntry,
|
|
2075
|
-
params,
|
|
2076
|
-
() =>
|
|
2077
|
-
resolveSegmentWithRevalidation(
|
|
2078
|
-
nonParallelEntry,
|
|
2079
|
-
routeKey,
|
|
2080
|
-
params,
|
|
2081
|
-
context,
|
|
2082
|
-
clientSegmentSet,
|
|
2083
|
-
prevParams,
|
|
2084
|
-
request,
|
|
2085
|
-
prevUrl,
|
|
2086
|
-
nextUrl,
|
|
2087
|
-
loaderPromises,
|
|
2088
|
-
actionContext,
|
|
2089
|
-
false // stale = false for fresh resolution
|
|
2090
|
-
),
|
|
2091
|
-
pathname
|
|
2092
|
-
);
|
|
2093
|
-
|
|
2094
|
-
allSegments.push(...resolved.segments);
|
|
2095
|
-
matchedIds.push(...resolved.matchedIds);
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
return { segments: allSegments, matchedIds };
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
/**
|
|
2102
|
-
* Wrapper for segment resolution with revalidation that adds error boundary handling
|
|
2103
|
-
* Similar to resolveWithErrorHandling but returns SegmentRevalidationResult
|
|
2104
|
-
*/
|
|
2105
|
-
async function resolveWithRevalidationErrorHandling(
|
|
2106
|
-
entry: EntryData,
|
|
2107
|
-
params: Record<string, string>,
|
|
2108
|
-
resolveFn: () => Promise<SegmentRevalidationResult>,
|
|
2109
|
-
pathname?: string,
|
|
2110
|
-
errorContext?: {
|
|
2111
|
-
request: Request;
|
|
2112
|
-
url: URL;
|
|
2113
|
-
routeKey?: string;
|
|
2114
|
-
env?: TEnv;
|
|
2115
|
-
isPartial?: boolean;
|
|
2116
|
-
requestStartTime?: number;
|
|
2117
|
-
}
|
|
2118
|
-
): Promise<SegmentRevalidationResult> {
|
|
2119
|
-
try {
|
|
2120
|
-
return await resolveFn();
|
|
2121
|
-
} catch (error) {
|
|
2122
|
-
// Don't catch Response objects (middleware short-circuit)
|
|
2123
|
-
if (error instanceof Response) {
|
|
2124
|
-
throw error;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// Handle DataNotFoundError separately - look for notFoundBoundary first
|
|
2128
|
-
if (error instanceof DataNotFoundError) {
|
|
2129
|
-
const notFoundFallback = findNearestNotFoundBoundary(entry);
|
|
2130
|
-
|
|
2131
|
-
if (notFoundFallback) {
|
|
2132
|
-
// Create notFound info
|
|
2133
|
-
const notFoundInfo = createNotFoundInfo(
|
|
2134
|
-
error,
|
|
2135
|
-
entry.shortCode,
|
|
2136
|
-
entry.type,
|
|
2137
|
-
pathname
|
|
2138
|
-
);
|
|
2139
|
-
|
|
2140
|
-
// Invoke onError with notFound context
|
|
2141
|
-
if (errorContext) {
|
|
2142
|
-
callOnError(error, "handler", {
|
|
2143
|
-
request: errorContext.request,
|
|
2144
|
-
url: errorContext.url,
|
|
2145
|
-
routeKey: errorContext.routeKey,
|
|
2146
|
-
params,
|
|
2147
|
-
segmentId: entry.shortCode,
|
|
2148
|
-
segmentType: entry.type as any,
|
|
2149
|
-
env: errorContext.env,
|
|
2150
|
-
isPartial: errorContext.isPartial,
|
|
2151
|
-
handledByBoundary: true,
|
|
2152
|
-
metadata: { notFound: true, message: notFoundInfo.message },
|
|
2153
|
-
requestStartTime: errorContext.requestStartTime,
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
console.log(
|
|
2158
|
-
`[Router] NotFound caught by notFoundBoundary in ${entry.shortCode}:`,
|
|
2159
|
-
notFoundInfo.message
|
|
2160
|
-
);
|
|
2161
|
-
|
|
2162
|
-
// Set response status to 404 for notFound
|
|
2163
|
-
const reqCtx = getRequestContext();
|
|
2164
|
-
if (reqCtx) {
|
|
2165
|
-
reqCtx.res = new Response(null, { status: 404, headers: reqCtx.res.headers });
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
// Create notFound segment
|
|
2169
|
-
const notFoundSegment = createNotFoundSegment(
|
|
2170
|
-
notFoundInfo,
|
|
2171
|
-
notFoundFallback,
|
|
2172
|
-
entry,
|
|
2173
|
-
params
|
|
2174
|
-
);
|
|
2175
|
-
|
|
2176
|
-
// Return with the notFound segment and its ID as matched
|
|
2177
|
-
return {
|
|
2178
|
-
segments: [notFoundSegment],
|
|
2179
|
-
matchedIds: [notFoundSegment.id],
|
|
2180
|
-
};
|
|
2181
|
-
}
|
|
2182
|
-
// If no notFoundBoundary, fall through to error boundary handling
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
// Find nearest error boundary
|
|
2186
|
-
const fallback = findNearestErrorBoundary(entry);
|
|
2187
|
-
|
|
2188
|
-
// Determine segment type for error info
|
|
2189
|
-
const segmentType: ErrorInfo["segmentType"] = entry.type;
|
|
2190
|
-
|
|
2191
|
-
// Create error info
|
|
2192
|
-
const errorInfo = createErrorInfo(error, entry.shortCode, segmentType);
|
|
2193
|
-
|
|
2194
|
-
// Use default fallback if no error boundary found
|
|
2195
|
-
const effectiveFallback = fallback ?? DefaultErrorFallback;
|
|
2196
|
-
|
|
2197
|
-
// Invoke onError callback
|
|
2198
|
-
if (errorContext) {
|
|
2199
|
-
callOnError(error, "handler", {
|
|
2200
|
-
request: errorContext.request,
|
|
2201
|
-
url: errorContext.url,
|
|
2202
|
-
routeKey: errorContext.routeKey,
|
|
2203
|
-
params,
|
|
2204
|
-
segmentId: entry.shortCode,
|
|
2205
|
-
segmentType: entry.type as any,
|
|
2206
|
-
env: errorContext.env,
|
|
2207
|
-
isPartial: errorContext.isPartial,
|
|
2208
|
-
handledByBoundary: !!fallback,
|
|
2209
|
-
requestStartTime: errorContext.requestStartTime,
|
|
2210
|
-
});
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
console.log(
|
|
2214
|
-
`[Router] Error caught by ${fallback ? "error boundary" : "default fallback"} in ${entry.shortCode}:`,
|
|
2215
|
-
errorInfo.message
|
|
2216
|
-
);
|
|
2217
|
-
|
|
2218
|
-
// Set response status to 500 for error
|
|
2219
|
-
{
|
|
2220
|
-
const reqCtx = getRequestContext();
|
|
2221
|
-
if (reqCtx) {
|
|
2222
|
-
reqCtx.res = new Response(null, { status: 500, headers: reqCtx.res.headers });
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
// Create error segment
|
|
2227
|
-
const errorSegment = createErrorSegment(
|
|
2228
|
-
errorInfo,
|
|
2229
|
-
effectiveFallback,
|
|
2230
|
-
entry,
|
|
2231
|
-
params
|
|
2232
|
-
);
|
|
2233
|
-
|
|
2234
|
-
// Return with the error segment and its ID as matched
|
|
2235
|
-
return {
|
|
2236
|
-
segments: [errorSegment],
|
|
2237
|
-
matchedIds: [errorSegment.id],
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
/**
|
|
2243
|
-
* Result of resolving segments with revalidation
|
|
2244
|
-
* Contains both segments to render and all matched segment IDs
|
|
2245
|
-
*/
|
|
2246
|
-
interface SegmentRevalidationResult {
|
|
2247
|
-
segments: ResolvedSegment[];
|
|
2248
|
-
matchedIds: string[];
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
/**
|
|
2252
|
-
* Action context type for revalidation
|
|
2253
|
-
*/
|
|
2254
|
-
type ActionContext = {
|
|
2255
|
-
actionId?: string;
|
|
2256
|
-
actionUrl?: URL;
|
|
2257
|
-
actionResult?: any;
|
|
2258
|
-
formData?: FormData;
|
|
2259
|
-
};
|
|
2260
|
-
|
|
2261
|
-
/**
|
|
2262
|
-
* Helper: Resolve parallel segments with revalidation
|
|
2263
|
-
* Parallels now have their own loaders, revalidate functions, and loading components
|
|
2264
|
-
*/
|
|
2265
|
-
async function resolveParallelSegmentsWithRevalidation(
|
|
2266
|
-
entry: EntryData,
|
|
2267
|
-
params: Record<string, string>,
|
|
2268
|
-
context: HandlerContext<any, TEnv>,
|
|
2269
|
-
belongsToRoute: boolean,
|
|
2270
|
-
clientSegmentIds: Set<string>,
|
|
2271
|
-
prevParams: Record<string, string>,
|
|
2272
|
-
request: Request,
|
|
2273
|
-
prevUrl: URL,
|
|
2274
|
-
nextUrl: URL,
|
|
2275
|
-
routeKey: string,
|
|
2276
|
-
actionContext?: ActionContext,
|
|
2277
|
-
stale?: boolean
|
|
2278
|
-
): Promise<SegmentRevalidationResult> {
|
|
2279
|
-
const segments: ResolvedSegment[] = [];
|
|
2280
|
-
const matchedIds: string[] = [];
|
|
2281
|
-
|
|
2282
|
-
for (const parallelEntry of entry.parallel) {
|
|
2283
|
-
invariant(
|
|
2284
|
-
parallelEntry.type === "parallel",
|
|
2285
|
-
`Expected parallel entry, got: ${parallelEntry.type}`
|
|
2286
|
-
);
|
|
2287
|
-
|
|
2288
|
-
// Step 1: Process each slot handler FIRST (they trigger loaders via ctx.use())
|
|
2289
|
-
const slots = parallelEntry.handler as Record<
|
|
2290
|
-
`@${string}`,
|
|
2291
|
-
| ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
2292
|
-
| ReactNode
|
|
2293
|
-
>;
|
|
2294
|
-
|
|
2295
|
-
for (const [slot, handler] of Object.entries(slots)) {
|
|
2296
|
-
// Use parent entry's shortCode so segment tree correctly associates parallel with parent
|
|
2297
|
-
const parallelId = `${entry.shortCode}.${slot}`;
|
|
2298
|
-
|
|
2299
|
-
// Include in matchedIds if:
|
|
2300
|
-
// - Client sent empty segments (HMR/full refetch), OR
|
|
2301
|
-
// - Client already has this parallel segment, OR
|
|
2302
|
-
// - This is a route-scoped parallel (belongsToRoute=true) that should appear
|
|
2303
|
-
// Intercepts (like @modal) are handled separately via resolveInterceptEntry.
|
|
2304
|
-
const isFullRefetch = clientSegmentIds.size === 0;
|
|
2305
|
-
if (
|
|
2306
|
-
isFullRefetch ||
|
|
2307
|
-
clientSegmentIds.has(parallelId) ||
|
|
2308
|
-
belongsToRoute
|
|
2309
|
-
) {
|
|
2310
|
-
matchedIds.push(parallelId);
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
const component = await revalidate(
|
|
2314
|
-
async () => {
|
|
2315
|
-
// If client sent empty segments (HMR/full refetch), always render
|
|
2316
|
-
if (isFullRefetch) return true;
|
|
2317
|
-
|
|
2318
|
-
// If client doesn't have this parallel:
|
|
2319
|
-
// - Route-scoped parallels (belongsToRoute=true): render them when navigating to the route
|
|
2320
|
-
// - Parent chain parallels (belongsToRoute=false): don't suddenly appear
|
|
2321
|
-
// Intercepts are handled separately via resolveInterceptEntry.
|
|
2322
|
-
if (!clientSegmentIds.has(parallelId)) return belongsToRoute;
|
|
2323
|
-
|
|
2324
|
-
const dummySegment: ResolvedSegment = {
|
|
2325
|
-
id: parallelId,
|
|
2326
|
-
namespace: parallelEntry.id,
|
|
2327
|
-
type: "parallel",
|
|
2328
|
-
index: 0,
|
|
2329
|
-
component: null as any,
|
|
2330
|
-
params,
|
|
2331
|
-
slot,
|
|
2332
|
-
belongsToRoute,
|
|
2333
|
-
parallelName: `${parallelEntry.id}.${slot}`,
|
|
2334
|
-
};
|
|
2335
|
-
|
|
2336
|
-
// Use parallel's own revalidate functions
|
|
2337
|
-
return await evaluateRevalidation({
|
|
2338
|
-
segment: dummySegment,
|
|
2339
|
-
prevParams,
|
|
2340
|
-
getPrevSegment: null,
|
|
2341
|
-
request,
|
|
2342
|
-
prevUrl,
|
|
2343
|
-
nextUrl,
|
|
2344
|
-
revalidations: parallelEntry.revalidate.map((fn, i) => ({
|
|
2345
|
-
name: `revalidate${i}`,
|
|
2346
|
-
fn,
|
|
2347
|
-
})),
|
|
2348
|
-
routeKey,
|
|
2349
|
-
context,
|
|
2350
|
-
actionContext,
|
|
2351
|
-
stale,
|
|
2352
|
-
});
|
|
2353
|
-
},
|
|
2354
|
-
async () => {
|
|
2355
|
-
// If loading is defined, don't await (stream with Suspense)
|
|
2356
|
-
// Don't track parallel handlers - they shouldn't block handle data
|
|
2357
|
-
if (parallelEntry.loading) {
|
|
2358
|
-
const result =
|
|
2359
|
-
typeof handler === "function" ? handler(context) : handler;
|
|
2360
|
-
return result;
|
|
2361
|
-
}
|
|
2362
|
-
return typeof handler === "function"
|
|
2363
|
-
? await handler(context)
|
|
2364
|
-
: handler;
|
|
2365
|
-
},
|
|
2366
|
-
() => null
|
|
2367
|
-
);
|
|
2368
|
-
|
|
2369
|
-
segments.push({
|
|
2370
|
-
id: parallelId,
|
|
2371
|
-
namespace: parallelEntry.id,
|
|
2372
|
-
type: "parallel",
|
|
2373
|
-
index: 0,
|
|
2374
|
-
component,
|
|
2375
|
-
loading:
|
|
2376
|
-
parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
2377
|
-
params,
|
|
2378
|
-
slot,
|
|
2379
|
-
belongsToRoute,
|
|
2380
|
-
parallelName: `${parallelEntry.id}.${slot}`,
|
|
2381
|
-
});
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
// Step 2: Resolve loaders AFTER handlers have run
|
|
2385
|
-
// If loading is defined, do NOT await loaders - keeps handler Promises pending for Suspense
|
|
2386
|
-
// Loader data flows through component props (via ctx.use() in handler)
|
|
2387
|
-
if (!parallelEntry.loading) {
|
|
2388
|
-
const loaderResult = await resolveLoadersWithRevalidation(
|
|
2389
|
-
parallelEntry,
|
|
2390
|
-
context,
|
|
2391
|
-
belongsToRoute,
|
|
2392
|
-
clientSegmentIds,
|
|
2393
|
-
prevParams,
|
|
2394
|
-
request,
|
|
2395
|
-
prevUrl,
|
|
2396
|
-
nextUrl,
|
|
2397
|
-
routeKey,
|
|
2398
|
-
actionContext,
|
|
2399
|
-
entry.shortCode, // Pass parent's shortCode for segment ID association
|
|
2400
|
-
stale
|
|
2401
|
-
);
|
|
2402
|
-
segments.push(...loaderResult.segments);
|
|
2403
|
-
matchedIds.push(...loaderResult.matchedIds);
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
return { segments, matchedIds };
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
/**
|
|
2411
|
-
* Helper: Resolve entry handler (layout, cache, or route) with revalidation
|
|
2412
|
-
* Extracted to reduce duplication between layout, cache, and route branches
|
|
2413
|
-
*/
|
|
2414
|
-
async function resolveEntryHandlerWithRevalidation(
|
|
2415
|
-
entry: Exclude<EntryData, { type: "parallel" }>,
|
|
2416
|
-
params: Record<string, string>,
|
|
2417
|
-
context: HandlerContext<any, TEnv>,
|
|
2418
|
-
belongsToRoute: boolean,
|
|
2419
|
-
clientSegmentIds: Set<string>,
|
|
2420
|
-
prevParams: Record<string, string>,
|
|
2421
|
-
request: Request,
|
|
2422
|
-
prevUrl: URL,
|
|
2423
|
-
nextUrl: URL,
|
|
2424
|
-
routeKey: string,
|
|
2425
|
-
actionContext?: ActionContext,
|
|
2426
|
-
stale?: boolean
|
|
2427
|
-
): Promise<{ segment: ResolvedSegment; matchedId: string }> {
|
|
2428
|
-
const matchedId = entry.shortCode;
|
|
2429
|
-
|
|
2430
|
-
const component = await revalidate(
|
|
2431
|
-
async () => {
|
|
2432
|
-
const hasSegment = clientSegmentIds.has(entry.shortCode);
|
|
2433
|
-
console.log(
|
|
2434
|
-
`[Router.resolveEntryHandler] ${entry.shortCode} (${entry.type}): client has=${hasSegment}, belongsToRoute=${belongsToRoute}`
|
|
2435
|
-
);
|
|
2436
|
-
if (!hasSegment) return true;
|
|
2437
|
-
|
|
2438
|
-
const dummySegment: ResolvedSegment = {
|
|
2439
|
-
id: entry.shortCode,
|
|
2440
|
-
namespace: entry.id,
|
|
2441
|
-
type:
|
|
2442
|
-
entry.type === "cache"
|
|
2443
|
-
? "layout"
|
|
2444
|
-
: (entry.type as "layout" | "route"),
|
|
2445
|
-
index: 0,
|
|
2446
|
-
component: null as any,
|
|
2447
|
-
params,
|
|
2448
|
-
belongsToRoute,
|
|
2449
|
-
...(entry.type === "layout" || entry.type === "cache"
|
|
2450
|
-
? { layoutName: entry.id }
|
|
2451
|
-
: {}),
|
|
2452
|
-
};
|
|
2453
|
-
|
|
2454
|
-
const shouldRevalidate = await evaluateRevalidation({
|
|
2455
|
-
segment: dummySegment,
|
|
2456
|
-
prevParams,
|
|
2457
|
-
getPrevSegment: null,
|
|
2458
|
-
request,
|
|
2459
|
-
prevUrl,
|
|
2460
|
-
nextUrl,
|
|
2461
|
-
revalidations: entry.revalidate.map((fn, i) => ({
|
|
2462
|
-
name: `revalidate${i}`,
|
|
2463
|
-
fn,
|
|
2464
|
-
})),
|
|
2465
|
-
routeKey,
|
|
2466
|
-
context,
|
|
2467
|
-
actionContext,
|
|
2468
|
-
stale,
|
|
2469
|
-
});
|
|
2470
|
-
console.log(
|
|
2471
|
-
`[Router.resolveEntryHandler] ${entry.shortCode}: evaluateRevalidation returned ${shouldRevalidate}`
|
|
2472
|
-
);
|
|
2473
|
-
return shouldRevalidate;
|
|
2474
|
-
},
|
|
2475
|
-
async () => {
|
|
2476
|
-
// Set current segment ID for handle data attribution
|
|
2477
|
-
context._currentSegmentId = entry.shortCode;
|
|
2478
|
-
if (entry.type === "layout" || entry.type === "cache") {
|
|
2479
|
-
return typeof entry.handler === "function"
|
|
2480
|
-
? await entry.handler(context)
|
|
2481
|
-
: entry.handler;
|
|
2482
|
-
}
|
|
2483
|
-
// entry.type === "route" - handler is always callable
|
|
2484
|
-
const routeEntry = entry as Extract<EntryData, { type: "route" }>;
|
|
2485
|
-
// For routes with loading: keep promise pending for navigation (not actions)
|
|
2486
|
-
// This allows client's use() to suspend and show loading skeleton
|
|
2487
|
-
if (!routeEntry.loading) {
|
|
2488
|
-
return await routeEntry.handler(context);
|
|
2489
|
-
}
|
|
2490
|
-
if (!actionContext) {
|
|
2491
|
-
// NOT awaited - keeps promise pending, but track for completion
|
|
2492
|
-
const result = routeEntry.handler(context);
|
|
2493
|
-
return {
|
|
2494
|
-
content: result instanceof Promise ? trackHandler(result) : result,
|
|
2495
|
-
};
|
|
2496
|
-
}
|
|
2497
|
-
console.log(
|
|
2498
|
-
`[Router] Resolving action route with awaited value: ${entry.id}`
|
|
2499
|
-
);
|
|
2500
|
-
// For actions: await handler and return value directly (not wrapped in Promise)
|
|
2501
|
-
// This ensures component instanceof Promise is false in segment-system,
|
|
2502
|
-
// avoiding RouteContentWrapper/Suspense and maintaining consistent tree structure
|
|
2503
|
-
return {
|
|
2504
|
-
content: Promise.resolve(await routeEntry.handler(context)),
|
|
2505
|
-
};
|
|
2506
|
-
},
|
|
2507
|
-
() => null
|
|
2508
|
-
);
|
|
2509
|
-
|
|
2510
|
-
// Extract component from wrapper object if needed (used to prevent promise auto-resolution)
|
|
2511
|
-
const resolvedComponent =
|
|
2512
|
-
component && typeof component === "object" && "content" in component
|
|
2513
|
-
? (component as { content: ReactNode }).content
|
|
2514
|
-
: component;
|
|
2515
|
-
|
|
2516
|
-
const segment: ResolvedSegment = {
|
|
2517
|
-
id: entry.shortCode,
|
|
2518
|
-
namespace: entry.id,
|
|
2519
|
-
type:
|
|
2520
|
-
entry.type === "cache" ? "layout" : (entry.type as "layout" | "route"),
|
|
2521
|
-
index: 0,
|
|
2522
|
-
component: resolvedComponent,
|
|
2523
|
-
loading: entry.loading === false ? null : entry.loading,
|
|
2524
|
-
params,
|
|
2525
|
-
belongsToRoute,
|
|
2526
|
-
...(entry.type === "layout" || entry.type === "cache"
|
|
2527
|
-
? { layoutName: entry.id }
|
|
2528
|
-
: {}),
|
|
2529
|
-
};
|
|
2530
|
-
|
|
2531
|
-
return { segment, matchedId };
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
/**
|
|
2535
|
-
* Resolve segments with revalidation awareness (for partial rendering)
|
|
2536
|
-
* Same as resolveSegment but conditionally executes handlers based on revalidation
|
|
2537
|
-
* Returns both segments to render AND all matched segment IDs (including skipped ones)
|
|
2538
|
-
* Cache entries are handled like layouts (they emit segments)
|
|
2539
|
-
* Parallel entries are handled separately via resolveParallelSegmentsWithRevalidation
|
|
2540
|
-
*/
|
|
2541
|
-
async function resolveSegmentWithRevalidation(
|
|
2542
|
-
entry: Exclude<EntryData, { type: "parallel" }>,
|
|
2543
|
-
routeKey: string,
|
|
2544
|
-
params: Record<string, string>,
|
|
2545
|
-
context: HandlerContext<any, TEnv>,
|
|
2546
|
-
clientSegmentIds: Set<string>,
|
|
2547
|
-
prevParams: Record<string, string>,
|
|
2548
|
-
request: Request,
|
|
2549
|
-
prevUrl: URL,
|
|
2550
|
-
nextUrl: URL,
|
|
2551
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
2552
|
-
actionContext?: ActionContext,
|
|
2553
|
-
stale?: boolean
|
|
2554
|
-
): Promise<SegmentRevalidationResult> {
|
|
2555
|
-
const segments: ResolvedSegment[] = [];
|
|
2556
|
-
const matchedIds: string[] = [];
|
|
2557
|
-
|
|
2558
|
-
const belongsToRoute = entry.type === "route";
|
|
2559
|
-
|
|
2560
|
-
// Note: Middleware is now collected and executed at the top level (coreRequestHandler)
|
|
2561
|
-
|
|
2562
|
-
// Step 1: Run loaders with revalidation
|
|
2563
|
-
const loaderResult = await resolveLoadersWithRevalidation(
|
|
2564
|
-
entry,
|
|
2565
|
-
context,
|
|
2566
|
-
belongsToRoute,
|
|
2567
|
-
clientSegmentIds,
|
|
2568
|
-
prevParams,
|
|
2569
|
-
request,
|
|
2570
|
-
prevUrl,
|
|
2571
|
-
nextUrl,
|
|
2572
|
-
routeKey,
|
|
2573
|
-
actionContext,
|
|
2574
|
-
undefined, // shortCodeOverride
|
|
2575
|
-
stale
|
|
2576
|
-
);
|
|
2577
|
-
segments.push(...loaderResult.segments);
|
|
2578
|
-
matchedIds.push(...loaderResult.matchedIds);
|
|
2579
|
-
|
|
2580
|
-
// Step 3: Process orphan layouts (for routes, these come before parallels)
|
|
2581
|
-
if (entry.type === "route") {
|
|
2582
|
-
for (const orphan of entry.layout) {
|
|
2583
|
-
const orphanResult = await resolveOrphanLayoutWithRevalidation(
|
|
2584
|
-
orphan,
|
|
2585
|
-
params,
|
|
2586
|
-
context,
|
|
2587
|
-
clientSegmentIds,
|
|
2588
|
-
prevParams,
|
|
2589
|
-
request,
|
|
2590
|
-
prevUrl,
|
|
2591
|
-
nextUrl,
|
|
2592
|
-
routeKey,
|
|
2593
|
-
loaderPromises,
|
|
2594
|
-
true, // Route's orphan layouts belong to the route
|
|
2595
|
-
actionContext,
|
|
2596
|
-
stale
|
|
2597
|
-
);
|
|
2598
|
-
segments.push(...orphanResult.segments);
|
|
2599
|
-
matchedIds.push(...orphanResult.matchedIds);
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
|
-
// Step 4: Process parallel segments
|
|
2604
|
-
const parallelResult = await resolveParallelSegmentsWithRevalidation(
|
|
2605
|
-
entry,
|
|
2606
|
-
params,
|
|
2607
|
-
context,
|
|
2608
|
-
belongsToRoute,
|
|
2609
|
-
clientSegmentIds,
|
|
2610
|
-
prevParams,
|
|
2611
|
-
request,
|
|
2612
|
-
prevUrl,
|
|
2613
|
-
nextUrl,
|
|
2614
|
-
routeKey,
|
|
2615
|
-
actionContext,
|
|
2616
|
-
stale
|
|
2617
|
-
);
|
|
2618
|
-
segments.push(...parallelResult.segments);
|
|
2619
|
-
matchedIds.push(...parallelResult.matchedIds);
|
|
2620
|
-
|
|
2621
|
-
// Step 5: Process orphan layouts (for layouts/cache, these come after parallels)
|
|
2622
|
-
if (entry.type === "layout" || entry.type === "cache") {
|
|
2623
|
-
for (const orphan of entry.layout) {
|
|
2624
|
-
const orphanResult = await resolveOrphanLayoutWithRevalidation(
|
|
2625
|
-
orphan,
|
|
2626
|
-
params,
|
|
2627
|
-
context,
|
|
2628
|
-
clientSegmentIds,
|
|
2629
|
-
prevParams,
|
|
2630
|
-
request,
|
|
2631
|
-
prevUrl,
|
|
2632
|
-
nextUrl,
|
|
2633
|
-
routeKey,
|
|
2634
|
-
loaderPromises,
|
|
2635
|
-
false, // Parent chain layouts don't belong to specific route
|
|
2636
|
-
actionContext,
|
|
2637
|
-
stale
|
|
2638
|
-
);
|
|
2639
|
-
segments.push(...orphanResult.segments);
|
|
2640
|
-
matchedIds.push(...orphanResult.matchedIds);
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
// Step 6: Execute main handler with revalidation
|
|
2645
|
-
const handlerResult = await resolveEntryHandlerWithRevalidation(
|
|
2646
|
-
entry,
|
|
2647
|
-
params,
|
|
2648
|
-
context,
|
|
2649
|
-
belongsToRoute,
|
|
2650
|
-
clientSegmentIds,
|
|
2651
|
-
prevParams,
|
|
2652
|
-
request,
|
|
2653
|
-
prevUrl,
|
|
2654
|
-
nextUrl,
|
|
2655
|
-
routeKey,
|
|
2656
|
-
actionContext,
|
|
2657
|
-
stale
|
|
2658
|
-
);
|
|
2659
|
-
segments.push(handlerResult.segment);
|
|
2660
|
-
matchedIds.push(handlerResult.matchedId);
|
|
2661
|
-
|
|
2662
|
-
return { segments, matchedIds };
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
/**
|
|
2666
|
-
* Helper: Resolve orphan layout with revalidation
|
|
2667
|
-
* Returns both segments to render AND all matched segment IDs (including skipped ones)
|
|
2668
|
-
*/
|
|
2669
|
-
async function resolveOrphanLayoutWithRevalidation(
|
|
2670
|
-
orphan: EntryData,
|
|
2671
|
-
params: Record<string, string>,
|
|
2672
|
-
context: HandlerContext<any, TEnv>,
|
|
2673
|
-
clientSegmentIds: Set<string>,
|
|
2674
|
-
prevParams: Record<string, string>,
|
|
2675
|
-
request: Request,
|
|
2676
|
-
prevUrl: URL,
|
|
2677
|
-
nextUrl: URL,
|
|
2678
|
-
routeKey: string,
|
|
2679
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
2680
|
-
belongsToRoute: boolean,
|
|
2681
|
-
actionContext?: {
|
|
2682
|
-
actionId?: string;
|
|
2683
|
-
actionUrl?: URL;
|
|
2684
|
-
actionResult?: any;
|
|
2685
|
-
formData?: FormData;
|
|
2686
|
-
},
|
|
2687
|
-
stale?: boolean
|
|
2688
|
-
): Promise<SegmentRevalidationResult> {
|
|
2689
|
-
invariant(
|
|
2690
|
-
orphan.type === "layout" || orphan.type === "cache",
|
|
2691
|
-
`Expected orphan to be a layout or cache, got: ${orphan.type}`
|
|
2692
|
-
);
|
|
2693
|
-
|
|
2694
|
-
const segments: ResolvedSegment[] = [];
|
|
2695
|
-
const matchedIds: string[] = [];
|
|
2696
|
-
|
|
2697
|
-
// Note: Orphan middleware is now collected and executed at the top level (coreRequestHandler)
|
|
2698
|
-
|
|
2699
|
-
// Step 1: Run orphan loaders with revalidation
|
|
2700
|
-
const loaderResult = await resolveLoadersWithRevalidation(
|
|
2701
|
-
orphan,
|
|
2702
|
-
context,
|
|
2703
|
-
belongsToRoute,
|
|
2704
|
-
clientSegmentIds,
|
|
2705
|
-
prevParams,
|
|
2706
|
-
request,
|
|
2707
|
-
prevUrl,
|
|
2708
|
-
nextUrl,
|
|
2709
|
-
routeKey,
|
|
2710
|
-
actionContext,
|
|
2711
|
-
undefined, // shortCodeOverride
|
|
2712
|
-
stale
|
|
2713
|
-
);
|
|
2714
|
-
segments.push(...loaderResult.segments);
|
|
2715
|
-
matchedIds.push(...loaderResult.matchedIds);
|
|
2716
|
-
|
|
2717
|
-
// Step 3: Process orphan parallel segments with revalidation
|
|
2718
|
-
// Parallels now have their own loaders, revalidate functions, and loading components
|
|
2719
|
-
for (const parallelEntry of orphan.parallel) {
|
|
2720
|
-
invariant(
|
|
2721
|
-
parallelEntry.type === "parallel",
|
|
2722
|
-
`Expected parallel entry, got: ${parallelEntry.type}`
|
|
2723
|
-
);
|
|
2724
|
-
|
|
2725
|
-
// Step 3a: Resolve parallel's loaders with revalidation
|
|
2726
|
-
const loaderResult = await resolveLoadersWithRevalidation(
|
|
2727
|
-
parallelEntry,
|
|
2728
|
-
context,
|
|
2729
|
-
belongsToRoute,
|
|
2730
|
-
clientSegmentIds,
|
|
2731
|
-
prevParams,
|
|
2732
|
-
request,
|
|
2733
|
-
prevUrl,
|
|
2734
|
-
nextUrl,
|
|
2735
|
-
routeKey,
|
|
2736
|
-
actionContext,
|
|
2737
|
-
undefined, // shortCodeOverride
|
|
2738
|
-
stale
|
|
2739
|
-
);
|
|
2740
|
-
segments.push(...loaderResult.segments);
|
|
2741
|
-
matchedIds.push(...loaderResult.matchedIds);
|
|
2742
|
-
|
|
2743
|
-
// Step 3b: Process each slot in the parallel handler
|
|
2744
|
-
const slots = parallelEntry.handler as Record<
|
|
2745
|
-
`@${string}`,
|
|
2746
|
-
| ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
2747
|
-
| ReactNode
|
|
2748
|
-
>;
|
|
2749
|
-
|
|
2750
|
-
for (const [slot, handler] of Object.entries(slots)) {
|
|
2751
|
-
const parallelId = `${parallelEntry.shortCode}.${slot}`;
|
|
2752
|
-
|
|
2753
|
-
// Always add to matchedIds
|
|
2754
|
-
matchedIds.push(parallelId);
|
|
2755
|
-
|
|
2756
|
-
const component = await revalidate(
|
|
2757
|
-
async () => {
|
|
2758
|
-
if (!clientSegmentIds.has(parallelId)) return true;
|
|
2759
|
-
|
|
2760
|
-
const dummySegment: ResolvedSegment = {
|
|
2761
|
-
id: parallelId,
|
|
2762
|
-
namespace: parallelEntry.id,
|
|
2763
|
-
type: "parallel",
|
|
2764
|
-
index: 0,
|
|
2765
|
-
component: null as any,
|
|
2766
|
-
params,
|
|
2767
|
-
slot,
|
|
2768
|
-
belongsToRoute,
|
|
2769
|
-
parallelName: `${parallelEntry.id}.${slot}`,
|
|
2770
|
-
};
|
|
2771
|
-
|
|
2772
|
-
// Use parallel's own revalidate functions
|
|
2773
|
-
return await evaluateRevalidation({
|
|
2774
|
-
segment: dummySegment,
|
|
2775
|
-
prevParams,
|
|
2776
|
-
getPrevSegment: null,
|
|
2777
|
-
request,
|
|
2778
|
-
prevUrl,
|
|
2779
|
-
nextUrl,
|
|
2780
|
-
revalidations: parallelEntry.revalidate.map((fn, i) => ({
|
|
2781
|
-
name: `revalidate${i}`,
|
|
2782
|
-
fn,
|
|
2783
|
-
})),
|
|
2784
|
-
routeKey,
|
|
2785
|
-
context,
|
|
2786
|
-
actionContext,
|
|
2787
|
-
stale,
|
|
2788
|
-
});
|
|
2789
|
-
},
|
|
2790
|
-
async () => {
|
|
2791
|
-
// If loading is defined, don't await (stream with Suspense)
|
|
2792
|
-
// Don't track parallel handlers - they shouldn't block handle data
|
|
2793
|
-
if (parallelEntry.loading) {
|
|
2794
|
-
const result =
|
|
2795
|
-
typeof handler === "function" ? handler(context) : handler;
|
|
2796
|
-
return result;
|
|
2797
|
-
}
|
|
2798
|
-
return typeof handler === "function"
|
|
2799
|
-
? await handler(context)
|
|
2800
|
-
: handler;
|
|
2801
|
-
},
|
|
2802
|
-
() => null
|
|
2803
|
-
);
|
|
2804
|
-
|
|
2805
|
-
segments.push({
|
|
2806
|
-
id: parallelId,
|
|
2807
|
-
namespace: parallelEntry.id,
|
|
2808
|
-
type: "parallel",
|
|
2809
|
-
index: 0,
|
|
2810
|
-
component,
|
|
2811
|
-
loading:
|
|
2812
|
-
parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
2813
|
-
params,
|
|
2814
|
-
slot,
|
|
2815
|
-
belongsToRoute,
|
|
2816
|
-
parallelName: `${parallelEntry.id}.${slot}`,
|
|
2817
|
-
});
|
|
2818
|
-
}
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
// Step 4: Execute orphan handler with revalidation
|
|
2822
|
-
// Always add orphan layout ID to matchedIds
|
|
2823
|
-
matchedIds.push(orphan.shortCode);
|
|
2824
|
-
|
|
2825
|
-
const component = await revalidate(
|
|
2826
|
-
async () => {
|
|
2827
|
-
if (!clientSegmentIds.has(orphan.shortCode)) return true;
|
|
2828
|
-
|
|
2829
|
-
const dummySegment: ResolvedSegment = {
|
|
2830
|
-
id: orphan.shortCode,
|
|
2831
|
-
namespace: orphan.id,
|
|
2832
|
-
type: "layout",
|
|
2833
|
-
index: 0,
|
|
2834
|
-
component: null as any,
|
|
2835
|
-
params,
|
|
2836
|
-
belongsToRoute,
|
|
2837
|
-
layoutName: orphan.id,
|
|
2838
|
-
};
|
|
2839
|
-
|
|
2840
|
-
return await evaluateRevalidation({
|
|
2841
|
-
segment: dummySegment,
|
|
2842
|
-
prevParams,
|
|
2843
|
-
getPrevSegment: null,
|
|
2844
|
-
request,
|
|
2845
|
-
prevUrl,
|
|
2846
|
-
nextUrl,
|
|
2847
|
-
revalidations: orphan.revalidate.map((fn, i) => ({
|
|
2848
|
-
name: `revalidate${i}`,
|
|
2849
|
-
fn,
|
|
2850
|
-
})),
|
|
2851
|
-
routeKey,
|
|
2852
|
-
context,
|
|
2853
|
-
actionContext,
|
|
2854
|
-
stale,
|
|
2855
|
-
});
|
|
2856
|
-
},
|
|
2857
|
-
async () =>
|
|
2858
|
-
typeof orphan.handler === "function"
|
|
2859
|
-
? await orphan.handler(context)
|
|
2860
|
-
: orphan.handler,
|
|
2861
|
-
() => null
|
|
2862
|
-
);
|
|
2863
|
-
|
|
2864
|
-
segments.push({
|
|
2865
|
-
id: orphan.shortCode,
|
|
2866
|
-
namespace: orphan.id,
|
|
2867
|
-
type: "layout",
|
|
2868
|
-
index: 0,
|
|
2869
|
-
component,
|
|
2870
|
-
params,
|
|
2871
|
-
belongsToRoute,
|
|
2872
|
-
layoutName: orphan.id,
|
|
2873
|
-
loading: orphan.loading === false ? null : orphan.loading,
|
|
2874
|
-
});
|
|
2875
|
-
|
|
2876
|
-
return { segments, matchedIds };
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
/**
|
|
2880
|
-
* Match request and return segments (document/SSR requests)
|
|
2881
|
-
*
|
|
2882
|
-
* Uses generator middleware pipeline for clean separation of concerns:
|
|
2883
|
-
* - cache-lookup: Check cache first
|
|
2884
|
-
* - segment-resolution: Resolve segments on cache miss
|
|
2885
|
-
* - cache-store: Store results in cache
|
|
2886
|
-
* - background-revalidation: SWR revalidation
|
|
2887
|
-
*/
|
|
2888
|
-
async function match(request: Request, env: TEnv): Promise<MatchResult> {
|
|
2889
|
-
// Build RouterContext with all closure functions needed by middleware
|
|
2890
|
-
const routerCtx: RouterContext<TEnv> = {
|
|
2891
|
-
findMatch,
|
|
2892
|
-
loadManifest,
|
|
2893
|
-
traverseBack,
|
|
2894
|
-
createHandlerContext,
|
|
2895
|
-
setupLoaderAccess,
|
|
2896
|
-
setupLoaderAccessSilent,
|
|
2897
|
-
getContext,
|
|
2898
|
-
getMetricsStore,
|
|
2899
|
-
createCacheScope,
|
|
2900
|
-
findInterceptForRoute,
|
|
2901
|
-
resolveAllSegmentsWithRevalidation,
|
|
2902
|
-
resolveInterceptEntry,
|
|
2903
|
-
evaluateRevalidation,
|
|
2904
|
-
getRequestContext,
|
|
2905
|
-
resolveAllSegments,
|
|
2906
|
-
createHandleStore,
|
|
2907
|
-
buildEntryRevalidateMap,
|
|
2908
|
-
resolveLoadersOnlyWithRevalidation,
|
|
2909
|
-
resolveInterceptLoadersOnly,
|
|
2910
|
-
resolveLoadersOnly,
|
|
2911
|
-
};
|
|
2912
|
-
|
|
2913
|
-
return runWithRouterContext(routerCtx, async () => {
|
|
2914
|
-
const result = await createMatchContextForFull(request, env);
|
|
2915
|
-
|
|
2916
|
-
// Handle redirect case
|
|
2917
|
-
if ("type" in result && result.type === "redirect") {
|
|
2918
|
-
return {
|
|
2919
|
-
segments: [],
|
|
2920
|
-
matched: [],
|
|
2921
|
-
diff: [],
|
|
2922
|
-
params: {},
|
|
2923
|
-
redirect: result.redirectUrl,
|
|
2924
|
-
};
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
const ctx = result as MatchContext<TEnv>;
|
|
2928
|
-
|
|
2929
|
-
try {
|
|
2930
|
-
const state = createPipelineState();
|
|
2931
|
-
const pipeline = createMatchPartialPipeline(ctx, state);
|
|
2932
|
-
return await collectMatchResult(pipeline, ctx, state);
|
|
2933
|
-
} catch (error) {
|
|
2934
|
-
if (error instanceof Response) throw error;
|
|
2935
|
-
// Report unhandled errors during full match pipeline
|
|
2936
|
-
callOnError(error, "routing", {
|
|
2937
|
-
request,
|
|
2938
|
-
url: ctx.url,
|
|
2939
|
-
env,
|
|
2940
|
-
isPartial: false,
|
|
2941
|
-
handledByBoundary: false,
|
|
2942
|
-
});
|
|
2943
|
-
throw sanitizeError(error);
|
|
2944
|
-
}
|
|
2945
|
-
});
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
/**
|
|
2949
|
-
* Match an error to the nearest error boundary and return error segments
|
|
2950
|
-
*
|
|
2951
|
-
* This method is used when an action or other operation fails and we need
|
|
2952
|
-
* to render the error boundary UI. It finds the nearest errorBoundary in
|
|
2953
|
-
* the route tree and renders it with the error info.
|
|
2954
|
-
*
|
|
2955
|
-
* The returned segments include all segments up to and including the error
|
|
2956
|
-
* boundary, with the error boundary's fallback rendered in place of its
|
|
2957
|
-
* normal outlet content.
|
|
2958
|
-
*/
|
|
2959
|
-
async function matchError(
|
|
2960
|
-
request: Request,
|
|
2961
|
-
_context: TEnv,
|
|
2962
|
-
error: unknown,
|
|
2963
|
-
segmentType: ErrorInfo["segmentType"] = "route"
|
|
2964
|
-
): Promise<MatchResult | null> {
|
|
2965
|
-
const url = new URL(request.url);
|
|
2966
|
-
const pathname = url.pathname;
|
|
2967
|
-
|
|
2968
|
-
console.log(`[Router.matchError] Matching error for ${pathname}`);
|
|
2969
|
-
|
|
2970
|
-
// Find the route match for the current URL
|
|
2971
|
-
const matched = findMatch(pathname);
|
|
2972
|
-
if (!matched) {
|
|
2973
|
-
console.warn(`[Router.matchError] No route matched for ${pathname}`);
|
|
2974
|
-
return null;
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
// Load manifest to get the entry chain
|
|
2978
|
-
const manifestEntry = await loadManifest(
|
|
2979
|
-
matched.entry,
|
|
2980
|
-
matched.routeKey,
|
|
2981
|
-
pathname,
|
|
2982
|
-
undefined, // No metrics for error matching
|
|
2983
|
-
false // Not SSR
|
|
2984
|
-
);
|
|
2985
|
-
|
|
2986
|
-
// Find the nearest error boundary in the entry chain
|
|
2987
|
-
// If none found, use a default "Internal Server Error" fallback
|
|
2988
|
-
const fallback = findNearestErrorBoundary(manifestEntry);
|
|
2989
|
-
const useDefaultFallback = !fallback;
|
|
2990
|
-
|
|
2991
|
-
// Create error info
|
|
2992
|
-
const errorInfo = createErrorInfo(
|
|
2993
|
-
error,
|
|
2994
|
-
manifestEntry.shortCode || "unknown",
|
|
2995
|
-
segmentType
|
|
2996
|
-
);
|
|
2997
|
-
|
|
2998
|
-
// Find which entry has the error boundary
|
|
2999
|
-
// Also checks orphan layouts (siblings) since they can have error boundaries too
|
|
3000
|
-
let entryWithBoundary: EntryData | null = null;
|
|
3001
|
-
let current: EntryData | null = manifestEntry;
|
|
3002
|
-
while (current) {
|
|
3003
|
-
// Check if this entry has an error boundary
|
|
3004
|
-
if (current.errorBoundary && current.errorBoundary.length > 0) {
|
|
3005
|
-
entryWithBoundary = current;
|
|
3006
|
-
break;
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
// Check orphan layouts/cache for error boundaries
|
|
3010
|
-
if (current.layout && current.layout.length > 0) {
|
|
3011
|
-
for (const orphan of current.layout) {
|
|
3012
|
-
if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
|
|
3013
|
-
entryWithBoundary = orphan;
|
|
3014
|
-
break;
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
if (entryWithBoundary) break;
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
current = current.parent;
|
|
3021
|
-
}
|
|
3022
|
-
|
|
3023
|
-
// Determine which entry has the error boundary and which entry should be replaced
|
|
3024
|
-
// The error content renders in the boundary's <Outlet />, not replacing the boundary itself
|
|
3025
|
-
let boundaryEntry: EntryData;
|
|
3026
|
-
let outletEntry: EntryData; // The entry that renders in boundaryEntry's outlet (gets replaced)
|
|
3027
|
-
|
|
3028
|
-
if (entryWithBoundary) {
|
|
3029
|
-
boundaryEntry = entryWithBoundary;
|
|
3030
|
-
|
|
3031
|
-
// Find the entry that renders in boundaryEntry's <Outlet />
|
|
3032
|
-
// Walk from manifestEntry toward boundaryEntry to find the direct outlet child
|
|
3033
|
-
outletEntry = manifestEntry;
|
|
3034
|
-
current = manifestEntry;
|
|
3035
|
-
|
|
3036
|
-
while (current) {
|
|
3037
|
-
// Case 1: current's direct parent is boundaryEntry
|
|
3038
|
-
if (current.parent === boundaryEntry) {
|
|
3039
|
-
outletEntry = current;
|
|
3040
|
-
break;
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
// Case 2: boundaryEntry is an orphan layout of current's parent
|
|
3044
|
-
// In this case, current renders in the orphan's outlet
|
|
3045
|
-
if (current.parent && current.parent.layout) {
|
|
3046
|
-
if (current.parent.layout.includes(boundaryEntry)) {
|
|
3047
|
-
outletEntry = current;
|
|
3048
|
-
break;
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
current = current.parent;
|
|
3053
|
-
}
|
|
3054
|
-
} else {
|
|
3055
|
-
// No user-defined error boundary - use root layout for the default fallback
|
|
3056
|
-
// Walk up to find the root entry (no parent)
|
|
3057
|
-
let rootEntry = manifestEntry;
|
|
3058
|
-
while (rootEntry.parent) {
|
|
3059
|
-
rootEntry = rootEntry.parent;
|
|
3060
|
-
}
|
|
3061
|
-
boundaryEntry = rootEntry;
|
|
3062
|
-
outletEntry = rootEntry; // For default, replace at root level
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
// Build the matched IDs list: all entries from root to the error boundary (inclusive)
|
|
3066
|
-
// These segments will be fetched from client cache (parent layouts + their loaders)
|
|
3067
|
-
const matchedIds: string[] = [];
|
|
3068
|
-
|
|
3069
|
-
// Walk from error boundary up to root and collect parent IDs
|
|
3070
|
-
current = boundaryEntry;
|
|
3071
|
-
const stack: {
|
|
3072
|
-
shortCode: string;
|
|
3073
|
-
loaderEntries: LoaderEntry[];
|
|
3074
|
-
}[] = [];
|
|
3075
|
-
while (current) {
|
|
3076
|
-
if (current.shortCode) {
|
|
3077
|
-
stack.push({
|
|
3078
|
-
shortCode: current.shortCode,
|
|
3079
|
-
loaderEntries: current.loader || [],
|
|
3080
|
-
});
|
|
3081
|
-
}
|
|
3082
|
-
current = current.parent;
|
|
3083
|
-
}
|
|
3084
|
-
// Reverse to get root-first order and build matchedIds including loaders
|
|
3085
|
-
for (const item of stack.reverse()) {
|
|
3086
|
-
matchedIds.push(item.shortCode);
|
|
3087
|
-
// Add loader segment IDs for this entry
|
|
3088
|
-
for (let i = 0; i < item.loaderEntries.length; i++) {
|
|
3089
|
-
const loaderId = item.loaderEntries[i].loader?.$$id || "unknown";
|
|
3090
|
-
matchedIds.push(`${item.shortCode}D${i}.${loaderId}`);
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
|
|
3094
|
-
// Set response status to 500 for error
|
|
3095
|
-
const reqCtx = getRequestContext();
|
|
3096
|
-
if (reqCtx) {
|
|
3097
|
-
reqCtx.res = new Response(null, { status: 500, headers: reqCtx.res.headers });
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
|
-
// Create the error segment using user's fallback or default
|
|
3101
|
-
// The error segment uses the outlet entry's ID so it replaces the outlet content
|
|
3102
|
-
// while keeping the boundary layout (and its UI) rendered
|
|
3103
|
-
const effectiveFallback = fallback || DefaultErrorFallback;
|
|
3104
|
-
const errorSegment = createErrorSegment(
|
|
3105
|
-
errorInfo,
|
|
3106
|
-
effectiveFallback,
|
|
3107
|
-
outletEntry, // Use outletEntry so error content renders in the boundary's outlet
|
|
3108
|
-
matched.params
|
|
3109
|
-
);
|
|
3110
|
-
|
|
3111
|
-
if (useDefaultFallback) {
|
|
3112
|
-
console.log(
|
|
3113
|
-
`[Router.matchError] Using default error boundary (no user-defined boundary found)`
|
|
3114
|
-
);
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
|
-
console.log(
|
|
3118
|
-
`[Router.matchError] Boundary: ${boundaryEntry.shortCode}, outlet replaced: ${outletEntry.shortCode}`
|
|
3119
|
-
);
|
|
3120
|
-
|
|
3121
|
-
// Error segment replaces the outlet content, not the boundary layout itself
|
|
3122
|
-
// matched contains all IDs from root to boundary (for caching parent layouts)
|
|
3123
|
-
// diff contains the outlet entry ID that is being replaced with error content
|
|
3124
|
-
return {
|
|
3125
|
-
segments: [errorSegment],
|
|
3126
|
-
matched: matchedIds,
|
|
3127
|
-
diff: [errorSegment.id],
|
|
3128
|
-
params: matched.params,
|
|
3129
|
-
};
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3132
|
-
/**
|
|
3133
|
-
* Create match context for full requests (document/SSR)
|
|
3134
|
-
* Simpler than partial - no revalidation, intercepts, or client state tracking
|
|
3135
|
-
*
|
|
3136
|
-
* @returns MatchContext with isFullMatch: true
|
|
3137
|
-
* @throws RouteNotFoundError if no route matches
|
|
3138
|
-
*/
|
|
3139
|
-
async function createMatchContextForFull(
|
|
3140
|
-
request: Request,
|
|
3141
|
-
env: TEnv
|
|
3142
|
-
): Promise<MatchContext<TEnv> | { type: "redirect"; redirectUrl: string }> {
|
|
3143
|
-
const url = new URL(request.url);
|
|
3144
|
-
const pathname = url.pathname;
|
|
3145
|
-
|
|
3146
|
-
// Initialize metrics store for this request
|
|
3147
|
-
const metricsStore = getMetricsStore();
|
|
3148
|
-
|
|
3149
|
-
// Track route matching
|
|
3150
|
-
const routeMatchStart = metricsStore ? performance.now() : 0;
|
|
3151
|
-
const matched = findMatch(pathname);
|
|
3152
|
-
if (metricsStore) {
|
|
3153
|
-
metricsStore.metrics.push({
|
|
3154
|
-
label: "route-matching",
|
|
3155
|
-
duration: performance.now() - routeMatchStart,
|
|
3156
|
-
startTime: routeMatchStart - metricsStore.requestStart,
|
|
3157
|
-
});
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
if (!matched) {
|
|
3161
|
-
throw new RouteNotFoundError(`No route matched for ${pathname}`, {
|
|
3162
|
-
cause: { pathname, method: request.method },
|
|
3163
|
-
});
|
|
3164
|
-
}
|
|
3165
|
-
|
|
3166
|
-
// Handle trailing slash redirect (pattern defines canonical form)
|
|
3167
|
-
if (matched.redirectTo) {
|
|
3168
|
-
return {
|
|
3169
|
-
type: "redirect",
|
|
3170
|
-
redirectUrl: matched.redirectTo + url.search,
|
|
3171
|
-
};
|
|
3172
|
-
}
|
|
3173
|
-
|
|
3174
|
-
// Load manifest with isSSR=true for document requests
|
|
3175
|
-
const manifestStart = metricsStore ? performance.now() : 0;
|
|
3176
|
-
const manifestEntry = await loadManifest(
|
|
3177
|
-
matched.entry,
|
|
3178
|
-
matched.routeKey,
|
|
3179
|
-
pathname,
|
|
3180
|
-
metricsStore,
|
|
3181
|
-
true // isSSR
|
|
3182
|
-
);
|
|
3183
|
-
if (metricsStore) {
|
|
3184
|
-
metricsStore.metrics.push({
|
|
3185
|
-
label: "manifest-loading",
|
|
3186
|
-
duration: performance.now() - manifestStart,
|
|
3187
|
-
startTime: manifestStart - metricsStore.requestStart,
|
|
3188
|
-
});
|
|
3189
|
-
}
|
|
3190
|
-
|
|
3191
|
-
// Collect route-level middleware
|
|
3192
|
-
const routeMiddleware = collectRouteMiddleware(
|
|
3193
|
-
traverseBack(manifestEntry),
|
|
3194
|
-
matched.params
|
|
3195
|
-
);
|
|
3196
|
-
|
|
3197
|
-
// Extract bindings from context
|
|
3198
|
-
const bindings = (env as any)?.Bindings ?? env;
|
|
3199
|
-
|
|
3200
|
-
const handlerContext = createHandlerContext(
|
|
3201
|
-
matched.params,
|
|
3202
|
-
request,
|
|
3203
|
-
url.searchParams,
|
|
3204
|
-
pathname,
|
|
3205
|
-
url,
|
|
3206
|
-
bindings,
|
|
3207
|
-
mergedRouteMap,
|
|
3208
|
-
matched.routeKey
|
|
3209
|
-
);
|
|
3210
|
-
|
|
3211
|
-
// Create request-scoped loader promises map
|
|
3212
|
-
const loaderPromises = new Map<string, Promise<any>>();
|
|
3213
|
-
setupLoaderAccess(handlerContext, loaderPromises);
|
|
3214
|
-
|
|
3215
|
-
// Get store for metrics context
|
|
3216
|
-
const Store = getContext().getOrCreateStore(matched.routeKey);
|
|
3217
|
-
// Add run helper for cleaner middleware code
|
|
3218
|
-
Store.run = <T>(fn: () => T | Promise<T>) =>
|
|
3219
|
-
getContext().runWithStore(
|
|
3220
|
-
Store,
|
|
3221
|
-
Store.namespace || "#router",
|
|
3222
|
-
Store.parent,
|
|
3223
|
-
fn
|
|
3224
|
-
);
|
|
3225
|
-
if (metricsStore) {
|
|
3226
|
-
Store.metrics = metricsStore;
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
// Collect entries and build cache scope
|
|
3230
|
-
const entries = [...traverseBack(manifestEntry)];
|
|
3231
|
-
let cacheScope: CacheScope | null = null;
|
|
3232
|
-
for (const entry of entries) {
|
|
3233
|
-
if (entry.cache) {
|
|
3234
|
-
cacheScope = createCacheScope(entry.cache, cacheScope);
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
|
|
3238
|
-
// Full match context - no intercepts, no client state, no revalidation
|
|
3239
|
-
return {
|
|
3240
|
-
request,
|
|
3241
|
-
url,
|
|
3242
|
-
pathname,
|
|
3243
|
-
env,
|
|
3244
|
-
bindings,
|
|
3245
|
-
clientSegmentIds: [],
|
|
3246
|
-
clientSegmentSet: new Set(),
|
|
3247
|
-
stale: false,
|
|
3248
|
-
prevUrl: url, // Same as current for full match
|
|
3249
|
-
prevParams: {},
|
|
3250
|
-
prevMatch: null,
|
|
3251
|
-
matched,
|
|
3252
|
-
manifestEntry,
|
|
3253
|
-
entries,
|
|
3254
|
-
routeKey: matched.routeKey,
|
|
3255
|
-
localRouteName: matched.routeKey.includes(".")
|
|
3256
|
-
? matched.routeKey.split(".").pop()!
|
|
3257
|
-
: matched.routeKey,
|
|
3258
|
-
handlerContext,
|
|
3259
|
-
loaderPromises,
|
|
3260
|
-
routeMap: mergedRouteMap,
|
|
3261
|
-
metricsStore,
|
|
3262
|
-
Store,
|
|
3263
|
-
interceptContextMatch: null,
|
|
3264
|
-
interceptSelectorContext: {
|
|
3265
|
-
from: url,
|
|
3266
|
-
to: url,
|
|
3267
|
-
params: matched.params,
|
|
3268
|
-
request,
|
|
3269
|
-
env,
|
|
3270
|
-
segments: { path: [], ids: [] },
|
|
3271
|
-
},
|
|
3272
|
-
isSameRouteNavigation: false,
|
|
3273
|
-
interceptResult: null,
|
|
3274
|
-
cacheScope,
|
|
3275
|
-
isIntercept: false,
|
|
3276
|
-
actionContext: undefined,
|
|
3277
|
-
isAction: false,
|
|
3278
|
-
routeMiddleware,
|
|
3279
|
-
isFullMatch: true,
|
|
3280
|
-
};
|
|
3281
|
-
}
|
|
3282
|
-
|
|
3283
|
-
/**
|
|
3284
|
-
* Create match context for partial requests (navigation/actions)
|
|
3285
|
-
* Extracts all setup logic from matchPartial into a reusable context builder
|
|
3286
|
-
*
|
|
3287
|
-
* @returns MatchContext if setup successful, null if should fall back to full render
|
|
3288
|
-
* @throws RouteNotFoundError if no route matches
|
|
3289
|
-
*/
|
|
3290
|
-
async function createMatchContextForPartial(
|
|
3291
|
-
request: Request,
|
|
3292
|
-
env: TEnv,
|
|
3293
|
-
actionContext?: ActionContext
|
|
3294
|
-
): Promise<MatchContext<TEnv> | null> {
|
|
3295
|
-
const url = new URL(request.url);
|
|
3296
|
-
const pathname = url.pathname;
|
|
3297
|
-
|
|
3298
|
-
// Track request start time for duration in onError (local to this request)
|
|
3299
|
-
const requestStartTime = performance.now();
|
|
3300
|
-
|
|
3301
|
-
// Initialize metrics store for this request
|
|
3302
|
-
const metricsStore = getMetricsStore();
|
|
3303
|
-
|
|
3304
|
-
// Extract client state from query params and header
|
|
3305
|
-
const clientSegmentIds =
|
|
3306
|
-
url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
|
|
3307
|
-
const stale = url.searchParams.get("_rsc_stale") === "true";
|
|
3308
|
-
const previousUrl =
|
|
3309
|
-
request.headers.get("X-RSC-Router-Client-Path") ||
|
|
3310
|
-
request.headers.get("Referer");
|
|
3311
|
-
const interceptSourceUrl = request.headers.get(
|
|
3312
|
-
"X-RSC-Router-Intercept-Source"
|
|
3313
|
-
);
|
|
3314
|
-
|
|
3315
|
-
if (!previousUrl) {
|
|
3316
|
-
return null; // Fall back to full render
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
|
-
const prevUrl = new URL(previousUrl, url.origin);
|
|
3320
|
-
const interceptContextUrl = interceptSourceUrl
|
|
3321
|
-
? new URL(interceptSourceUrl, url.origin)
|
|
3322
|
-
: prevUrl;
|
|
3323
|
-
|
|
3324
|
-
// Track route matching
|
|
3325
|
-
const routeMatchStart = metricsStore ? performance.now() : 0;
|
|
3326
|
-
const prevMatch = findMatch(prevUrl.pathname);
|
|
3327
|
-
const prevParams = prevMatch?.params || {};
|
|
3328
|
-
const interceptContextMatch = interceptSourceUrl
|
|
3329
|
-
? findMatch(interceptContextUrl.pathname)
|
|
3330
|
-
: prevMatch;
|
|
3331
|
-
|
|
3332
|
-
const matched = findMatch(pathname);
|
|
3333
|
-
|
|
3334
|
-
if (metricsStore) {
|
|
3335
|
-
metricsStore.metrics.push({
|
|
3336
|
-
label: "route-matching",
|
|
3337
|
-
duration: performance.now() - routeMatchStart,
|
|
3338
|
-
startTime: routeMatchStart - metricsStore.requestStart,
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
if (!matched) {
|
|
3343
|
-
throw new RouteNotFoundError(`No route matched for ${pathname}`, {
|
|
3344
|
-
cause: { pathname, method: request.method, previousUrl },
|
|
3345
|
-
});
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
if (matched.redirectTo) {
|
|
3349
|
-
return null; // Fall back to full match for redirects
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
|
-
// Check if routes are from different route groups
|
|
3353
|
-
if (prevMatch && prevMatch.entry !== matched.entry) {
|
|
3354
|
-
console.log(
|
|
3355
|
-
`[Router.matchPartial] Route group changed: ${prevMatch.routeKey} → ${matched.routeKey}, falling back to full render`
|
|
3356
|
-
);
|
|
3357
|
-
return null;
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
// Load manifest
|
|
3361
|
-
const manifestStart = metricsStore ? performance.now() : 0;
|
|
3362
|
-
const manifestEntry = await loadManifest(
|
|
3363
|
-
matched.entry,
|
|
3364
|
-
matched.routeKey,
|
|
3365
|
-
pathname,
|
|
3366
|
-
metricsStore,
|
|
3367
|
-
false
|
|
3368
|
-
);
|
|
3369
|
-
if (metricsStore) {
|
|
3370
|
-
metricsStore.metrics.push({
|
|
3371
|
-
label: "manifest-loading",
|
|
3372
|
-
duration: performance.now() - manifestStart,
|
|
3373
|
-
startTime: manifestStart - metricsStore.requestStart,
|
|
3374
|
-
});
|
|
3375
|
-
}
|
|
3376
|
-
|
|
3377
|
-
// Collect route middleware
|
|
3378
|
-
const routeMiddleware = collectRouteMiddleware(
|
|
3379
|
-
traverseBack(manifestEntry),
|
|
3380
|
-
matched.params
|
|
3381
|
-
);
|
|
3382
|
-
|
|
3383
|
-
// Create handler context
|
|
3384
|
-
const bindings = (env as any)?.Bindings ?? env;
|
|
3385
|
-
const handlerContext = createHandlerContext(
|
|
3386
|
-
matched.params,
|
|
3387
|
-
request,
|
|
3388
|
-
url.searchParams,
|
|
3389
|
-
pathname,
|
|
3390
|
-
url,
|
|
3391
|
-
bindings,
|
|
3392
|
-
mergedRouteMap,
|
|
3393
|
-
matched.routeKey
|
|
3394
|
-
);
|
|
3395
|
-
|
|
3396
|
-
const clientSegmentSet = new Set(clientSegmentIds);
|
|
3397
|
-
console.log(
|
|
3398
|
-
`[Router.matchPartial] Client segments:`,
|
|
3399
|
-
Array.from(clientSegmentSet)
|
|
3400
|
-
);
|
|
3401
|
-
|
|
3402
|
-
// Set up loader promises
|
|
3403
|
-
const loaderPromises = new Map<string, Promise<any>>();
|
|
3404
|
-
setupLoaderAccess(handlerContext, loaderPromises);
|
|
3405
|
-
|
|
3406
|
-
// Get store for metrics context
|
|
3407
|
-
const Store = getContext().getOrCreateStore(matched.routeKey);
|
|
3408
|
-
// Add run helper for cleaner middleware code
|
|
3409
|
-
Store.run = <T>(fn: () => T | Promise<T>) =>
|
|
3410
|
-
getContext().runWithStore(
|
|
3411
|
-
Store,
|
|
3412
|
-
Store.namespace || "#router",
|
|
3413
|
-
Store.parent,
|
|
3414
|
-
fn
|
|
3415
|
-
);
|
|
3416
|
-
if (metricsStore) {
|
|
3417
|
-
Store.metrics = metricsStore;
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
// Intercept detection
|
|
3421
|
-
const isSameRouteNavigation = !!(
|
|
3422
|
-
interceptContextMatch &&
|
|
3423
|
-
interceptContextMatch.routeKey === matched.routeKey
|
|
3424
|
-
);
|
|
3425
|
-
|
|
3426
|
-
if (interceptSourceUrl) {
|
|
3427
|
-
console.log(`[Router.matchPartial] Intercept context detected:`);
|
|
3428
|
-
console.log(` - Current URL: ${pathname}`);
|
|
3429
|
-
console.log(` - Intercept source: ${interceptSourceUrl}`);
|
|
3430
|
-
console.log(` - Context match: ${interceptContextMatch?.routeKey}`);
|
|
3431
|
-
console.log(` - Current route: ${matched.routeKey}`);
|
|
3432
|
-
console.log(` - Same route navigation: ${isSameRouteNavigation}`);
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
|
-
const localRouteName = matched.routeKey.includes(".")
|
|
3436
|
-
? matched.routeKey.split(".").pop()!
|
|
3437
|
-
: matched.routeKey;
|
|
3438
|
-
|
|
3439
|
-
// Build intercept selector context
|
|
3440
|
-
const filteredSegmentIds = clientSegmentIds.filter((id) => {
|
|
3441
|
-
if (id.includes(".@")) return false;
|
|
3442
|
-
if (/D\d+\./.test(id)) return false;
|
|
3443
|
-
return true;
|
|
3444
|
-
});
|
|
3445
|
-
const interceptSelectorContext: InterceptSelectorContext = {
|
|
3446
|
-
from: prevUrl,
|
|
3447
|
-
to: url,
|
|
3448
|
-
params: matched.params,
|
|
3449
|
-
request,
|
|
3450
|
-
env,
|
|
3451
|
-
segments: {
|
|
3452
|
-
path: prevUrl.pathname.split("/").filter(Boolean),
|
|
3453
|
-
ids: filteredSegmentIds,
|
|
3454
|
-
},
|
|
3455
|
-
};
|
|
3456
|
-
const isAction = !!actionContext;
|
|
3457
|
-
|
|
3458
|
-
// Find intercept
|
|
3459
|
-
const clientHasInterceptSegments = [...clientSegmentSet].some((id) =>
|
|
3460
|
-
id.includes(".@")
|
|
3461
|
-
);
|
|
3462
|
-
const skipInterceptForAction = isAction && !clientHasInterceptSegments;
|
|
3463
|
-
const interceptResult =
|
|
3464
|
-
isSameRouteNavigation || skipInterceptForAction
|
|
3465
|
-
? null
|
|
3466
|
-
: findInterceptForRoute(
|
|
3467
|
-
matched.routeKey,
|
|
3468
|
-
manifestEntry.parent,
|
|
3469
|
-
interceptSelectorContext,
|
|
3470
|
-
isAction
|
|
3471
|
-
) ||
|
|
3472
|
-
(localRouteName !== matched.routeKey
|
|
3473
|
-
? findInterceptForRoute(
|
|
3474
|
-
localRouteName,
|
|
3475
|
-
manifestEntry.parent,
|
|
3476
|
-
interceptSelectorContext,
|
|
3477
|
-
isAction
|
|
3478
|
-
)
|
|
3479
|
-
: null);
|
|
3480
|
-
|
|
3481
|
-
// When leaving intercept, force route segment to render
|
|
3482
|
-
if (isSameRouteNavigation && manifestEntry.type === "route") {
|
|
3483
|
-
console.log(
|
|
3484
|
-
`[Router.matchPartial] Leaving intercept - forcing route segment render: ${manifestEntry.shortCode}`
|
|
3485
|
-
);
|
|
3486
|
-
clientSegmentSet.delete(manifestEntry.shortCode);
|
|
3487
|
-
}
|
|
3488
|
-
|
|
3489
|
-
// Collect entries and build cache scope
|
|
3490
|
-
const entries = [...traverseBack(manifestEntry)];
|
|
3491
|
-
let cacheScope: CacheScope | null = null;
|
|
3492
|
-
for (const entry of entries) {
|
|
3493
|
-
if (entry.cache) {
|
|
3494
|
-
cacheScope = createCacheScope(entry.cache, cacheScope);
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
|
|
3498
|
-
const isIntercept = !!interceptResult;
|
|
580
|
+
// Create findMatch with single-entry cache, bound to router state
|
|
581
|
+
const findMatch = createFindMatch<TEnv>({
|
|
582
|
+
routesEntries,
|
|
583
|
+
evaluateLazyEntry,
|
|
584
|
+
routerId,
|
|
585
|
+
});
|
|
3499
586
|
|
|
587
|
+
// Build a RouterContext once — shared by match, matchPartial, matchForPrerender
|
|
588
|
+
function buildRouterContext(): RouterContext<TEnv> {
|
|
3500
589
|
return {
|
|
3501
|
-
request,
|
|
3502
|
-
url,
|
|
3503
|
-
pathname,
|
|
3504
|
-
env,
|
|
3505
|
-
bindings,
|
|
3506
|
-
clientSegmentIds,
|
|
3507
|
-
clientSegmentSet,
|
|
3508
|
-
stale,
|
|
3509
|
-
prevUrl,
|
|
3510
|
-
prevParams,
|
|
3511
|
-
prevMatch,
|
|
3512
|
-
matched,
|
|
3513
|
-
manifestEntry,
|
|
3514
|
-
entries,
|
|
3515
|
-
routeKey: matched.routeKey,
|
|
3516
|
-
localRouteName,
|
|
3517
|
-
handlerContext,
|
|
3518
|
-
loaderPromises,
|
|
3519
|
-
routeMap: mergedRouteMap,
|
|
3520
|
-
metricsStore,
|
|
3521
|
-
Store,
|
|
3522
|
-
interceptContextMatch,
|
|
3523
|
-
interceptSelectorContext,
|
|
3524
|
-
isSameRouteNavigation,
|
|
3525
|
-
interceptResult,
|
|
3526
|
-
cacheScope,
|
|
3527
|
-
isIntercept,
|
|
3528
|
-
actionContext,
|
|
3529
|
-
isAction,
|
|
3530
|
-
routeMiddleware,
|
|
3531
|
-
isFullMatch: false,
|
|
3532
|
-
};
|
|
3533
|
-
}
|
|
3534
|
-
|
|
3535
|
-
/**
|
|
3536
|
-
* Match partial request with revalidation
|
|
3537
|
-
*
|
|
3538
|
-
* Uses generator middleware pipeline for clean separation of concerns:
|
|
3539
|
-
* - cache-lookup: Check cache first
|
|
3540
|
-
* - segment-resolution: Resolve segments on cache miss
|
|
3541
|
-
* - intercept-resolution: Handle intercept routes
|
|
3542
|
-
* - cache-store: Store results in cache
|
|
3543
|
-
* - background-revalidation: SWR revalidation
|
|
3544
|
-
*/
|
|
3545
|
-
async function matchPartial(
|
|
3546
|
-
request: Request,
|
|
3547
|
-
context: TEnv,
|
|
3548
|
-
actionContext?: ActionContext
|
|
3549
|
-
): Promise<MatchResult | null> {
|
|
3550
|
-
// Build RouterContext with all closure functions needed by middleware
|
|
3551
|
-
const routerCtx: RouterContext<TEnv> = {
|
|
3552
590
|
findMatch,
|
|
3553
591
|
loadManifest,
|
|
3554
592
|
traverseBack,
|
|
@@ -3568,279 +606,303 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3568
606
|
buildEntryRevalidateMap,
|
|
3569
607
|
resolveLoadersOnlyWithRevalidation,
|
|
3570
608
|
resolveInterceptLoadersOnly,
|
|
609
|
+
resolveLoadersOnly,
|
|
610
|
+
telemetry: telemetrySink,
|
|
3571
611
|
};
|
|
3572
|
-
|
|
3573
|
-
return runWithRouterContext(routerCtx, async () => {
|
|
3574
|
-
const ctx = await createMatchContextForPartial(
|
|
3575
|
-
request,
|
|
3576
|
-
context,
|
|
3577
|
-
actionContext
|
|
3578
|
-
);
|
|
3579
|
-
if (!ctx) return null;
|
|
3580
|
-
|
|
3581
|
-
try {
|
|
3582
|
-
const state = createPipelineState();
|
|
3583
|
-
const pipeline = createMatchPartialPipeline(ctx, state);
|
|
3584
|
-
return await collectMatchResult(pipeline, ctx, state);
|
|
3585
|
-
} catch (error) {
|
|
3586
|
-
if (error instanceof Response) throw error;
|
|
3587
|
-
// Report unhandled errors during partial match pipeline
|
|
3588
|
-
callOnError(error, actionContext ? "action" : "revalidation", {
|
|
3589
|
-
request,
|
|
3590
|
-
url: ctx.url,
|
|
3591
|
-
env: context,
|
|
3592
|
-
actionId: actionContext?.actionId,
|
|
3593
|
-
isPartial: true,
|
|
3594
|
-
handledByBoundary: false,
|
|
3595
|
-
});
|
|
3596
|
-
throw sanitizeError(error);
|
|
3597
|
-
}
|
|
3598
|
-
});
|
|
3599
612
|
}
|
|
3600
613
|
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
): Promise<{
|
|
3609
|
-
routeMiddleware?: Array<{
|
|
3610
|
-
handler: import("./router/middleware.js").MiddlewareFn;
|
|
3611
|
-
params: Record<string, string>;
|
|
3612
|
-
}>;
|
|
3613
|
-
} | null> {
|
|
3614
|
-
const url = new URL(request.url);
|
|
3615
|
-
const pathname = url.pathname;
|
|
3616
|
-
|
|
3617
|
-
// Quick route matching
|
|
3618
|
-
const matched = findMatch(pathname);
|
|
3619
|
-
if (!matched) {
|
|
3620
|
-
return null;
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
// Skip redirect check - will be handled in full match
|
|
3624
|
-
if (matched.redirectTo) {
|
|
3625
|
-
return { routeMiddleware: undefined };
|
|
3626
|
-
}
|
|
614
|
+
// Prerender/static match deps (bind closure state for extracted functions)
|
|
615
|
+
const prerenderDeps = {
|
|
616
|
+
findMatch,
|
|
617
|
+
buildRouterContext,
|
|
618
|
+
mergedRouteMap,
|
|
619
|
+
resolveAllSegments,
|
|
620
|
+
};
|
|
3627
621
|
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
622
|
+
async function matchForPrerender(
|
|
623
|
+
pathname: string,
|
|
624
|
+
params: Record<string, string>,
|
|
625
|
+
buildVars?: Record<string, any>,
|
|
626
|
+
isPassthroughRoute?: boolean,
|
|
627
|
+
buildEnv?: TEnv,
|
|
628
|
+
devMode?: boolean,
|
|
629
|
+
) {
|
|
630
|
+
return _matchForPrerender(
|
|
3632
631
|
pathname,
|
|
3633
|
-
|
|
3634
|
-
|
|
632
|
+
params,
|
|
633
|
+
prerenderDeps,
|
|
634
|
+
buildVars,
|
|
635
|
+
isPassthroughRoute,
|
|
636
|
+
buildEnv,
|
|
637
|
+
devMode,
|
|
3635
638
|
);
|
|
639
|
+
}
|
|
3636
640
|
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
641
|
+
async function renderStaticSegment(
|
|
642
|
+
handler: Function,
|
|
643
|
+
handlerId: string,
|
|
644
|
+
routeName?: string,
|
|
645
|
+
buildEnv?: TEnv,
|
|
646
|
+
devMode?: boolean,
|
|
647
|
+
) {
|
|
648
|
+
return _renderStaticSegment<TEnv>(
|
|
649
|
+
handler,
|
|
650
|
+
handlerId,
|
|
651
|
+
mergedRouteMap,
|
|
652
|
+
routeName,
|
|
653
|
+
buildEnv,
|
|
654
|
+
devMode,
|
|
3642
655
|
);
|
|
3643
|
-
|
|
3644
|
-
return {
|
|
3645
|
-
routeMiddleware: routeMiddleware.length > 0 ? routeMiddleware : undefined,
|
|
3646
|
-
};
|
|
3647
656
|
}
|
|
3648
657
|
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
// Merge routes into the href map
|
|
3660
|
-
// Keys stay unchanged for composability - only URL patterns get prefixed
|
|
3661
|
-
const routeEntries = routes as Record<string, string>;
|
|
3662
|
-
for (const [key, pattern] of Object.entries(routeEntries)) {
|
|
3663
|
-
// Build prefixed pattern: "/shop" + "/cart" -> "/shop/cart"
|
|
3664
|
-
const prefixedPattern =
|
|
3665
|
-
prefix && pattern !== "/"
|
|
3666
|
-
? `${prefix}${pattern}`
|
|
3667
|
-
: prefix && pattern === "/"
|
|
3668
|
-
? prefix
|
|
3669
|
-
: pattern;
|
|
3670
|
-
|
|
3671
|
-
// Runtime validation: warn if key already exists with different pattern
|
|
3672
|
-
const existingPattern = mergedRouteMap[key];
|
|
3673
|
-
if (existingPattern !== undefined && existingPattern !== prefixedPattern) {
|
|
3674
|
-
console.warn(
|
|
3675
|
-
`[rsc-router] Route key conflict: "${key}" already maps to "${existingPattern}", ` +
|
|
3676
|
-
`overwriting with "${prefixedPattern}". Use unique key names to avoid this.`
|
|
3677
|
-
);
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
|
-
// Use original key - enables reusable route modules
|
|
3681
|
-
mergedRouteMap[key] = prefixedPattern;
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
|
-
// Auto-register route map for runtime href() usage
|
|
3685
|
-
registerRouteMap(mergedRouteMap);
|
|
3686
|
-
|
|
3687
|
-
// Extract trailing slash config if present (attached by route())
|
|
3688
|
-
const trailingSlashConfig = (routes as any).__trailingSlash as
|
|
3689
|
-
| Record<string, TrailingSlashMode>
|
|
3690
|
-
| undefined;
|
|
3691
|
-
|
|
3692
|
-
// Create builder object so .use() can return it
|
|
3693
|
-
const builder: RouteBuilder<RouteDefinition, TEnv, any, TNewRoutes> = {
|
|
3694
|
-
use(
|
|
3695
|
-
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
3696
|
-
middleware?: MiddlewareFn<TEnv>
|
|
3697
|
-
) {
|
|
3698
|
-
// Mount-scoped middleware - prefix is the mount prefix
|
|
3699
|
-
addMiddleware(patternOrMiddleware, middleware, prefix || null);
|
|
3700
|
-
return builder;
|
|
3701
|
-
},
|
|
3702
|
-
|
|
3703
|
-
map(
|
|
3704
|
-
handler:
|
|
3705
|
-
| ((helpers: InlineRouteHelpers<TNewRoutes, TEnv>) => Array<AllUseItems>)
|
|
3706
|
-
| (() =>
|
|
3707
|
-
| Array<AllUseItems>
|
|
3708
|
-
| Promise<{ default: () => Array<AllUseItems> }>
|
|
3709
|
-
| Promise<() => Array<AllUseItems>>)
|
|
3710
|
-
) {
|
|
3711
|
-
// Store handler as-is - detection happens at call time based on return type
|
|
3712
|
-
// Both patterns use the same signature:
|
|
3713
|
-
// - Inline: ({ route }) => [...] - receives helpers, returns Array
|
|
3714
|
-
// - Lazy: () => import(...) - ignores helpers, returns Promise
|
|
3715
|
-
routesEntries.push({
|
|
3716
|
-
prefix,
|
|
3717
|
-
routes: routes as ResolvedRouteMap<any>,
|
|
3718
|
-
trailingSlash: trailingSlashConfig,
|
|
3719
|
-
handler: handler as any,
|
|
3720
|
-
mountIndex: currentMountIndex,
|
|
3721
|
-
});
|
|
3722
|
-
// Return router with accumulated types
|
|
3723
|
-
// At runtime this is the same object, but TypeScript tracks the accumulated route types
|
|
3724
|
-
return router as any;
|
|
3725
|
-
},
|
|
3726
|
-
|
|
3727
|
-
// Expose accumulated route map for typeof extraction
|
|
3728
|
-
get routeMap() {
|
|
3729
|
-
return mergedRouteMap as TNewRoutes;
|
|
3730
|
-
},
|
|
3731
|
-
};
|
|
658
|
+
// Create match handler functions bound to router state
|
|
659
|
+
const matchHandlers = createMatchHandlers<TEnv>({
|
|
660
|
+
buildRouterContext,
|
|
661
|
+
callOnError,
|
|
662
|
+
matchApiDeps,
|
|
663
|
+
defaultErrorBoundary,
|
|
664
|
+
findMatch,
|
|
665
|
+
findInterceptForRoute,
|
|
666
|
+
telemetry: telemetrySink,
|
|
667
|
+
});
|
|
3732
668
|
|
|
3733
|
-
|
|
3734
|
-
}
|
|
669
|
+
const { match, matchPartial, matchError, previewMatch } = matchHandlers;
|
|
3735
670
|
|
|
3736
671
|
/**
|
|
3737
672
|
* Router instance
|
|
3738
673
|
* The type system tracks accumulated routes through the builder chain
|
|
3739
674
|
* Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
|
|
3740
675
|
*/
|
|
3741
|
-
const router:
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
//
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
676
|
+
const router: RSCRouterInternal<TEnv, {}> = {
|
|
677
|
+
__brand: RSC_ROUTER_BRAND,
|
|
678
|
+
id: routerId,
|
|
679
|
+
basename,
|
|
680
|
+
|
|
681
|
+
routes(patternsOrBuilder: UrlPatterns<TEnv> | UrlBuilder<TEnv>): any {
|
|
682
|
+
// Wrap builder functions in urls() automatically
|
|
683
|
+
const urlPatterns: UrlPatterns<TEnv> =
|
|
684
|
+
typeof patternsOrBuilder === "function"
|
|
685
|
+
? (urls(patternsOrBuilder) as UrlPatterns<TEnv>)
|
|
686
|
+
: patternsOrBuilder;
|
|
687
|
+
|
|
688
|
+
// Store reference for runtime manifest generation
|
|
689
|
+
storedUrlPatterns = urlPatterns;
|
|
690
|
+
const currentMountIndex = mountIndex++;
|
|
691
|
+
|
|
692
|
+
// Create manifest and patterns maps for route registration
|
|
693
|
+
const manifest = new Map<string, EntryData>();
|
|
694
|
+
const routePatterns = new Map<string, string>();
|
|
695
|
+
const patternsByPrefix = new Map<string, Map<string, string>>();
|
|
696
|
+
const trailingSlashMap = new Map<string, TrailingSlashMode>();
|
|
697
|
+
|
|
698
|
+
// Run the handler once to extract patterns for route matching.
|
|
699
|
+
// Note: loadManifest will re-run the handler to register entries in its context.
|
|
700
|
+
// Lazy includes are detected in the return value and handled separately.
|
|
701
|
+
//
|
|
702
|
+
// Pattern extraction must use the same mountIndex and MapRootLayout root
|
|
703
|
+
// parent as loadManifest so that shortCodes produced here match those at
|
|
704
|
+
// runtime. include() captures the current parent and counters; if those
|
|
705
|
+
// shortCodes diverge from the runtime tree the segment reconciliation on
|
|
706
|
+
// the client will see a full mismatch and remount the entire page.
|
|
707
|
+
const syntheticMapRoot: EntryData = {
|
|
708
|
+
type: "layout",
|
|
709
|
+
id: `#synthetic-maproot-M${currentMountIndex}`,
|
|
710
|
+
shortCode: `M${currentMountIndex}L0`,
|
|
711
|
+
parent: null,
|
|
712
|
+
handler: MapRootLayout,
|
|
713
|
+
middleware: [],
|
|
714
|
+
revalidate: [],
|
|
715
|
+
errorBoundary: [],
|
|
716
|
+
notFoundBoundary: [],
|
|
717
|
+
layout: [],
|
|
718
|
+
parallel: {},
|
|
719
|
+
intercept: [],
|
|
720
|
+
loader: [],
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
let handlerResult: AllUseItems[] = [];
|
|
724
|
+
RSCRouterContext.run(
|
|
725
|
+
{
|
|
726
|
+
manifest,
|
|
727
|
+
patterns: routePatterns,
|
|
728
|
+
patternsByPrefix,
|
|
729
|
+
trailingSlash: trailingSlashMap,
|
|
730
|
+
namespace: "root",
|
|
731
|
+
parent: syntheticMapRoot,
|
|
732
|
+
counters: {},
|
|
733
|
+
mountIndex: currentMountIndex,
|
|
734
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
735
|
+
// basename sets the initial URL prefix so all path() patterns
|
|
736
|
+
// are registered with the prefix (e.g. "/admin" + "/users" = "/admin/users").
|
|
737
|
+
// No namePrefix — route names stay unprefixed.
|
|
738
|
+
...(basename ? { urlPrefix: basename } : {}),
|
|
739
|
+
},
|
|
740
|
+
() => {
|
|
741
|
+
handlerResult = urlPatterns.handler() as AllUseItems[];
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// Convert trailingSlash map to object for the router
|
|
746
|
+
const trailingSlashConfig =
|
|
747
|
+
trailingSlashMap.size > 0
|
|
748
|
+
? Object.fromEntries(trailingSlashMap)
|
|
749
|
+
: undefined;
|
|
750
|
+
|
|
751
|
+
// Collect route keys that have prerender handlers (for non-trie match path)
|
|
752
|
+
let prerenderRouteKeys: Set<string> | undefined;
|
|
753
|
+
let passthroughRouteKeys: Set<string> | undefined;
|
|
754
|
+
for (const [name, entry] of manifest.entries()) {
|
|
755
|
+
if (entry.type === "route" && entry.isPrerender) {
|
|
756
|
+
if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
|
|
757
|
+
prerenderRouteKeys.add(name);
|
|
758
|
+
if (entry.isPassthrough === true) {
|
|
759
|
+
if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
|
|
760
|
+
passthroughRouteKeys.add(name);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Create separate RouteEntry for each URL prefix group
|
|
766
|
+
// This enables prefix-based short-circuit optimization
|
|
767
|
+
if (patternsByPrefix.size > 0) {
|
|
768
|
+
for (const [prefix, prefixPatterns] of patternsByPrefix.entries()) {
|
|
769
|
+
const routesObject: Record<string, string> = {};
|
|
770
|
+
for (const [name, pattern] of prefixPatterns.entries()) {
|
|
771
|
+
routesObject[name] = pattern;
|
|
3780
772
|
}
|
|
3781
|
-
);
|
|
3782
773
|
|
|
3783
|
-
|
|
774
|
+
routesEntries.push({
|
|
775
|
+
// prefix is "" because patterns already include the URL prefix
|
|
776
|
+
// (e.g., "/site/:locale/user1/:id" not just "/user1/:id")
|
|
777
|
+
prefix: "",
|
|
778
|
+
// staticPrefix is the actual prefix for short-circuit optimization
|
|
779
|
+
staticPrefix: extractStaticPrefix(prefix),
|
|
780
|
+
routes: routesObject as ResolvedRouteMap<any>,
|
|
781
|
+
trailingSlash: trailingSlashConfig,
|
|
782
|
+
handler: urlPatterns.handler,
|
|
783
|
+
mountIndex: currentMountIndex,
|
|
784
|
+
routerId,
|
|
785
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
786
|
+
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
787
|
+
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
// Fallback: no prefix grouping, use flat patterns map
|
|
3784
792
|
const routesObject: Record<string, string> = {};
|
|
3785
|
-
for (const [name, pattern] of
|
|
793
|
+
for (const [name, pattern] of routePatterns.entries()) {
|
|
3786
794
|
routesObject[name] = pattern;
|
|
3787
795
|
}
|
|
3788
796
|
|
|
3789
|
-
// Store the ORIGINAL handler - loadManifest will re-run it to register manifest entries
|
|
3790
|
-
// Convert trailingSlash map to object for the router
|
|
3791
|
-
const trailingSlashConfig = trailingSlashMap.size > 0
|
|
3792
|
-
? Object.fromEntries(trailingSlashMap)
|
|
3793
|
-
: undefined;
|
|
3794
|
-
|
|
3795
797
|
routesEntries.push({
|
|
3796
798
|
prefix: "",
|
|
799
|
+
staticPrefix: "",
|
|
3797
800
|
routes: routesObject as ResolvedRouteMap<any>,
|
|
3798
801
|
trailingSlash: trailingSlashConfig,
|
|
3799
802
|
handler: urlPatterns.handler,
|
|
3800
803
|
mountIndex: currentMountIndex,
|
|
804
|
+
routerId,
|
|
805
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
806
|
+
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
807
|
+
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
3801
808
|
});
|
|
809
|
+
}
|
|
3802
810
|
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
811
|
+
// Build route map from registered patterns
|
|
812
|
+
for (const [name, pattern] of routePatterns.entries()) {
|
|
813
|
+
// Runtime validation: warn if key already exists with different pattern.
|
|
814
|
+
// Skip warning for entries that came from the static seed — the gen file
|
|
815
|
+
// can be stale during HMR, so runtime registration is authoritative.
|
|
816
|
+
const existingPattern = mergedRouteMap[name];
|
|
817
|
+
if (
|
|
818
|
+
existingPattern !== undefined &&
|
|
819
|
+
existingPattern !== pattern &&
|
|
820
|
+
!seededNames.has(name)
|
|
821
|
+
) {
|
|
822
|
+
console.warn(
|
|
823
|
+
`[@rangojs/router] Route name conflict: "${name}" already maps to "${existingPattern}", ` +
|
|
824
|
+
`overwriting with "${pattern}". Use unique route names to avoid this.`,
|
|
825
|
+
);
|
|
3814
826
|
}
|
|
827
|
+
mergedRouteMap[name] = pattern;
|
|
828
|
+
seededNames.delete(name);
|
|
829
|
+
}
|
|
3815
830
|
|
|
3816
|
-
|
|
3817
|
-
|
|
831
|
+
// Detect lazy includes in handler result and create placeholder entries
|
|
832
|
+
const lazyIncludes = findLazyIncludes(handlerResult);
|
|
3818
833
|
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
834
|
+
// Create placeholder RouteEntry for each lazy include
|
|
835
|
+
for (const lazyInclude of lazyIncludes) {
|
|
836
|
+
// Compute the full URL prefix (combining parent prefix if any)
|
|
837
|
+
const fullPrefix = lazyInclude.context.urlPrefix
|
|
838
|
+
? lazyInclude.context.urlPrefix + lazyInclude.prefix
|
|
839
|
+
: lazyInclude.prefix;
|
|
3822
840
|
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
841
|
+
const lazyEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
|
|
842
|
+
prefix: "",
|
|
843
|
+
staticPrefix: extractStaticPrefix(fullPrefix),
|
|
844
|
+
routes: {} as ResolvedRouteMap<any>, // Empty until first match
|
|
845
|
+
trailingSlash: trailingSlashConfig,
|
|
846
|
+
handler: urlPatterns.handler,
|
|
847
|
+
mountIndex: mountIndex++,
|
|
848
|
+
routerId,
|
|
849
|
+
// Lazy evaluation fields
|
|
850
|
+
lazy: true,
|
|
851
|
+
lazyPatterns: lazyInclude.patterns,
|
|
852
|
+
lazyContext: lazyInclude.context,
|
|
853
|
+
lazyEvaluated: false,
|
|
854
|
+
_lazyPrefix: lazyInclude.prefix,
|
|
855
|
+
};
|
|
856
|
+
// Insert lazy entry before any entry whose staticPrefix is a
|
|
857
|
+
// prefix of (but shorter than) this lazy entry's staticPrefix.
|
|
858
|
+
// This ensures more specific lazy includes are matched before
|
|
859
|
+
// less specific eager entries (e.g., "/href/nested" before "/href/:id").
|
|
860
|
+
const lazyPrefix = lazyEntry.staticPrefix;
|
|
861
|
+
let insertIndex = routesEntries.length;
|
|
862
|
+
if (lazyPrefix) {
|
|
863
|
+
for (let i = 0; i < routesEntries.length; i++) {
|
|
864
|
+
const existing = routesEntries[i]!;
|
|
865
|
+
if (
|
|
866
|
+
lazyPrefix.startsWith(existing.staticPrefix) &&
|
|
867
|
+
lazyPrefix.length > existing.staticPrefix.length
|
|
868
|
+
) {
|
|
869
|
+
insertIndex = i;
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
routesEntries.splice(insertIndex, 0, lazyEntry);
|
|
3827
875
|
}
|
|
3828
|
-
|
|
3829
|
-
|
|
876
|
+
|
|
877
|
+
// Auto-register route map for runtime reverse() usage
|
|
878
|
+
registerRouteMap(mergedRouteMap);
|
|
879
|
+
|
|
880
|
+
return router;
|
|
3830
881
|
},
|
|
3831
882
|
|
|
3832
883
|
use(
|
|
3833
884
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
3834
|
-
middleware?: MiddlewareFn<TEnv
|
|
885
|
+
middleware?: MiddlewareFn<TEnv>,
|
|
3835
886
|
): any {
|
|
3836
|
-
//
|
|
3837
|
-
|
|
887
|
+
// Auto-prefix pattern with basename so router-level middleware
|
|
888
|
+
// patterns are router-relative (e.g. "/users/*" matches "/app/users/*").
|
|
889
|
+
if (basename && typeof patternOrMiddleware === "string") {
|
|
890
|
+
const pattern = patternOrMiddleware;
|
|
891
|
+
const prefixed =
|
|
892
|
+
pattern === "/*" || pattern === "*"
|
|
893
|
+
? `${basename}/*`
|
|
894
|
+
: `${basename}${pattern}`;
|
|
895
|
+
addMiddleware(prefixed, middleware, null);
|
|
896
|
+
} else {
|
|
897
|
+
addMiddleware(patternOrMiddleware, middleware, null);
|
|
898
|
+
}
|
|
3838
899
|
return router;
|
|
3839
900
|
},
|
|
3840
901
|
|
|
3841
902
|
// Type-safe URL builder using merged route map
|
|
3842
903
|
// Types are tracked through the builder chain via TRoutes parameter
|
|
3843
|
-
|
|
904
|
+
// Seeded with static route names from the generated file (injected by Vite)
|
|
905
|
+
reverse: createReverse(mergedRouteMap),
|
|
3844
906
|
|
|
3845
907
|
// Expose accumulated route map for typeof extraction
|
|
3846
908
|
// Returns {} initially, but builder chain accumulates specific route types
|
|
@@ -3863,14 +925,130 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3863
925
|
// Expose resolved theme configuration for NavigationProvider and MetaTags
|
|
3864
926
|
themeConfig: resolvedThemeConfig,
|
|
3865
927
|
|
|
928
|
+
// Expose resolved cache profiles for per-request resolution
|
|
929
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
930
|
+
|
|
931
|
+
// Expose prefetch cache settings
|
|
932
|
+
prefetchCacheControl,
|
|
933
|
+
prefetchCacheTTL,
|
|
934
|
+
|
|
935
|
+
// Expose warmup enabled flag for handler and client
|
|
936
|
+
warmupEnabled,
|
|
937
|
+
|
|
938
|
+
// Expose router-wide performance debugging for request-level metrics setup
|
|
939
|
+
debugPerformance,
|
|
940
|
+
|
|
941
|
+
// Expose debug manifest flag for handler
|
|
942
|
+
allowDebugManifest: allowDebugManifestOption,
|
|
943
|
+
|
|
944
|
+
// Expose origin check configuration for handler (default: enabled)
|
|
945
|
+
originCheck: originCheckOption ?? true,
|
|
946
|
+
|
|
947
|
+
// Expose SSR configuration for handler
|
|
948
|
+
ssr: ssrOption,
|
|
949
|
+
|
|
950
|
+
// Expose resolved timeouts for RSC handler
|
|
951
|
+
timeouts: resolvedTimeouts,
|
|
952
|
+
onTimeout,
|
|
953
|
+
|
|
3866
954
|
// Expose global middleware for RSC handler
|
|
3867
955
|
middleware: globalMiddleware,
|
|
3868
956
|
|
|
3869
|
-
match,
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
957
|
+
match: (request: Request, input: RouterRequestInput<TEnv> = {}) => {
|
|
958
|
+
const env = input.env ?? ({} as TEnv);
|
|
959
|
+
return match(request, env);
|
|
960
|
+
},
|
|
961
|
+
matchForPrerender,
|
|
962
|
+
renderStaticSegment,
|
|
963
|
+
matchPartial: (
|
|
964
|
+
request: Request,
|
|
965
|
+
input: RouterRequestInput<TEnv> = {},
|
|
966
|
+
actionContext?: Parameters<typeof matchPartial>[2],
|
|
967
|
+
) => {
|
|
968
|
+
const env = input.env ?? ({} as TEnv);
|
|
969
|
+
return matchPartial(request, env, actionContext);
|
|
970
|
+
},
|
|
971
|
+
matchError: (
|
|
972
|
+
request: Request,
|
|
973
|
+
input: RouterRequestInput<TEnv> | undefined,
|
|
974
|
+
error: unknown,
|
|
975
|
+
segmentType?: Parameters<typeof matchError>[3],
|
|
976
|
+
) => {
|
|
977
|
+
const env = input?.env ?? ({} as TEnv);
|
|
978
|
+
return matchError(request, env, error, segmentType);
|
|
979
|
+
},
|
|
980
|
+
previewMatch: (request: Request, input: RouterRequestInput<TEnv> = {}) => {
|
|
981
|
+
const env = input.env ?? ({} as TEnv);
|
|
982
|
+
return previewMatch(request, env);
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
// Expose nonce provider for fetch
|
|
986
|
+
nonce,
|
|
987
|
+
|
|
988
|
+
// Expose version for fetch
|
|
989
|
+
version,
|
|
990
|
+
|
|
991
|
+
// Expose urlpatterns for runtime manifest generation
|
|
992
|
+
get urlpatterns() {
|
|
993
|
+
return storedUrlPatterns ?? undefined;
|
|
994
|
+
},
|
|
995
|
+
|
|
996
|
+
// Expose source file for per-router type generation
|
|
997
|
+
__sourceFile,
|
|
998
|
+
|
|
999
|
+
// Expose basename for runtime manifest generation
|
|
1000
|
+
__basename: basename,
|
|
1001
|
+
|
|
1002
|
+
// RSC request handler (lazily created on first call)
|
|
1003
|
+
fetch: (() => {
|
|
1004
|
+
// Handler is created on first call and reused
|
|
1005
|
+
let handler:
|
|
1006
|
+
| ((
|
|
1007
|
+
request: Request,
|
|
1008
|
+
input: RouterRequestInput<TEnv>,
|
|
1009
|
+
) => Promise<Response>)
|
|
1010
|
+
| null = null;
|
|
1011
|
+
|
|
1012
|
+
return async (request: Request, input: RouterRequestInput<TEnv> = {}) => {
|
|
1013
|
+
// Trigger lazy import of per-router manifest data before route matching.
|
|
1014
|
+
// No-op if data is already loaded or no loader is registered.
|
|
1015
|
+
await ensureRouterManifest(routerId);
|
|
1016
|
+
if (!handler) {
|
|
1017
|
+
// Lazy import deferred to first request to avoid dev mode issues
|
|
1018
|
+
const { createRSCHandler } = await import("./rsc/handler.js");
|
|
1019
|
+
// Cast: handler.ts still accepts (request, env) — will be updated
|
|
1020
|
+
// separately to accept RouterRequestInput.
|
|
1021
|
+
handler = createRSCHandler({
|
|
1022
|
+
router: router as any,
|
|
1023
|
+
cache,
|
|
1024
|
+
nonce,
|
|
1025
|
+
version,
|
|
1026
|
+
}) as (
|
|
1027
|
+
request: Request,
|
|
1028
|
+
input: RouterRequestInput<TEnv>,
|
|
1029
|
+
) => Promise<Response>;
|
|
1030
|
+
}
|
|
1031
|
+
return handler!(request, input);
|
|
1032
|
+
};
|
|
1033
|
+
})(),
|
|
1034
|
+
|
|
1035
|
+
// Low-level route matching for request classification
|
|
1036
|
+
findMatch: (pathname: string, metricsStore?: any) =>
|
|
1037
|
+
findMatch(pathname, metricsStore),
|
|
1038
|
+
|
|
1039
|
+
// Debug utility for manifest inspection
|
|
1040
|
+
debugManifest: () => buildDebugManifest<TEnv>(routesEntries),
|
|
3873
1041
|
};
|
|
3874
1042
|
|
|
1043
|
+
// Register router in the global registry for build-time discovery
|
|
1044
|
+
RouterRegistry.set(routerId, router);
|
|
1045
|
+
|
|
1046
|
+
// If urls option was provided, auto-register them
|
|
1047
|
+
if (typeof urlsOption === "function") {
|
|
1048
|
+
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1049
|
+
} else if (urlsOption) {
|
|
1050
|
+
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
3875
1053
|
return router;
|
|
3876
1054
|
}
|