@rangojs/router 0.0.0-experimental.110 → 0.0.0-experimental.112
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 +41 -37
- package/dist/vite/index.js +144 -191
- package/package.json +17 -14
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/rango/SKILL.md +20 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/navigation-bridge.ts +4 -0
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +7 -21
- package/src/browser/partial-update.ts +8 -16
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +16 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/generate-manifest.ts +29 -31
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/router-processing.ts +37 -9
- package/src/build/runtime-discovery.ts +9 -20
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +29 -0
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +1 -0
- package/src/response-utils.ts +9 -0
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +231 -259
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/match-result.ts +32 -30
- package/src/router/middleware.ts +46 -78
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +3 -2
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/segment-system.tsx +5 -39
- package/src/server/context.ts +76 -35
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +2 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +20 -115
- package/src/urls/urls-function.ts +1 -5
- package/src/vite/discovery/discover-routers.ts +10 -22
- package/src/vite/discovery/route-types-writer.ts +38 -82
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- 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-internal-ids.ts +34 -62
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/router-discovery.ts +71 -26
- package/src/vite/utils/shared-utils.ts +13 -1
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createResponseWithMergedHeaders,
|
|
9
9
|
carryOverRedirectHeaders,
|
|
10
10
|
} from "./helpers.js";
|
|
11
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
11
12
|
|
|
12
13
|
// W3 -----------------------------------------------------------------------
|
|
13
14
|
|
|
@@ -18,16 +19,14 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export function extractRedirectResponse(value: unknown): Response | null {
|
|
20
21
|
if (!(value instanceof Response)) return null;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
22
|
+
if (!isRedirectResponse(value)) return null;
|
|
23
|
+
const location = value.headers.get("Location")!;
|
|
24
|
+
const redirect = createResponseWithMergedHeaders(null, {
|
|
25
|
+
status: value.status,
|
|
26
|
+
headers: { Location: location },
|
|
27
|
+
});
|
|
28
|
+
carryOverRedirectHeaders(value, redirect);
|
|
29
|
+
return redirect;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
hasBodyContent,
|
|
28
28
|
createResponseWithMergedHeaders,
|
|
29
29
|
createSimpleRedirectResponse,
|
|
30
|
-
|
|
30
|
+
interceptRedirectForPartial,
|
|
31
31
|
} from "./helpers.js";
|
|
32
32
|
import type { HandlerContext } from "./handler-context.js";
|
|
33
33
|
|
|
@@ -111,49 +111,25 @@ export async function executeServerAction<TEnv>(
|
|
|
111
111
|
loadedAction = await ctx.loadServerAction(actionId);
|
|
112
112
|
const data = await loadedAction!.apply(null, args);
|
|
113
113
|
|
|
114
|
-
// Intercept redirect
|
|
115
|
-
//
|
|
116
|
-
// and the revalidation step would run unnecessarily.
|
|
114
|
+
// Intercept redirect Responses: serializing one as the action returnValue
|
|
115
|
+
// would fail, and revalidation would run needlessly.
|
|
117
116
|
if (data instanceof Response) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (locationState) {
|
|
124
|
-
redirect = ctx.createRedirectFlightResponse(
|
|
125
|
-
redirectUrl,
|
|
126
|
-
resolveLocationStateEntries(locationState),
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
130
|
-
}
|
|
131
|
-
carryOverRedirectHeaders(data, redirect);
|
|
132
|
-
return redirect;
|
|
133
|
-
}
|
|
117
|
+
const intercepted = interceptRedirectForPartial(
|
|
118
|
+
data,
|
|
119
|
+
ctx.createRedirectFlightResponse,
|
|
120
|
+
);
|
|
121
|
+
if (intercepted) return intercepted;
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
returnValue = { ok: true, data };
|
|
137
125
|
} catch (error) {
|
|
138
126
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
139
127
|
if (error instanceof Response) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let redirect: Response;
|
|
146
|
-
if (locationState) {
|
|
147
|
-
redirect = ctx.createRedirectFlightResponse(
|
|
148
|
-
redirectUrl,
|
|
149
|
-
resolveLocationStateEntries(locationState),
|
|
150
|
-
);
|
|
151
|
-
} else {
|
|
152
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
153
|
-
}
|
|
154
|
-
carryOverRedirectHeaders(error, redirect);
|
|
155
|
-
return redirect;
|
|
156
|
-
}
|
|
128
|
+
const intercepted = interceptRedirectForPartial(
|
|
129
|
+
error,
|
|
130
|
+
ctx.createRedirectFlightResponse,
|
|
131
|
+
);
|
|
132
|
+
if (intercepted) return intercepted;
|
|
157
133
|
|
|
158
134
|
// Non-redirect Response thrown from action — this will be treated
|
|
159
135
|
// as a regular error and routed to the error boundary. Warn in dev
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -126,3 +126,19 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
|
126
126
|
|
|
127
127
|
return true;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
// Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
|
|
131
|
+
// from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
|
|
132
|
+
// Accept header as needing SSR; this treats it as RSC).
|
|
133
|
+
export function isRscRequest(
|
|
134
|
+
request: Request,
|
|
135
|
+
url: URL,
|
|
136
|
+
isPartial: boolean,
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
isPartial ||
|
|
140
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
141
|
+
!url.searchParams.has("__html")) ||
|
|
142
|
+
url.searchParams.has("__rsc")
|
|
143
|
+
);
|
|
144
|
+
}
|
package/src/segment-system.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { createElement, type ReactNode, type ComponentType } from "react";
|
|
|
3
3
|
import { OutletProvider } from "./client.js";
|
|
4
4
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
5
5
|
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
6
|
-
import {
|
|
6
|
+
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
7
7
|
import { invariant } from "./errors.js";
|
|
8
8
|
import {
|
|
9
9
|
RouteContentWrapper,
|
|
@@ -59,42 +59,6 @@ function restoreParallelLoaderMarkers(
|
|
|
59
59
|
return nextSegments ?? segments;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
64
|
-
*/
|
|
65
|
-
function resolveLoaderData(
|
|
66
|
-
resolvedData: any[],
|
|
67
|
-
loaderIds: string[],
|
|
68
|
-
): { loaderData: Record<string, any>; errorFallback: ReactNode } {
|
|
69
|
-
const loaderData: Record<string, any> = {};
|
|
70
|
-
let errorFallback: ReactNode = null;
|
|
71
|
-
|
|
72
|
-
for (let i = 0; i < loaderIds.length; i++) {
|
|
73
|
-
const id = loaderIds[i];
|
|
74
|
-
const result = resolvedData[i];
|
|
75
|
-
|
|
76
|
-
if (!isLoaderDataResult(result)) {
|
|
77
|
-
// Legacy format - direct data
|
|
78
|
-
loaderData[id] = result;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (result.ok) {
|
|
83
|
-
loaderData[id] = result.data;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Error case
|
|
88
|
-
if (result.fallback) {
|
|
89
|
-
errorFallback = result.fallback;
|
|
90
|
-
} else {
|
|
91
|
-
throw new Error(result.error.message);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { loaderData, errorFallback };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
62
|
/**
|
|
99
63
|
* Options for renderSegments
|
|
100
64
|
*/
|
|
@@ -337,13 +301,15 @@ export async function renderSegments(
|
|
|
337
301
|
|
|
338
302
|
// Prepare loader data if there are loaders
|
|
339
303
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
340
|
-
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
341
304
|
|
|
342
305
|
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
343
306
|
// This ensures cached segments (which may not have loader segments) have the same
|
|
344
307
|
// tree structure as fresh segments, preventing React remounts
|
|
345
308
|
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
346
309
|
if (loading !== undefined && loading !== null) {
|
|
310
|
+
// Aggregate built here only — the loaderless and no-loading branches don't
|
|
311
|
+
// read it (the latter builds its own per-parallel promises).
|
|
312
|
+
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
347
313
|
content = createElement(LoaderBoundary, {
|
|
348
314
|
key: `loader-boundary-${key}`,
|
|
349
315
|
loaderDataPromise:
|
|
@@ -387,7 +353,7 @@ export async function renderSegments(
|
|
|
387
353
|
)
|
|
388
354
|
: Promise.resolve([]);
|
|
389
355
|
const resolvedData = await layoutLoaderDataPromise;
|
|
390
|
-
const { loaderData, errorFallback } =
|
|
356
|
+
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
391
357
|
resolvedData,
|
|
392
358
|
layoutLoaderIds,
|
|
393
359
|
);
|
package/src/server/context.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
ShouldRevalidateFn,
|
|
11
11
|
TransitionConfig,
|
|
12
12
|
} from "../types";
|
|
13
|
-
import { invariant } from "../errors";
|
|
13
|
+
import { invariant, DslContextError } from "../errors";
|
|
14
14
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
15
15
|
|
|
16
16
|
// ============================================================================
|
|
@@ -71,6 +71,10 @@ export type EntryPropCommon = {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
+
* Attachments resolved by walking the parent chain, not owned by the entry:
|
|
75
|
+
* middleware composes downward; revalidate and the error/notFound boundaries are
|
|
76
|
+
* resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
|
|
77
|
+
*
|
|
74
78
|
* @internal This type is an implementation detail and may change without notice.
|
|
75
79
|
*/
|
|
76
80
|
export type EntryPropDatas = {
|
|
@@ -80,6 +84,16 @@ export type EntryPropDatas = {
|
|
|
80
84
|
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
81
85
|
};
|
|
82
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Render-time presentation fields shared by every entry variant.
|
|
89
|
+
*
|
|
90
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
91
|
+
*/
|
|
92
|
+
export type EntryPropRender = {
|
|
93
|
+
loading?: ReactNode | false;
|
|
94
|
+
transition?: TransitionConfig;
|
|
95
|
+
};
|
|
96
|
+
|
|
83
97
|
/**
|
|
84
98
|
* Loader entry stored in EntryData
|
|
85
99
|
* Contains the loader definition and its revalidation rules
|
|
@@ -158,11 +172,9 @@ export type InterceptEntry = {
|
|
|
158
172
|
};
|
|
159
173
|
|
|
160
174
|
export interface ParallelEntryData
|
|
161
|
-
extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
|
|
175
|
+
extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
|
|
162
176
|
type: "parallel";
|
|
163
177
|
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
164
|
-
loading?: ReactNode | false;
|
|
165
|
-
transition?: TransitionConfig;
|
|
166
178
|
/** Set when any parallel slot is a Static definition */
|
|
167
179
|
isStaticPrerender?: true;
|
|
168
180
|
/** Per-slot static handler $$ids for build-time store lookup */
|
|
@@ -171,6 +183,13 @@ export interface ParallelEntryData
|
|
|
171
183
|
|
|
172
184
|
export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
|
|
173
185
|
|
|
186
|
+
/**
|
|
187
|
+
* This entry's own structural children plus its owned loaders. `loader` lives
|
|
188
|
+
* here (not in EntryPropDatas) because loaders are owned by the entry, not
|
|
189
|
+
* inherited from ancestors.
|
|
190
|
+
*
|
|
191
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
192
|
+
*/
|
|
174
193
|
export type EntryPropSegments = {
|
|
175
194
|
loader: LoaderEntry[];
|
|
176
195
|
layout: EntryData[];
|
|
@@ -182,8 +201,6 @@ export type EntryData =
|
|
|
182
201
|
| ({
|
|
183
202
|
type: "route";
|
|
184
203
|
handler: Handler<any, any, any>;
|
|
185
|
-
loading?: ReactNode | false;
|
|
186
|
-
transition?: TransitionConfig;
|
|
187
204
|
/** URL pattern for this route (used by path() in urls()) */
|
|
188
205
|
pattern?: string;
|
|
189
206
|
/** Set when handler is a Prerender definition */
|
|
@@ -205,29 +222,28 @@ export type EntryData =
|
|
|
205
222
|
responseType?: string;
|
|
206
223
|
} & EntryPropCommon &
|
|
207
224
|
EntryPropDatas &
|
|
208
|
-
EntryPropSegments
|
|
225
|
+
EntryPropSegments &
|
|
226
|
+
EntryPropRender)
|
|
209
227
|
| ({
|
|
210
228
|
type: "layout";
|
|
211
229
|
handler: ReactNode | Handler<any, any, any>;
|
|
212
|
-
loading?: ReactNode | false;
|
|
213
|
-
transition?: TransitionConfig;
|
|
214
230
|
/** Set when handler is a Static definition (build-time only) */
|
|
215
231
|
isStaticPrerender?: true;
|
|
216
232
|
/** Static handler $$id for build-time store lookup */
|
|
217
233
|
staticHandlerId?: string;
|
|
218
234
|
} & EntryPropCommon &
|
|
219
235
|
EntryPropDatas &
|
|
220
|
-
EntryPropSegments
|
|
236
|
+
EntryPropSegments &
|
|
237
|
+
EntryPropRender)
|
|
221
238
|
| ParallelEntryData
|
|
222
239
|
| ({
|
|
223
240
|
type: "cache";
|
|
224
241
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
225
242
|
handler: ReactNode | Handler<any, any, any>;
|
|
226
|
-
loading?: ReactNode | false;
|
|
227
|
-
transition?: TransitionConfig;
|
|
228
243
|
} & EntryPropCommon &
|
|
229
244
|
EntryPropDatas &
|
|
230
|
-
EntryPropSegments
|
|
245
|
+
EntryPropSegments &
|
|
246
|
+
EntryPropRender);
|
|
231
247
|
|
|
232
248
|
/**
|
|
233
249
|
* Tracked include info for build-time manifest generation
|
|
@@ -307,6 +323,24 @@ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
|
|
|
307
323
|
globalThis as any
|
|
308
324
|
)[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
|
|
309
325
|
|
|
326
|
+
/** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
|
|
327
|
+
const SHORT_CODE_PREFIX: Record<
|
|
328
|
+
"layout" | "parallel" | "route" | "loader" | "cache",
|
|
329
|
+
string
|
|
330
|
+
> = {
|
|
331
|
+
layout: "L",
|
|
332
|
+
parallel: "P",
|
|
333
|
+
route: "R",
|
|
334
|
+
loader: "D",
|
|
335
|
+
cache: "C",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** Post-increment a named per-store counter, returning the prior value. */
|
|
339
|
+
function bumpCounter(store: HelperContext, key: string): number {
|
|
340
|
+
store.counters[key] ??= 0;
|
|
341
|
+
return store.counters[key]++;
|
|
342
|
+
}
|
|
343
|
+
|
|
310
344
|
export const getContext = (): {
|
|
311
345
|
context: AsyncLocalStorage<HelperContext>;
|
|
312
346
|
getStore: () => HelperContext;
|
|
@@ -373,10 +407,7 @@ export const getContext = (): {
|
|
|
373
407
|
) => {
|
|
374
408
|
const store = context.getStore();
|
|
375
409
|
invariant(store, "No context RangoContext available");
|
|
376
|
-
store
|
|
377
|
-
const index = store.counters[type];
|
|
378
|
-
store.counters[type] = index + 1;
|
|
379
|
-
return `$${type}.${index}`;
|
|
410
|
+
return `$${type}.${bumpCounter(store, type)}`;
|
|
380
411
|
},
|
|
381
412
|
getShortCode: (
|
|
382
413
|
type: "layout" | "parallel" | "route" | "loader" | "cache",
|
|
@@ -385,16 +416,7 @@ export const getContext = (): {
|
|
|
385
416
|
invariant(store, "No context RangoContext available");
|
|
386
417
|
|
|
387
418
|
const parent = store.parent;
|
|
388
|
-
const prefix =
|
|
389
|
-
type === "layout"
|
|
390
|
-
? "L"
|
|
391
|
-
: type === "parallel"
|
|
392
|
-
? "P"
|
|
393
|
-
: type === "loader"
|
|
394
|
-
? "D"
|
|
395
|
-
: type === "cache"
|
|
396
|
-
? "C"
|
|
397
|
-
: "R";
|
|
419
|
+
const prefix = SHORT_CODE_PREFIX[type];
|
|
398
420
|
const mountPrefix =
|
|
399
421
|
store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
|
|
400
422
|
|
|
@@ -405,10 +427,7 @@ export const getContext = (): {
|
|
|
405
427
|
const counterKey = mountPrefix
|
|
406
428
|
? `${mountPrefix}_root_${type}`
|
|
407
429
|
: `root_${type}`;
|
|
408
|
-
store
|
|
409
|
-
const index = store.counters[counterKey];
|
|
410
|
-
store.counters[counterKey] = index + 1;
|
|
411
|
-
return `${mountPrefix}${prefix}${index}`;
|
|
430
|
+
return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
|
|
412
431
|
} else {
|
|
413
432
|
// Child entry: use parent-scoped counter with includeScope appended.
|
|
414
433
|
// When we're evaluating a lazy include's direct children, includeScope
|
|
@@ -416,10 +435,7 @@ export const getContext = (): {
|
|
|
416
435
|
// parent's counter namespace so routes inside one include cannot
|
|
417
436
|
// collide with siblings declared outside it.
|
|
418
437
|
const counterKey = `${parent.shortCode}${includeScope}_${type}`;
|
|
419
|
-
store
|
|
420
|
-
const index = store.counters[counterKey];
|
|
421
|
-
store.counters[counterKey] = index + 1;
|
|
422
|
-
return `${parent.shortCode}${includeScope}${prefix}${index}`;
|
|
438
|
+
return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
|
|
423
439
|
}
|
|
424
440
|
},
|
|
425
441
|
runWithStore: <T>(
|
|
@@ -493,6 +509,31 @@ export const getContext = (): {
|
|
|
493
509
|
};
|
|
494
510
|
};
|
|
495
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Acquire the active DSL build context, throwing `message` if a helper was
|
|
514
|
+
* called outside a urls()/map() builder. Returns the store API and the live
|
|
515
|
+
* HelperContext so callers avoid a second getContext() lookup.
|
|
516
|
+
*/
|
|
517
|
+
export function requireDslContext(message: string): {
|
|
518
|
+
store: ReturnType<typeof getContext>;
|
|
519
|
+
ctx: HelperContext;
|
|
520
|
+
} {
|
|
521
|
+
const store = getContext();
|
|
522
|
+
const ctx = store.context.getStore();
|
|
523
|
+
if (!ctx) {
|
|
524
|
+
// The only reason the store is absent here is that a route-definition helper
|
|
525
|
+
// ran with no active RangoContext — i.e. outside a urls()/map() builder.
|
|
526
|
+
// Record that as the cause so the throw is self-explanatory, not a bare
|
|
527
|
+
// "must be called inside urls()" with no indication of the mechanism.
|
|
528
|
+
throw new DslContextError(message, {
|
|
529
|
+
cause:
|
|
530
|
+
"RangoContext store is undefined: a route-definition helper was called " +
|
|
531
|
+
"outside an active urls()/map() builder.",
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return { store, ctx };
|
|
535
|
+
}
|
|
536
|
+
|
|
496
537
|
/**
|
|
497
538
|
* Run a callback with specific URL and name prefixes
|
|
498
539
|
* Used by include() to apply prefixes to nested patterns
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { AllUseItems, IncludeItem } from "../route-types.js";
|
|
2
2
|
import {
|
|
3
|
-
getContext,
|
|
4
|
-
runWithPrefixes,
|
|
5
3
|
getUrlPrefix,
|
|
6
4
|
getNamePrefix,
|
|
5
|
+
requireDslContext,
|
|
7
6
|
} from "../server/context";
|
|
8
7
|
import {
|
|
9
8
|
INTERNAL_INCLUDE_SCOPE_PREFIX,
|
|
@@ -26,28 +25,10 @@ function allocateInternalIncludeScopeId(
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
*
|
|
30
|
-
* This expands the include into actual route registrations
|
|
31
|
-
*/
|
|
32
|
-
function processIncludeItem(item: IncludeItem): AllUseItems[] {
|
|
33
|
-
const { prefix, patterns } = item;
|
|
34
|
-
const namePrefix =
|
|
35
|
-
(item as IncludeItem & { _lazyContext?: { namePrefix?: string } })
|
|
36
|
-
._lazyContext?.namePrefix ?? item.options?.name;
|
|
37
|
-
|
|
38
|
-
// Execute the nested patterns' handler with URL and name prefixes
|
|
39
|
-
// The urlPrefix being set tells nested urls() to skip RootLayout wrapping
|
|
40
|
-
return runWithPrefixes(prefix, namePrefix, () => {
|
|
41
|
-
// Call the nested patterns' handler - this registers routes with prefixed patterns/names
|
|
42
|
-
return (patterns as UrlPatterns).handler();
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Recursively process items, expanding any IncludeItems
|
|
48
|
-
* Returns items with IncludeItems expanded into actual route items
|
|
28
|
+
* Recursively walk items, recursing into layout children.
|
|
49
29
|
*
|
|
50
|
-
*
|
|
30
|
+
* All includes are lazy and kept as-is; the router expands them on the first
|
|
31
|
+
* matching request.
|
|
51
32
|
*/
|
|
52
33
|
export function processItems(items: readonly AllUseItems[]): AllUseItems[] {
|
|
53
34
|
const result: AllUseItems[] = [];
|
|
@@ -56,26 +37,8 @@ export function processItems(items: readonly AllUseItems[]): AllUseItems[] {
|
|
|
56
37
|
if (!item) continue;
|
|
57
38
|
|
|
58
39
|
if (item.type === "include") {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
lazy?: boolean;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Lazy includes are NOT expanded here - kept for router to handle
|
|
65
|
-
if (includeItem.lazy) {
|
|
66
|
-
result.push(item);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Eager includes are already expanded during include() call
|
|
71
|
-
if (includeItem._expanded) {
|
|
72
|
-
// Items were expanded immediately - just process them recursively
|
|
73
|
-
result.push(...processItems(includeItem._expanded));
|
|
74
|
-
} else {
|
|
75
|
-
// Fallback for legacy include items without _expanded
|
|
76
|
-
const expanded = processIncludeItem(item as IncludeItem);
|
|
77
|
-
result.push(...processItems(expanded));
|
|
78
|
-
}
|
|
40
|
+
// All includes are lazy; the router expands them on first matching request.
|
|
41
|
+
result.push(item);
|
|
79
42
|
} else if (item.type === "layout" && (item as any).uses) {
|
|
80
43
|
// Process nested items in layout
|
|
81
44
|
const layoutItem = item as any;
|
|
@@ -92,13 +55,9 @@ export function processItems(items: readonly AllUseItems[]): AllUseItems[] {
|
|
|
92
55
|
/**
|
|
93
56
|
* Create include() helper for composing URL patterns
|
|
94
57
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* With `lazy: true`, patterns are NOT expanded at definition time. Instead,
|
|
100
|
-
* they're evaluated on first request that matches the prefix. This improves
|
|
101
|
-
* cold start time for apps with many routes.
|
|
58
|
+
* All includes are lazy: the nested patterns are NOT expanded at definition
|
|
59
|
+
* time. Instead they are evaluated on the first request that matches the
|
|
60
|
+
* prefix, which improves cold start time for apps with many routes.
|
|
102
61
|
*/
|
|
103
62
|
export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
104
63
|
return (
|
|
@@ -106,9 +65,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
|
106
65
|
patterns: UrlPatterns<TEnv>,
|
|
107
66
|
options?: IncludeOptions,
|
|
108
67
|
): IncludeItem => {
|
|
109
|
-
const
|
|
110
|
-
const ctx = store.getStore();
|
|
111
|
-
if (!ctx) throw new Error("include() must be called inside urls()");
|
|
68
|
+
const { ctx } = requireDslContext("include() must be called inside urls()");
|
|
112
69
|
|
|
113
70
|
const explicitName = options?.name;
|
|
114
71
|
const hasExplicitName = hasExplicitNameOption(options);
|
package/src/urls/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ export type {
|
|
|
13
13
|
UnnamedRoute,
|
|
14
14
|
LocalOnlyInclude,
|
|
15
15
|
PathOptions,
|
|
16
|
-
PathDefinition,
|
|
17
16
|
UrlPatterns,
|
|
18
17
|
IncludeOptions,
|
|
19
18
|
} from "./pattern-types.js";
|
|
@@ -22,8 +21,6 @@ export type {
|
|
|
22
21
|
export type {
|
|
23
22
|
ExtractRoutes,
|
|
24
23
|
ExtractResponses,
|
|
25
|
-
ExtractRouteNames,
|
|
26
|
-
ExtractPathParams,
|
|
27
24
|
ResponseError,
|
|
28
25
|
ResponseEnvelope,
|
|
29
26
|
RouteResponse,
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { Handler } from "../types.js";
|
|
3
|
-
import type {
|
|
4
|
-
AllUseItems,
|
|
5
|
-
RouteItem,
|
|
6
|
-
RouteUseItem,
|
|
7
|
-
UseItems,
|
|
8
|
-
} from "../route-types.js";
|
|
3
|
+
import type { RouteItem, RouteUseItem, UseItems } from "../route-types.js";
|
|
9
4
|
import {
|
|
10
|
-
getContext,
|
|
11
5
|
getUrlPrefix,
|
|
12
6
|
getNamePrefix,
|
|
13
7
|
getRootScoped,
|
|
8
|
+
requireDslContext,
|
|
14
9
|
} from "../server/context";
|
|
15
10
|
import { invariant, DataNotFoundError } from "../errors";
|
|
16
11
|
import { validateUserRouteName } from "../route-name.js";
|
|
@@ -39,35 +34,10 @@ import {
|
|
|
39
34
|
resolveHandlerUse,
|
|
40
35
|
mergeHandlerUse,
|
|
41
36
|
} from "../route-definition/resolve-handler-use.js";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const isValidUseItem = (item: any): item is AllUseItems | undefined | null => {
|
|
47
|
-
return (
|
|
48
|
-
typeof item === "undefined" ||
|
|
49
|
-
item === null ||
|
|
50
|
-
(item &&
|
|
51
|
-
typeof item === "object" &&
|
|
52
|
-
"type" in item &&
|
|
53
|
-
[
|
|
54
|
-
"layout",
|
|
55
|
-
"route",
|
|
56
|
-
"middleware",
|
|
57
|
-
"revalidate",
|
|
58
|
-
"parallel",
|
|
59
|
-
"intercept",
|
|
60
|
-
"loader",
|
|
61
|
-
"loading",
|
|
62
|
-
"errorBoundary",
|
|
63
|
-
"notFoundBoundary",
|
|
64
|
-
"when",
|
|
65
|
-
"cache",
|
|
66
|
-
"transition",
|
|
67
|
-
"include",
|
|
68
|
-
].includes(item.type))
|
|
69
|
-
);
|
|
70
|
-
};
|
|
37
|
+
import {
|
|
38
|
+
emptySegmentBase,
|
|
39
|
+
runAndValidateUseItems,
|
|
40
|
+
} from "../route-definition/dsl-helpers.js";
|
|
71
41
|
|
|
72
42
|
/**
|
|
73
43
|
* Apply URL prefix to a pattern
|
|
@@ -112,9 +82,9 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
112
82
|
optionsOrUse?: PathOptions | (() => UseItems<RouteUseItem>),
|
|
113
83
|
maybeUse?: () => UseItems<RouteUseItem>,
|
|
114
84
|
): RouteItem => {
|
|
115
|
-
const store =
|
|
116
|
-
|
|
117
|
-
|
|
85
|
+
const { store, ctx } = requireDslContext(
|
|
86
|
+
"path() must be called inside urls()",
|
|
87
|
+
);
|
|
118
88
|
|
|
119
89
|
invariant(
|
|
120
90
|
!ctx.parent || ctx.parent.type !== "parallel",
|
|
@@ -214,6 +184,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
214
184
|
: () => handler;
|
|
215
185
|
|
|
216
186
|
const entry = {
|
|
187
|
+
...emptySegmentBase(),
|
|
217
188
|
id: namespace,
|
|
218
189
|
shortCode: store.getShortCode("route"),
|
|
219
190
|
type: "route" as const,
|
|
@@ -221,15 +192,6 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
221
192
|
handler: wrappedHandler,
|
|
222
193
|
// Store the PREFIXED pattern for route matching
|
|
223
194
|
pattern: prefixedPattern,
|
|
224
|
-
loading: undefined,
|
|
225
|
-
middleware: [],
|
|
226
|
-
revalidate: [],
|
|
227
|
-
errorBoundary: [],
|
|
228
|
-
notFoundBoundary: [],
|
|
229
|
-
layout: [],
|
|
230
|
-
parallel: {},
|
|
231
|
-
intercept: [],
|
|
232
|
-
loader: [],
|
|
233
195
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
234
196
|
...(isPassthroughHandler(handler)
|
|
235
197
|
? {
|
|
@@ -301,10 +263,13 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
301
263
|
|
|
302
264
|
// Run merged use callback (handler.use defaults + explicit use) if present
|
|
303
265
|
if (mergedUse) {
|
|
304
|
-
const result =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
266
|
+
const result = runAndValidateUseItems(
|
|
267
|
+
store,
|
|
268
|
+
namespace,
|
|
269
|
+
entry,
|
|
270
|
+
mergedUse,
|
|
271
|
+
"path",
|
|
272
|
+
"use",
|
|
308
273
|
);
|
|
309
274
|
return { name: namespace, type: "route", uses: result } as RouteItem;
|
|
310
275
|
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
AllUseItems,
|
|
5
|
-
RouteUseItem,
|
|
6
|
-
UrlPatternsBrand,
|
|
7
|
-
} from "../route-types.js";
|
|
1
|
+
import type { TrailingSlashMode } from "../types.js";
|
|
2
|
+
import type { AllUseItems, UrlPatternsBrand } from "../route-types.js";
|
|
8
3
|
import type { SearchSchema } from "../search-params.js";
|
|
9
4
|
import { RESPONSE_TYPE } from "./response-types.js";
|
|
10
5
|
import type { DefaultEnv } from "../types.js";
|
|
@@ -54,16 +49,6 @@ export interface PathOptions<
|
|
|
54
49
|
[RESPONSE_TYPE]?: string;
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
/**
|
|
58
|
-
* Internal representation of a URL pattern definition
|
|
59
|
-
*/
|
|
60
|
-
export interface PathDefinition {
|
|
61
|
-
pattern: string;
|
|
62
|
-
name?: string;
|
|
63
|
-
handler: ReactNode | Handler<any, any, any>;
|
|
64
|
-
use?: RouteUseItem[];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
52
|
/**
|
|
68
53
|
* Result of urls() - contains the route definitions
|
|
69
54
|
*/
|
|
@@ -72,8 +57,6 @@ export interface UrlPatterns<
|
|
|
72
57
|
TRoutes extends Record<string, any> = Record<string, string>,
|
|
73
58
|
TResponses extends Record<string, unknown> = Record<string, unknown>,
|
|
74
59
|
> {
|
|
75
|
-
/** Internal: route definitions */
|
|
76
|
-
readonly definitions: PathDefinition[];
|
|
77
60
|
/** Internal: compiled handler function */
|
|
78
61
|
readonly handler: () => AllUseItems[];
|
|
79
62
|
/** Internal: trailing slash config per route name */
|