@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dacec167
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 +2151 -846
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- 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 +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- 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 +778 -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 +117 -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 +76 -28
- 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 +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- 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 +25 -0
- package/src/browser/rsc-router.tsx +64 -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 +2 -0
- package/src/build/route-trie.ts +52 -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 +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- 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 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +53 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- 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/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- 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 +101 -17
- 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 +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- 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/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- 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/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 +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- 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 +57 -0
- package/src/testing/flight-tree.ts +320 -0
- package/src/testing/flight.entry.ts +39 -0
- package/src/testing/flight.ts +197 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +331 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +188 -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 +270 -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 +5 -6
- 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 +0 -3
- 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 +26 -116
- 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 +101 -51
- 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 +21 -5
- 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
|
@@ -11,12 +11,16 @@ import {
|
|
|
11
11
|
getContext,
|
|
12
12
|
getNamePrefix,
|
|
13
13
|
getUrlPrefix,
|
|
14
|
+
requireDslContext,
|
|
14
15
|
type EntryData,
|
|
16
|
+
type EntryPropDatas,
|
|
17
|
+
type EntryPropSegments,
|
|
18
|
+
type HelperContext,
|
|
15
19
|
type InterceptEntry,
|
|
16
20
|
} from "../server/context";
|
|
17
21
|
import { invariant } from "../errors";
|
|
18
22
|
import { isCachedFunction } from "../cache/taint.js";
|
|
19
|
-
import {
|
|
23
|
+
import { RangoContext } from "../server/context";
|
|
20
24
|
import { isStaticHandler } from "../static-handler.js";
|
|
21
25
|
import RootLayout from "../server/root-layout";
|
|
22
26
|
import type {
|
|
@@ -38,6 +42,7 @@ import type {
|
|
|
38
42
|
} from "../route-types.js";
|
|
39
43
|
import type { RouteHelpers } from "./helpers-types.js";
|
|
40
44
|
import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
|
|
45
|
+
import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* Check if an item contains routes (directly or inside nested structures like cache).
|
|
@@ -55,19 +60,111 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
|
|
|
55
60
|
if (item.type === "layout" && item.uses) {
|
|
56
61
|
return item.uses.some((child) => hasRoutesInItem(child));
|
|
57
62
|
}
|
|
63
|
+
if (item.type === "middleware" && item.uses) {
|
|
64
|
+
return item.uses.some((child) => hasRoutesInItem(child));
|
|
65
|
+
}
|
|
58
66
|
return false;
|
|
59
67
|
};
|
|
60
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Fresh empty collections shared by every from-scratch segment entry. Returns
|
|
71
|
+
* new arrays/objects per call so no two entries share mutable references.
|
|
72
|
+
* mountPath is intentionally NOT included here — each call site adds it from
|
|
73
|
+
* getUrlPrefix() where applicable: the route() and transition() helpers add
|
|
74
|
+
* none, while path() (which also builds a `type: "route"` entry) and the
|
|
75
|
+
* structural helpers (layout/cache/middleware/parallel) do.
|
|
76
|
+
*/
|
|
77
|
+
const emptySegmentBase = (): EntryPropDatas &
|
|
78
|
+
EntryPropSegments & { loading: undefined } => ({
|
|
79
|
+
loading: undefined,
|
|
80
|
+
middleware: [],
|
|
81
|
+
revalidate: [],
|
|
82
|
+
errorBoundary: [],
|
|
83
|
+
notFoundBoundary: [],
|
|
84
|
+
layout: [],
|
|
85
|
+
parallel: {},
|
|
86
|
+
intercept: [],
|
|
87
|
+
loader: [],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Run a children/use callback as a nested scope, flatten the result, and assert
|
|
92
|
+
* every item is a valid use item. `kind` preserves the existing error wording
|
|
93
|
+
* ("use()" vs "children" callback).
|
|
94
|
+
*/
|
|
95
|
+
function runAndValidateUseItems(
|
|
96
|
+
store: ReturnType<typeof getContext>,
|
|
97
|
+
namespace: string,
|
|
98
|
+
entry: EntryData,
|
|
99
|
+
cb: () => any,
|
|
100
|
+
label: string,
|
|
101
|
+
kind: "use" | "children",
|
|
102
|
+
): AllUseItems[] {
|
|
103
|
+
const result = store.run(namespace, entry, cb)?.flat(3);
|
|
104
|
+
return validateUseItems(result, namespace, label, kind);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Assert an already-invoked, flattened callback result is a use-item array. */
|
|
108
|
+
function validateUseItems(
|
|
109
|
+
result: any,
|
|
110
|
+
namespace: string,
|
|
111
|
+
label: string,
|
|
112
|
+
kind: "use" | "children",
|
|
113
|
+
): AllUseItems[] {
|
|
114
|
+
invariant(
|
|
115
|
+
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
116
|
+
`${label}() ${kind === "use" ? "use()" : "children"} callback must return an array of use items [${namespace}]`,
|
|
117
|
+
);
|
|
118
|
+
return result as AllUseItems[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True when a children/use result contains no routes (directly or nested). */
|
|
122
|
+
const isOrphan = (result: AllUseItems[]): boolean =>
|
|
123
|
+
!result.some((item) => item != null && hasRoutesInItem(item));
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Register a routeless structural entry as an orphan sibling: clear its parent
|
|
127
|
+
* pointer so it leaves the middleware/parent-pointer chain (LOAD-BEARING — see
|
|
128
|
+
* docs/tree-structure.md) and push it onto the parent's layout[] so it renders
|
|
129
|
+
* as a wrapper. Used by cache()/middleware()/transition(); layout() runs extra
|
|
130
|
+
* validation and registers inline.
|
|
131
|
+
*/
|
|
132
|
+
const attachOrphanSibling = (
|
|
133
|
+
parent: EntryData | null,
|
|
134
|
+
entry: EntryData,
|
|
135
|
+
): void => {
|
|
136
|
+
entry.parent = null;
|
|
137
|
+
if (parent && "layout" in parent) parent.layout.push(entry);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run `fn` with `ctx.parent` temporarily redirected to `temp` — a satellite
|
|
142
|
+
* entry that captures the attachments declared by a use() callback — restoring
|
|
143
|
+
* the original parent afterward, including on throw. loader()/intercept() each
|
|
144
|
+
* build their own tempParent shape (intercept keeps a loading get/set accessor
|
|
145
|
+
* and a captured-layouts array); this only centralizes the save/restore.
|
|
146
|
+
*/
|
|
147
|
+
function withParent<T>(ctx: HelperContext, temp: EntryData, fn: () => T): T {
|
|
148
|
+
const original = ctx.parent;
|
|
149
|
+
ctx.parent = temp;
|
|
150
|
+
try {
|
|
151
|
+
return fn();
|
|
152
|
+
} finally {
|
|
153
|
+
ctx.parent = original;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
61
157
|
const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
62
|
-
const ctx =
|
|
63
|
-
|
|
158
|
+
const { store, ctx } = requireDslContext(
|
|
159
|
+
"revalidate() must be called inside urls()",
|
|
160
|
+
);
|
|
64
161
|
|
|
65
162
|
// Attach to last entry in stack
|
|
66
163
|
const parent = ctx.parent;
|
|
67
164
|
if (!parent || !("revalidate" in parent)) {
|
|
68
165
|
invariant(false, "No parent entry available for revalidate()");
|
|
69
166
|
}
|
|
70
|
-
const name = `$${
|
|
167
|
+
const name = `$${store.getNextIndex("revalidate")}`;
|
|
71
168
|
parent.revalidate.push(fn);
|
|
72
169
|
return { name, type: "revalidate" } as RevalidateItem;
|
|
73
170
|
};
|
|
@@ -105,15 +202,16 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
|
105
202
|
* ```
|
|
106
203
|
*/
|
|
107
204
|
const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
|
|
108
|
-
const ctx =
|
|
109
|
-
|
|
205
|
+
const { store, ctx } = requireDslContext(
|
|
206
|
+
"errorBoundary() must be called inside urls()",
|
|
207
|
+
);
|
|
110
208
|
|
|
111
209
|
// Attach to parent entry in stack
|
|
112
210
|
const parent = ctx.parent;
|
|
113
211
|
if (!parent || !("errorBoundary" in parent)) {
|
|
114
212
|
invariant(false, "No parent entry available for errorBoundary()");
|
|
115
213
|
}
|
|
116
|
-
const name = `$${
|
|
214
|
+
const name = `$${store.getNextIndex("errorBoundary")}`;
|
|
117
215
|
parent.errorBoundary.push(fallback);
|
|
118
216
|
return { name, type: "errorBoundary" } as ErrorBoundaryItem;
|
|
119
217
|
};
|
|
@@ -152,15 +250,16 @@ const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
|
|
|
152
250
|
const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
|
|
153
251
|
fallback,
|
|
154
252
|
) => {
|
|
155
|
-
const ctx =
|
|
156
|
-
|
|
253
|
+
const { store, ctx } = requireDslContext(
|
|
254
|
+
"notFoundBoundary() must be called inside urls()",
|
|
255
|
+
);
|
|
157
256
|
|
|
158
257
|
// Attach to parent entry in stack
|
|
159
258
|
const parent = ctx.parent;
|
|
160
259
|
if (!parent || !("notFoundBoundary" in parent)) {
|
|
161
260
|
invariant(false, "No parent entry available for notFoundBoundary()");
|
|
162
261
|
}
|
|
163
|
-
const name = `$${
|
|
262
|
+
const name = `$${store.getNextIndex("notFoundBoundary")}`;
|
|
164
263
|
parent.notFoundBoundary.push(fallback);
|
|
165
264
|
return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
|
|
166
265
|
};
|
|
@@ -174,8 +273,9 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
|
|
|
174
273
|
* for the intercept to activate.
|
|
175
274
|
*/
|
|
176
275
|
const when: RouteHelpers<any, any>["when"] = (fn) => {
|
|
177
|
-
const ctx =
|
|
178
|
-
|
|
276
|
+
const { store, ctx } = requireDslContext(
|
|
277
|
+
"when() must be called inside intercept()",
|
|
278
|
+
);
|
|
179
279
|
|
|
180
280
|
// The when() function needs to be captured by the intercept's tempParent
|
|
181
281
|
// which should have a `when` array. If not present, we're not inside intercept()
|
|
@@ -187,7 +287,7 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
|
|
|
187
287
|
);
|
|
188
288
|
}
|
|
189
289
|
|
|
190
|
-
const name = `$${
|
|
290
|
+
const name = `$${store.getNextIndex("when")}`;
|
|
191
291
|
parent.when.push(fn);
|
|
192
292
|
return { name, type: "when" } as WhenItem;
|
|
193
293
|
};
|
|
@@ -214,9 +314,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
214
314
|
| (() => UseItems<AllUseItems>),
|
|
215
315
|
maybeChildren?: () => UseItems<AllUseItems>,
|
|
216
316
|
) => {
|
|
217
|
-
const store =
|
|
218
|
-
|
|
219
|
-
|
|
317
|
+
const { store, ctx } = requireDslContext(
|
|
318
|
+
"cache() must be called inside urls()",
|
|
319
|
+
);
|
|
220
320
|
|
|
221
321
|
// Handle overloaded signature
|
|
222
322
|
let options: PartialCacheOptions | false;
|
|
@@ -229,7 +329,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
229
329
|
} else if (typeof optionsOrChildren === "string") {
|
|
230
330
|
// cache('profileName') or cache('profileName', () => [...])
|
|
231
331
|
// Resolve from context-scoped profiles (set per-router via HelperContext).
|
|
232
|
-
const ctxStore =
|
|
332
|
+
const ctxStore = RangoContext.getStore();
|
|
233
333
|
const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
|
|
234
334
|
invariant(
|
|
235
335
|
profile,
|
|
@@ -268,26 +368,18 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
268
368
|
// Create orphan cache entry (like orphan layout)
|
|
269
369
|
// Subsequent siblings in the same array will attach to this entry
|
|
270
370
|
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
271
|
-
const
|
|
371
|
+
const urlPrefix = getUrlPrefix();
|
|
272
372
|
|
|
273
373
|
const entry = {
|
|
374
|
+
...emptySegmentBase(),
|
|
274
375
|
id: namespace,
|
|
275
376
|
shortCode: store.getShortCode("cache"),
|
|
276
377
|
type: "cache",
|
|
277
378
|
parent: parent, // link to current parent for hierarchy
|
|
278
379
|
cache: cacheConfig,
|
|
279
380
|
handler: RootLayout,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
revalidate: [],
|
|
283
|
-
errorBoundary: [],
|
|
284
|
-
notFoundBoundary: [],
|
|
285
|
-
layout: [],
|
|
286
|
-
parallel: {},
|
|
287
|
-
intercept: [],
|
|
288
|
-
loader: [],
|
|
289
|
-
...(cacheUrlPrefix ? { mountPath: cacheUrlPrefix } : {}),
|
|
290
|
-
} as EntryData;
|
|
381
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
382
|
+
} satisfies EntryData;
|
|
291
383
|
|
|
292
384
|
// Attach to parent's layout array (cache entries are structural like layouts)
|
|
293
385
|
if (parent && "layout" in parent) {
|
|
@@ -301,13 +393,23 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
301
393
|
return { name: namespace, type: "cache" } as CacheItem;
|
|
302
394
|
}
|
|
303
395
|
|
|
396
|
+
// Inside a loader() use() callback, only the direct form — cache()/cache(opts)/
|
|
397
|
+
// cache("profile") — writes cache config to the loader entry. The wrapper
|
|
398
|
+
// form creates a structural cache boundary with its own children scope, which
|
|
399
|
+
// has no effect on the loader and would silently no-op.
|
|
400
|
+
invariant(
|
|
401
|
+
!(ctx.parent && (ctx.parent as any).type === "loader"),
|
|
402
|
+
"cache() wrapper form is not valid inside loader() use(). Use cache({...}) without children to configure the loader's cache.",
|
|
403
|
+
);
|
|
404
|
+
|
|
304
405
|
// With children: create a cache entry (like layout with caching semantics)
|
|
305
406
|
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
306
407
|
const cacheShortCode = store.getShortCode("cache");
|
|
307
408
|
|
|
308
|
-
const
|
|
409
|
+
const urlPrefix = getUrlPrefix();
|
|
309
410
|
|
|
310
411
|
const entry = {
|
|
412
|
+
...emptySegmentBase(),
|
|
311
413
|
id: namespace,
|
|
312
414
|
shortCode: cacheShortCode,
|
|
313
415
|
type: "cache",
|
|
@@ -315,48 +417,57 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
315
417
|
cache: cacheConfig,
|
|
316
418
|
// Cache entries render like layouts (with Outlet as default handler)
|
|
317
419
|
handler: RootLayout, // RootLayout just renders <Outlet />
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
revalidate: [],
|
|
321
|
-
errorBoundary: [],
|
|
322
|
-
notFoundBoundary: [],
|
|
323
|
-
layout: [],
|
|
324
|
-
parallel: {},
|
|
325
|
-
intercept: [],
|
|
326
|
-
loader: [],
|
|
327
|
-
...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
|
|
328
|
-
} as EntryData;
|
|
420
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
421
|
+
} satisfies EntryData;
|
|
329
422
|
|
|
330
423
|
// Run children with cache entry as parent
|
|
331
|
-
const result =
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
424
|
+
const result = runAndValidateUseItems(
|
|
425
|
+
store,
|
|
426
|
+
namespace,
|
|
427
|
+
entry,
|
|
428
|
+
children,
|
|
429
|
+
"cache",
|
|
430
|
+
"children",
|
|
336
431
|
);
|
|
337
432
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
Array.isArray(result) &&
|
|
342
|
-
result.some((item) => hasRoutesInItem(item));
|
|
343
|
-
|
|
344
|
-
if (!hasRoutes) {
|
|
345
|
-
const parent = ctx.parent;
|
|
346
|
-
if (parent && "layout" in parent) {
|
|
347
|
-
// Attach to parent's layout array (cache entries are structural like layouts)
|
|
348
|
-
entry.parent = null;
|
|
349
|
-
parent.layout.push(entry);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
433
|
+
// Cache entries are structural like layouts: with no routes inside, register
|
|
434
|
+
// as an orphan sibling.
|
|
435
|
+
if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
|
|
352
436
|
|
|
353
437
|
return { name: namespace, type: "cache", uses: result } as CacheItem;
|
|
354
438
|
};
|
|
355
439
|
|
|
356
|
-
const middleware: RouteHelpers<any, any>["middleware"] = (...
|
|
440
|
+
const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
441
|
+
// Four call forms:
|
|
442
|
+
// middleware(fn) — single fn, sibling
|
|
443
|
+
// middleware(fn, () => [...]) — single fn, wrapping
|
|
444
|
+
// middleware([fn1, fn2]) — array, sibling
|
|
445
|
+
// middleware([fn1, fn2], () => [...]) — array, wrapping
|
|
446
|
+
const isArray = Array.isArray(args[0]);
|
|
447
|
+
|
|
448
|
+
// Reject the removed variadic form before executing anything.
|
|
449
|
+
// middleware(fn1, fn2, fn3) — 3+ args, always wrong.
|
|
450
|
+
// middleware(fn1, fn2) where fn2 is a middleware fn (length >= 1), not a
|
|
451
|
+
// children callback (length === 0) — legacy two-fn form, reject early.
|
|
452
|
+
if (
|
|
453
|
+
args.length > 2 ||
|
|
454
|
+
(!isArray &&
|
|
455
|
+
args.length === 2 &&
|
|
456
|
+
typeof args[1] === "function" &&
|
|
457
|
+
args[1].length > 0)
|
|
458
|
+
) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
"middleware() no longer accepts variadic arguments. " +
|
|
461
|
+
"Use middleware([fn1, fn2, ...]) instead of middleware(fn1, fn2, ...).",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const fns: MiddlewareFn<any>[] = isArray ? args[0] : [args[0]];
|
|
466
|
+
const children: (() => any[]) | undefined =
|
|
467
|
+
typeof args[1] === "function" ? args[1] : undefined;
|
|
468
|
+
|
|
357
469
|
// Prevent "use cache" functions from being used as middleware.
|
|
358
|
-
|
|
359
|
-
for (const f of fn) {
|
|
470
|
+
for (const f of fns) {
|
|
360
471
|
if (isCachedFunction(f)) {
|
|
361
472
|
throw new Error(
|
|
362
473
|
`A "use cache" function cannot be used as middleware. ` +
|
|
@@ -367,23 +478,68 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...fn) => {
|
|
|
367
478
|
}
|
|
368
479
|
}
|
|
369
480
|
|
|
370
|
-
const ctx =
|
|
371
|
-
|
|
481
|
+
const { store, ctx } = requireDslContext(
|
|
482
|
+
"middleware() must be called inside urls()",
|
|
483
|
+
);
|
|
372
484
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
485
|
+
if (!children) {
|
|
486
|
+
// Sibling mode: attach to parent entry
|
|
487
|
+
const parent = ctx.parent;
|
|
488
|
+
if (!parent || !("middleware" in parent)) {
|
|
489
|
+
invariant(false, "No parent entry available for middleware()");
|
|
490
|
+
}
|
|
491
|
+
const name = `$${store.getNextIndex("middleware")}`;
|
|
492
|
+
parent.middleware.push(...fns);
|
|
493
|
+
return { name, type: "middleware" } as MiddlewareItem;
|
|
377
494
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
495
|
+
|
|
496
|
+
// Wrapping mode: create a transparent layout that carries the middleware
|
|
497
|
+
const mwIndex = store.getNextIndex("middleware");
|
|
498
|
+
const namespace = `${ctx.namespace}.${mwIndex}`;
|
|
499
|
+
|
|
500
|
+
const urlPrefix = getUrlPrefix();
|
|
501
|
+
const entry = {
|
|
502
|
+
...emptySegmentBase(),
|
|
503
|
+
id: namespace,
|
|
504
|
+
shortCode: store.getShortCode("layout"),
|
|
505
|
+
type: "layout",
|
|
506
|
+
parent: ctx.parent,
|
|
507
|
+
handler: RootLayout,
|
|
508
|
+
middleware: [...fns],
|
|
509
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
510
|
+
} satisfies EntryData;
|
|
511
|
+
|
|
512
|
+
// Run children callback. If the second arg was actually a middleware fn
|
|
513
|
+
// (old variadic form: middleware(mw1, mw2)), this will return a non-array
|
|
514
|
+
// and the invariant below gives a clear migration error.
|
|
515
|
+
const rawResult = store.run(namespace, entry, children);
|
|
516
|
+
|
|
517
|
+
invariant(
|
|
518
|
+
Array.isArray(rawResult),
|
|
519
|
+
"middleware(fn, children) expects the second argument to return an array of use items. " +
|
|
520
|
+
"To pass multiple middleware, use middleware([fn1, fn2]).",
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const result = validateUseItems(
|
|
524
|
+
rawResult.flat(3),
|
|
525
|
+
namespace,
|
|
526
|
+
"middleware",
|
|
527
|
+
"children",
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
name: namespace,
|
|
534
|
+
type: "middleware",
|
|
535
|
+
uses: result,
|
|
536
|
+
} as MiddlewareItem;
|
|
381
537
|
};
|
|
382
538
|
|
|
383
539
|
const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
384
|
-
const store =
|
|
385
|
-
|
|
386
|
-
|
|
540
|
+
const { store, ctx } = requireDslContext(
|
|
541
|
+
"parallel() must be called inside urls()",
|
|
542
|
+
);
|
|
387
543
|
|
|
388
544
|
if (!ctx.parent || !ctx.parent?.parallel) {
|
|
389
545
|
invariant(false, "No parent entry available for parallel()");
|
|
@@ -398,13 +554,25 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
398
554
|
|
|
399
555
|
const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
|
|
400
556
|
|
|
401
|
-
// Unwrap
|
|
557
|
+
// Unwrap slot values. A slot value can be:
|
|
558
|
+
// - a Handler / ReactNode (legacy form)
|
|
559
|
+
// - a Static() definition (build-time only)
|
|
560
|
+
// - a slot descriptor `{ handler, use? }` for slot-local overrides
|
|
561
|
+
// The descriptor's `use` runs after the broadcast `use` for that slot,
|
|
562
|
+
// so single-assignment items like `loading()` placed there win without
|
|
563
|
+
// affecting siblings.
|
|
402
564
|
const unwrappedSlots: Record<string, any> = {};
|
|
565
|
+
const slotLocalUses: Record<string, (() => any[]) | undefined> = {};
|
|
403
566
|
let hasStaticSlot = false;
|
|
404
567
|
const staticSlotIds: Record<string, string> = {};
|
|
405
|
-
for (const [slotName,
|
|
568
|
+
for (const [slotName, rawSlot] of Object.entries(
|
|
406
569
|
slots as Record<string, any>,
|
|
407
570
|
)) {
|
|
571
|
+
let slotHandler: any = rawSlot;
|
|
572
|
+
if (isSlotDescriptor(rawSlot)) {
|
|
573
|
+
slotHandler = rawSlot.handler;
|
|
574
|
+
slotLocalUses[slotName] = rawSlot.use;
|
|
575
|
+
}
|
|
408
576
|
if (isStaticHandler(slotHandler)) {
|
|
409
577
|
hasStaticSlot = true;
|
|
410
578
|
unwrappedSlots[slotName] = slotHandler.handler;
|
|
@@ -423,20 +591,12 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
423
591
|
// Create full EntryData for parallel with its own loaders/revalidate/loading
|
|
424
592
|
const parallelUrlPrefix = getUrlPrefix();
|
|
425
593
|
const entry = {
|
|
594
|
+
...emptySegmentBase(),
|
|
426
595
|
id: namespace,
|
|
427
596
|
shortCode: store.getShortCode("parallel"),
|
|
428
597
|
type: "parallel",
|
|
429
598
|
parent: null, // Parallels don't participate in parent chain traversal
|
|
430
599
|
handler: unwrappedSlots,
|
|
431
|
-
loading: undefined, // Allow loading() to attach loading state
|
|
432
|
-
middleware: [],
|
|
433
|
-
revalidate: [],
|
|
434
|
-
errorBoundary: [],
|
|
435
|
-
notFoundBoundary: [],
|
|
436
|
-
layout: [],
|
|
437
|
-
parallel: {},
|
|
438
|
-
intercept: [],
|
|
439
|
-
loader: [],
|
|
440
600
|
...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
|
|
441
601
|
...(hasStaticSlot
|
|
442
602
|
? {
|
|
@@ -471,18 +631,33 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
471
631
|
}),
|
|
472
632
|
} satisfies EntryData;
|
|
473
633
|
|
|
474
|
-
// Per-slot
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
634
|
+
// Per-slot merge order (narrowest-scope-wins for single-assignment items
|
|
635
|
+
// like loading()):
|
|
636
|
+
// 1. handler.use — defaults baked into the handler
|
|
637
|
+
// 2. shared `use` — broadcast at the parallel() call site
|
|
638
|
+
// 3. slot-local `use` — per-slot override via `{ handler, use }` descriptor
|
|
639
|
+
// Items that accumulate (loader, middleware, revalidate, …) compose
|
|
640
|
+
// across all three layers regardless of order.
|
|
641
|
+
const rawSlot = (slots as Record<string, any>)[slotName];
|
|
642
|
+
const slotHandlerForUse = isSlotDescriptor(rawSlot)
|
|
643
|
+
? rawSlot.handler
|
|
644
|
+
: rawSlot;
|
|
645
|
+
const slotHandlerUse = resolveHandlerUse(slotHandlerForUse);
|
|
646
|
+
const slotLocalUse = slotLocalUses[slotName];
|
|
647
|
+
const explicitUse = combineExplicitUses(use, slotLocalUse);
|
|
648
|
+
const slotMergedUse = mergeHandlerUse(
|
|
649
|
+
slotHandlerUse,
|
|
650
|
+
explicitUse,
|
|
651
|
+
"parallel",
|
|
652
|
+
);
|
|
481
653
|
if (slotMergedUse) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
654
|
+
runAndValidateUseItems(
|
|
655
|
+
store,
|
|
656
|
+
namespace,
|
|
657
|
+
slotEntry,
|
|
658
|
+
slotMergedUse,
|
|
659
|
+
"parallel",
|
|
660
|
+
"use",
|
|
486
661
|
);
|
|
487
662
|
}
|
|
488
663
|
|
|
@@ -491,6 +666,28 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
491
666
|
return { name: namespace, type: "parallel" } as ParallelItem;
|
|
492
667
|
};
|
|
493
668
|
|
|
669
|
+
function isSlotDescriptor(
|
|
670
|
+
value: unknown,
|
|
671
|
+
): value is { handler: unknown; use?: () => any[] } {
|
|
672
|
+
return (
|
|
673
|
+
typeof value === "object" &&
|
|
674
|
+
value !== null &&
|
|
675
|
+
!("__brand" in value) &&
|
|
676
|
+
"handler" in value &&
|
|
677
|
+
typeof (value as any).handler !== "undefined"
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function combineExplicitUses(
|
|
682
|
+
sharedUse: (() => any[]) | undefined,
|
|
683
|
+
slotLocalUse: (() => any[]) | undefined,
|
|
684
|
+
): (() => any[]) | undefined {
|
|
685
|
+
if (!sharedUse && !slotLocalUse) return undefined;
|
|
686
|
+
if (!slotLocalUse) return sharedUse;
|
|
687
|
+
if (!sharedUse) return slotLocalUse;
|
|
688
|
+
return () => [...sharedUse(), ...slotLocalUse()];
|
|
689
|
+
}
|
|
690
|
+
|
|
494
691
|
/**
|
|
495
692
|
* Intercept helper - defines an intercepting route for soft navigation
|
|
496
693
|
*/
|
|
@@ -500,9 +697,9 @@ const intercept = (
|
|
|
500
697
|
handler: any,
|
|
501
698
|
use?: () => any[],
|
|
502
699
|
) => {
|
|
503
|
-
const store =
|
|
504
|
-
|
|
505
|
-
|
|
700
|
+
const { store, ctx } = requireDslContext(
|
|
701
|
+
"intercept() must be called inside urls()",
|
|
702
|
+
);
|
|
506
703
|
|
|
507
704
|
if (!ctx.parent || !ctx.parent?.intercept) {
|
|
508
705
|
invariant(false, "No parent entry available for intercept()");
|
|
@@ -541,15 +738,13 @@ const intercept = (
|
|
|
541
738
|
|
|
542
739
|
// Run merged use callback to collect loaders, revalidate, middleware, etc.
|
|
543
740
|
if (mergedUse) {
|
|
544
|
-
//
|
|
545
|
-
// so that middleware, loader, revalidate attach to the intercept entry
|
|
546
|
-
const originalParent = ctx.parent;
|
|
547
|
-
|
|
548
|
-
// Capture layouts in a temporary array
|
|
741
|
+
// Capture layout() calls into a temporary array
|
|
549
742
|
const capturedLayouts: EntryData[] = [];
|
|
550
743
|
|
|
744
|
+
// Temporary parent so middleware/loader/revalidate/when attach to the
|
|
745
|
+
// intercept entry; the loading get/set accessor mirrors writes onto `entry`.
|
|
551
746
|
const tempParent = {
|
|
552
|
-
...
|
|
747
|
+
...ctx.parent,
|
|
553
748
|
middleware: entry.middleware,
|
|
554
749
|
revalidate: entry.revalidate,
|
|
555
750
|
errorBoundary: entry.errorBoundary,
|
|
@@ -557,7 +752,6 @@ const intercept = (
|
|
|
557
752
|
loader: entry.loader,
|
|
558
753
|
layout: capturedLayouts, // Capture layout() calls
|
|
559
754
|
when: entry.when, // Capture when() conditions
|
|
560
|
-
// Use getter/setter to capture loading on the entry
|
|
561
755
|
get loading() {
|
|
562
756
|
return entry.loading;
|
|
563
757
|
},
|
|
@@ -565,12 +759,10 @@ const intercept = (
|
|
|
565
759
|
entry.loading = value;
|
|
566
760
|
},
|
|
567
761
|
};
|
|
568
|
-
ctx.parent = tempParent as EntryData;
|
|
569
762
|
|
|
570
|
-
const result =
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
ctx.parent = originalParent;
|
|
763
|
+
const result = withParent(ctx, tempParent as EntryData, () =>
|
|
764
|
+
mergedUse()?.flat(3),
|
|
765
|
+
);
|
|
574
766
|
|
|
575
767
|
// Extract layout from captured layouts (use first one if multiple)
|
|
576
768
|
// Layout inside intercept should always be ReactNode or Handler, not Record slots
|
|
@@ -580,10 +772,7 @@ const intercept = (
|
|
|
580
772
|
| Handler<any, any, any>;
|
|
581
773
|
}
|
|
582
774
|
|
|
583
|
-
|
|
584
|
-
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
585
|
-
`intercept() use() callback must return an array of use items [${namespace}]`,
|
|
586
|
-
);
|
|
775
|
+
validateUseItems(result, namespace, "intercept", "use");
|
|
587
776
|
}
|
|
588
777
|
|
|
589
778
|
ctx.parent.intercept.push(entry);
|
|
@@ -593,10 +782,10 @@ const intercept = (
|
|
|
593
782
|
/**
|
|
594
783
|
* Loader helper - attaches a loader to the current entry
|
|
595
784
|
*/
|
|
596
|
-
const
|
|
597
|
-
const store =
|
|
598
|
-
|
|
599
|
-
|
|
785
|
+
const loader: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
786
|
+
const { store, ctx } = requireDslContext(
|
|
787
|
+
"loader() must be called inside urls()",
|
|
788
|
+
);
|
|
600
789
|
|
|
601
790
|
// Attach to last entry in stack
|
|
602
791
|
if (!ctx.parent || !ctx.parent?.loader) {
|
|
@@ -611,25 +800,28 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
611
800
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
612
801
|
};
|
|
613
802
|
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
803
|
+
// Merge handler.use defaults (attached to the loader definition) with explicit use
|
|
804
|
+
const handlerUseFn = resolveHandlerUse(loaderDef);
|
|
805
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "loader");
|
|
806
|
+
|
|
807
|
+
// If any use callback is in effect, run it to collect revalidation rules and cache config
|
|
808
|
+
if (mergedUse) {
|
|
618
809
|
// Create a temporary "parent" with type "loader" so cache() can detect it.
|
|
619
810
|
// Save existing .cache to distinguish inherited config from newly set config.
|
|
620
|
-
const parentCache = (
|
|
811
|
+
const parentCache = (ctx.parent as any).cache;
|
|
621
812
|
const tempParent = {
|
|
622
|
-
...
|
|
813
|
+
...ctx.parent,
|
|
623
814
|
type: "loader",
|
|
624
815
|
revalidate: loaderEntry.revalidate,
|
|
625
816
|
};
|
|
626
|
-
ctx.parent = tempParent as EntryData;
|
|
627
817
|
|
|
628
|
-
const result =
|
|
818
|
+
const result = withParent(ctx, tempParent as EntryData, () =>
|
|
819
|
+
mergedUse()?.flat(3),
|
|
820
|
+
);
|
|
629
821
|
|
|
630
822
|
// Copy cache config only if cache() was called during the use() callback.
|
|
631
|
-
// The spread
|
|
632
|
-
//
|
|
823
|
+
// The spread may carry an inherited .cache from a parent cache() boundary —
|
|
824
|
+
// only copy if it was newly set.
|
|
633
825
|
if (
|
|
634
826
|
(tempParent as any).cache &&
|
|
635
827
|
(tempParent as any).cache !== parentCache
|
|
@@ -637,13 +829,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
637
829
|
(loaderEntry as any).cache = (tempParent as any).cache;
|
|
638
830
|
}
|
|
639
831
|
|
|
640
|
-
|
|
641
|
-
ctx.parent = originalParent;
|
|
642
|
-
|
|
643
|
-
invariant(
|
|
644
|
-
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
645
|
-
`loader() use() callback must return an array of use items [${name}]`,
|
|
646
|
-
);
|
|
832
|
+
validateUseItems(result, name, "loader", "use");
|
|
647
833
|
}
|
|
648
834
|
|
|
649
835
|
ctx.parent.loader.push(loaderEntry);
|
|
@@ -654,10 +840,10 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
654
840
|
* Loading helper - attaches a loading component to the current entry
|
|
655
841
|
* Loading components are static (no context) and shown during navigation
|
|
656
842
|
*/
|
|
657
|
-
const
|
|
658
|
-
const store =
|
|
659
|
-
|
|
660
|
-
|
|
843
|
+
const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
844
|
+
const { store, ctx } = requireDslContext(
|
|
845
|
+
"loading() must be called inside urls()",
|
|
846
|
+
);
|
|
661
847
|
|
|
662
848
|
const parent = ctx.parent;
|
|
663
849
|
if (!parent || !("loading" in parent)) {
|
|
@@ -680,10 +866,13 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
680
866
|
};
|
|
681
867
|
|
|
682
868
|
/**
|
|
683
|
-
* Transition helper -
|
|
684
|
-
*
|
|
869
|
+
* Transition helper - opts the entry (or a wrapped group of routes) into
|
|
870
|
+
* transition-driven navigation by attaching a TransitionConfig. This drives the
|
|
871
|
+
* commit through startTransition (content hold on all React versions) and, on
|
|
872
|
+
* experimental React, places a `<ViewTransition>` boundary unless
|
|
873
|
+
* `viewTransition: false`. See skills/view-transitions for the matrix.
|
|
685
874
|
*/
|
|
686
|
-
const
|
|
875
|
+
const transition = (
|
|
687
876
|
configOrChildren?: TransitionConfig | (() => UseItems<AllUseItems>),
|
|
688
877
|
maybeChildren?: () => UseItems<AllUseItems>,
|
|
689
878
|
): TransitionItem => {
|
|
@@ -697,9 +886,9 @@ const transitionFn = (
|
|
|
697
886
|
const children: (() => UseItems<AllUseItems>) | undefined =
|
|
698
887
|
typeof configOrChildren === "function" ? configOrChildren : maybeChildren;
|
|
699
888
|
|
|
700
|
-
const store =
|
|
701
|
-
|
|
702
|
-
|
|
889
|
+
const { store, ctx } = requireDslContext(
|
|
890
|
+
"transition() must be called inside urls()",
|
|
891
|
+
);
|
|
703
892
|
|
|
704
893
|
const name = `$${store.getNextIndex("transition")}`;
|
|
705
894
|
|
|
@@ -716,68 +905,43 @@ const transitionFn = (
|
|
|
716
905
|
// Position 2: wrapper — create a transparent layout with transition config
|
|
717
906
|
const namespace = `${ctx.namespace}.${store.getNextIndex("transition")}`;
|
|
718
907
|
const entry = {
|
|
908
|
+
...emptySegmentBase(),
|
|
719
909
|
id: namespace,
|
|
720
910
|
shortCode: store.getShortCode("layout"),
|
|
721
911
|
type: "layout",
|
|
722
912
|
parent: ctx.parent,
|
|
723
913
|
handler: RootLayout,
|
|
724
|
-
loading: undefined,
|
|
725
914
|
transition: config,
|
|
726
|
-
|
|
727
|
-
revalidate: [],
|
|
728
|
-
errorBoundary: [],
|
|
729
|
-
notFoundBoundary: [],
|
|
730
|
-
layout: [],
|
|
731
|
-
parallel: {},
|
|
732
|
-
intercept: [],
|
|
733
|
-
loader: [],
|
|
734
|
-
} as EntryData;
|
|
735
|
-
|
|
736
|
-
const result = store.run(namespace, entry, children)?.flat(3);
|
|
915
|
+
} satisfies EntryData;
|
|
737
916
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
917
|
+
const result = runAndValidateUseItems(
|
|
918
|
+
store,
|
|
919
|
+
namespace,
|
|
920
|
+
entry,
|
|
921
|
+
children,
|
|
922
|
+
"transition",
|
|
923
|
+
"children",
|
|
741
924
|
);
|
|
742
925
|
|
|
743
|
-
|
|
744
|
-
result &&
|
|
745
|
-
Array.isArray(result) &&
|
|
746
|
-
result.some((item) => hasRoutesInItem(item));
|
|
747
|
-
|
|
748
|
-
if (!hasRoutes) {
|
|
749
|
-
const parent = ctx.parent;
|
|
750
|
-
if (parent && "layout" in parent) {
|
|
751
|
-
entry.parent = null;
|
|
752
|
-
parent.layout.push(entry);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
926
|
+
if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
|
|
755
927
|
|
|
756
928
|
return { name: namespace, type: "transition" } as TransitionItem;
|
|
757
929
|
};
|
|
758
930
|
|
|
759
|
-
const
|
|
760
|
-
const store =
|
|
761
|
-
|
|
762
|
-
|
|
931
|
+
const route: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
932
|
+
const { store, ctx } = requireDslContext(
|
|
933
|
+
"route() must be called inside urls()",
|
|
934
|
+
);
|
|
763
935
|
|
|
764
936
|
const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${name}`;
|
|
765
937
|
|
|
766
938
|
const entry = {
|
|
939
|
+
...emptySegmentBase(),
|
|
767
940
|
id: namespace,
|
|
768
941
|
shortCode: store.getShortCode("route"),
|
|
769
942
|
type: "route",
|
|
770
943
|
parent: ctx.parent,
|
|
771
944
|
handler: handler as unknown as Handler<any, any, any>,
|
|
772
|
-
loading: undefined, // Allow loading() to attach loading state
|
|
773
|
-
middleware: [],
|
|
774
|
-
revalidate: [],
|
|
775
|
-
errorBoundary: [],
|
|
776
|
-
notFoundBoundary: [],
|
|
777
|
-
layout: [],
|
|
778
|
-
parallel: {},
|
|
779
|
-
intercept: [],
|
|
780
|
-
loader: [],
|
|
781
945
|
} satisfies EntryData;
|
|
782
946
|
|
|
783
947
|
/* We will throw if user is registring same route name twice */
|
|
@@ -792,10 +956,13 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
792
956
|
const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
|
|
793
957
|
/* Run use and attach handlers */
|
|
794
958
|
if (mergedUse) {
|
|
795
|
-
const result =
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
959
|
+
const result = runAndValidateUseItems(
|
|
960
|
+
store,
|
|
961
|
+
namespace,
|
|
962
|
+
entry,
|
|
963
|
+
mergedUse,
|
|
964
|
+
"route",
|
|
965
|
+
"use",
|
|
799
966
|
);
|
|
800
967
|
return { name: namespace, type: "route", uses: result } as RouteItem;
|
|
801
968
|
}
|
|
@@ -805,9 +972,9 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
805
972
|
};
|
|
806
973
|
|
|
807
974
|
const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
808
|
-
const store =
|
|
809
|
-
|
|
810
|
-
|
|
975
|
+
const { store, ctx } = requireDslContext(
|
|
976
|
+
"layout() must be called inside urls()",
|
|
977
|
+
);
|
|
811
978
|
|
|
812
979
|
invariant(
|
|
813
980
|
!ctx.parent || ctx.parent.type !== "parallel",
|
|
@@ -825,20 +992,12 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
825
992
|
|
|
826
993
|
const urlPrefix = getUrlPrefix();
|
|
827
994
|
const entry = {
|
|
995
|
+
...emptySegmentBase(),
|
|
828
996
|
id: namespace,
|
|
829
997
|
shortCode,
|
|
830
998
|
type: "layout",
|
|
831
999
|
parent: ctx.parent,
|
|
832
1000
|
handler: unwrappedHandler,
|
|
833
|
-
loading: undefined, // Allow loading() to attach loading state
|
|
834
|
-
middleware: [],
|
|
835
|
-
revalidate: [],
|
|
836
|
-
errorBoundary: [],
|
|
837
|
-
notFoundBoundary: [],
|
|
838
|
-
parallel: {},
|
|
839
|
-
intercept: [],
|
|
840
|
-
layout: [],
|
|
841
|
-
loader: [],
|
|
842
1001
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
843
1002
|
...(isStatic
|
|
844
1003
|
? {
|
|
@@ -860,11 +1019,13 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
860
1019
|
// Run merged use callback if present
|
|
861
1020
|
let result: AllUseItems[] | undefined;
|
|
862
1021
|
if (mergedUse) {
|
|
863
|
-
result =
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1022
|
+
result = runAndValidateUseItems(
|
|
1023
|
+
store,
|
|
1024
|
+
namespace,
|
|
1025
|
+
entry,
|
|
1026
|
+
mergedUse,
|
|
1027
|
+
"layout",
|
|
1028
|
+
"use",
|
|
868
1029
|
);
|
|
869
1030
|
}
|
|
870
1031
|
|
|
@@ -906,9 +1067,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
906
1067
|
`Orphan layouts can only be defined inside route or layout > check [${namespace}]`,
|
|
907
1068
|
);
|
|
908
1069
|
|
|
909
|
-
|
|
910
|
-
entry.parent = null;
|
|
911
|
-
parent.layout.push(entry);
|
|
1070
|
+
attachOrphanSibling(parent, entry);
|
|
912
1071
|
}
|
|
913
1072
|
}
|
|
914
1073
|
|
|
@@ -921,33 +1080,15 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
921
1080
|
} as LayoutItem;
|
|
922
1081
|
};
|
|
923
1082
|
|
|
924
|
-
const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
(item
|
|
929
|
-
typeof item === "object" &&
|
|
930
|
-
"type" in item &&
|
|
931
|
-
[
|
|
932
|
-
"layout",
|
|
933
|
-
"route",
|
|
934
|
-
"middleware",
|
|
935
|
-
"revalidate",
|
|
936
|
-
"parallel",
|
|
937
|
-
"intercept",
|
|
938
|
-
"loader",
|
|
939
|
-
"loading",
|
|
940
|
-
"errorBoundary",
|
|
941
|
-
"notFoundBoundary",
|
|
942
|
-
"when",
|
|
943
|
-
"cache",
|
|
944
|
-
"transition",
|
|
945
|
-
"include", // For urls() include() helper
|
|
946
|
-
].includes(item.type))
|
|
947
|
-
);
|
|
948
|
-
};
|
|
1083
|
+
const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
|
|
1084
|
+
item == null ||
|
|
1085
|
+
(typeof item === "object" &&
|
|
1086
|
+
"type" in item &&
|
|
1087
|
+
ALL_USE_ITEM_TYPES.has(item.type));
|
|
949
1088
|
|
|
950
|
-
//
|
|
1089
|
+
// DSL helpers exported for direct import from @rangojs/router and for
|
|
1090
|
+
// assembly into the RouteHelpers object in helper-factories.ts. The route-item
|
|
1091
|
+
// types are discriminated by their `type` literal, so the helpers carry no brand.
|
|
951
1092
|
export {
|
|
952
1093
|
layout,
|
|
953
1094
|
cache,
|
|
@@ -958,25 +1099,11 @@ export {
|
|
|
958
1099
|
when,
|
|
959
1100
|
errorBoundary,
|
|
960
1101
|
notFoundBoundary,
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const isOrphanLayout = (item: AllUseItems): boolean => {
|
|
967
|
-
return (
|
|
968
|
-
item.type === "layout" &&
|
|
969
|
-
!item.uses?.some((child) => hasRoutesInItem(child))
|
|
970
|
-
);
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
// Internal exports used by helper-factories.ts
|
|
974
|
-
export {
|
|
975
|
-
routeFn,
|
|
976
|
-
loaderFn,
|
|
977
|
-
loadingFn,
|
|
978
|
-
transitionFn,
|
|
979
|
-
hasRoutesInItem,
|
|
1102
|
+
route,
|
|
1103
|
+
loader,
|
|
1104
|
+
loading,
|
|
1105
|
+
transition,
|
|
980
1106
|
isValidUseItem,
|
|
981
|
-
|
|
1107
|
+
emptySegmentBase,
|
|
1108
|
+
runAndValidateUseItems,
|
|
982
1109
|
};
|