@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80
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 +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4960 -935
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- 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-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 +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- 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 +6 -1
- 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 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -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 +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -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 +571 -17
- 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 +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -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/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -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 +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- 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 +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- 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 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- 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 +41 -21
- 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 +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- 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 +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- 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 +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- 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 +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- 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 +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -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 -1623
- 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 +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- 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 +15 -1133
- 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/{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/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +918 -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} +25 -29
- package/src/vite/utils/prerender-utils.ts +221 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search parameter schema types and runtime utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides a lightweight schema system for typed query parameters.
|
|
5
|
+
* When a route defines a `search` schema, ctx.search provides a typed
|
|
6
|
+
* object with parsed values. ctx.searchParams always remains a standard
|
|
7
|
+
* URLSearchParams instance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Schema Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/** Supported scalar types for search params (append ? for optional). */
|
|
15
|
+
export type SearchSchemaValue =
|
|
16
|
+
| "string"
|
|
17
|
+
| "number"
|
|
18
|
+
| "boolean"
|
|
19
|
+
| "string?"
|
|
20
|
+
| "number?"
|
|
21
|
+
| "boolean?";
|
|
22
|
+
|
|
23
|
+
/** A search schema maps param names to their type descriptors. */
|
|
24
|
+
export type SearchSchema = Record<string, SearchSchemaValue>;
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Type-Level Schema Resolution
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/** Strip trailing `?` from a schema value to get the base type. */
|
|
31
|
+
type BaseType<T extends string> = T extends `${infer B}?` ? B : T;
|
|
32
|
+
|
|
33
|
+
/** Map a base type string to its TypeScript type. */
|
|
34
|
+
type ResolveBaseType<T extends string> = T extends "string"
|
|
35
|
+
? string
|
|
36
|
+
: T extends "number"
|
|
37
|
+
? number
|
|
38
|
+
: T extends "boolean"
|
|
39
|
+
? boolean
|
|
40
|
+
: never;
|
|
41
|
+
|
|
42
|
+
/** Keys whose schema value does NOT end with `?`. */
|
|
43
|
+
type RequiredKeys<T extends SearchSchema> = {
|
|
44
|
+
[K in keyof T]: T[K] extends `${string}?` ? never : K;
|
|
45
|
+
}[keyof T];
|
|
46
|
+
|
|
47
|
+
/** Keys whose schema value ends with `?`. */
|
|
48
|
+
type OptionalKeys<T extends SearchSchema> = {
|
|
49
|
+
[K in keyof T]: T[K] extends `${string}?` ? K : never;
|
|
50
|
+
}[keyof T];
|
|
51
|
+
|
|
52
|
+
/** Flatten an intersection type into a single object type. */
|
|
53
|
+
type Simplify<T> = { [K in keyof T]: T[K] };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a SearchSchema to its typed object.
|
|
57
|
+
*
|
|
58
|
+
* Both required and optional params resolve to `T | undefined` at the handler
|
|
59
|
+
* level. The required/optional distinction is a consumer-facing contract
|
|
60
|
+
* (e.g., for href() and reverse() autocomplete) — it tells callers which
|
|
61
|
+
* params the route expects, but the handler must still check for undefined
|
|
62
|
+
* since the framework cannot trust the client to send all required params.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* type S = { q: "string"; page: "number?"; sort: "string?" };
|
|
66
|
+
* type R = ResolveSearchSchema<S>;
|
|
67
|
+
* // { q: string | undefined; page?: number; sort?: string }
|
|
68
|
+
*/
|
|
69
|
+
export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
|
|
70
|
+
{
|
|
71
|
+
[K in RequiredKeys<T> & string]:
|
|
72
|
+
| ResolveBaseType<BaseType<T[K]>>
|
|
73
|
+
| undefined;
|
|
74
|
+
} & {
|
|
75
|
+
[K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
|
|
76
|
+
}
|
|
77
|
+
>;
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Route-Level Type Extraction
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
/** Resolve the global route map from RegisteredRoutes or GeneratedRouteMap. */
|
|
84
|
+
type GlobalRouteMap = keyof RSCRouter.RegisteredRoutes extends never
|
|
85
|
+
? keyof RSCRouter.GeneratedRouteMap extends never
|
|
86
|
+
? Record<string, string>
|
|
87
|
+
: RSCRouter.GeneratedRouteMap
|
|
88
|
+
: RSCRouter.RegisteredRoutes;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extract the resolved search params type for a named route.
|
|
92
|
+
* Looks up the search schema from the route map and resolves it.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* // Given: path("/search", handler, { name: "search", search: { q: "string", page: "number?" } })
|
|
97
|
+
* type Params = RouteSearchParams<"search">;
|
|
98
|
+
* // { q: string; page?: number }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export type RouteSearchParams<TName extends string, TRouteMap = never> = [
|
|
102
|
+
TRouteMap,
|
|
103
|
+
] extends [never]
|
|
104
|
+
? ExtractAndResolveSearch<GlobalRouteMap, TName>
|
|
105
|
+
: ExtractAndResolveSearch<TRouteMap, TName>;
|
|
106
|
+
|
|
107
|
+
type ExtractAndResolveSearch<TRouteMap, TName> = TName extends keyof TRouteMap
|
|
108
|
+
? TRouteMap[TName] extends { readonly search: infer S extends SearchSchema }
|
|
109
|
+
? ResolveSearchSchema<S>
|
|
110
|
+
: {}
|
|
111
|
+
: {};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract the route params type for a named route.
|
|
115
|
+
* Looks up the path pattern from the route map and extracts params.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // Given: path("/blog/:slug", handler, { name: "blogPost" })
|
|
120
|
+
* type Params = RouteParams<"blogPost">;
|
|
121
|
+
* // { slug: string }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export type RouteParams<TName extends string, TRouteMap = never> = [
|
|
125
|
+
TRouteMap,
|
|
126
|
+
] extends [never]
|
|
127
|
+
? ExtractRouteParamsFromMap<GlobalRouteMap, TName>
|
|
128
|
+
: ExtractRouteParamsFromMap<TRouteMap, TName>;
|
|
129
|
+
|
|
130
|
+
type ExtractRouteParamsFromMap<TRouteMap, TName> = TName extends keyof TRouteMap
|
|
131
|
+
? TRouteMap[TName] extends string
|
|
132
|
+
? ExtractParamsFromPattern<TRouteMap[TName]>
|
|
133
|
+
: TRouteMap[TName] extends { readonly path: infer P extends string }
|
|
134
|
+
? ExtractParamsFromPattern<P>
|
|
135
|
+
: {}
|
|
136
|
+
: {};
|
|
137
|
+
|
|
138
|
+
/** Parse "a|b|c" into "a" | "b" | "c" */
|
|
139
|
+
type ParseConstraint<T extends string> =
|
|
140
|
+
T extends `${infer First}|${infer Rest}` ? First | ParseConstraint<Rest> : T;
|
|
141
|
+
|
|
142
|
+
/** Minimal inline param extraction (avoids importing from types.ts to prevent circular deps). */
|
|
143
|
+
type ExtractParamsFromPattern<T extends string> =
|
|
144
|
+
T extends `${string}:${infer Param}/${infer Rest}`
|
|
145
|
+
? Param extends `${infer Name}(${infer C})?`
|
|
146
|
+
? {
|
|
147
|
+
[K in Name]?: ParseConstraint<C>;
|
|
148
|
+
} & ExtractParamsFromPattern<`/${Rest}`>
|
|
149
|
+
: Param extends `${infer Name}(${infer C})`
|
|
150
|
+
? {
|
|
151
|
+
[K in Name]: ParseConstraint<C>;
|
|
152
|
+
} & ExtractParamsFromPattern<`/${Rest}`>
|
|
153
|
+
: Param extends `${infer Name}?`
|
|
154
|
+
? { [K in Name]?: string } & ExtractParamsFromPattern<`/${Rest}`>
|
|
155
|
+
: { [K in Param]: string } & ExtractParamsFromPattern<`/${Rest}`>
|
|
156
|
+
: T extends `${string}:${infer Param}`
|
|
157
|
+
? Param extends `${infer Name}(${infer C})?`
|
|
158
|
+
? { [K in Name]?: ParseConstraint<C> }
|
|
159
|
+
: Param extends `${infer Name}(${infer C})`
|
|
160
|
+
? { [K in Name]: ParseConstraint<C> }
|
|
161
|
+
: Param extends `${infer Name}?`
|
|
162
|
+
? { [K in Name]?: string }
|
|
163
|
+
: { [K in Param]: string }
|
|
164
|
+
: {};
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Runtime Parser
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse URLSearchParams into a typed object using the given schema.
|
|
172
|
+
*
|
|
173
|
+
* - `"string"` / `"string?"` - kept as-is
|
|
174
|
+
* - `"number"` / `"number?"` - coerced via `Number()`; NaN treated as missing
|
|
175
|
+
* - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
|
|
176
|
+
*
|
|
177
|
+
* Missing params (both required and optional) are omitted from the result
|
|
178
|
+
* (undefined). The required/optional distinction is a consumer-facing contract
|
|
179
|
+
* only — the handler must check for undefined.
|
|
180
|
+
*/
|
|
181
|
+
export function parseSearchParams<T extends SearchSchema>(
|
|
182
|
+
searchParams: URLSearchParams,
|
|
183
|
+
schema: T,
|
|
184
|
+
): ResolveSearchSchema<T> {
|
|
185
|
+
const result: Record<string, unknown> = {};
|
|
186
|
+
|
|
187
|
+
for (const [key, descriptor] of Object.entries(schema)) {
|
|
188
|
+
const isOptional = descriptor.endsWith("?");
|
|
189
|
+
const baseType = isOptional ? descriptor.slice(0, -1) : descriptor;
|
|
190
|
+
const raw = searchParams.get(key);
|
|
191
|
+
|
|
192
|
+
if (raw === null) {
|
|
193
|
+
// Missing params are omitted (undefined) regardless of required/optional
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (baseType === "string") {
|
|
198
|
+
result[key] = raw;
|
|
199
|
+
} else if (baseType === "number") {
|
|
200
|
+
const num = Number(raw);
|
|
201
|
+
if (!Number.isNaN(num)) {
|
|
202
|
+
result[key] = num;
|
|
203
|
+
}
|
|
204
|
+
// NaN treated as missing (undefined)
|
|
205
|
+
} else if (baseType === "boolean") {
|
|
206
|
+
result[key] = raw === "true" || raw === "1";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result as ResolveSearchSchema<T>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// Runtime Serializer
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Serialize a typed search params object to a query string (without leading `?`).
|
|
219
|
+
* Skips `undefined` and `null` values.
|
|
220
|
+
*/
|
|
221
|
+
export function serializeSearchParams(params: Record<string, unknown>): string {
|
|
222
|
+
const parts: string[] = [];
|
|
223
|
+
for (const [key, value] of Object.entries(params)) {
|
|
224
|
+
if (value === undefined || value === null) continue;
|
|
225
|
+
parts.push(
|
|
226
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return parts.join("&");
|
|
230
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable Promise wrappers keyed on the component itself. Objects (React
|
|
5
|
+
* elements, functions, lazy payloads) land in a WeakMap so entries GC when
|
|
6
|
+
* the underlying component is released; primitives (string, number, boolean,
|
|
7
|
+
* null) land in a Map so memoization still applies to text-/null-backed
|
|
8
|
+
* segments like those in partial-update flows. Keeping this cache outside
|
|
9
|
+
* the segment eliminates preservation fields on ResolvedSegment — it survives
|
|
10
|
+
* reconciliation naturally because the component ref is what's stable.
|
|
11
|
+
*
|
|
12
|
+
* Browser-only. On the server each SSR render needs a fresh pending promise
|
|
13
|
+
* so Suspense can emit the loading fallback HTML before content streams. A
|
|
14
|
+
* shared already-resolved promise has `.status === "fulfilled"` attached by
|
|
15
|
+
* React on its first observation — subsequent `use()` calls return
|
|
16
|
+
* synchronously without suspending, so the Suspense fallback never makes it
|
|
17
|
+
* into the initial HTML. Route-definition components share refs across
|
|
18
|
+
* requests, so a global cache would leak tracked state between renders.
|
|
19
|
+
*/
|
|
20
|
+
const IS_BROWSER = typeof window !== "undefined";
|
|
21
|
+
const objectContentCache = IS_BROWSER
|
|
22
|
+
? new WeakMap<object, Promise<ReactNode>>()
|
|
23
|
+
: null;
|
|
24
|
+
const primitiveContentCache = IS_BROWSER
|
|
25
|
+
? new Map<unknown, Promise<ReactNode>>()
|
|
26
|
+
: null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return a stable Promise wrapping `component`, memoized on the component ref.
|
|
30
|
+
*
|
|
31
|
+
* A fresh `Promise.resolve(component)` each render would suspend for one
|
|
32
|
+
* microtask and briefly commit the loading fallback inside Suspender — the
|
|
33
|
+
* intercept / parallel-slot flicker this indirection prevents. Reusing the
|
|
34
|
+
* same Promise ref keeps React's `use()` in "known fulfilled" state after
|
|
35
|
+
* the first observation.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function getMemoizedContentPromise(
|
|
40
|
+
component: ReactNode,
|
|
41
|
+
): Promise<ReactNode> {
|
|
42
|
+
if (component instanceof Promise) {
|
|
43
|
+
return component as Promise<ReactNode>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!objectContentCache || !primitiveContentCache) {
|
|
47
|
+
return Promise.resolve(component);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (component !== null && typeof component === "object") {
|
|
51
|
+
const cached = objectContentCache.get(component);
|
|
52
|
+
if (cached) {
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
const promise = Promise.resolve(component);
|
|
56
|
+
objectContentCache.set(component, promise);
|
|
57
|
+
return promise;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cached = primitiveContentCache.get(component);
|
|
61
|
+
if (cached) {
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
const promise = Promise.resolve(component);
|
|
65
|
+
primitiveContentCache.set(component, promise);
|
|
66
|
+
return promise;
|
|
67
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache of aggregate Promise.all results keyed on the first loader's
|
|
5
|
+
* `loaderData` reference. Each entry holds the source refs it was built from
|
|
6
|
+
* plus the resulting Promise/array; lookup scans entries for the matching
|
|
7
|
+
* source array (typically a single entry, since distinct loader groups rarely
|
|
8
|
+
* share a first source). Object first-refs live in a WeakMap (auto-GC);
|
|
9
|
+
* primitive first-refs (strings/numbers/booleans/null) live in a Map so
|
|
10
|
+
* loaders that resolve to primitive data are memoized too — bounded in
|
|
11
|
+
* practice by the application's loader set.
|
|
12
|
+
*
|
|
13
|
+
* Keying externally means reconciliation's fresh segment objects no longer
|
|
14
|
+
* drop memoization — the cache survives as long as the underlying loader
|
|
15
|
+
* segments do, and GC collects entries when those loaders are released
|
|
16
|
+
* (object keys only).
|
|
17
|
+
*
|
|
18
|
+
* Browser-only. On the server each SSR render needs a fresh Promise so
|
|
19
|
+
* Suspense can actually suspend and emit the loading fallback HTML before
|
|
20
|
+
* content streams. A shared already-resolved promise has `.status` attached
|
|
21
|
+
* by React on first `use()`; subsequent observations return synchronously
|
|
22
|
+
* and skip the fallback. The zero-loader case is especially prone because
|
|
23
|
+
* every empty-loader site would otherwise share one promise across requests.
|
|
24
|
+
*/
|
|
25
|
+
const IS_BROWSER = typeof window !== "undefined";
|
|
26
|
+
|
|
27
|
+
interface LoaderCacheEntry {
|
|
28
|
+
sources: any[];
|
|
29
|
+
promise: Promise<any[]> | any[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const objectLoaderCache = IS_BROWSER
|
|
33
|
+
? new WeakMap<object, LoaderCacheEntry[]>()
|
|
34
|
+
: null;
|
|
35
|
+
const primitiveLoaderCache = IS_BROWSER
|
|
36
|
+
? new Map<unknown, LoaderCacheEntry[]>()
|
|
37
|
+
: null;
|
|
38
|
+
|
|
39
|
+
// In the browser, a single shared empty aggregate is safe (and desirable) —
|
|
40
|
+
// reusing the same resolved promise keeps React's `use()` in a known-fulfilled
|
|
41
|
+
// state across renders. On the server it would leak `.status = "fulfilled"`
|
|
42
|
+
// across requests and skip the Suspense fallback, so we rebuild on each call.
|
|
43
|
+
const SHARED_EMPTY_LOADER_PROMISE: Promise<any[]> | null = IS_BROWSER
|
|
44
|
+
? Promise.resolve([])
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
function hasSameReferences(a: any[], b: any[]): boolean {
|
|
48
|
+
if (a.length !== b.length) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
for (let i = 0; i < a.length; i++) {
|
|
52
|
+
if (a[i] !== b[i]) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
|
|
60
|
+
if (loaders.length === 0) {
|
|
61
|
+
return Promise.resolve([]);
|
|
62
|
+
}
|
|
63
|
+
return Promise.all(
|
|
64
|
+
loaders.map((loader) =>
|
|
65
|
+
loader.loaderData instanceof Promise
|
|
66
|
+
? loader.loaderData
|
|
67
|
+
: Promise.resolve(loader.loaderData),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isObjectLike(value: unknown): value is object {
|
|
73
|
+
return (
|
|
74
|
+
value !== null && (typeof value === "object" || typeof value === "function")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Memoize an aggregate Promise.all for a set of loader segments. Reusing the
|
|
80
|
+
* same aggregate across renders — invalidated only when any underlying
|
|
81
|
+
* loader.loaderData ref changes — keeps React's `use()` in "known fulfilled"
|
|
82
|
+
* state and prevents a fresh Promise.all from suspending (and briefly
|
|
83
|
+
* committing the Suspense fallback) on every partial update that doesn't
|
|
84
|
+
* actually change loader data.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
export function getMemoizedLoaderPromise(
|
|
89
|
+
loaders: ResolvedSegment[],
|
|
90
|
+
): Promise<any[]> | any[] {
|
|
91
|
+
if (loaders.length === 0) {
|
|
92
|
+
return SHARED_EMPTY_LOADER_PROMISE ?? buildLoaderPromise(loaders);
|
|
93
|
+
}
|
|
94
|
+
if (!objectLoaderCache || !primitiveLoaderCache) {
|
|
95
|
+
return buildLoaderPromise(loaders);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sources = loaders.map((loader) => loader.loaderData);
|
|
99
|
+
const first = sources[0];
|
|
100
|
+
const entries = isObjectLike(first)
|
|
101
|
+
? objectLoaderCache.get(first)
|
|
102
|
+
: primitiveLoaderCache.get(first);
|
|
103
|
+
|
|
104
|
+
if (entries) {
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (hasSameReferences(entry.sources, sources)) {
|
|
107
|
+
return entry.promise;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const promise = buildLoaderPromise(loaders);
|
|
113
|
+
const newEntry: LoaderCacheEntry = { sources, promise };
|
|
114
|
+
if (entries) {
|
|
115
|
+
entries.push(newEntry);
|
|
116
|
+
} else if (isObjectLike(first)) {
|
|
117
|
+
objectLoaderCache.set(first, [newEntry]);
|
|
118
|
+
} else {
|
|
119
|
+
primitiveLoaderCache.set(first, [newEntry]);
|
|
120
|
+
}
|
|
121
|
+
return promise;
|
|
122
|
+
}
|
package/src/segment-system.tsx
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import { createElement, type ReactNode, type ComponentType } from "react";
|
|
2
3
|
import { OutletProvider } from "./client.js";
|
|
3
4
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
4
|
-
import type {
|
|
5
|
-
ResolvedSegment,
|
|
6
|
-
LoaderDataResult,
|
|
7
|
-
RootLayoutProps,
|
|
8
|
-
} from "./types.js";
|
|
5
|
+
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
9
6
|
import { isLoaderDataResult } from "./types.js";
|
|
10
7
|
import { invariant } from "./errors.js";
|
|
11
8
|
import {
|
|
@@ -13,6 +10,54 @@ import {
|
|
|
13
10
|
LoaderBoundary,
|
|
14
11
|
} from "./route-content-wrapper.js";
|
|
15
12
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
|
+
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
14
|
+
import { getMemoizedLoaderPromise } from "./segment-loader-promise.js";
|
|
15
|
+
|
|
16
|
+
// ViewTransition is only available in React experimental.
|
|
17
|
+
// Access via namespace import to avoid compile-time errors on stable React.
|
|
18
|
+
const ReactViewTransition: any =
|
|
19
|
+
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
20
|
+
|
|
21
|
+
function restoreParallelLoaderMarkers(
|
|
22
|
+
segments: ResolvedSegment[],
|
|
23
|
+
): ResolvedSegment[] {
|
|
24
|
+
const parallelLoadingByNamespace = new Map<string, ReactNode>();
|
|
25
|
+
let nextSegments: ResolvedSegment[] | null = null;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < segments.length; i++) {
|
|
28
|
+
const segment = segments[i];
|
|
29
|
+
|
|
30
|
+
if (segment.type === "parallel") {
|
|
31
|
+
if (
|
|
32
|
+
segment.namespace &&
|
|
33
|
+
segment.loading !== undefined &&
|
|
34
|
+
segment.loading !== null &&
|
|
35
|
+
segment.loading !== false
|
|
36
|
+
) {
|
|
37
|
+
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parallelLoading = segment.namespace
|
|
47
|
+
? parallelLoadingByNamespace.get(segment.namespace)
|
|
48
|
+
: undefined;
|
|
49
|
+
if (parallelLoading === undefined) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!nextSegments) {
|
|
54
|
+
nextSegments = segments.slice();
|
|
55
|
+
}
|
|
56
|
+
nextSegments[i] = { ...segment, parallelLoading };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return nextSegments ?? segments;
|
|
60
|
+
}
|
|
16
61
|
|
|
17
62
|
/**
|
|
18
63
|
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
@@ -137,6 +182,10 @@ export async function renderSegments(
|
|
|
137
182
|
} = options || {};
|
|
138
183
|
|
|
139
184
|
const temporalLazyRefs: Promise<any>[] = [];
|
|
185
|
+
const normalizedSegments = restoreParallelLoaderMarkers(segments);
|
|
186
|
+
const normalizedInterceptSegments = interceptSegments
|
|
187
|
+
? restoreParallelLoaderMarkers(interceptSegments)
|
|
188
|
+
: undefined;
|
|
140
189
|
|
|
141
190
|
/**
|
|
142
191
|
* Registers promises from lazy/async components for awaiting.
|
|
@@ -161,7 +210,7 @@ export async function renderSegments(
|
|
|
161
210
|
);
|
|
162
211
|
}
|
|
163
212
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
164
|
-
const tree = segmentTreeWalk(
|
|
213
|
+
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
165
214
|
// Render content segments as siblings
|
|
166
215
|
let content: ReactNode = null;
|
|
167
216
|
for (const node of tree) {
|
|
@@ -210,33 +259,34 @@ export async function renderSegments(
|
|
|
210
259
|
}
|
|
211
260
|
|
|
212
261
|
let nodeContent: ReactNode =
|
|
213
|
-
loading !== null && loading
|
|
262
|
+
loading !== null && loading !== undefined && loading !== false
|
|
214
263
|
? createElement(RouteContentWrapper, {
|
|
215
264
|
key: `suspense-loading-${id}`,
|
|
216
|
-
content:
|
|
217
|
-
resolvedComponent instanceof Promise
|
|
218
|
-
? resolvedComponent
|
|
219
|
-
: Promise.resolve(resolvedComponent),
|
|
265
|
+
content: getMemoizedContentPromise(resolvedComponent),
|
|
220
266
|
fallback: loading,
|
|
221
267
|
segmentId: id,
|
|
222
268
|
})
|
|
223
269
|
: registerLazyRef(resolvedComponent);
|
|
270
|
+
|
|
271
|
+
// Wrap with <ViewTransition> if transition config exists (React experimental only).
|
|
272
|
+
// An empty config ({}) creates a bare <ViewTransition> boundary that participates
|
|
273
|
+
// in transitions without adding custom animation classes. Named element-level
|
|
274
|
+
// <ViewTransition> components inside (with name/share props) morph independently
|
|
275
|
+
// from the parent's default cross-fade.
|
|
276
|
+
if (ReactViewTransition && node.segment.transition) {
|
|
277
|
+
nodeContent = createElement(ReactViewTransition, {
|
|
278
|
+
...node.segment.transition,
|
|
279
|
+
children: nodeContent,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
224
283
|
// Common props for OutletProvider
|
|
225
284
|
const outletContent: ReactNode =
|
|
226
285
|
node.segment.type === "layout" ? content : null;
|
|
227
286
|
|
|
228
287
|
// Prepare loader data if there are loaders
|
|
229
288
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
230
|
-
const loaderDataPromise =
|
|
231
|
-
loaderEntries.length > 0
|
|
232
|
-
? Promise.all(
|
|
233
|
-
loaderEntries.map((loader) =>
|
|
234
|
-
loader.loaderData instanceof Promise
|
|
235
|
-
? loader.loaderData
|
|
236
|
-
: Promise.resolve(loader.loaderData),
|
|
237
|
-
),
|
|
238
|
-
)
|
|
239
|
-
: Promise.resolve([]);
|
|
289
|
+
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
240
290
|
|
|
241
291
|
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
242
292
|
// This ensures cached segments (which may not have loader segments) have the same
|
|
@@ -265,13 +315,68 @@ export async function renderSegments(
|
|
|
265
315
|
children: nodeContent,
|
|
266
316
|
});
|
|
267
317
|
} else {
|
|
268
|
-
// Has loaders but no loading skeleton
|
|
269
|
-
|
|
318
|
+
// Has loaders but no loading skeleton.
|
|
319
|
+
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
320
|
+
// layout-owned loaders are awaited (they gate the layout content).
|
|
321
|
+
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
322
|
+
const parallelOwnedLoaders = loaderEntries.filter(
|
|
323
|
+
(l) => !!l.parallelLoading,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Await only layout-owned loaders
|
|
327
|
+
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
328
|
+
const layoutLoaderDataPromise =
|
|
329
|
+
layoutLoaders.length > 0
|
|
330
|
+
? Promise.all(
|
|
331
|
+
layoutLoaders.map((l) =>
|
|
332
|
+
l.loaderData instanceof Promise
|
|
333
|
+
? l.loaderData
|
|
334
|
+
: Promise.resolve(l.loaderData),
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
: Promise.resolve([]);
|
|
338
|
+
const resolvedData = await layoutLoaderDataPromise;
|
|
270
339
|
const { loaderData, errorFallback } = resolveLoaderData(
|
|
271
340
|
resolvedData,
|
|
272
|
-
|
|
341
|
+
layoutLoaderIds,
|
|
273
342
|
);
|
|
274
343
|
|
|
344
|
+
// Parallel-owned loaders: attach to their owning parallel segment
|
|
345
|
+
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
346
|
+
if (parallelOwnedLoaders.length > 0) {
|
|
347
|
+
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
348
|
+
|
|
349
|
+
for (const loader of parallelOwnedLoaders) {
|
|
350
|
+
if (!loader.namespace) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const existing = loadersByParallelNamespace.get(loader.namespace);
|
|
354
|
+
if (existing) {
|
|
355
|
+
existing.push(loader);
|
|
356
|
+
} else {
|
|
357
|
+
loadersByParallelNamespace.set(loader.namespace, [loader]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const p of node.parallel) {
|
|
362
|
+
if (!p.loading || !p.namespace) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
|
|
367
|
+
if (!ownedLoaders || ownedLoaders.length === 0) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
372
|
+
const aggregated = getMemoizedLoaderPromise(ownedLoaders);
|
|
373
|
+
p.loaderDataPromise =
|
|
374
|
+
(forceAwait || isAction) && aggregated instanceof Promise
|
|
375
|
+
? await aggregated
|
|
376
|
+
: aggregated;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
275
380
|
content = createElement(OutletProvider, {
|
|
276
381
|
key,
|
|
277
382
|
content: outletContent,
|
|
@@ -407,20 +512,13 @@ function* segmentTreeWalk(
|
|
|
407
512
|
}
|
|
408
513
|
}
|
|
409
514
|
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
nonParallels.sort((a, b) => {
|
|
415
|
-
if (a.id.length !== b.id.length) {
|
|
416
|
-
return a.id.length - b.id.length;
|
|
417
|
-
}
|
|
418
|
-
return a.id.localeCompare(b.id);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Iterate bottom-to-top using reverse() to process leaf segments first
|
|
515
|
+
// Segments arrive in root-to-leaf order from the server (resolveSegment
|
|
516
|
+
// and resolveSegmentWithRevalidation push segments in this order).
|
|
517
|
+
// All consumers (reconcileSegments, cache) preserve this order.
|
|
518
|
+
// No sorting needed — iterate bottom-to-top to process leaf segments first.
|
|
422
519
|
// This processes route/leaf layouts first, then parent layouts
|
|
423
520
|
// Note: We reverse the array to iterate from end to start (bottom-to-top)
|
|
521
|
+
|
|
424
522
|
for (let i = nonParallels.length - 1; i >= 0; i--) {
|
|
425
523
|
const segment = nonParallels[i];
|
|
426
524
|
|