@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125
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/dist/bin/rango.js +10 -6
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +55 -48
- package/package.json +61 -21
- package/skills/caching/SKILL.md +2 -1
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +12 -0
- package/skills/route/SKILL.md +10 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +118 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +26 -51
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +1 -83
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -99
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -2
- package/src/browser/validate-redirect-origin.ts +4 -5
- package/src/build/route-trie.ts +3 -0
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +27 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +94 -46
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +11 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +2 -48
- package/src/cache/profile-registry.ts +7 -3
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +1 -22
- package/src/client.tsx +14 -38
- package/src/component-utils.ts +19 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +42 -3
- package/src/index.ts +31 -1
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +58 -3
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +11 -1
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -30
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +1 -54
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -21
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-cookies.ts +0 -13
- package/src/router/middleware-types.ts +0 -115
- package/src/router/middleware.ts +7 -30
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +1 -33
- package/src/router/prerender-match.ts +33 -45
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +5 -58
- package/src/router/router-context.ts +0 -26
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +10 -13
- package/src/router/segment-resolution/revalidation.ts +5 -42
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +63 -40
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +40 -9
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/helpers.ts +34 -0
- package/src/rsc/origin-guard.ts +0 -12
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +30 -28
- package/src/rsc/types.ts +2 -1
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +74 -77
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +119 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +127 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +186 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +311 -0
- package/src/testing/render-route.tsx +504 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +16 -13
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +6 -7
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +3 -1
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/use-cache-transform.ts +0 -36
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +1 -108
- package/src/vite/router-discovery.ts +2 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
package/src/segment-system.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { createElement, type ReactNode, type ComponentType } from "react";
|
|
3
|
-
import { OutletProvider } from "./
|
|
3
|
+
import { OutletProvider } from "./outlet-provider.js";
|
|
4
4
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
5
5
|
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
6
6
|
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
@@ -11,13 +11,25 @@ import {
|
|
|
11
11
|
} from "./route-content-wrapper.js";
|
|
12
12
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
13
|
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildLoaderPromise,
|
|
16
|
+
getMemoizedLoaderPromise,
|
|
17
|
+
} from "./segment-loader-promise.js";
|
|
15
18
|
|
|
16
19
|
// ViewTransition is only available in React experimental.
|
|
17
20
|
// Access via namespace import to avoid compile-time errors on stable React.
|
|
18
21
|
const ReactViewTransition: any =
|
|
19
22
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
20
23
|
|
|
24
|
+
// A loading skeleton is renderable only when it is a real ReactNode value.
|
|
25
|
+
// `false` is treated as "not renderable" here. This is the three-term gate;
|
|
26
|
+
// the distinct two-term gate at the LoaderBoundary site deliberately treats
|
|
27
|
+
// `false` as "create a boundary without a RouteContentWrapper"
|
|
28
|
+
// (tree-structure.md), so it must NOT use this helper.
|
|
29
|
+
function isRenderableLoading(loading: ReactNode): boolean {
|
|
30
|
+
return loading !== undefined && loading !== null && loading !== false;
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
function restoreParallelLoaderMarkers(
|
|
22
34
|
segments: ResolvedSegment[],
|
|
23
35
|
): ResolvedSegment[] {
|
|
@@ -28,12 +40,7 @@ function restoreParallelLoaderMarkers(
|
|
|
28
40
|
const segment = segments[i];
|
|
29
41
|
|
|
30
42
|
if (segment.type === "parallel") {
|
|
31
|
-
if (
|
|
32
|
-
segment.namespace &&
|
|
33
|
-
segment.loading !== undefined &&
|
|
34
|
-
segment.loading !== null &&
|
|
35
|
-
segment.loading !== false
|
|
36
|
-
) {
|
|
43
|
+
if (segment.namespace && isRenderableLoading(segment.loading)) {
|
|
37
44
|
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
38
45
|
}
|
|
39
46
|
continue;
|
|
@@ -142,8 +149,14 @@ function wrapDefaultOutletContent(
|
|
|
142
149
|
/**
|
|
143
150
|
* Render segments into a React tree with proper layout nesting
|
|
144
151
|
*
|
|
145
|
-
* Layouts nest using OutletProvider
|
|
146
|
-
* render as
|
|
152
|
+
* Layouts nest using OutletProvider; a layout receives the inner content via
|
|
153
|
+
* its `<Outlet />`. Parallel segments do NOT render as inline Fragment siblings
|
|
154
|
+
* — they flow through OutletContext.parallel and are resolved where a layout
|
|
155
|
+
* places `<ParallelOutlet name="@sidebar" />` (or `<Outlet name="@sidebar" />`).
|
|
156
|
+
*
|
|
157
|
+
* The result is always wrapped in RootErrorBoundary so unhandled errors never
|
|
158
|
+
* blank the screen. When `options.rootLayout` is provided it wraps the error
|
|
159
|
+
* boundary at the OUTERMOST level (so the app shell survives errors).
|
|
147
160
|
*
|
|
148
161
|
* Error segments are treated like route segments - they render their fallback
|
|
149
162
|
* component in place of the failed segment. When an error occurs in a handler,
|
|
@@ -155,27 +168,30 @@ function wrapDefaultOutletContent(
|
|
|
155
168
|
* notFoundBoundary's fallback component.
|
|
156
169
|
*
|
|
157
170
|
* @param segments - Array of resolved segments to render
|
|
158
|
-
* @returns ReactNode
|
|
171
|
+
* @returns Promise resolving to the ReactNode tree (the function is async)
|
|
159
172
|
*
|
|
160
173
|
* @example
|
|
161
174
|
* ```typescript
|
|
162
175
|
* const segments = [
|
|
163
|
-
* { id: 'L0.0', type: 'layout', component: <
|
|
164
|
-
* { id: '
|
|
165
|
-
* { id: '
|
|
166
|
-
* { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
176
|
+
* { id: 'L0.0', type: 'layout', component: <BlogLayout /> },
|
|
177
|
+
* { id: 'L0R1', type: 'route', component: <BlogPost /> },
|
|
178
|
+
* { id: 'L0R1.@sidebar', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
167
179
|
* ];
|
|
168
180
|
*
|
|
169
|
-
*
|
|
170
|
-
* //
|
|
171
|
-
*
|
|
172
|
-
* //
|
|
173
|
-
* //
|
|
174
|
-
* //
|
|
175
|
-
* //
|
|
181
|
+
* // BlogLayout renders <Outlet /> for the route and
|
|
182
|
+
* // <ParallelOutlet name="@sidebar" /> for the parallel slot.
|
|
183
|
+
* const tree = await renderSegments(segments, { rootLayout: RootLayout });
|
|
184
|
+
* // Results in (outermost first):
|
|
185
|
+
* // <RootLayout>
|
|
186
|
+
* // <RootErrorBoundary>
|
|
187
|
+
* // <OutletProvider segment={BlogLayout} parallel={[Sidebar]}>
|
|
188
|
+
* // <BlogPost />
|
|
189
|
+
* // </OutletProvider>
|
|
190
|
+
* // </RootErrorBoundary>
|
|
191
|
+
* // </RootLayout>
|
|
176
192
|
*
|
|
177
193
|
* // For server actions, pass isAction to await components:
|
|
178
|
-
* const tree = renderSegments(segments, { isAction: true });
|
|
194
|
+
* const tree = await renderSegments(segments, { isAction: true });
|
|
179
195
|
* ```
|
|
180
196
|
*/
|
|
181
197
|
export async function renderSegments(
|
|
@@ -283,31 +299,25 @@ export async function renderSegments(
|
|
|
283
299
|
.map(([k, v]) => `${k}=${v}`)
|
|
284
300
|
.join(",")
|
|
285
301
|
: "";
|
|
286
|
-
const key =
|
|
302
|
+
const key = paramStr ? `${id}-${paramStr}` : id;
|
|
287
303
|
|
|
288
|
-
// Get loader entries for this node
|
|
289
304
|
const loaderEntries = node.loaders.filter(
|
|
290
305
|
(loader) => loader.loaderId && loader.loaderData !== undefined,
|
|
291
306
|
);
|
|
292
307
|
|
|
293
|
-
// Determine the component content (with or without Suspense wrapper)
|
|
294
|
-
// Wrap when loading skeleton defined OR component is Promise (needs Suspense)
|
|
295
|
-
// During actions, await component Promise to prevent Suspense from triggering
|
|
296
|
-
// This keeps existing content visible instead of showing loading skeleton
|
|
297
308
|
let resolvedComponent = component;
|
|
298
309
|
if (isAction && component instanceof Promise) {
|
|
299
310
|
resolvedComponent = await component;
|
|
300
311
|
}
|
|
301
312
|
|
|
302
|
-
let nodeContent: ReactNode =
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
: registerLazyRef(resolvedComponent);
|
|
313
|
+
let nodeContent: ReactNode = isRenderableLoading(loading)
|
|
314
|
+
? createElement(RouteContentWrapper, {
|
|
315
|
+
key: `suspense-loading-${id}`,
|
|
316
|
+
content: getMemoizedContentPromise(resolvedComponent),
|
|
317
|
+
fallback: loading,
|
|
318
|
+
segmentId: id,
|
|
319
|
+
})
|
|
320
|
+
: registerLazyRef(resolvedComponent);
|
|
311
321
|
|
|
312
322
|
// Wrap with <ViewTransition> if transition config exists (React experimental only).
|
|
313
323
|
// An empty config ({}) creates a bare <ViewTransition> boundary that participates
|
|
@@ -351,13 +361,7 @@ export async function renderSegments(
|
|
|
351
361
|
// Prepare loader data if there are loaders
|
|
352
362
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
353
363
|
|
|
354
|
-
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
355
|
-
// This ensures cached segments (which may not have loader segments) have the same
|
|
356
|
-
// tree structure as fresh segments, preventing React remounts
|
|
357
|
-
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
358
364
|
if (loading !== undefined && loading !== null) {
|
|
359
|
-
// Aggregate built here only — the loaderless and no-loading branches don't
|
|
360
|
-
// read it (the latter builds its own per-parallel promises).
|
|
361
365
|
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
362
366
|
content = createElement(LoaderBoundary, {
|
|
363
367
|
key: `loader-boundary-${key}`,
|
|
@@ -372,7 +376,6 @@ export async function renderSegments(
|
|
|
372
376
|
children: nodeContent,
|
|
373
377
|
});
|
|
374
378
|
} else if (loaderEntries.length === 0) {
|
|
375
|
-
// No loaders, no loading - simple OutletProvider
|
|
376
379
|
content = createElement(OutletProvider, {
|
|
377
380
|
key,
|
|
378
381
|
content: outletContent,
|
|
@@ -381,34 +384,18 @@ export async function renderSegments(
|
|
|
381
384
|
children: nodeContent,
|
|
382
385
|
});
|
|
383
386
|
} else {
|
|
384
|
-
// Has loaders but no loading skeleton.
|
|
385
|
-
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
386
|
-
// layout-owned loaders are awaited (they gate the layout content).
|
|
387
387
|
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
388
388
|
const parallelOwnedLoaders = loaderEntries.filter(
|
|
389
389
|
(l) => !!l.parallelLoading,
|
|
390
390
|
);
|
|
391
391
|
|
|
392
|
-
// Await only layout-owned loaders
|
|
393
392
|
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
394
|
-
const
|
|
395
|
-
layoutLoaders.length > 0
|
|
396
|
-
? Promise.all(
|
|
397
|
-
layoutLoaders.map((l) =>
|
|
398
|
-
l.loaderData instanceof Promise
|
|
399
|
-
? l.loaderData
|
|
400
|
-
: Promise.resolve(l.loaderData),
|
|
401
|
-
),
|
|
402
|
-
)
|
|
403
|
-
: Promise.resolve([]);
|
|
404
|
-
const resolvedData = await layoutLoaderDataPromise;
|
|
393
|
+
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
405
394
|
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
406
395
|
resolvedData,
|
|
407
396
|
layoutLoaderIds,
|
|
408
397
|
);
|
|
409
398
|
|
|
410
|
-
// Parallel-owned loaders: attach to their owning parallel segment
|
|
411
|
-
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
412
399
|
if (parallelOwnedLoaders.length > 0) {
|
|
413
400
|
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
414
401
|
|
|
@@ -465,8 +452,6 @@ export async function renderSegments(
|
|
|
465
452
|
}
|
|
466
453
|
}
|
|
467
454
|
|
|
468
|
-
// Always wrap with root error boundary to prevent white screens
|
|
469
|
-
// This catches any unhandled errors that bubble up from the segment tree
|
|
470
455
|
const errorBoundaryWrapped = createElement(RootErrorBoundary, {
|
|
471
456
|
children: content,
|
|
472
457
|
});
|
|
@@ -474,11 +459,8 @@ export async function renderSegments(
|
|
|
474
459
|
await Promise.allSettled(temporalLazyRefs);
|
|
475
460
|
}
|
|
476
461
|
|
|
477
|
-
// Build the final result, optionally wrapped with root layout
|
|
478
462
|
let result: ReactNode = errorBoundaryWrapped;
|
|
479
463
|
|
|
480
|
-
// If rootLayout is provided, wrap the error boundary with it
|
|
481
|
-
// This ensures the app shell stays mounted even during errors (prevents FOUC)
|
|
482
464
|
if (RootLayout) {
|
|
483
465
|
result = createElement(RootLayout, {
|
|
484
466
|
children: errorBoundaryWrapped,
|
|
@@ -516,6 +498,28 @@ export async function renderSegments(
|
|
|
516
498
|
* @param segments - Main segments from the route tree
|
|
517
499
|
* @param interceptSegments - Optional intercept segments to inject
|
|
518
500
|
*/
|
|
501
|
+
// Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
|
|
502
|
+
// parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
|
|
503
|
+
// loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
|
|
504
|
+
// slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
|
|
505
|
+
// (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
|
|
506
|
+
// is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
507
|
+
// dropped the loader's data.
|
|
508
|
+
function loaderParentId(loaderSegmentId: string): string {
|
|
509
|
+
return loaderSegmentId.replace(/D\d+\..*$/, "");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Append a value to the array stored under `key`, creating the array on first
|
|
513
|
+
// use. Single Map lookup (vs the has/get!().push double-lookup idiom).
|
|
514
|
+
function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
|
|
515
|
+
const arr = map.get(key);
|
|
516
|
+
if (arr) {
|
|
517
|
+
arr.push(value);
|
|
518
|
+
} else {
|
|
519
|
+
map.set(key, [value]);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
519
523
|
function* segmentTreeWalk(
|
|
520
524
|
segments: ResolvedSegment[],
|
|
521
525
|
interceptSegments?: ResolvedSegment[],
|
|
@@ -536,19 +540,12 @@ function* segmentTreeWalk(
|
|
|
536
540
|
// Extract parent ID from parallel ID
|
|
537
541
|
// Example: "L0R1L0.@sidebar" → "L0R1L0"
|
|
538
542
|
const parentId = segment.id.split(".")[0];
|
|
539
|
-
|
|
540
|
-
parallelsByParent.set(parentId, []);
|
|
541
|
-
}
|
|
542
|
-
parallelsByParent.get(parentId)!.push(segment);
|
|
543
|
+
pushToGroup(parallelsByParent, parentId, segment);
|
|
543
544
|
} else if (segment.type === "loader") {
|
|
544
545
|
// Extract parent ID from loader ID
|
|
545
|
-
// Example: "L0D0.cart" → "L0"
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (!loadersByParent.has(parentId)) {
|
|
549
|
-
loadersByParent.set(parentId, []);
|
|
550
|
-
}
|
|
551
|
-
loadersByParent.get(parentId)!.push(segment);
|
|
546
|
+
// Example: "L0D0.cart" → "L0"; "L0.@DetailD0.x" → "L0.@Detail"
|
|
547
|
+
const parentId = loaderParentId(segment.id);
|
|
548
|
+
pushToGroup(loadersByParent, parentId, segment);
|
|
552
549
|
} else {
|
|
553
550
|
// Layout, route, error, and notFound segments are all rendered in the tree
|
|
554
551
|
// Error/notFound segments replace the failed segment with fallback UI
|
|
@@ -563,17 +560,11 @@ function* segmentTreeWalk(
|
|
|
563
560
|
if (intercept.type === "parallel" && intercept.slot) {
|
|
564
561
|
// Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
|
|
565
562
|
const parentId = intercept.id.split(".")[0];
|
|
566
|
-
|
|
567
|
-
parallelsByParent.set(parentId, []);
|
|
568
|
-
}
|
|
569
|
-
parallelsByParent.get(parentId)!.push(intercept);
|
|
563
|
+
pushToGroup(parallelsByParent, parentId, intercept);
|
|
570
564
|
} else if (intercept.type === "loader") {
|
|
571
|
-
// Intercept loaders - extract parent from loader ID
|
|
572
|
-
const parentId = intercept.id
|
|
573
|
-
|
|
574
|
-
loadersByParent.set(parentId, []);
|
|
575
|
-
}
|
|
576
|
-
loadersByParent.get(parentId)!.push(intercept);
|
|
565
|
+
// Intercept loaders - extract parent from loader ID (slot name preserved)
|
|
566
|
+
const parentId = loaderParentId(intercept.id);
|
|
567
|
+
pushToGroup(loadersByParent, parentId, intercept);
|
|
577
568
|
}
|
|
578
569
|
}
|
|
579
570
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { CookieOptions } from "../router/middleware-types.js";
|
|
11
|
-
import { getRequestContext } from "./request-context.js";
|
|
11
|
+
import { getRequestContext, _getRequestContext } from "./request-context.js";
|
|
12
12
|
import { isInsideCacheScope } from "./context.js";
|
|
13
13
|
import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
|
|
14
14
|
|
|
@@ -168,6 +168,57 @@ export function headers(): ReadonlyHeaders {
|
|
|
168
168
|
}) as unknown as ReadonlyHeaders;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Force the calling client's caches to miss from now on, from the server seat:
|
|
173
|
+
* write a rotated `Set-Cookie` for the rango state. The responding client
|
|
174
|
+
* applies it on receipt, and its history cache is marked stale by the
|
|
175
|
+
* jar-divergence observer at its next read. Per-client and lazy — it rotates
|
|
176
|
+
* only the client that receives this response, not every client.
|
|
177
|
+
*
|
|
178
|
+
* Idempotent within a request (one `Set-Cookie`). Inert (a dev warning) when
|
|
179
|
+
* called outside a request context. Like `cookies()`, it throws inside a
|
|
180
|
+
* `"use cache"` / `cache()` boundary, but is allowed from a loader (loaders are
|
|
181
|
+
* the dynamic holes of a cached document).
|
|
182
|
+
*/
|
|
183
|
+
export function invalidateClientCache(): void {
|
|
184
|
+
const ctx = _getRequestContext();
|
|
185
|
+
if (!ctx) {
|
|
186
|
+
if (process.env.NODE_ENV !== "production") {
|
|
187
|
+
console.warn(
|
|
188
|
+
"[rango] invalidateClientCache() was called outside a request context; ignored.",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
assertNotInsideCacheContext(ctx, "invalidateClientCache");
|
|
194
|
+
ctx._rotateStateCookie();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Suppress a server action's automatic client-cache invalidation: tell the
|
|
199
|
+
* action bridge this action changed nothing a route renders, so it should leave
|
|
200
|
+
* the client's state and caches alone (no rotation, no prefetch wipe, no
|
|
201
|
+
* broadcast, no revalidation refetch). Per-response, not per-action-definition —
|
|
202
|
+
* only the execution knows whether anything changed.
|
|
203
|
+
*
|
|
204
|
+
* Sets an internal response header the bridge reads. Idempotent within a
|
|
205
|
+
* request. Inert (a dev warning) outside a request context — there is no
|
|
206
|
+
* automatic invalidation to suppress.
|
|
207
|
+
*/
|
|
208
|
+
export function keepClientCache(): void {
|
|
209
|
+
const ctx = _getRequestContext();
|
|
210
|
+
if (!ctx) {
|
|
211
|
+
if (process.env.NODE_ENV !== "production") {
|
|
212
|
+
console.warn(
|
|
213
|
+
"[rango] keepClientCache() was called outside a request context; ignored.",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
assertNotInsideCacheContext(ctx, "keepClientCache");
|
|
219
|
+
ctx._setKeepCacheDirective();
|
|
220
|
+
}
|
|
221
|
+
|
|
171
222
|
/**
|
|
172
223
|
* Create a CookieStore backed by a RequestContext.
|
|
173
224
|
* @internal Shared between cookies() shorthand and context methods.
|
|
@@ -45,10 +45,6 @@ function createLateHandlePushError(
|
|
|
45
45
|
return error;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Deep clone handle data to create a snapshot.
|
|
50
|
-
* @internal
|
|
51
|
-
*/
|
|
52
48
|
function cloneHandleData(data: HandleData): HandleData {
|
|
53
49
|
const clone: HandleData = {};
|
|
54
50
|
for (const handleName in data) {
|
|
@@ -205,11 +201,9 @@ export function createHandleStore(): HandleStore {
|
|
|
205
201
|
return {
|
|
206
202
|
track<T>(promise: Promise<T>): Promise<T> {
|
|
207
203
|
inflightCount++;
|
|
208
|
-
// Use .then(
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// rejection on a new branch that nobody catches, which can crash
|
|
212
|
-
// the server process.
|
|
204
|
+
// Use .then() instead of .finally() to avoid creating an unhandled rejection
|
|
205
|
+
// branch when the promise rejects. .finally() re-throws on a new branch that
|
|
206
|
+
// can crash the process if not caught.
|
|
213
207
|
const onSettle = () => {
|
|
214
208
|
inflightCount--;
|
|
215
209
|
notifyDrain();
|
|
@@ -255,42 +249,32 @@ export function createHandleStore(): HandleStore {
|
|
|
255
249
|
},
|
|
256
250
|
|
|
257
251
|
async *stream(): AsyncGenerator<HandleData, void, unknown> {
|
|
258
|
-
// Auto-seal: stream() is called after all track() registrations.
|
|
259
252
|
sealInternal();
|
|
260
253
|
|
|
261
|
-
// Set up completion handler
|
|
262
254
|
this.settled.then(() => {
|
|
263
255
|
completed = true;
|
|
264
256
|
signalEmission();
|
|
265
257
|
});
|
|
266
258
|
|
|
267
|
-
//
|
|
268
|
-
// This allows multiple handles pushing in quick succession to be batched
|
|
259
|
+
// Batch rapid synchronous pushes with initial delay
|
|
269
260
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
270
261
|
|
|
271
|
-
// If we already have data, yield the accumulated state
|
|
272
262
|
if (Object.keys(data).length > 0) {
|
|
273
|
-
// Clear pending emissions since we're yielding current state
|
|
274
263
|
pendingEmissions = [];
|
|
275
264
|
const snapshot = cloneHandleData(data);
|
|
276
265
|
yield snapshot;
|
|
277
266
|
}
|
|
278
267
|
|
|
279
|
-
// Continue streaming on each push
|
|
280
268
|
while (!completed) {
|
|
281
269
|
await waitForEmission();
|
|
282
270
|
|
|
283
|
-
// Yield all pending emissions (yield latest only)
|
|
284
271
|
if (pendingEmissions.length > 0) {
|
|
285
|
-
// Skip intermediate states, yield the latest
|
|
286
272
|
const latest = pendingEmissions[pendingEmissions.length - 1];
|
|
287
273
|
pendingEmissions = [];
|
|
288
274
|
yield latest;
|
|
289
275
|
}
|
|
290
276
|
}
|
|
291
277
|
|
|
292
|
-
// Final yield only if there are pending emissions that weren't yielded
|
|
293
|
-
// (handles that pushed after our last yield but before completion)
|
|
294
278
|
if (pendingEmissions.length > 0) {
|
|
295
279
|
yield cloneHandleData(data);
|
|
296
280
|
}
|
|
@@ -314,12 +298,11 @@ export function createHandleStore(): HandleStore {
|
|
|
314
298
|
if (!data[handleName]) {
|
|
315
299
|
data[handleName] = {};
|
|
316
300
|
}
|
|
317
|
-
// Replace
|
|
318
|
-
//
|
|
319
|
-
//
|
|
301
|
+
// Replace (not append) to avoid handle bleeding between routes.
|
|
302
|
+
// Cached segment restoration should replace existing data for that
|
|
303
|
+
// segment, not accumulate on top of data from a different route.
|
|
320
304
|
data[handleName][segmentId] = [...segmentHandles[handleName]];
|
|
321
305
|
}
|
|
322
|
-
// Trigger emission for streaming
|
|
323
306
|
pendingEmissions.push(cloneHandleData(data));
|
|
324
307
|
signalEmission();
|
|
325
308
|
},
|
|
@@ -11,17 +11,10 @@ import {
|
|
|
11
11
|
type LoaderRegistryEntry,
|
|
12
12
|
} from "./fetchable-loader-store.js";
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// The source of truth is fetchableLoaderRegistry in loader.ts, which is populated
|
|
17
|
-
// when createLoader() runs. This cache exists to:
|
|
18
|
-
// 1. Avoid repeated lookups/imports for the same loader
|
|
19
|
-
// 2. Support lazy loading in production (loaders imported on-demand)
|
|
20
|
-
// 3. Provide a stable reference for the RSC handler
|
|
14
|
+
// Cache populated by getLoaderLazy() when loaders are first accessed.
|
|
15
|
+
// Source of truth is fetchableLoaderRegistry in loader.ts (populated on createLoader).
|
|
21
16
|
const loaderRegistry = new Map<string, LoaderRegistryEntry>();
|
|
22
17
|
|
|
23
|
-
// Lazy import map - set by the loader manifest
|
|
24
|
-
// Maps loader $$id to a function that imports the loader module
|
|
25
18
|
type LazyLoaderImport = () => Promise<{ $$id: string }>;
|
|
26
19
|
let lazyLoaderImports: Map<string, LazyLoaderImport> | null = null;
|
|
27
20
|
|
|
@@ -44,30 +37,25 @@ export function setLoaderImports(
|
|
|
44
37
|
export async function getLoaderLazy(
|
|
45
38
|
id: string,
|
|
46
39
|
): Promise<LoaderRegistryEntry | undefined> {
|
|
47
|
-
//
|
|
48
|
-
// createLoader() updates it
|
|
49
|
-
// here ensures we pick up the fresh function after a loader file change.
|
|
40
|
+
// Check fetchableLoaderRegistry first — it's the source of truth.
|
|
41
|
+
// createLoader() updates it on HMR, ensuring fresh functions after file changes.
|
|
50
42
|
const fetchable = getFetchableLoader(id);
|
|
51
43
|
if (fetchable) {
|
|
52
44
|
loaderRegistry.set(id, fetchable);
|
|
53
45
|
return fetchable;
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
// Fall back to local cache (populated by previous lazy imports in production)
|
|
57
48
|
const existing = loaderRegistry.get(id);
|
|
58
49
|
if (existing) {
|
|
59
50
|
return existing;
|
|
60
51
|
}
|
|
61
52
|
|
|
62
|
-
// Try to lazy load from the import map (production mode)
|
|
63
53
|
if (lazyLoaderImports && lazyLoaderImports.size > 0) {
|
|
64
54
|
const lazyImport = lazyLoaderImports.get(id);
|
|
65
55
|
if (lazyImport) {
|
|
66
56
|
try {
|
|
67
|
-
// Import the loader module - this triggers createLoader which registers fn
|
|
68
57
|
await lazyImport();
|
|
69
58
|
|
|
70
|
-
// Now try to get from fetchable registry (createLoader registered it)
|
|
71
59
|
const registered = getFetchableLoader(id);
|
|
72
60
|
if (registered) {
|
|
73
61
|
loaderRegistry.set(id, registered);
|
|
@@ -79,18 +67,14 @@ export async function getLoaderLazy(
|
|
|
79
67
|
}
|
|
80
68
|
}
|
|
81
69
|
|
|
82
|
-
// Dev
|
|
83
|
-
// ID format in dev: "src/path/to/file.ts#ExportName"
|
|
70
|
+
// Dev fallback: parse ID (format: "src/path/to/file.ts#ExportName") and import
|
|
84
71
|
const hashIndex = id.indexOf("#");
|
|
85
72
|
if (hashIndex !== -1) {
|
|
86
73
|
const filePath = id.slice(0, hashIndex);
|
|
87
74
|
|
|
88
75
|
try {
|
|
89
|
-
// In dev mode, Vite handles dynamic imports
|
|
90
|
-
// Just importing the module triggers createLoader which registers the fn
|
|
91
76
|
await import(/* @vite-ignore */ `/${filePath}`);
|
|
92
77
|
|
|
93
|
-
// Now try to get from fetchable registry
|
|
94
78
|
const registered = getFetchableLoader(id);
|
|
95
79
|
if (registered) {
|
|
96
80
|
loaderRegistry.set(id, registered);
|
|
@@ -115,15 +99,12 @@ export function registerLoaderById(loader: {
|
|
|
115
99
|
if (!loader.$$id) {
|
|
116
100
|
return;
|
|
117
101
|
}
|
|
118
|
-
// For fetchable loaders, fn is stored in the fetchable registry by $$id.
|
|
119
|
-
// Always re-check the fetchable registry so HMR picks up the new function.
|
|
120
102
|
const fetchable = getFetchableLoader(loader.$$id);
|
|
121
103
|
if (fetchable) {
|
|
122
104
|
loaderRegistry.set(loader.$$id, fetchable);
|
|
123
105
|
return;
|
|
124
106
|
}
|
|
125
107
|
|
|
126
|
-
// Fall back to using fn from the loader object (non-fetchable loaders)
|
|
127
108
|
if (loader.fn) {
|
|
128
109
|
loaderRegistry.set(loader.$$id, {
|
|
129
110
|
fn: loader.fn,
|