@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.12 → 3.2.0-ultramodern.120
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/cjs/cli/index.js +47 -9
- package/dist/cjs/cli/routeSplitting.js +87 -0
- package/dist/cjs/cli/tanstackTypes.js +230 -63
- package/dist/cjs/cli.js +12 -8
- package/dist/cjs/runtime/DefaultNotFound.js +9 -5
- package/dist/cjs/runtime/basepathRewrite.js +12 -8
- package/dist/cjs/runtime/dataMutation.js +9 -5
- package/dist/cjs/runtime/hooks.js +9 -5
- package/dist/cjs/runtime/hydrationBoundary.js +48 -0
- package/dist/cjs/runtime/index.js +330 -74
- package/dist/cjs/runtime/lifecycle.js +15 -11
- package/dist/cjs/runtime/outlet.js +58 -0
- package/dist/cjs/runtime/plugin.js +203 -98
- package/dist/cjs/runtime/plugin.node.js +38 -16
- package/dist/cjs/runtime/plugin.worker.js +53 -0
- package/dist/cjs/runtime/prefetchLink.js +10 -6
- package/dist/cjs/runtime/routeTree.js +81 -17
- package/dist/cjs/runtime/rsc/ClientSlot.js +9 -5
- package/dist/cjs/runtime/rsc/CompositeComponent.js +9 -5
- package/dist/cjs/runtime/rsc/ReplayableStream.js +14 -9
- package/dist/cjs/runtime/rsc/RscNodeRenderer.js +9 -5
- package/dist/cjs/runtime/rsc/SlotContext.js +9 -5
- package/dist/cjs/runtime/rsc/client.js +9 -5
- package/dist/cjs/runtime/rsc/createRscProxy.js +9 -5
- package/dist/cjs/runtime/rsc/index.js +9 -5
- package/dist/cjs/runtime/rsc/payloadRouter.js +9 -5
- package/dist/cjs/runtime/rsc/server.js +9 -5
- package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +9 -5
- package/dist/cjs/runtime/rsc/symbols.js +20 -15
- package/dist/cjs/runtime/types.js +31 -1
- package/dist/cjs/runtime/utils.js +9 -5
- package/dist/cjs/runtime.js +9 -5
- package/dist/esm/cli/index.mjs +28 -6
- package/dist/esm/cli/routeSplitting.mjs +43 -0
- package/dist/esm/cli/tanstackTypes.mjs +219 -59
- package/dist/esm/runtime/hydrationBoundary.mjs +10 -0
- package/dist/esm/runtime/index.mjs +3 -2
- package/dist/esm/runtime/outlet.mjs +17 -0
- package/dist/esm/runtime/plugin.mjs +197 -96
- package/dist/esm/runtime/plugin.node.mjs +30 -12
- package/dist/esm/runtime/plugin.worker.mjs +1 -0
- package/dist/esm/runtime/prefetchLink.mjs +1 -1
- package/dist/esm/runtime/routeTree.mjs +73 -13
- package/dist/esm/runtime/types.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +28 -6
- package/dist/esm-node/cli/routeSplitting.mjs +44 -0
- package/dist/esm-node/cli/tanstackTypes.mjs +219 -59
- package/dist/esm-node/runtime/hydrationBoundary.mjs +11 -0
- package/dist/esm-node/runtime/index.mjs +3 -2
- package/dist/esm-node/runtime/outlet.mjs +18 -0
- package/dist/esm-node/runtime/plugin.mjs +197 -96
- package/dist/esm-node/runtime/plugin.node.mjs +30 -12
- package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
- package/dist/esm-node/runtime/routeTree.mjs +73 -13
- package/dist/esm-node/runtime/types.mjs +7 -0
- package/dist/types/cli/index.d.ts +7 -1
- package/dist/types/cli/routeSplitting.d.ts +29 -0
- package/dist/types/cli/tanstackTypes.d.ts +9 -0
- package/dist/types/runtime/hooks.d.ts +9 -24
- package/dist/types/runtime/hydrationBoundary.d.ts +2 -0
- package/dist/types/runtime/index.d.ts +5 -2
- package/dist/types/runtime/outlet.d.ts +2 -0
- package/dist/types/runtime/plugin.d.ts +1 -1
- package/dist/types/runtime/plugin.node.d.ts +1 -1
- package/dist/types/runtime/plugin.worker.d.ts +1 -0
- package/dist/types/runtime/types.d.ts +7 -0
- package/package.json +20 -20
- package/src/cli/index.ts +59 -2
- package/src/cli/routeSplitting.ts +81 -0
- package/src/cli/tanstackTypes.ts +347 -67
- package/src/runtime/hydrationBoundary.tsx +12 -0
- package/src/runtime/index.tsx +107 -2
- package/src/runtime/outlet.tsx +48 -0
- package/src/runtime/plugin.node.tsx +58 -8
- package/src/runtime/plugin.tsx +372 -157
- package/src/runtime/plugin.worker.tsx +4 -0
- package/src/runtime/prefetchLink.tsx +1 -1
- package/src/runtime/routeTree.ts +194 -23
- package/src/runtime/ssr-shim.d.ts +1 -3
- package/src/runtime/types.ts +13 -0
- package/tests/router/cli.test.ts +315 -0
- package/tests/router/fastDefaults.test.ts +25 -0
- package/tests/router/hydrationBoundary.test.tsx +23 -0
- package/tests/router/prefetchLink.test.tsx +43 -7
- package/tests/router/routeTree.test.ts +416 -1
- package/tests/router/tanstackTypes.test.ts +415 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type TanstackRouteCodeSplittingOption =
|
|
2
|
+
| boolean
|
|
3
|
+
| {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type TanstackRsbuildRouteSplittingProfile = {
|
|
8
|
+
defaultConfig: {
|
|
9
|
+
output: {
|
|
10
|
+
splitRouteChunks: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
modernRouteChunks: {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
owner: 'modern';
|
|
16
|
+
};
|
|
17
|
+
builderChunkSplit: {
|
|
18
|
+
owner: 'modern-rsbuild';
|
|
19
|
+
preserved: true;
|
|
20
|
+
};
|
|
21
|
+
tanstackStartRspackSplitter: {
|
|
22
|
+
compatible: boolean;
|
|
23
|
+
reason: string;
|
|
24
|
+
clientDeleteNodes: string[];
|
|
25
|
+
routeFactoryCalls: string[];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TANSTACK_START_ROUTE_FACTORY_CALLS = [
|
|
30
|
+
'createFileRoute',
|
|
31
|
+
'createRootRoute',
|
|
32
|
+
'createRootRouteWithContext',
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const TANSTACK_START_ROUTE_FACTORY_REGEX =
|
|
36
|
+
/\b(createFileRoute|createRootRoute|createRootRouteWithContext)\s*(?:<|\()/;
|
|
37
|
+
|
|
38
|
+
export function isTanstackStartRouteModuleSource(source: string) {
|
|
39
|
+
return TANSTACK_START_ROUTE_FACTORY_REGEX.test(source);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveTanstackRouteCodeSplittingEnabled(
|
|
43
|
+
option?: TanstackRouteCodeSplittingOption,
|
|
44
|
+
) {
|
|
45
|
+
if (typeof option === 'boolean') {
|
|
46
|
+
return option;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return option?.enabled ?? true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createTanstackRsbuildRouteSplittingProfile(opts: {
|
|
53
|
+
routeCodeSplitting?: TanstackRouteCodeSplittingOption;
|
|
54
|
+
}): TanstackRsbuildRouteSplittingProfile {
|
|
55
|
+
return {
|
|
56
|
+
defaultConfig: {
|
|
57
|
+
output: {
|
|
58
|
+
splitRouteChunks: resolveTanstackRouteCodeSplittingEnabled(
|
|
59
|
+
opts.routeCodeSplitting,
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
modernRouteChunks: {
|
|
64
|
+
enabled: resolveTanstackRouteCodeSplittingEnabled(
|
|
65
|
+
opts.routeCodeSplitting,
|
|
66
|
+
),
|
|
67
|
+
owner: 'modern',
|
|
68
|
+
},
|
|
69
|
+
builderChunkSplit: {
|
|
70
|
+
owner: 'modern-rsbuild',
|
|
71
|
+
preserved: true,
|
|
72
|
+
},
|
|
73
|
+
tanstackStartRspackSplitter: {
|
|
74
|
+
compatible: false,
|
|
75
|
+
reason:
|
|
76
|
+
'TanStack Start Rsbuild route splitting is tied to TanStack file-route factory modules; Modern generates TanStack route trees from Modern route metadata and owns route chunking through output.splitRouteChunks.',
|
|
77
|
+
clientDeleteNodes: ['ssr', 'server', 'headers'],
|
|
78
|
+
routeFactoryCalls: [...TANSTACK_START_ROUTE_FACTORY_CALLS],
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/src/cli/tanstackTypes.ts
CHANGED
|
@@ -86,6 +86,17 @@ function pickModernLoaderModule(route: NestedRouteForCli | PageRoute) {
|
|
|
86
86
|
return { loaderPath, inline };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function pickRouteSearchContractModules(route: NestedRouteForCli | PageRoute) {
|
|
90
|
+
const validateSearchPath = (route as any).validateSearch;
|
|
91
|
+
const loaderDepsPath = (route as any).loaderDeps;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
validateSearchPath:
|
|
95
|
+
typeof validateSearchPath === 'string' ? validateSearchPath : null,
|
|
96
|
+
loaderDepsPath: typeof loaderDepsPath === 'string' ? loaderDepsPath : null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
function isPathlessLayout(route: NestedRouteForCli | PageRoute) {
|
|
90
101
|
return (
|
|
91
102
|
(route as any).type === 'nested' &&
|
|
@@ -126,6 +137,137 @@ function createRouteStaticDataSnippet(opts: {
|
|
|
126
137
|
)}\n }),`;
|
|
127
138
|
}
|
|
128
139
|
|
|
140
|
+
const LOCALE_PARAM_SEGMENTS = new Set([
|
|
141
|
+
':lang',
|
|
142
|
+
':locale',
|
|
143
|
+
':language',
|
|
144
|
+
'$lang',
|
|
145
|
+
'$locale',
|
|
146
|
+
'$language',
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
type CanonicalAwareRoute = (NestedRouteForCli | PageRoute) & {
|
|
150
|
+
modernCanonicalPath?: string;
|
|
151
|
+
index?: boolean;
|
|
152
|
+
isRoot?: boolean;
|
|
153
|
+
children?: CanonicalAwareRoute[];
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function paramsTypeForCanonicalPath(canonicalPath: string): string {
|
|
157
|
+
const fields: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const segment of canonicalPath.split('/')) {
|
|
160
|
+
if (!segment) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (segment === '*' || segment === '$') {
|
|
164
|
+
fields.push(`'_splat'?: string`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (segment.startsWith('{-$') && segment.endsWith('}')) {
|
|
168
|
+
fields.push(`${JSON.stringify(segment.slice(3, -1))}?: string`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (segment.startsWith('$')) {
|
|
172
|
+
fields.push(`${JSON.stringify(segment.slice(1))}: string`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (segment.startsWith(':')) {
|
|
176
|
+
const optional = segment.endsWith('?');
|
|
177
|
+
const name = segment.slice(1, optional ? undefined : segment.length);
|
|
178
|
+
fields.push(
|
|
179
|
+
`${JSON.stringify(optional ? name.slice(0, -1) : name)}${
|
|
180
|
+
optional ? '?' : ''
|
|
181
|
+
}: string`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return fields.length > 0
|
|
187
|
+
? `{ ${fields.join('; ')} }`
|
|
188
|
+
: 'Record<string, never>';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Derive the canonical (language-agnostic) route map for an entry: the
|
|
193
|
+
* leading locale param is stripped and localized physical variants (routes
|
|
194
|
+
* carrying `modernCanonicalPath` metadata from `@modern-js/plugin-i18n`)
|
|
195
|
+
* collapse to their canonical pattern. Returns `null` when the entry has no
|
|
196
|
+
* i18n routing surface (no locale param and no localized variants), so plain
|
|
197
|
+
* TanStack apps never get a `@modern-js/plugin-i18n` module augmentation.
|
|
198
|
+
*/
|
|
199
|
+
export function collectCanonicalRoutesForEntry(
|
|
200
|
+
routes: (NestedRouteForCli | PageRoute)[],
|
|
201
|
+
): Record<string, string> | null {
|
|
202
|
+
const canonicalParams = new Map<string, string>();
|
|
203
|
+
let hasI18nSurface = false;
|
|
204
|
+
|
|
205
|
+
const normalizeJoined = (joined: string): string => {
|
|
206
|
+
const collapsed = joined.replace(/\/+/g, '/');
|
|
207
|
+
const withLeading = collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
|
|
208
|
+
return withLeading.length > 1
|
|
209
|
+
? withLeading.replace(/\/+$/, '')
|
|
210
|
+
: withLeading;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const record = (canonicalPath: string) => {
|
|
214
|
+
const normalized = normalizeJoined(canonicalPath || '/');
|
|
215
|
+
const key = toTanstackPath(normalized);
|
|
216
|
+
if (!canonicalParams.has(key)) {
|
|
217
|
+
canonicalParams.set(key, paramsTypeForCanonicalPath(normalized));
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const visit = (route: CanonicalAwareRoute, parentPath: string) => {
|
|
222
|
+
let currentPath = parentPath;
|
|
223
|
+
|
|
224
|
+
if (typeof route.modernCanonicalPath === 'string') {
|
|
225
|
+
hasI18nSurface = true;
|
|
226
|
+
currentPath = normalizeJoined(route.modernCanonicalPath);
|
|
227
|
+
} else if (typeof route.path === 'string' && route.path.length > 0) {
|
|
228
|
+
const segments = route.path
|
|
229
|
+
.replace(/\[(.+?)\]/g, ':$1')
|
|
230
|
+
.split('/')
|
|
231
|
+
.filter(Boolean);
|
|
232
|
+
if (parentPath === '' && LOCALE_PARAM_SEGMENTS.has(segments[0])) {
|
|
233
|
+
hasI18nSurface = true;
|
|
234
|
+
segments.shift();
|
|
235
|
+
}
|
|
236
|
+
currentPath = segments.length
|
|
237
|
+
? normalizeJoined(`${parentPath}/${segments.join('/')}`)
|
|
238
|
+
: parentPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const children = route.children;
|
|
242
|
+
if (children && children.length > 0) {
|
|
243
|
+
for (const child of children) {
|
|
244
|
+
visit(child, currentPath);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Leaf page or index route: a navigable target.
|
|
250
|
+
record(currentPath || '/');
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const rootModern = routes.find(
|
|
254
|
+
route => (route as CanonicalAwareRoute).isRoot,
|
|
255
|
+
) as CanonicalAwareRoute | undefined;
|
|
256
|
+
const topLevel = rootModern ? (rootModern.children ?? []) : routes;
|
|
257
|
+
|
|
258
|
+
for (const route of topLevel) {
|
|
259
|
+
visit(route as CanonicalAwareRoute, '');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!hasI18nSurface || canonicalParams.size === 0) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return Object.fromEntries(
|
|
267
|
+
[...canonicalParams.entries()].sort(([a], [b]) => a.localeCompare(b)),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
129
271
|
export async function isTanstackRouterFrameworkEnabled(
|
|
130
272
|
appContext: AppToolsContext,
|
|
131
273
|
): Promise<boolean> {
|
|
@@ -182,9 +324,29 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
182
324
|
const statements: string[] = [];
|
|
183
325
|
|
|
184
326
|
const loaderImportMap = new Map<string, string>();
|
|
327
|
+
const searchContractImportMap = new Map<string, string>();
|
|
328
|
+
const usedRouteVarNames = new Set<string>();
|
|
185
329
|
let loaderIndex = 0;
|
|
330
|
+
let validateSearchIndex = 0;
|
|
331
|
+
let loaderDepsIndex = 0;
|
|
186
332
|
let routeIndex = 0;
|
|
187
333
|
|
|
334
|
+
const resolveRouteModuleNoExt = async (aliasedNoExtPath: string) => {
|
|
335
|
+
const prefix = `${appContext.internalSrcAlias}/`;
|
|
336
|
+
let absNoExt: string;
|
|
337
|
+
if (aliasedNoExtPath.startsWith(prefix)) {
|
|
338
|
+
const rel = aliasedNoExtPath.slice(prefix.length);
|
|
339
|
+
absNoExt = path.join(appContext.srcDirectory, rel);
|
|
340
|
+
} else if (path.isAbsolute(aliasedNoExtPath)) {
|
|
341
|
+
absNoExt = aliasedNoExtPath;
|
|
342
|
+
} else {
|
|
343
|
+
// Unknown format; treat as already relative to src.
|
|
344
|
+
absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return resolveFileNoExt(absNoExt);
|
|
348
|
+
};
|
|
349
|
+
|
|
188
350
|
const getImportNamesForLoader = async (
|
|
189
351
|
aliasedNoExtPath: string,
|
|
190
352
|
inline: boolean,
|
|
@@ -201,19 +363,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
201
363
|
};
|
|
202
364
|
}
|
|
203
365
|
|
|
204
|
-
const
|
|
205
|
-
let absNoExt: string;
|
|
206
|
-
if (aliasedNoExtPath.startsWith(prefix)) {
|
|
207
|
-
const rel = aliasedNoExtPath.slice(prefix.length);
|
|
208
|
-
absNoExt = path.join(appContext.srcDirectory, rel);
|
|
209
|
-
} else if (path.isAbsolute(aliasedNoExtPath)) {
|
|
210
|
-
absNoExt = aliasedNoExtPath;
|
|
211
|
-
} else {
|
|
212
|
-
// Unknown format; treat as already relative to src.
|
|
213
|
-
absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const resolvedNoExt = await resolveFileNoExt(absNoExt);
|
|
366
|
+
const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
|
|
217
367
|
if (!resolvedNoExt) {
|
|
218
368
|
return null;
|
|
219
369
|
}
|
|
@@ -242,10 +392,49 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
242
392
|
return { loaderName: importName, actionName };
|
|
243
393
|
};
|
|
244
394
|
|
|
395
|
+
const getImportNameForSearchContract = async (
|
|
396
|
+
aliasedNoExtPath: string,
|
|
397
|
+
exportName: 'validateSearch' | 'loaderDeps',
|
|
398
|
+
) => {
|
|
399
|
+
const key = `${exportName}:${aliasedNoExtPath}`;
|
|
400
|
+
const existing = searchContractImportMap.get(key);
|
|
401
|
+
if (existing) {
|
|
402
|
+
return existing;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
|
|
406
|
+
if (!resolvedNoExt) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const relImport = normalizeRelativeImport(
|
|
411
|
+
path.relative(outDir, resolvedNoExt),
|
|
412
|
+
);
|
|
413
|
+
const importName =
|
|
414
|
+
exportName === 'validateSearch'
|
|
415
|
+
? `validateSearch_${validateSearchIndex++}`
|
|
416
|
+
: `loaderDeps_${loaderDepsIndex++}`;
|
|
417
|
+
imports.push(
|
|
418
|
+
`import { ${exportName} as ${importName} } from ${quote(relImport)};`,
|
|
419
|
+
);
|
|
420
|
+
searchContractImportMap.set(key, importName);
|
|
421
|
+
return importName;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const reserveRouteVarName = (preferred: string) => {
|
|
425
|
+
let candidate = preferred;
|
|
426
|
+
let suffix = 1;
|
|
427
|
+
while (usedRouteVarNames.has(candidate)) {
|
|
428
|
+
candidate = `${preferred}_${suffix++}`;
|
|
429
|
+
}
|
|
430
|
+
usedRouteVarNames.add(candidate);
|
|
431
|
+
return candidate;
|
|
432
|
+
};
|
|
433
|
+
|
|
245
434
|
const createRouteVarName = (route: NestedRouteForCli | PageRoute) => {
|
|
246
435
|
const id = (route as any).id as string | undefined;
|
|
247
436
|
const base = id ? makeLegalIdentifier(id) : `r_${routeIndex++}`;
|
|
248
|
-
return `route_${base}
|
|
437
|
+
return reserveRouteVarName(`route_${base}`);
|
|
249
438
|
};
|
|
250
439
|
|
|
251
440
|
const buildRoute = async (opts: {
|
|
@@ -267,6 +456,19 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
267
456
|
: null;
|
|
268
457
|
const loaderName = loaderImports?.loaderName || null;
|
|
269
458
|
const actionName = loaderImports?.actionName || null;
|
|
459
|
+
const searchContractInfo = pickRouteSearchContractModules(route);
|
|
460
|
+
const validateSearchName = searchContractInfo.validateSearchPath
|
|
461
|
+
? await getImportNameForSearchContract(
|
|
462
|
+
searchContractInfo.validateSearchPath,
|
|
463
|
+
'validateSearch',
|
|
464
|
+
)
|
|
465
|
+
: null;
|
|
466
|
+
const loaderDepsName = searchContractInfo.loaderDepsPath
|
|
467
|
+
? await getImportNameForSearchContract(
|
|
468
|
+
searchContractInfo.loaderDepsPath,
|
|
469
|
+
'loaderDeps',
|
|
470
|
+
)
|
|
471
|
+
: null;
|
|
270
472
|
|
|
271
473
|
const rawPath = (route as any).path as string | undefined;
|
|
272
474
|
const hasSplat = typeof rawPath === 'string' && rawPath.includes('*');
|
|
@@ -286,6 +488,12 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
286
488
|
`loader: modernLoaderToTanstack({ hasSplat: ${hasSplat} }, ${loaderName}),`,
|
|
287
489
|
);
|
|
288
490
|
}
|
|
491
|
+
if (validateSearchName) {
|
|
492
|
+
routeOpts.push(`validateSearch: ${validateSearchName},`);
|
|
493
|
+
}
|
|
494
|
+
if (loaderDepsName) {
|
|
495
|
+
routeOpts.push(`loaderDeps: ${loaderDepsName},`);
|
|
496
|
+
}
|
|
289
497
|
|
|
290
498
|
const staticDataSnippet = createRouteStaticDataSnippet({
|
|
291
499
|
modernRouteId: (route as any).id as string | undefined,
|
|
@@ -296,18 +504,27 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
296
504
|
routeOpts.push(staticDataSnippet);
|
|
297
505
|
}
|
|
298
506
|
|
|
299
|
-
statements.push(
|
|
300
|
-
`const ${varName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
|
|
301
|
-
);
|
|
302
|
-
|
|
303
507
|
const children = (route as any).children as
|
|
304
508
|
| Array<NestedRouteForCli | PageRoute>
|
|
305
509
|
| undefined;
|
|
510
|
+
const hasChildren = Boolean(children && children.length > 0);
|
|
511
|
+
const routeCtorVarName = hasChildren
|
|
512
|
+
? reserveRouteVarName(`${varName}__base`)
|
|
513
|
+
: varName;
|
|
514
|
+
|
|
515
|
+
statements.push(
|
|
516
|
+
`const ${routeCtorVarName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
|
|
517
|
+
);
|
|
518
|
+
|
|
306
519
|
if (children && children.length > 0) {
|
|
307
520
|
const childVars = await Promise.all(
|
|
308
|
-
children.map(child =>
|
|
521
|
+
children.map(child =>
|
|
522
|
+
buildRoute({ parentVar: routeCtorVarName, route: child }),
|
|
523
|
+
),
|
|
524
|
+
);
|
|
525
|
+
statements.push(
|
|
526
|
+
`const ${varName} = ${routeCtorVarName}.addChildren([${childVars.join(', ')}]);`,
|
|
309
527
|
);
|
|
310
|
-
statements.push(`${varName}.addChildren([${childVars.join(', ')}]);`);
|
|
311
528
|
}
|
|
312
529
|
|
|
313
530
|
return varName;
|
|
@@ -326,6 +543,21 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
326
543
|
: null;
|
|
327
544
|
const rootLoaderName = rootLoaderImports?.loaderName || null;
|
|
328
545
|
const rootActionName = rootLoaderImports?.actionName || null;
|
|
546
|
+
const rootSearchContractInfo = rootModern
|
|
547
|
+
? pickRouteSearchContractModules(rootModern)
|
|
548
|
+
: null;
|
|
549
|
+
const rootValidateSearchName = rootSearchContractInfo?.validateSearchPath
|
|
550
|
+
? await getImportNameForSearchContract(
|
|
551
|
+
rootSearchContractInfo.validateSearchPath,
|
|
552
|
+
'validateSearch',
|
|
553
|
+
)
|
|
554
|
+
: null;
|
|
555
|
+
const rootLoaderDepsName = rootSearchContractInfo?.loaderDepsPath
|
|
556
|
+
? await getImportNameForSearchContract(
|
|
557
|
+
rootSearchContractInfo.loaderDepsPath,
|
|
558
|
+
'loaderDeps',
|
|
559
|
+
)
|
|
560
|
+
: null;
|
|
329
561
|
|
|
330
562
|
const topLevelVars = await Promise.all(
|
|
331
563
|
topLevel.map(route => buildRoute({ parentVar: 'rootRoute', route })),
|
|
@@ -337,12 +569,19 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
337
569
|
`loader: modernLoaderToTanstack({ hasSplat: false }, ${rootLoaderName}),`,
|
|
338
570
|
);
|
|
339
571
|
}
|
|
572
|
+
if (rootValidateSearchName) {
|
|
573
|
+
rootOpts.push(`validateSearch: ${rootValidateSearchName},`);
|
|
574
|
+
}
|
|
575
|
+
if (rootLoaderDepsName) {
|
|
576
|
+
rootOpts.push(`loaderDeps: ${rootLoaderDepsName},`);
|
|
577
|
+
}
|
|
340
578
|
|
|
341
579
|
const routerGenTs = `/* eslint-disable */
|
|
342
580
|
// This file is auto-generated by Modern.js. Do not edit manually.
|
|
343
581
|
|
|
344
582
|
import {
|
|
345
583
|
createMemoryHistory,
|
|
584
|
+
modernTanstackRouterFastDefaults,
|
|
346
585
|
createRootRouteWithContext,
|
|
347
586
|
createRoute,
|
|
348
587
|
createRouter,
|
|
@@ -370,7 +609,7 @@ function isRedirectResponse(res: Response) {
|
|
|
370
609
|
}
|
|
371
610
|
|
|
372
611
|
function throwTanstackRedirect(location: string) {
|
|
373
|
-
const target = location
|
|
612
|
+
const target = location.length > 0 ? location : '/';
|
|
374
613
|
try {
|
|
375
614
|
void new URL(target);
|
|
376
615
|
throw redirect({ href: target });
|
|
@@ -396,21 +635,87 @@ function createRouteStaticData(opts: {
|
|
|
396
635
|
modernRouteAction?: unknown;
|
|
397
636
|
modernRouteLoader?: unknown;
|
|
398
637
|
}) {
|
|
399
|
-
const staticData:
|
|
638
|
+
const staticData: {
|
|
639
|
+
modernRouteId?: string;
|
|
640
|
+
modernRouteAction?: unknown;
|
|
641
|
+
modernRouteLoader?: unknown;
|
|
642
|
+
} = {};
|
|
400
643
|
|
|
401
|
-
if (opts.modernRouteId) {
|
|
644
|
+
if (typeof opts.modernRouteId === 'string' && opts.modernRouteId.length > 0) {
|
|
402
645
|
staticData.modernRouteId = opts.modernRouteId;
|
|
403
646
|
}
|
|
404
647
|
|
|
405
|
-
if (opts.modernRouteLoader) {
|
|
648
|
+
if (typeof opts.modernRouteLoader !== 'undefined') {
|
|
406
649
|
staticData.modernRouteLoader = opts.modernRouteLoader;
|
|
407
650
|
}
|
|
408
651
|
|
|
409
|
-
if (opts.modernRouteAction) {
|
|
652
|
+
if (typeof opts.modernRouteAction !== 'undefined') {
|
|
410
653
|
staticData.modernRouteAction = opts.modernRouteAction;
|
|
411
654
|
}
|
|
412
655
|
|
|
413
|
-
return
|
|
656
|
+
return staticData;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function getLoaderSignal(ctx: any): AbortSignal {
|
|
660
|
+
const abortSignal = ctx?.abortController?.signal;
|
|
661
|
+
if (abortSignal instanceof AbortSignal) {
|
|
662
|
+
return abortSignal;
|
|
663
|
+
}
|
|
664
|
+
if (ctx?.signal instanceof AbortSignal) {
|
|
665
|
+
return ctx.signal;
|
|
666
|
+
}
|
|
667
|
+
return new AbortController().signal;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function getLoaderHref(ctx: any): string {
|
|
671
|
+
if (typeof ctx?.location === 'string') {
|
|
672
|
+
return ctx.location;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const publicHref = ctx?.location?.publicHref;
|
|
676
|
+
if (typeof publicHref === 'string') {
|
|
677
|
+
return publicHref;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const href = ctx?.location?.href;
|
|
681
|
+
if (typeof href === 'string') {
|
|
682
|
+
return href;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const urlHref = ctx?.location?.url?.href;
|
|
686
|
+
return typeof urlHref === 'string' ? urlHref : '';
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function getLoaderParams(ctx: any): Record<string, string> {
|
|
690
|
+
return typeof ctx?.params === 'object' && ctx.params !== null ? ctx.params : {};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function handleModernLoaderResult<LoaderResult>(result: LoaderResult): LoaderResult {
|
|
694
|
+
if (isResponse(result)) {
|
|
695
|
+
if (isRedirectResponse(result)) {
|
|
696
|
+
const location = result.headers.get('Location') ?? '/';
|
|
697
|
+
throwTanstackRedirect(location);
|
|
698
|
+
}
|
|
699
|
+
if (result.status === 404) {
|
|
700
|
+
throw notFound();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function handleModernLoaderError(err: unknown): never {
|
|
708
|
+
if (isResponse(err)) {
|
|
709
|
+
if (isRedirectResponse(err)) {
|
|
710
|
+
const location = err.headers.get('Location') ?? '/';
|
|
711
|
+
throwTanstackRedirect(location);
|
|
712
|
+
}
|
|
713
|
+
if (err.status === 404) {
|
|
714
|
+
throw notFound();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
throw err;
|
|
414
719
|
}
|
|
415
720
|
|
|
416
721
|
function modernLoaderToTanstack<TLoader extends (args: any) => any>(
|
|
@@ -419,57 +724,31 @@ function modernLoaderToTanstack<TLoader extends (args: any) => any>(
|
|
|
419
724
|
) {
|
|
420
725
|
type LoaderResult = Awaited<ReturnType<TLoader>>;
|
|
421
726
|
|
|
422
|
-
return
|
|
727
|
+
return (ctx: any): Promise<LoaderResult> => {
|
|
423
728
|
try {
|
|
424
|
-
const signal
|
|
425
|
-
ctx?.abortController?.signal ||
|
|
426
|
-
ctx?.signal ||
|
|
427
|
-
new AbortController().signal;
|
|
729
|
+
const signal = getLoaderSignal(ctx);
|
|
428
730
|
const baseRequest: Request | undefined =
|
|
429
731
|
ctx?.context?.request instanceof Request ? ctx.context.request : undefined;
|
|
430
732
|
|
|
431
|
-
const href =
|
|
432
|
-
typeof ctx?.location === 'string'
|
|
433
|
-
? ctx.location
|
|
434
|
-
: ctx?.location?.publicHref ||
|
|
435
|
-
ctx?.location?.href ||
|
|
436
|
-
ctx?.location?.url?.href ||
|
|
437
|
-
'';
|
|
733
|
+
const href = getLoaderHref(ctx);
|
|
438
734
|
|
|
439
|
-
const request = baseRequest
|
|
735
|
+
const request = baseRequest !== undefined
|
|
440
736
|
? new Request(baseRequest, { signal })
|
|
441
737
|
: new Request(href, { signal });
|
|
442
738
|
|
|
443
|
-
const params = mapParamsForModernLoader(ctx
|
|
444
|
-
|
|
445
|
-
const result = await (modernLoader as any)({
|
|
446
|
-
request,
|
|
447
|
-
params,
|
|
448
|
-
context: ctx?.context?.requestContext,
|
|
449
|
-
});
|
|
739
|
+
const params = mapParamsForModernLoader(getLoaderParams(ctx), opts.hasSplat);
|
|
450
740
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return result as LoaderResult;
|
|
741
|
+
return Promise.resolve(
|
|
742
|
+
(modernLoader as any)({
|
|
743
|
+
request,
|
|
744
|
+
params,
|
|
745
|
+
context: ctx?.context?.requestContext,
|
|
746
|
+
}),
|
|
747
|
+
)
|
|
748
|
+
.then((result: LoaderResult) => handleModernLoaderResult(result))
|
|
749
|
+
.catch(handleModernLoaderError);
|
|
462
750
|
} catch (err) {
|
|
463
|
-
|
|
464
|
-
if (isRedirectResponse(err)) {
|
|
465
|
-
const location = err.headers.get('Location') || '/';
|
|
466
|
-
throwTanstackRedirect(location);
|
|
467
|
-
}
|
|
468
|
-
if (err.status === 404) {
|
|
469
|
-
throw notFound();
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
throw err;
|
|
751
|
+
handleModernLoaderError(err);
|
|
473
752
|
}
|
|
474
753
|
};
|
|
475
754
|
}
|
|
@@ -492,6 +771,7 @@ ${statements.join('\n\n')}
|
|
|
492
771
|
export const routeTree = rootRoute.addChildren([${topLevelVars.join(', ')}]);
|
|
493
772
|
|
|
494
773
|
export const router = createRouter({
|
|
774
|
+
...modernTanstackRouterFastDefaults,
|
|
495
775
|
routeTree,
|
|
496
776
|
history: createMemoryHistory({
|
|
497
777
|
initialEntries: ['/'],
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactElement, Suspense } from 'react';
|
|
2
|
+
|
|
3
|
+
export function wrapTanstackSsrHydrationBoundary(
|
|
4
|
+
routerContent: ReactElement,
|
|
5
|
+
shouldWrap: boolean,
|
|
6
|
+
) {
|
|
7
|
+
if (shouldWrap) {
|
|
8
|
+
return <Suspense fallback={null}>{routerContent}</Suspense>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return routerContent;
|
|
12
|
+
}
|