@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.204030a9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +242 -55
- package/dist/bin/rango.js +277 -99
- package/dist/vite/index.js +2929 -1132
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +159 -10
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +262 -51
- package/skills/host-router/SKILL.md +243 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +291 -31
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +125 -102
- package/skills/rango/SKILL.md +242 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +91 -8
- package/skills/router-setup/SKILL.md +98 -8
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +511 -188
- package/skills/typesafety/SKILL.md +354 -50
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +92 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +183 -18
- package/src/browser/navigation-client.ts +187 -57
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +143 -40
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +200 -75
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +81 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +69 -26
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +5 -1
- package/src/client.tsx +95 -284
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +2 -5
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +15 -40
- package/src/index.ts +92 -76
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +105 -39
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +175 -23
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +129 -193
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +124 -18
- package/src/router/metrics.ts +239 -14
- package/src/router/middleware-types.ts +61 -31
- package/src/router/middleware.ts +226 -124
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +118 -19
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +85 -9
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +91 -29
- package/src/router/router-options.ts +89 -19
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +240 -23
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +483 -289
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +38 -15
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +120 -32
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +524 -370
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -21
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +39 -10
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +69 -89
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +39 -47
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +19 -3
- package/src/search-params.ts +20 -17
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +285 -63
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +228 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +151 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +112 -0
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +197 -79
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -6
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +63 -91
- package/src/vite/discovery/discover-routers.ts +106 -53
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +222 -107
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +50 -13
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +10 -3
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +924 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +98 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/route-definition/route-function.ts +0 -119
package/src/server/context.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
ShouldRevalidateFn,
|
|
11
11
|
TransitionConfig,
|
|
12
12
|
} from "../types";
|
|
13
|
-
import { invariant } from "../errors";
|
|
13
|
+
import { invariant, DslContextError } from "../errors";
|
|
14
14
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
15
15
|
|
|
16
16
|
// ============================================================================
|
|
@@ -26,6 +26,7 @@ export interface PerformanceMetric {
|
|
|
26
26
|
label: string; // e.g., "route-matching", "loader:UserLoader"
|
|
27
27
|
duration: number; // milliseconds
|
|
28
28
|
startTime: number; // relative to request start
|
|
29
|
+
depth?: number; // nesting level for hierarchical display (0 = top-level)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -39,7 +40,7 @@ export interface MetricsStore {
|
|
|
39
40
|
metrics: PerformanceMetric[];
|
|
40
41
|
}
|
|
41
42
|
// ============================================================================
|
|
42
|
-
//
|
|
43
|
+
// Rango Context
|
|
43
44
|
// ============================================================================
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -70,6 +71,10 @@ export type EntryPropCommon = {
|
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
/**
|
|
74
|
+
* Attachments resolved by walking the parent chain, not owned by the entry:
|
|
75
|
+
* middleware composes downward; revalidate and the error/notFound boundaries are
|
|
76
|
+
* resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
|
|
77
|
+
*
|
|
73
78
|
* @internal This type is an implementation detail and may change without notice.
|
|
74
79
|
*/
|
|
75
80
|
export type EntryPropDatas = {
|
|
@@ -79,6 +84,16 @@ export type EntryPropDatas = {
|
|
|
79
84
|
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
80
85
|
};
|
|
81
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Render-time presentation fields shared by every entry variant.
|
|
89
|
+
*
|
|
90
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
91
|
+
*/
|
|
92
|
+
export type EntryPropRender = {
|
|
93
|
+
loading?: ReactNode | false;
|
|
94
|
+
transition?: TransitionConfig;
|
|
95
|
+
};
|
|
96
|
+
|
|
82
97
|
/**
|
|
83
98
|
* Loader entry stored in EntryData
|
|
84
99
|
* Contains the loader definition and its revalidation rules
|
|
@@ -156,10 +171,29 @@ export type InterceptEntry = {
|
|
|
156
171
|
when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
|
|
157
172
|
};
|
|
158
173
|
|
|
174
|
+
export interface ParallelEntryData
|
|
175
|
+
extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
|
|
176
|
+
type: "parallel";
|
|
177
|
+
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
178
|
+
/** Set when any parallel slot is a Static definition */
|
|
179
|
+
isStaticPrerender?: true;
|
|
180
|
+
/** Per-slot static handler $$ids for build-time store lookup */
|
|
181
|
+
staticHandlerIds?: Record<string, string>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* This entry's own structural children plus its owned loaders. `loader` lives
|
|
188
|
+
* here (not in EntryPropDatas) because loaders are owned by the entry, not
|
|
189
|
+
* inherited from ancestors.
|
|
190
|
+
*
|
|
191
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
192
|
+
*/
|
|
159
193
|
export type EntryPropSegments = {
|
|
160
194
|
loader: LoaderEntry[];
|
|
161
195
|
layout: EntryData[];
|
|
162
|
-
parallel:
|
|
196
|
+
parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
|
|
163
197
|
intercept: InterceptEntry[]; // intercept definitions for soft navigation
|
|
164
198
|
};
|
|
165
199
|
|
|
@@ -167,8 +201,6 @@ export type EntryData =
|
|
|
167
201
|
| ({
|
|
168
202
|
type: "route";
|
|
169
203
|
handler: Handler<any, any, any>;
|
|
170
|
-
loading?: ReactNode | false;
|
|
171
|
-
transition?: TransitionConfig;
|
|
172
204
|
/** URL pattern for this route (used by path() in urls()) */
|
|
173
205
|
pattern?: string;
|
|
174
206
|
/** Set when handler is a Prerender definition */
|
|
@@ -176,8 +208,12 @@ export type EntryData =
|
|
|
176
208
|
/** Original PrerenderHandlerDefinition (for build-time getParams access) */
|
|
177
209
|
prerenderDef?: {
|
|
178
210
|
getParams?: (ctx: any) => Promise<any[]> | any[];
|
|
179
|
-
options?: {
|
|
211
|
+
options?: { concurrency?: number };
|
|
180
212
|
};
|
|
213
|
+
/** Set when route is wrapped with Passthrough() — has a separate live handler */
|
|
214
|
+
isPassthrough?: true;
|
|
215
|
+
/** Live handler for runtime fallback (only set on Passthrough routes) */
|
|
216
|
+
liveHandler?: Handler<any, any, any>;
|
|
181
217
|
/** Set when handler is a Static definition (build-time only) */
|
|
182
218
|
isStaticPrerender?: true;
|
|
183
219
|
/** Static handler $$id for build-time store lookup */
|
|
@@ -186,40 +222,28 @@ export type EntryData =
|
|
|
186
222
|
responseType?: string;
|
|
187
223
|
} & EntryPropCommon &
|
|
188
224
|
EntryPropDatas &
|
|
189
|
-
EntryPropSegments
|
|
225
|
+
EntryPropSegments &
|
|
226
|
+
EntryPropRender)
|
|
190
227
|
| ({
|
|
191
228
|
type: "layout";
|
|
192
229
|
handler: ReactNode | Handler<any, any, any>;
|
|
193
|
-
loading?: ReactNode | false;
|
|
194
|
-
transition?: TransitionConfig;
|
|
195
230
|
/** Set when handler is a Static definition (build-time only) */
|
|
196
231
|
isStaticPrerender?: true;
|
|
197
232
|
/** Static handler $$id for build-time store lookup */
|
|
198
233
|
staticHandlerId?: string;
|
|
199
234
|
} & EntryPropCommon &
|
|
200
235
|
EntryPropDatas &
|
|
201
|
-
EntryPropSegments
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
205
|
-
loading?: ReactNode | false;
|
|
206
|
-
transition?: TransitionConfig;
|
|
207
|
-
/** Set when any parallel slot is a Static definition */
|
|
208
|
-
isStaticPrerender?: true;
|
|
209
|
-
/** Per-slot static handler $$ids for build-time store lookup */
|
|
210
|
-
staticHandlerIds?: Record<string, string>;
|
|
211
|
-
} & EntryPropCommon &
|
|
212
|
-
EntryPropDatas &
|
|
213
|
-
EntryPropSegments)
|
|
236
|
+
EntryPropSegments &
|
|
237
|
+
EntryPropRender)
|
|
238
|
+
| ParallelEntryData
|
|
214
239
|
| ({
|
|
215
240
|
type: "cache";
|
|
216
241
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
217
242
|
handler: ReactNode | Handler<any, any, any>;
|
|
218
|
-
loading?: ReactNode | false;
|
|
219
|
-
transition?: TransitionConfig;
|
|
220
243
|
} & EntryPropCommon &
|
|
221
244
|
EntryPropDatas &
|
|
222
|
-
EntryPropSegments
|
|
245
|
+
EntryPropSegments &
|
|
246
|
+
EntryPropRender);
|
|
223
247
|
|
|
224
248
|
/**
|
|
225
249
|
* Tracked include info for build-time manifest generation
|
|
@@ -269,6 +293,25 @@ interface HelperContext {
|
|
|
269
293
|
string,
|
|
270
294
|
import("../cache/profile-registry.js").CacheProfile
|
|
271
295
|
>;
|
|
296
|
+
/** True when resolving handlers inside a cache() DSL boundary.
|
|
297
|
+
* Read by ctx.get() to guard non-cacheable variable reads. */
|
|
298
|
+
insideCacheScope?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Include scope string applied to direct-descendant shortCodes.
|
|
301
|
+
*
|
|
302
|
+
* Each `include(...)` call allocates a sibling-positional token like `I0`,
|
|
303
|
+
* `I1` from its parent's include counter and stores the composed scope
|
|
304
|
+
* (`${parentScope}I${idx}`) in its lazyContext. When the include's handler
|
|
305
|
+
* evaluates lazily, the store's `includeScope` is set from that context so
|
|
306
|
+
* every direct-descendant shortCode is generated as
|
|
307
|
+
* `${parent.shortCode}${includeScope}${prefix}${index}` — preventing
|
|
308
|
+
* collisions with siblings declared outside the include.
|
|
309
|
+
*
|
|
310
|
+
* The scope is NOT propagated through `store.run(...)`, so layouts /
|
|
311
|
+
* parallels / caches inside the include absorb the scope into their own
|
|
312
|
+
* shortCodes and their children start fresh.
|
|
313
|
+
*/
|
|
314
|
+
includeScope?: string;
|
|
272
315
|
}
|
|
273
316
|
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
274
317
|
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
@@ -276,10 +319,28 @@ interface HelperContext {
|
|
|
276
319
|
// hold references to the old instance — causing getStore() to return
|
|
277
320
|
// undefined even inside a run() callback.
|
|
278
321
|
const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
|
|
279
|
-
export const
|
|
322
|
+
export const RangoContext: AsyncLocalStorage<HelperContext> = ((
|
|
280
323
|
globalThis as any
|
|
281
324
|
)[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
|
|
282
325
|
|
|
326
|
+
/** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
|
|
327
|
+
const SHORT_CODE_PREFIX: Record<
|
|
328
|
+
"layout" | "parallel" | "route" | "loader" | "cache",
|
|
329
|
+
string
|
|
330
|
+
> = {
|
|
331
|
+
layout: "L",
|
|
332
|
+
parallel: "P",
|
|
333
|
+
route: "R",
|
|
334
|
+
loader: "D",
|
|
335
|
+
cache: "C",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** Post-increment a named per-store counter, returning the prior value. */
|
|
339
|
+
function bumpCounter(store: HelperContext, key: string): number {
|
|
340
|
+
store.counters[key] ??= 0;
|
|
341
|
+
return store.counters[key]++;
|
|
342
|
+
}
|
|
343
|
+
|
|
283
344
|
export const getContext = (): {
|
|
284
345
|
context: AsyncLocalStorage<HelperContext>;
|
|
285
346
|
getStore: () => HelperContext;
|
|
@@ -303,12 +364,12 @@ export const getContext = (): {
|
|
|
303
364
|
callback: (...args: any[]) => T,
|
|
304
365
|
) => T;
|
|
305
366
|
} => {
|
|
306
|
-
const context =
|
|
367
|
+
const context = RangoContext;
|
|
307
368
|
|
|
308
369
|
return {
|
|
309
370
|
context,
|
|
310
371
|
getOrCreateStore: (forRoute?: string): HelperContext => {
|
|
311
|
-
let store =
|
|
372
|
+
let store = RangoContext.getStore();
|
|
312
373
|
if (!store) {
|
|
313
374
|
store = {
|
|
314
375
|
manifest: new Map<string, EntryData>(),
|
|
@@ -328,7 +389,7 @@ export const getContext = (): {
|
|
|
328
389
|
const store = context.getStore();
|
|
329
390
|
if (!store) {
|
|
330
391
|
throw new Error(
|
|
331
|
-
"
|
|
392
|
+
"Rango context store is not available. Make sure to run within Rango context.",
|
|
332
393
|
);
|
|
333
394
|
}
|
|
334
395
|
return store;
|
|
@@ -345,48 +406,36 @@ export const getContext = (): {
|
|
|
345
406
|
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
|
|
346
407
|
) => {
|
|
347
408
|
const store = context.getStore();
|
|
348
|
-
invariant(store, "No context
|
|
349
|
-
store
|
|
350
|
-
const index = store.counters[type];
|
|
351
|
-
store.counters[type] = index + 1;
|
|
352
|
-
return `$${type}.${index}`;
|
|
409
|
+
invariant(store, "No context RangoContext available");
|
|
410
|
+
return `$${type}.${bumpCounter(store, type)}`;
|
|
353
411
|
},
|
|
354
412
|
getShortCode: (
|
|
355
413
|
type: "layout" | "parallel" | "route" | "loader" | "cache",
|
|
356
414
|
) => {
|
|
357
415
|
const store = context.getStore();
|
|
358
|
-
invariant(store, "No context
|
|
416
|
+
invariant(store, "No context RangoContext available");
|
|
359
417
|
|
|
360
418
|
const parent = store.parent;
|
|
361
|
-
const prefix =
|
|
362
|
-
type === "layout"
|
|
363
|
-
? "L"
|
|
364
|
-
: type === "parallel"
|
|
365
|
-
? "P"
|
|
366
|
-
: type === "loader"
|
|
367
|
-
? "D"
|
|
368
|
-
: type === "cache"
|
|
369
|
-
? "C"
|
|
370
|
-
: "R";
|
|
419
|
+
const prefix = SHORT_CODE_PREFIX[type];
|
|
371
420
|
const mountPrefix =
|
|
372
421
|
store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
|
|
373
422
|
|
|
423
|
+
const includeScope = store.includeScope ?? "";
|
|
424
|
+
|
|
374
425
|
if (!parent) {
|
|
375
426
|
// Root entry: prefix with mount index and use mount-scoped counter
|
|
376
427
|
const counterKey = mountPrefix
|
|
377
428
|
? `${mountPrefix}_root_${type}`
|
|
378
429
|
: `root_${type}`;
|
|
379
|
-
store
|
|
380
|
-
const index = store.counters[counterKey];
|
|
381
|
-
store.counters[counterKey] = index + 1;
|
|
382
|
-
return `${mountPrefix}${prefix}${index}`;
|
|
430
|
+
return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
|
|
383
431
|
} else {
|
|
384
|
-
// Child entry: use parent-scoped counter
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
432
|
+
// Child entry: use parent-scoped counter with includeScope appended.
|
|
433
|
+
// When we're evaluating a lazy include's direct children, includeScope
|
|
434
|
+
// is a per-include token like "I0" / "I1I0" that partitions the
|
|
435
|
+
// parent's counter namespace so routes inside one include cannot
|
|
436
|
+
// collide with siblings declared outside it.
|
|
437
|
+
const counterKey = `${parent.shortCode}${includeScope}_${type}`;
|
|
438
|
+
return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
|
|
390
439
|
}
|
|
391
440
|
},
|
|
392
441
|
runWithStore: <T>(
|
|
@@ -413,6 +462,7 @@ export const getContext = (): {
|
|
|
413
462
|
rootScoped: store.rootScoped,
|
|
414
463
|
trackedIncludes: store.trackedIncludes,
|
|
415
464
|
cacheProfiles: store.cacheProfiles,
|
|
465
|
+
includeScope: store.includeScope,
|
|
416
466
|
},
|
|
417
467
|
callback,
|
|
418
468
|
);
|
|
@@ -459,6 +509,31 @@ export const getContext = (): {
|
|
|
459
509
|
};
|
|
460
510
|
};
|
|
461
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Acquire the active DSL build context, throwing `message` if a helper was
|
|
514
|
+
* called outside a urls()/map() builder. Returns the store API and the live
|
|
515
|
+
* HelperContext so callers avoid a second getContext() lookup.
|
|
516
|
+
*/
|
|
517
|
+
export function requireDslContext(message: string): {
|
|
518
|
+
store: ReturnType<typeof getContext>;
|
|
519
|
+
ctx: HelperContext;
|
|
520
|
+
} {
|
|
521
|
+
const store = getContext();
|
|
522
|
+
const ctx = store.context.getStore();
|
|
523
|
+
if (!ctx) {
|
|
524
|
+
// The only reason the store is absent here is that a route-definition helper
|
|
525
|
+
// ran with no active RangoContext — i.e. outside a urls()/map() builder.
|
|
526
|
+
// Record that as the cause so the throw is self-explanatory, not a bare
|
|
527
|
+
// "must be called inside urls()" with no indication of the mechanism.
|
|
528
|
+
throw new DslContextError(message, {
|
|
529
|
+
cause:
|
|
530
|
+
"RangoContext store is undefined: a route-definition helper was called " +
|
|
531
|
+
"outside an active urls()/map() builder.",
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return { store, ctx };
|
|
535
|
+
}
|
|
536
|
+
|
|
462
537
|
/**
|
|
463
538
|
* Run a callback with specific URL and name prefixes
|
|
464
539
|
* Used by include() to apply prefixes to nested patterns
|
|
@@ -468,7 +543,7 @@ export function runWithPrefixes<T>(
|
|
|
468
543
|
namePrefix: string | undefined,
|
|
469
544
|
callback: () => T,
|
|
470
545
|
): T {
|
|
471
|
-
const store =
|
|
546
|
+
const store = RangoContext.getStore();
|
|
472
547
|
if (!store) {
|
|
473
548
|
throw new Error("runWithPrefixes must be called within router context");
|
|
474
549
|
}
|
|
@@ -513,7 +588,7 @@ export function runWithPrefixes<T>(
|
|
|
513
588
|
? (store.rootScoped ?? false)
|
|
514
589
|
: store.rootScoped;
|
|
515
590
|
|
|
516
|
-
return
|
|
591
|
+
return RangoContext.run(
|
|
517
592
|
{
|
|
518
593
|
...store,
|
|
519
594
|
urlPrefix: combinedUrlPrefix,
|
|
@@ -528,7 +603,7 @@ export function runWithPrefixes<T>(
|
|
|
528
603
|
* Get current URL prefix from context
|
|
529
604
|
*/
|
|
530
605
|
export function getUrlPrefix(): string {
|
|
531
|
-
const store =
|
|
606
|
+
const store = RangoContext.getStore();
|
|
532
607
|
return store?.urlPrefix || "";
|
|
533
608
|
}
|
|
534
609
|
|
|
@@ -536,7 +611,7 @@ export function getUrlPrefix(): string {
|
|
|
536
611
|
* Get current name prefix from context
|
|
537
612
|
*/
|
|
538
613
|
export function getNamePrefix(): string | undefined {
|
|
539
|
-
const store =
|
|
614
|
+
const store = RangoContext.getStore();
|
|
540
615
|
return store?.namePrefix;
|
|
541
616
|
}
|
|
542
617
|
|
|
@@ -545,13 +620,87 @@ export function getNamePrefix(): string | undefined {
|
|
|
545
620
|
* Returns true at root or inside { name: "" } includes, false inside named includes.
|
|
546
621
|
*/
|
|
547
622
|
export function getRootScoped(): boolean {
|
|
548
|
-
const store =
|
|
623
|
+
const store = RangoContext.getStore();
|
|
549
624
|
return store?.rootScoped ?? true;
|
|
550
625
|
}
|
|
551
626
|
|
|
552
627
|
// Export HelperContext type for use in other modules
|
|
553
628
|
export type { HelperContext };
|
|
554
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Return an isolated copy of a lazy include's captured parent entry.
|
|
632
|
+
*
|
|
633
|
+
* DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
|
|
634
|
+
* Multiple include() scopes capture the *same* syntheticMapRoot as their
|
|
635
|
+
* parent, so without isolation one include's loaders/middleware leak into
|
|
636
|
+
* every other route that shares that root.
|
|
637
|
+
*
|
|
638
|
+
* The clone is shallow: only the mutable arrays are copied so each
|
|
639
|
+
* include pushes to its own list. The rest of the entry (id, shortCode,
|
|
640
|
+
* parent pointer, handler) stays shared, which is correct and cheap.
|
|
641
|
+
*/
|
|
642
|
+
export function getIsolatedLazyParent(
|
|
643
|
+
captured: EntryData | null | undefined,
|
|
644
|
+
): EntryData | null {
|
|
645
|
+
if (!captured) return null;
|
|
646
|
+
return {
|
|
647
|
+
...captured,
|
|
648
|
+
loader: [...captured.loader],
|
|
649
|
+
middleware: [...captured.middleware],
|
|
650
|
+
revalidate: [...captured.revalidate],
|
|
651
|
+
errorBoundary: [...captured.errorBoundary],
|
|
652
|
+
notFoundBoundary: [...captured.notFoundBoundary],
|
|
653
|
+
layout: [...captured.layout],
|
|
654
|
+
parallel: { ...captured.parallel },
|
|
655
|
+
intercept: [...captured.intercept],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function getParallelEntries(
|
|
660
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
661
|
+
): ParallelEntryData[] {
|
|
662
|
+
if (!parallels) return [];
|
|
663
|
+
if (Array.isArray(parallels)) {
|
|
664
|
+
return parallels.filter(
|
|
665
|
+
(entry): entry is ParallelEntryData => entry.type === "parallel",
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return Object.values(parallels).filter(
|
|
669
|
+
(entry): entry is ParallelEntryData => !!entry,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function getParallelSlotEntries(
|
|
674
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
675
|
+
): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
|
|
676
|
+
if (!parallels) return [];
|
|
677
|
+
|
|
678
|
+
if (Array.isArray(parallels)) {
|
|
679
|
+
return getParallelEntries(parallels).flatMap((entry) =>
|
|
680
|
+
(Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
|
|
681
|
+
slot,
|
|
682
|
+
entry,
|
|
683
|
+
})),
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return Object.entries(parallels)
|
|
688
|
+
.filter(([, entry]) => !!entry)
|
|
689
|
+
.map(([slot, entry]) => ({
|
|
690
|
+
slot: slot as `@${string}`,
|
|
691
|
+
entry: entry!,
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function getParallelSlotCount(
|
|
696
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
697
|
+
): number {
|
|
698
|
+
if (!parallels) return 0;
|
|
699
|
+
return Array.isArray(parallels)
|
|
700
|
+
? parallels.filter((entry) => entry?.type === "parallel").length
|
|
701
|
+
: Object.keys(parallels).length;
|
|
702
|
+
}
|
|
703
|
+
|
|
555
704
|
// ============================================================================
|
|
556
705
|
// Performance Metrics Helpers
|
|
557
706
|
// ============================================================================
|
|
@@ -567,8 +716,8 @@ export type { HelperContext };
|
|
|
567
716
|
* done(); // Records duration
|
|
568
717
|
* ```
|
|
569
718
|
*/
|
|
570
|
-
export function track(label: string): () => void {
|
|
571
|
-
const store =
|
|
719
|
+
export function track(label: string, depth?: number): () => void {
|
|
720
|
+
const store = RangoContext.getStore();
|
|
572
721
|
|
|
573
722
|
// No-op if context unavailable or metrics not enabled
|
|
574
723
|
if (!store?.metrics?.enabled) {
|
|
@@ -580,6 +729,79 @@ export function track(label: string): () => void {
|
|
|
580
729
|
return () => {
|
|
581
730
|
const duration =
|
|
582
731
|
performance.now() - store.metrics!.requestStart - startTime;
|
|
583
|
-
store.metrics!.metrics.push({
|
|
732
|
+
store.metrics!.metrics.push({
|
|
733
|
+
label,
|
|
734
|
+
duration,
|
|
735
|
+
startTime,
|
|
736
|
+
...(depth != null ? { depth } : {}),
|
|
737
|
+
});
|
|
584
738
|
};
|
|
585
739
|
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Separate ALS for tracking loader execution scope.
|
|
743
|
+
* Uses a dedicated ALS (not RangoContext) to avoid issues with
|
|
744
|
+
* nested RangoContext.run() calls in Vite's module runner.
|
|
745
|
+
*/
|
|
746
|
+
const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
|
|
747
|
+
const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
748
|
+
globalThis as any
|
|
749
|
+
)[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
750
|
+
|
|
751
|
+
// Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
|
|
752
|
+
// of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
|
|
753
|
+
// invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
|
|
754
|
+
// request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
|
|
755
|
+
// so rendered()/barrier/deadlock gating (which must distinguish DSL from
|
|
756
|
+
// handler-invoked loaders) is unchanged.
|
|
757
|
+
const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
|
|
758
|
+
const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
759
|
+
globalThis as any
|
|
760
|
+
)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Check if the current execution is inside a cache() DSL boundary.
|
|
764
|
+
* Returns false inside loader execution — loaders are always fresh
|
|
765
|
+
* (never cached), so non-cacheable reads are safe.
|
|
766
|
+
*/
|
|
767
|
+
export function isInsideCacheScope(): boolean {
|
|
768
|
+
if (RangoContext.getStore()?.insideCacheScope !== true) return false;
|
|
769
|
+
// Loaders are always fresh — even inside a cache() boundary, the loader
|
|
770
|
+
// function re-executes on every request. Skip the guard when running
|
|
771
|
+
// inside a loader.
|
|
772
|
+
if (loaderScopeALS.getStore()?.active) return false;
|
|
773
|
+
// Also exempt handler-invoked loaders: their bodies run in a loader-body
|
|
774
|
+
// scope (not the DSL loader scope above), so request-scoped reads inside any
|
|
775
|
+
// loader — however invoked — are safe (loaders always re-run fresh).
|
|
776
|
+
if (loaderBodyScopeALS.getStore()?.active) return false;
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Check if the current execution is inside a DSL loader scope
|
|
782
|
+
* (wrapped by runInsideLoaderScope). Used by rendered() barrier
|
|
783
|
+
* to distinguish DSL loaders from handler-invoked loaders.
|
|
784
|
+
*/
|
|
785
|
+
export function isInsideLoaderScope(): boolean {
|
|
786
|
+
return loaderScopeALS.getStore()?.active === true;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Run `fn` inside a loader scope. While active, cache-scope guards
|
|
791
|
+
* are bypassed because loaders are always fresh (never cached) and
|
|
792
|
+
* their side effects (setCookie, header, etc.) are safe.
|
|
793
|
+
*/
|
|
794
|
+
export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
795
|
+
return loaderScopeALS.run({ active: true }, fn);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Run `fn` inside a loader BODY scope. Marks loader-function execution for the
|
|
800
|
+
* cache-purity guard only (isInsideCacheScope), WITHOUT affecting
|
|
801
|
+
* isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
|
|
802
|
+
* and handler-invoked via ctx.use) so request-scoped reads inside a loader
|
|
803
|
+
* never trip the cache-scope guards — loaders always run fresh.
|
|
804
|
+
*/
|
|
805
|
+
export function runInsideLoaderBodyScope<T>(fn: () => T): T {
|
|
806
|
+
return loaderBodyScopeALS.run({ active: true }, fn);
|
|
807
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { CookieOptions } from "../router/middleware-types.js";
|
|
11
11
|
import { getRequestContext } from "./request-context.js";
|
|
12
|
+
import { isInsideCacheScope } from "./context.js";
|
|
12
13
|
import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -84,10 +85,23 @@ export interface ReadonlyHeaders {
|
|
|
84
85
|
type HeadersIterator<T> = IterableIterator<T>;
|
|
85
86
|
|
|
86
87
|
/**
|
|
87
|
-
* Throw if called inside a "use cache" function
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* Throw if called inside a cache boundary — either a "use cache" function
|
|
89
|
+
* (`INSIDE_CACHE_EXEC` stamped on ctx by the cache runtime) or a `cache()`
|
|
90
|
+
* DSL boundary (`isInsideCacheScope()` — the render-store flag set while
|
|
91
|
+
* resolving a `type: "cache"` route entry).
|
|
92
|
+
*
|
|
93
|
+
* Reading request-scoped data (cookies, headers) inside a cached scope
|
|
94
|
+
* produces per-request values that are NOT reflected in the cache key, so
|
|
95
|
+
* they would be frozen into the shared cache entry and served to the wrong
|
|
96
|
+
* users. This is the same hazard for both scopes: a `cache()` boundary caches
|
|
97
|
+
* everything except loaders (it is the document-level "PPR shell"), so a read
|
|
98
|
+
* here is baked into the shell exactly like a `"use cache"` return value is
|
|
99
|
+
* baked into its cache entry.
|
|
100
|
+
*
|
|
101
|
+
* `isInsideCacheScope()` returns false inside loaders (loaders always run
|
|
102
|
+
* fresh on every request, even on a cache hit), so reading cookies()/headers()
|
|
103
|
+
* from a loader is allowed — loaders are the dynamic "holes" of a cached
|
|
104
|
+
* document.
|
|
91
105
|
*/
|
|
92
106
|
function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
|
|
93
107
|
if (
|
|
@@ -106,6 +120,16 @@ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
|
|
|
106
120
|
` const data = await getCachedData(locale); // locale is now in the cache key`,
|
|
107
121
|
);
|
|
108
122
|
}
|
|
123
|
+
if (isInsideCacheScope()) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`${fnName}() cannot be called inside a cache() boundary. ` +
|
|
126
|
+
`A cache() scope caches everything except loaders, so request-scoped ` +
|
|
127
|
+
`data (cookies, headers) read here would be frozen into the shared ` +
|
|
128
|
+
`cached shell and served to other users. Read it inside a loader ` +
|
|
129
|
+
`instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
|
|
130
|
+
` loader("user", () => getUser(cookies().get("session")?.value));`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
|
|
@@ -13,6 +13,25 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Build a HandleData snapshot from a HandleStore using segment ordering.
|
|
18
|
+
* Reads data directly from the store for each segment in order.
|
|
19
|
+
*/
|
|
20
|
+
export function buildHandleSnapshot(
|
|
21
|
+
handleStore: HandleStore,
|
|
22
|
+
segmentOrder: string[],
|
|
23
|
+
): HandleData {
|
|
24
|
+
const data: HandleData = {};
|
|
25
|
+
for (const segmentId of segmentOrder) {
|
|
26
|
+
const segData = handleStore.getDataForSegment(segmentId);
|
|
27
|
+
for (const handleName in segData) {
|
|
28
|
+
if (!data[handleName]) data[handleName] = {};
|
|
29
|
+
data[handleName][segmentId] = segData[handleName];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
function createLateHandlePushError(
|
|
17
36
|
handleName: string,
|
|
18
37
|
segmentId: string,
|
|
@@ -44,20 +44,21 @@ export function setLoaderImports(
|
|
|
44
44
|
export async function getLoaderLazy(
|
|
45
45
|
id: string,
|
|
46
46
|
): Promise<LoaderRegistryEntry | undefined> {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return existing;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check the fetchable loader registry (populated by createLoader)
|
|
47
|
+
// Always check fetchableLoaderRegistry first — it's the source of truth.
|
|
48
|
+
// createLoader() updates it during module re-evaluation (HMR), so checking
|
|
49
|
+
// here ensures we pick up the fresh function after a loader file change.
|
|
54
50
|
const fetchable = getFetchableLoader(id);
|
|
55
51
|
if (fetchable) {
|
|
56
|
-
// Cache in main registry for future requests
|
|
57
52
|
loaderRegistry.set(id, fetchable);
|
|
58
53
|
return fetchable;
|
|
59
54
|
}
|
|
60
55
|
|
|
56
|
+
// Fall back to local cache (populated by previous lazy imports in production)
|
|
57
|
+
const existing = loaderRegistry.get(id);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
|
|
61
62
|
// Try to lazy load from the import map (production mode)
|
|
62
63
|
if (lazyLoaderImports && lazyLoaderImports.size > 0) {
|
|
63
64
|
const lazyImport = lazyLoaderImports.get(id);
|