@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d
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/README.md +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2154 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- 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 +229 -20
- package/skills/host-router/SKILL.md +45 -20
- 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 +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -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 +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +243 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +128 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +121 -0
- package/skills/testing/e2e-parity.md +124 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +127 -0
- package/skills/testing/loader.md +108 -0
- package/skills/testing/middleware.md +97 -0
- package/skills/testing/render-handler.md +102 -0
- package/skills/testing/response-routes.md +94 -0
- package/skills/testing/reverse-and-types.md +83 -0
- package/skills/testing/server-actions.md +89 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- 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 +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +104 -68
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +183 -44
- package/src/browser/prefetch/fetch.ts +228 -37
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- 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 +17 -9
- 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 +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +32 -1
- package/src/browser/rsc-router.tsx +69 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +95 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +32 -14
- package/src/host/index.ts +2 -2
- 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 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +54 -17
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +25 -7
- package/src/loader.ts +16 -9
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +27 -6
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +52 -30
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +57 -61
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +67 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -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 +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 +326 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +51 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +323 -0
- package/src/testing/render-route.tsx +590 -0
- package/src/testing/run-loader.ts +363 -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 +285 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- 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 +67 -26
- 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 +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- 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 +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- 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 +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- 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 +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- 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 +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/client.tsx
CHANGED
|
@@ -21,6 +21,83 @@ import {
|
|
|
21
21
|
} from "./route-content-wrapper.js";
|
|
22
22
|
import { OutletProvider } from "./outlet-provider.js";
|
|
23
23
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
24
|
+
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render the content for a named parallel/intercept slot segment.
|
|
28
|
+
*
|
|
29
|
+
* Shared by Outlet (with `name` prop) and ParallelOutlet — both resolve a
|
|
30
|
+
* segment from context.parallel by slot name and then render it through the
|
|
31
|
+
* same layout/loader/mountPath wrapping pipeline.
|
|
32
|
+
*/
|
|
33
|
+
function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
|
|
34
|
+
if (!segment) return null;
|
|
35
|
+
|
|
36
|
+
const content: ReactNode =
|
|
37
|
+
segment.loading || segment.component instanceof Promise ? (
|
|
38
|
+
<RouteContentWrapper
|
|
39
|
+
content={getMemoizedContentPromise(segment.component)}
|
|
40
|
+
fallback={segment.loading}
|
|
41
|
+
segmentId={segment.id}
|
|
42
|
+
/>
|
|
43
|
+
) : (
|
|
44
|
+
(segment.component ?? null)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const hasOwnLoaders = !!(segment.loaderDataPromise && segment.loaderIds);
|
|
48
|
+
const loaderWrapped = hasOwnLoaders ? (
|
|
49
|
+
<LoaderBoundary
|
|
50
|
+
loaderDataPromise={segment.loaderDataPromise!}
|
|
51
|
+
loaderIds={segment.loaderIds!}
|
|
52
|
+
fallback={segment.loading}
|
|
53
|
+
outletKey={segment.id + "-loader"}
|
|
54
|
+
outletContent={null}
|
|
55
|
+
segment={segment}
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</LoaderBoundary>
|
|
59
|
+
) : null;
|
|
60
|
+
|
|
61
|
+
let result: ReactNode;
|
|
62
|
+
if (segment.layout) {
|
|
63
|
+
// Layout renders immediately; if loaders exist, the LoaderBoundary becomes
|
|
64
|
+
// the outlet content so layout's <Outlet /> suspends until loaders resolve.
|
|
65
|
+
result = (
|
|
66
|
+
<OutletProvider
|
|
67
|
+
content={hasOwnLoaders ? loaderWrapped : content}
|
|
68
|
+
segment={segment}
|
|
69
|
+
>
|
|
70
|
+
{segment.layout}
|
|
71
|
+
</OutletProvider>
|
|
72
|
+
);
|
|
73
|
+
} else if (hasOwnLoaders) {
|
|
74
|
+
// No layout but has loaders — wrap content with LoaderBoundary for useLoader context.
|
|
75
|
+
// Common for intercept routes that use useLoader without a custom layout.
|
|
76
|
+
result = loaderWrapped;
|
|
77
|
+
} else {
|
|
78
|
+
result = content;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (segment.mountPath) {
|
|
82
|
+
return (
|
|
83
|
+
<MountContextProvider value={segment.mountPath}>
|
|
84
|
+
{result}
|
|
85
|
+
</MountContextProvider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useSlotSegment(
|
|
93
|
+
context: OutletContextValue | null,
|
|
94
|
+
name: `@${string}` | undefined,
|
|
95
|
+
): ResolvedSegment | null {
|
|
96
|
+
return useMemo(() => {
|
|
97
|
+
if (!name || !context?.parallel) return null;
|
|
98
|
+
return context.parallel.find((seg) => seg.slot === name) ?? null;
|
|
99
|
+
}, [context, name]);
|
|
100
|
+
}
|
|
24
101
|
|
|
25
102
|
/**
|
|
26
103
|
* Outlet component - renders child content in layouts
|
|
@@ -61,95 +138,10 @@ import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
|
61
138
|
*/
|
|
62
139
|
export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
63
140
|
const context = useContext(OutletContext);
|
|
141
|
+
const namedSegment = useSlotSegment(context, name);
|
|
64
142
|
|
|
65
|
-
// If name provided, render parallel/intercept content for that slot
|
|
66
143
|
if (name) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!segment) return null;
|
|
70
|
-
|
|
71
|
-
// Determine the content to render
|
|
72
|
-
let content: ReactNode;
|
|
73
|
-
if (segment.loading || segment.component instanceof Promise) {
|
|
74
|
-
// Use RouteContentWrapper to handle Suspense wrapping properly
|
|
75
|
-
content = (
|
|
76
|
-
<RouteContentWrapper
|
|
77
|
-
content={
|
|
78
|
-
segment.component instanceof Promise
|
|
79
|
-
? segment.component
|
|
80
|
-
: Promise.resolve(segment.component)
|
|
81
|
-
}
|
|
82
|
-
fallback={segment.loading}
|
|
83
|
-
segmentId={segment.id}
|
|
84
|
-
/>
|
|
85
|
-
);
|
|
86
|
-
} else {
|
|
87
|
-
content = segment.component ?? null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let result: ReactNode;
|
|
91
|
-
|
|
92
|
-
// If segment has a layout, wrap appropriately
|
|
93
|
-
if (segment.layout) {
|
|
94
|
-
// Check if this segment has loaders that need streaming
|
|
95
|
-
// The layout renders immediately, LoaderBoundary becomes the outlet content
|
|
96
|
-
// When layout renders <Outlet />, it gets the LoaderBoundary which suspends
|
|
97
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
98
|
-
const loaderAwareContent = (
|
|
99
|
-
<LoaderBoundary
|
|
100
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
101
|
-
loaderIds={segment.loaderIds}
|
|
102
|
-
fallback={segment.loading}
|
|
103
|
-
outletKey={segment.id + "-loader"}
|
|
104
|
-
outletContent={null}
|
|
105
|
-
segment={segment}
|
|
106
|
-
>
|
|
107
|
-
{content}
|
|
108
|
-
</LoaderBoundary>
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
result = (
|
|
112
|
-
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
113
|
-
{segment.layout}
|
|
114
|
-
</OutletProvider>
|
|
115
|
-
);
|
|
116
|
-
} else {
|
|
117
|
-
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
118
|
-
result = (
|
|
119
|
-
<OutletProvider content={content} segment={segment}>
|
|
120
|
-
{segment.layout}
|
|
121
|
-
</OutletProvider>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
125
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
126
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
127
|
-
result = (
|
|
128
|
-
<LoaderBoundary
|
|
129
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
130
|
-
loaderIds={segment.loaderIds}
|
|
131
|
-
fallback={segment.loading}
|
|
132
|
-
outletKey={segment.id + "-loader"}
|
|
133
|
-
outletContent={null}
|
|
134
|
-
segment={segment}
|
|
135
|
-
>
|
|
136
|
-
{content}
|
|
137
|
-
</LoaderBoundary>
|
|
138
|
-
);
|
|
139
|
-
} else {
|
|
140
|
-
result = content;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
144
|
-
if (segment.mountPath) {
|
|
145
|
-
return (
|
|
146
|
-
<MountContextProvider value={segment.mountPath}>
|
|
147
|
-
{result}
|
|
148
|
-
</MountContextProvider>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return result;
|
|
144
|
+
return renderSlotContent(namedSegment);
|
|
153
145
|
}
|
|
154
146
|
|
|
155
147
|
// Default: render child content
|
|
@@ -163,6 +155,7 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
163
155
|
|
|
164
156
|
return content;
|
|
165
157
|
}
|
|
158
|
+
|
|
166
159
|
/**
|
|
167
160
|
* ParallelOutlet component - renders content for a named parallel slot
|
|
168
161
|
*
|
|
@@ -187,94 +180,9 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
187
180
|
*/
|
|
188
181
|
export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
189
182
|
const context = useContext(OutletContext);
|
|
190
|
-
const segment =
|
|
191
|
-
if (!context?.parallel) return null;
|
|
192
|
-
return context.parallel.find((seg) => seg.slot === name) ?? null;
|
|
193
|
-
}, [context, name]);
|
|
194
|
-
|
|
195
|
-
if (!segment) return null;
|
|
196
|
-
|
|
197
|
-
// Determine the content to render
|
|
198
|
-
let content: ReactNode;
|
|
199
|
-
if (segment.loading || segment.component instanceof Promise) {
|
|
200
|
-
// Use RouteContentWrapper to handle Suspense wrapping properly
|
|
201
|
-
content = (
|
|
202
|
-
<RouteContentWrapper
|
|
203
|
-
content={
|
|
204
|
-
segment.component instanceof Promise
|
|
205
|
-
? segment.component
|
|
206
|
-
: Promise.resolve(segment.component)
|
|
207
|
-
}
|
|
208
|
-
fallback={segment.loading}
|
|
209
|
-
segmentId={segment.id}
|
|
210
|
-
/>
|
|
211
|
-
);
|
|
212
|
-
} else {
|
|
213
|
-
content = segment.component ?? null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
let result: ReactNode;
|
|
217
|
-
|
|
218
|
-
// If segment has a layout, wrap appropriately
|
|
219
|
-
if (segment.layout) {
|
|
220
|
-
// Check if this segment has loaders that need streaming
|
|
221
|
-
// The layout renders immediately, LoaderBoundary becomes the outlet content
|
|
222
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
223
|
-
const loaderAwareContent = (
|
|
224
|
-
<LoaderBoundary
|
|
225
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
226
|
-
loaderIds={segment.loaderIds}
|
|
227
|
-
fallback={segment.loading}
|
|
228
|
-
outletKey={segment.id + "-loader"}
|
|
229
|
-
outletContent={null}
|
|
230
|
-
segment={segment}
|
|
231
|
-
>
|
|
232
|
-
{content}
|
|
233
|
-
</LoaderBoundary>
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
result = (
|
|
237
|
-
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
238
|
-
{segment.layout}
|
|
239
|
-
</OutletProvider>
|
|
240
|
-
);
|
|
241
|
-
} else {
|
|
242
|
-
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
243
|
-
result = (
|
|
244
|
-
<OutletProvider content={content} segment={segment}>
|
|
245
|
-
{segment.layout}
|
|
246
|
-
</OutletProvider>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
250
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
251
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
252
|
-
result = (
|
|
253
|
-
<LoaderBoundary
|
|
254
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
255
|
-
loaderIds={segment.loaderIds}
|
|
256
|
-
fallback={segment.loading}
|
|
257
|
-
outletKey={segment.id + "-loader"}
|
|
258
|
-
outletContent={null}
|
|
259
|
-
segment={segment}
|
|
260
|
-
>
|
|
261
|
-
{content}
|
|
262
|
-
</LoaderBoundary>
|
|
263
|
-
);
|
|
264
|
-
} else {
|
|
265
|
-
result = content;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
269
|
-
if (segment.mountPath) {
|
|
270
|
-
return (
|
|
271
|
-
<MountContextProvider value={segment.mountPath}>
|
|
272
|
-
{result}
|
|
273
|
-
</MountContextProvider>
|
|
274
|
-
);
|
|
275
|
-
}
|
|
183
|
+
const segment = useSlotSegment(context, name);
|
|
276
184
|
|
|
277
|
-
return
|
|
185
|
+
return renderSlotContent(segment);
|
|
278
186
|
}
|
|
279
187
|
|
|
280
188
|
// OutletProvider is defined in outlet-provider.tsx to break a circular
|
|
@@ -306,6 +214,7 @@ export function useOutlet(): ReactNode {
|
|
|
306
214
|
export {
|
|
307
215
|
useLoader,
|
|
308
216
|
useFetchLoader,
|
|
217
|
+
useRefreshLoaders,
|
|
309
218
|
type LoadFunction,
|
|
310
219
|
type UseLoaderResult,
|
|
311
220
|
type UseFetchLoaderResult,
|
|
@@ -501,37 +410,15 @@ export {
|
|
|
501
410
|
type LocationStateOptions,
|
|
502
411
|
} from "./browser/react/location-state.js";
|
|
503
412
|
|
|
504
|
-
// Type-safe href for client-side path validation
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
type PatternToPath,
|
|
509
|
-
type PathResponse,
|
|
510
|
-
} from "./href-client.js";
|
|
413
|
+
// Type-safe href for client-side path validation. The path and response types
|
|
414
|
+
// are ambient as `Rango.Path` / `Rango.PathResponse` (declared in
|
|
415
|
+
// href-client.ts) — no import needed.
|
|
416
|
+
export { href, type PatternToPath } from "./href-client.js";
|
|
511
417
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
* Type guard for checking if a response envelope contains an error.
|
|
517
|
-
*
|
|
518
|
-
* @example
|
|
519
|
-
* ```typescript
|
|
520
|
-
* const result: ResponseEnvelope<Product> = await fetch(url).then(r => r.json());
|
|
521
|
-
* if (isResponseError(result)) {
|
|
522
|
-
* console.log(result.error.message, result.error.code);
|
|
523
|
-
* return;
|
|
524
|
-
* }
|
|
525
|
-
* result.data // fully typed as Product
|
|
526
|
-
* ```
|
|
527
|
-
*/
|
|
528
|
-
export function isResponseError<T>(
|
|
529
|
-
result: import("./urls.js").ResponseEnvelope<T>,
|
|
530
|
-
): result is import("./urls.js").ResponseEnvelope<T> & {
|
|
531
|
-
error: import("./urls.js").ResponseError;
|
|
532
|
-
} {
|
|
533
|
-
return result.error !== undefined;
|
|
534
|
-
}
|
|
418
|
+
// Problem Details (RFC 9457) error body type for consuming JSON response routes.
|
|
419
|
+
// On a non-2xx response, `await res.json()` yields this shape; on success the
|
|
420
|
+
// body is the bare value (no envelope). Discriminate on `res.ok` / status.
|
|
421
|
+
export type { ProblemDetails } from "./urls.js";
|
|
535
422
|
|
|
536
423
|
// Mount context for include() scoped components
|
|
537
424
|
export { useMount } from "./browser/react/use-mount.js";
|
|
@@ -540,8 +427,12 @@ export { MountContext } from "./browser/react/mount-context.js";
|
|
|
540
427
|
// Mount-aware href hook - auto-prefixes paths with include() mount
|
|
541
428
|
export { useHref } from "./browser/react/use-href.js";
|
|
542
429
|
|
|
430
|
+
// Mount-aware reverse hook - resolves dot-prefixed names against an imported
|
|
431
|
+
// generated routes map (from a urls() module's .gen.ts).
|
|
432
|
+
export { useReverse } from "./browser/react/use-reverse.js";
|
|
433
|
+
|
|
543
434
|
// Type-safe scoped reverse function for scopedReverse<typeof patterns>()
|
|
544
|
-
export type { ScopedReverseFunction } from "./reverse.js";
|
|
435
|
+
export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js";
|
|
545
436
|
|
|
546
437
|
// Loader definition type - for typing loader props in client components
|
|
547
438
|
export type { LoaderDefinition } from "./types.js";
|
package/src/context-var.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* interface PaginationData { current: number; total: number }
|
|
13
13
|
* export const Pagination = createVar<PaginationData>();
|
|
14
14
|
*
|
|
15
|
-
* // Non-cacheable var — throws
|
|
15
|
+
* // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
|
|
16
16
|
* export const User = createVar<UserData>({ cache: false });
|
|
17
17
|
*
|
|
18
18
|
* // handler
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export interface ContextVar<T> {
|
|
27
27
|
readonly __brand: "context-var";
|
|
28
28
|
readonly key: symbol;
|
|
29
|
-
/** When false,
|
|
29
|
+
/** When false, ctx.get(var) throws inside a cache() boundary. */
|
|
30
30
|
readonly cache: boolean;
|
|
31
31
|
/** Phantom field to carry the type parameter. Never set at runtime. */
|
|
32
32
|
readonly __type?: T;
|
|
@@ -35,9 +35,9 @@ export interface ContextVar<T> {
|
|
|
35
35
|
export interface ContextVarOptions {
|
|
36
36
|
/**
|
|
37
37
|
* When false, marks this variable as non-cacheable.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Reading this var with ctx.get() inside a cache() boundary throws. Use for
|
|
39
|
+
* inherently request-specific data (user sessions, auth tokens, etc.) that
|
|
40
|
+
* must never be baked into cached segments.
|
|
41
41
|
*
|
|
42
42
|
* @default true
|
|
43
43
|
*/
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { isLoaderDataResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Shared by segment-system (server) and LoaderResolver (client) so the
|
|
5
|
+
// legacy/ok/error-fallback/throw decode of resolved loader values lives once.
|
|
6
|
+
// Last failing loader wins errorFallback; an error without a fallback throws.
|
|
7
|
+
export function decodeLoaderResults(
|
|
8
|
+
resolvedData: any[],
|
|
9
|
+
loaderIds: string[],
|
|
10
|
+
): { loaderData: Record<string, any>; errorFallback: ReactNode } {
|
|
11
|
+
const loaderData: Record<string, any> = {};
|
|
12
|
+
let errorFallback: ReactNode = null;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < loaderIds.length; i++) {
|
|
15
|
+
const id = loaderIds[i];
|
|
16
|
+
const result = resolvedData[i];
|
|
17
|
+
|
|
18
|
+
if (!isLoaderDataResult(result)) {
|
|
19
|
+
loaderData[id] = result;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
loaderData[id] = result.data;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (result.fallback) {
|
|
29
|
+
errorFallback = result.fallback;
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(result.error.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { loaderData, errorFallback };
|
|
36
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom error classes for
|
|
2
|
+
* Custom error classes for Rango
|
|
3
3
|
*
|
|
4
4
|
* All errors include:
|
|
5
5
|
* - Descriptive names for easy identification
|
|
@@ -27,6 +27,17 @@ export class RouteNotFoundError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// name fallback covers cross-realm errors (Vite dev dupes, RSC serialization)
|
|
31
|
+
// where instanceof fails.
|
|
32
|
+
export function isRouteNotFoundError(
|
|
33
|
+
error: unknown,
|
|
34
|
+
): error is RouteNotFoundError {
|
|
35
|
+
return (
|
|
36
|
+
error instanceof RouteNotFoundError ||
|
|
37
|
+
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Thrown when data is not found (e.g., product with ID doesn't exist)
|
|
32
43
|
* Use this in handlers/loaders to trigger the nearest notFoundBoundary
|
|
@@ -109,6 +120,24 @@ export class BuildError extends Error {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Thrown when a route-definition DSL helper (route/layout/loader/cache/…) is
|
|
125
|
+
* called outside an active urls()/map() builder, so there is no
|
|
126
|
+
* AsyncLocalStorage build context to attach to. The message names the specific
|
|
127
|
+
* helper and how to fix it; the `cause` records the mechanical reason so the
|
|
128
|
+
* failure mode is identifiable (not conflated with an unrelated throw).
|
|
129
|
+
*/
|
|
130
|
+
export class DslContextError extends Error {
|
|
131
|
+
name = "DslContextError" as const;
|
|
132
|
+
cause?: unknown;
|
|
133
|
+
|
|
134
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
135
|
+
super(message);
|
|
136
|
+
Object.setPrototypeOf(this, DslContextError.prototype);
|
|
137
|
+
this.cause = options?.cause;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
/**
|
|
113
142
|
* Thrown when a network request fails (server unreachable, no internet, etc.)
|
|
114
143
|
* This error triggers the root error boundary with retry capability.
|
|
@@ -196,7 +225,6 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
196
225
|
export class RouterError extends Error {
|
|
197
226
|
name = "RouterError" as const;
|
|
198
227
|
code: string;
|
|
199
|
-
type?: string;
|
|
200
228
|
status: number;
|
|
201
229
|
cause?: unknown;
|
|
202
230
|
|
|
@@ -205,7 +233,6 @@ export class RouterError extends Error {
|
|
|
205
233
|
message: string,
|
|
206
234
|
options?: {
|
|
207
235
|
status?: number;
|
|
208
|
-
type?: string;
|
|
209
236
|
cause?: unknown;
|
|
210
237
|
},
|
|
211
238
|
) {
|
|
@@ -213,7 +240,6 @@ export class RouterError extends Error {
|
|
|
213
240
|
Object.setPrototypeOf(this, RouterError.prototype);
|
|
214
241
|
this.code = code;
|
|
215
242
|
this.status = options?.status ?? 500;
|
|
216
|
-
this.type = options?.type;
|
|
217
243
|
this.cause = options?.cause;
|
|
218
244
|
}
|
|
219
245
|
}
|
package/src/handle.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { missingInjectedIdError } from "./missing-id-error.js";
|
|
2
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Handle definition for accumulating data across route segments.
|
|
3
6
|
*
|
|
@@ -43,6 +46,11 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
43
46
|
// Used by useHandle() to recover collect when handle is deserialized from RSC prop.
|
|
44
47
|
const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
|
|
45
48
|
|
|
49
|
+
// Monotonic counter for runtime fallback ids (see createHandle). Module-scoped
|
|
50
|
+
// and deterministic, so each createHandle() call gets a stable, unique id within
|
|
51
|
+
// the process. Only used when no build id was injected (a bare unit test).
|
|
52
|
+
let runtimeHandleIdCounter = 0;
|
|
53
|
+
|
|
46
54
|
/**
|
|
47
55
|
* Look up a collect function from the registry by handle $$id.
|
|
48
56
|
* Returns undefined if not registered (falls back to defaultCollect in useHandle).
|
|
@@ -93,14 +101,26 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
93
101
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
94
102
|
__injectedId?: string,
|
|
95
103
|
): Handle<TData, TAccumulated> {
|
|
96
|
-
|
|
104
|
+
let handleId = __injectedId ?? "";
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
// No build-injected id. Under a test runner: fall back to a synthetic id so the
|
|
107
|
+
// collect registers below and the handle is exercisable in tests (useHandle,
|
|
108
|
+
// collectHandle, renderRoute's `handles` run the REAL collect). Otherwise (dev
|
|
109
|
+
// or a real build) it means an UNSUPPORTED handler shape the plugin skipped —
|
|
110
|
+
// fail loud. The rich, stack-parsing diagnostic stays behind the NODE_ENV check
|
|
111
|
+
// so a production build folds it away and tree-shakes missing-id-error.ts out,
|
|
112
|
+
// shipping the small throw instead. isUnderTestRunner() is runtime-safe.
|
|
113
|
+
if (!handleId) {
|
|
114
|
+
if (isUnderTestRunner()) {
|
|
115
|
+
handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
|
|
116
|
+
} else if (process.env.NODE_ENV !== "production") {
|
|
117
|
+
throw missingInjectedIdError("Handle", "createHandle");
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"[rango] Handle is missing $$id — the build plugin did not inject one. " +
|
|
121
|
+
"Export it as `export const X = createHandle(...)`.",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
const collectFn =
|
|
@@ -109,12 +129,10 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
109
129
|
|
|
110
130
|
// Register collect in module-level registry so useHandle() can recover it
|
|
111
131
|
// when the handle is deserialized from RSC props (toJSON strips collect).
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
);
|
|
117
|
-
}
|
|
132
|
+
collectRegistry.set(
|
|
133
|
+
handleId,
|
|
134
|
+
collectFn as (segments: unknown[][]) => unknown,
|
|
135
|
+
);
|
|
118
136
|
|
|
119
137
|
return {
|
|
120
138
|
__brand: "handle" as const,
|
|
@@ -151,7 +169,7 @@ export function collectHandleData<TData, TAccumulated>(
|
|
|
151
169
|
const collectFn = getCollectFn(handle.$$id);
|
|
152
170
|
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
153
171
|
console.warn(
|
|
154
|
-
`[
|
|
172
|
+
`[rango] Handle "${handle.$$id}" has no registered collect function. ` +
|
|
155
173
|
`Falling back to flat array. Ensure the handle module is imported so ` +
|
|
156
174
|
`createHandle() runs and registers the collect function.`,
|
|
157
175
|
);
|
package/src/host/index.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* const router = createHostRouter();
|
|
13
13
|
*
|
|
14
|
-
* router.host(['.']).
|
|
15
|
-
* router.host(['admin.*']).
|
|
14
|
+
* router.host(['.']).lazy(() => import('./apps/main'));
|
|
15
|
+
* router.host(['admin.*']).lazy(() => import('./apps/admin'));
|
|
16
16
|
*
|
|
17
17
|
* export default {
|
|
18
18
|
* fetch(request) {
|