@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121
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 +89 -31
- package/dist/cjs/cli/routeSplitting.js +55 -0
- package/dist/cjs/cli/tanstackTypes.js +172 -170
- package/dist/cjs/cli.js +12 -8
- package/dist/cjs/runtime/basepathRewrite.js +12 -8
- package/dist/cjs/runtime/dataMutation.js +9 -5
- package/dist/cjs/runtime/hooks.js +20 -19
- package/dist/cjs/runtime/hydrationBoundary.js +48 -0
- package/dist/cjs/runtime/index.js +79 -35
- package/dist/cjs/runtime/lifecycle.js +21 -91
- package/dist/cjs/runtime/loaderBridge.js +173 -0
- package/dist/cjs/runtime/outlet.js +58 -0
- package/dist/cjs/runtime/plugin.js +195 -114
- package/dist/cjs/runtime/plugin.node.js +45 -45
- package/dist/cjs/runtime/plugin.worker.js +53 -0
- package/dist/cjs/runtime/pluginCore.js +55 -0
- package/dist/cjs/runtime/prefetchLink.js +10 -6
- package/dist/cjs/runtime/register.js +56 -0
- package/dist/cjs/runtime/routeTree.js +74 -207
- package/dist/cjs/runtime/router.js +41 -0
- 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 +44 -6
- 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/state.js +45 -0
- package/dist/cjs/runtime/types.js +31 -1
- package/dist/cjs/runtime/utils.js +9 -10
- package/dist/cjs/runtime.js +9 -5
- package/dist/esm/cli/index.mjs +75 -27
- package/dist/esm/cli/routeSplitting.mjs +14 -0
- package/dist/esm/cli/tanstackTypes.mjs +158 -160
- package/dist/esm/runtime/hooks.mjs +1 -8
- package/dist/esm/runtime/hydrationBoundary.mjs +10 -0
- package/dist/esm/runtime/index.mjs +5 -2
- package/dist/esm/runtime/lifecycle.mjs +1 -82
- package/dist/esm/runtime/loaderBridge.mjs +114 -0
- package/dist/esm/runtime/outlet.mjs +17 -0
- package/dist/esm/runtime/plugin.mjs +191 -114
- package/dist/esm/runtime/plugin.node.mjs +40 -44
- package/dist/esm/runtime/plugin.worker.mjs +1 -0
- package/dist/esm/runtime/pluginCore.mjs +14 -0
- package/dist/esm/runtime/prefetchLink.mjs +1 -1
- package/dist/esm/runtime/register.mjs +18 -0
- package/dist/esm/runtime/routeTree.mjs +59 -193
- package/dist/esm/runtime/router.mjs +2 -0
- package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
- package/dist/esm/runtime/state.mjs +7 -0
- package/dist/esm/runtime/types.mjs +7 -0
- package/dist/esm/runtime/utils.mjs +0 -5
- package/dist/esm-node/cli/index.mjs +75 -27
- package/dist/esm-node/cli/routeSplitting.mjs +15 -0
- package/dist/esm-node/cli/tanstackTypes.mjs +158 -160
- package/dist/esm-node/runtime/hooks.mjs +1 -8
- package/dist/esm-node/runtime/hydrationBoundary.mjs +11 -0
- package/dist/esm-node/runtime/index.mjs +5 -2
- package/dist/esm-node/runtime/lifecycle.mjs +1 -82
- package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
- package/dist/esm-node/runtime/outlet.mjs +18 -0
- package/dist/esm-node/runtime/plugin.mjs +191 -114
- package/dist/esm-node/runtime/plugin.node.mjs +40 -44
- package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
- package/dist/esm-node/runtime/pluginCore.mjs +15 -0
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
- package/dist/esm-node/runtime/register.mjs +19 -0
- package/dist/esm-node/runtime/routeTree.mjs +59 -193
- package/dist/esm-node/runtime/router.mjs +3 -0
- package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
- package/dist/esm-node/runtime/state.mjs +8 -0
- package/dist/esm-node/runtime/types.mjs +7 -0
- package/dist/esm-node/runtime/utils.mjs +0 -5
- package/dist/types/cli/index.d.ts +14 -1
- package/dist/types/cli/routeSplitting.d.ts +20 -0
- package/dist/types/cli/tanstackTypes.d.ts +21 -1
- package/dist/types/runtime/hooks.d.ts +8 -33
- package/dist/types/runtime/hydrationBoundary.d.ts +2 -0
- package/dist/types/runtime/index.d.ts +8 -3
- package/dist/types/runtime/lifecycle.d.ts +7 -22
- package/dist/types/runtime/loaderBridge.d.ts +48 -0
- package/dist/types/runtime/outlet.d.ts +2 -0
- package/dist/types/runtime/plugin.d.ts +2 -15
- package/dist/types/runtime/plugin.node.d.ts +2 -15
- package/dist/types/runtime/plugin.worker.d.ts +1 -0
- package/dist/types/runtime/pluginCore.d.ts +21 -0
- package/dist/types/runtime/register.d.ts +9 -0
- package/dist/types/runtime/routeTree.d.ts +0 -2
- package/dist/types/runtime/router.d.ts +14 -0
- package/dist/types/runtime/state.d.ts +16 -0
- package/dist/types/runtime/types.d.ts +14 -53
- package/package.json +42 -40
- package/rstest.config.mts +6 -0
- package/src/cli/index.ts +162 -23
- package/src/cli/routeSplitting.ts +43 -0
- package/src/cli/tanstackTypes.ts +331 -187
- package/src/runtime/hooks.ts +10 -27
- package/src/runtime/hydrationBoundary.tsx +12 -0
- package/src/runtime/index.tsx +17 -7
- package/src/runtime/lifecycle.ts +16 -151
- package/src/runtime/loaderBridge.ts +257 -0
- package/src/runtime/outlet.tsx +48 -0
- package/src/runtime/plugin.node.tsx +72 -85
- package/src/runtime/plugin.tsx +361 -206
- package/src/runtime/plugin.worker.tsx +4 -0
- package/src/runtime/pluginCore.ts +48 -0
- package/src/runtime/prefetchLink.tsx +1 -1
- package/src/runtime/register.ts +58 -0
- package/src/runtime/routeTree.ts +163 -354
- package/src/runtime/router.ts +15 -0
- package/src/runtime/rsc/payloadRouter.ts +45 -2
- package/src/runtime/ssr-shim.d.ts +1 -3
- package/src/runtime/state.ts +29 -0
- package/src/runtime/types.ts +32 -66
- package/src/runtime/utils.tsx +3 -6
- package/tests/router/cli.test.ts +586 -5
- package/tests/router/fastDefaults.test.ts +25 -0
- package/tests/router/hooks.test.ts +26 -0
- package/tests/router/hydrationBoundary.test.tsx +23 -0
- package/tests/router/loaderBridge.test.ts +211 -0
- package/tests/router/packageSurface.test.ts +24 -0
- package/tests/router/prefetchLink.test.tsx +43 -7
- package/tests/router/register.test.ts +46 -0
- package/tests/router/routeTree.test.ts +381 -81
- package/tests/router/rsc.test.tsx +70 -0
- package/tests/router/tanstackTypes.test.ts +573 -1
- package/dist/cjs/runtime/DefaultNotFound.js +0 -47
- package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
- package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
- package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
- package/src/runtime/DefaultNotFound.tsx +0 -15
package/src/cli/tanstackTypes.ts
CHANGED
|
@@ -1,31 +1,10 @@
|
|
|
1
1
|
// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off strictBooleanExpressions:off
|
|
2
2
|
import type { AppToolsContext } from '@modern-js/app-tools';
|
|
3
|
+
import { getPathWithoutExt, makeLegalIdentifier } from '@modern-js/runtime/cli';
|
|
3
4
|
import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
|
|
4
|
-
import { findExists, formatImportPath,
|
|
5
|
+
import { findExists, formatImportPath, slash } from '@modern-js/utils';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
|
|
7
|
-
const reservedWords =
|
|
8
|
-
'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public';
|
|
9
|
-
const builtins =
|
|
10
|
-
'arguments Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl';
|
|
11
|
-
const forbidList = new Set<string>(`${reservedWords} ${builtins}`.split(' '));
|
|
12
|
-
|
|
13
|
-
function makeLegalIdentifier(str: string) {
|
|
14
|
-
const identifier = str
|
|
15
|
-
.replace(/-(\w)/g, (_, letter) => letter.toUpperCase())
|
|
16
|
-
.replace(/[^$_a-zA-Z0-9]/g, '_');
|
|
17
|
-
|
|
18
|
-
if (/\d/.test(identifier[0]) || forbidList.has(identifier)) {
|
|
19
|
-
return `_${identifier}`;
|
|
20
|
-
}
|
|
21
|
-
return identifier || '_';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getPathWithoutExt(filename: string) {
|
|
25
|
-
const extname = path.extname(filename);
|
|
26
|
-
return extname ? filename.slice(0, -extname.length) : filename;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
8
|
const JS_OR_TS_EXTS = [
|
|
30
9
|
'.js',
|
|
31
10
|
'.jsx',
|
|
@@ -86,6 +65,17 @@ function pickModernLoaderModule(route: NestedRouteForCli | PageRoute) {
|
|
|
86
65
|
return { loaderPath, inline };
|
|
87
66
|
}
|
|
88
67
|
|
|
68
|
+
function pickRouteSearchContractModules(route: NestedRouteForCli | PageRoute) {
|
|
69
|
+
const validateSearchPath = (route as any).validateSearch;
|
|
70
|
+
const loaderDepsPath = (route as any).loaderDeps;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
validateSearchPath:
|
|
74
|
+
typeof validateSearchPath === 'string' ? validateSearchPath : null,
|
|
75
|
+
loaderDepsPath: typeof loaderDepsPath === 'string' ? loaderDepsPath : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
89
79
|
function isPathlessLayout(route: NestedRouteForCli | PageRoute) {
|
|
90
80
|
return (
|
|
91
81
|
(route as any).type === 'nested' &&
|
|
@@ -126,27 +116,154 @@ function createRouteStaticDataSnippet(opts: {
|
|
|
126
116
|
)}\n }),`;
|
|
127
117
|
}
|
|
128
118
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
119
|
+
const LOCALE_PARAM_SEGMENTS = new Set([
|
|
120
|
+
':lang',
|
|
121
|
+
':locale',
|
|
122
|
+
':language',
|
|
123
|
+
'$lang',
|
|
124
|
+
'$locale',
|
|
125
|
+
'$language',
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
type CanonicalAwareRoute = (NestedRouteForCli | PageRoute) & {
|
|
129
|
+
modernCanonicalPath?: string;
|
|
130
|
+
index?: boolean;
|
|
131
|
+
isRoot?: boolean;
|
|
132
|
+
children?: CanonicalAwareRoute[];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function paramsTypeForCanonicalPath(canonicalPath: string): string {
|
|
136
|
+
const fields: string[] = [];
|
|
137
|
+
|
|
138
|
+
for (const segment of canonicalPath.split('/')) {
|
|
139
|
+
if (!segment) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (segment === '*' || segment === '$') {
|
|
143
|
+
fields.push(`'_splat'?: string`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (segment.startsWith('{-$') && segment.endsWith('}')) {
|
|
147
|
+
fields.push(`${JSON.stringify(segment.slice(3, -1))}?: string`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (segment.startsWith('$')) {
|
|
151
|
+
fields.push(`${JSON.stringify(segment.slice(1))}: string`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (segment.startsWith(':')) {
|
|
155
|
+
const optional = segment.endsWith('?');
|
|
156
|
+
const name = segment.slice(1, optional ? undefined : segment.length);
|
|
157
|
+
fields.push(
|
|
158
|
+
`${JSON.stringify(optional ? name.slice(0, -1) : name)}${
|
|
159
|
+
optional ? '?' : ''
|
|
160
|
+
}: string`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
141
163
|
}
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
return fields.length > 0
|
|
166
|
+
? `{ ${fields.join('; ')} }`
|
|
167
|
+
: 'Record<string, never>';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type CollectCanonicalRoutesOptions = {
|
|
171
|
+
/**
|
|
172
|
+
* Whether a leading `:lang`/`:locale`/`:language` route param may be
|
|
173
|
+
* treated as an i18n locale prefix. This MUST only be enabled when
|
|
174
|
+
* `@modern-js/plugin-i18n` is actually installed: the emitted
|
|
175
|
+
* `declare module '@modern-js/plugin-i18n/runtime'` augmentation breaks
|
|
176
|
+
* typechecking (TS2664) for apps that hand-roll a `/:lang/` param without
|
|
177
|
+
* the plugin. Routes carrying `modernCanonicalPath` metadata are always
|
|
178
|
+
* honored — only plugin-i18n produces them.
|
|
179
|
+
*/
|
|
180
|
+
localeParamHeuristic?: boolean;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Derive the canonical (language-agnostic) route map for an entry: the
|
|
185
|
+
* leading locale param is stripped and localized physical variants (routes
|
|
186
|
+
* carrying `modernCanonicalPath` metadata from `@modern-js/plugin-i18n`)
|
|
187
|
+
* collapse to their canonical pattern. Returns `null` when the entry has no
|
|
188
|
+
* i18n routing surface (no locale param and no localized variants), so plain
|
|
189
|
+
* TanStack apps never get a `@modern-js/plugin-i18n` module augmentation.
|
|
190
|
+
*/
|
|
191
|
+
export function collectCanonicalRoutesForEntry(
|
|
192
|
+
routes: (NestedRouteForCli | PageRoute)[],
|
|
193
|
+
options: CollectCanonicalRoutesOptions = {},
|
|
194
|
+
): Record<string, string> | null {
|
|
195
|
+
const { localeParamHeuristic = true } = options;
|
|
196
|
+
const canonicalParams = new Map<string, string>();
|
|
197
|
+
let hasI18nSurface = false;
|
|
198
|
+
|
|
199
|
+
const normalizeJoined = (joined: string): string => {
|
|
200
|
+
const collapsed = joined.replace(/\/+/g, '/');
|
|
201
|
+
const withLeading = collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
|
|
202
|
+
return withLeading.length > 1
|
|
203
|
+
? withLeading.replace(/\/+$/, '')
|
|
204
|
+
: withLeading;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const record = (canonicalPath: string) => {
|
|
208
|
+
const normalized = normalizeJoined(canonicalPath || '/');
|
|
209
|
+
const key = toTanstackPath(normalized);
|
|
210
|
+
if (!canonicalParams.has(key)) {
|
|
211
|
+
canonicalParams.set(key, paramsTypeForCanonicalPath(normalized));
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const visit = (route: CanonicalAwareRoute, parentPath: string) => {
|
|
216
|
+
let currentPath = parentPath;
|
|
217
|
+
|
|
218
|
+
if (typeof route.modernCanonicalPath === 'string') {
|
|
219
|
+
hasI18nSurface = true;
|
|
220
|
+
currentPath = normalizeJoined(route.modernCanonicalPath);
|
|
221
|
+
} else if (typeof route.path === 'string' && route.path.length > 0) {
|
|
222
|
+
const segments = route.path
|
|
223
|
+
.replace(/\[(.+?)\]/g, ':$1')
|
|
224
|
+
.split('/')
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
if (
|
|
227
|
+
localeParamHeuristic &&
|
|
228
|
+
parentPath === '' &&
|
|
229
|
+
LOCALE_PARAM_SEGMENTS.has(segments[0])
|
|
230
|
+
) {
|
|
231
|
+
hasI18nSurface = true;
|
|
232
|
+
segments.shift();
|
|
233
|
+
}
|
|
234
|
+
currentPath = segments.length
|
|
235
|
+
? normalizeJoined(`${parentPath}/${segments.join('/')}`)
|
|
236
|
+
: parentPath;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const children = route.children;
|
|
240
|
+
if (children && children.length > 0) {
|
|
241
|
+
for (const child of children) {
|
|
242
|
+
visit(child, currentPath);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Leaf page or index route: a navigable target.
|
|
248
|
+
record(currentPath || '/');
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const rootModern = routes.find(
|
|
252
|
+
route => (route as CanonicalAwareRoute).isRoot,
|
|
253
|
+
) as CanonicalAwareRoute | undefined;
|
|
254
|
+
const topLevel = rootModern ? (rootModern.children ?? []) : routes;
|
|
255
|
+
|
|
256
|
+
for (const route of topLevel) {
|
|
257
|
+
visit(route as CanonicalAwareRoute, '');
|
|
149
258
|
}
|
|
259
|
+
|
|
260
|
+
if (!hasI18nSurface || canonicalParams.size === 0) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return Object.fromEntries(
|
|
265
|
+
[...canonicalParams.entries()].sort(([a], [b]) => a.localeCompare(b)),
|
|
266
|
+
);
|
|
150
267
|
}
|
|
151
268
|
|
|
152
269
|
export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
@@ -182,9 +299,66 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
182
299
|
const statements: string[] = [];
|
|
183
300
|
|
|
184
301
|
const loaderImportMap = new Map<string, string>();
|
|
302
|
+
const componentImportMap = new Map<string, Promise<string | null>>();
|
|
303
|
+
const searchContractImportMap = new Map<string, string>();
|
|
304
|
+
const usedRouteVarNames = new Set<string>();
|
|
185
305
|
let loaderIndex = 0;
|
|
306
|
+
let componentIndex = 0;
|
|
307
|
+
let validateSearchIndex = 0;
|
|
308
|
+
let loaderDepsIndex = 0;
|
|
186
309
|
let routeIndex = 0;
|
|
187
310
|
|
|
311
|
+
const getImportNameForComponent = (
|
|
312
|
+
componentPath: unknown,
|
|
313
|
+
): Promise<string | null> => {
|
|
314
|
+
if (typeof componentPath !== 'string' || componentPath.length === 0) {
|
|
315
|
+
return Promise.resolve(null);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Cache the in-flight promise: sibling routes sharing a component module
|
|
319
|
+
// are generated concurrently and must reuse one import.
|
|
320
|
+
let pendingImportName = componentImportMap.get(componentPath);
|
|
321
|
+
if (!pendingImportName) {
|
|
322
|
+
pendingImportName = (async () => {
|
|
323
|
+
// Resolve through the same machinery as loaders: the raw `_component`
|
|
324
|
+
// value carries the internal `@_modern_js_src` alias, which the app's
|
|
325
|
+
// tsconfig does not map — the generated file must use relative
|
|
326
|
+
// imports.
|
|
327
|
+
const resolvedNoExt = await resolveRouteModuleNoExt(componentPath);
|
|
328
|
+
if (!resolvedNoExt) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const relImport = normalizeRelativeImport(
|
|
333
|
+
path.relative(outDir, resolvedNoExt),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const componentName = `component_${componentIndex++}`;
|
|
337
|
+
imports.push(`import ${componentName} from ${quote(relImport)};`);
|
|
338
|
+
return componentName;
|
|
339
|
+
})();
|
|
340
|
+
componentImportMap.set(componentPath, pendingImportName);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return pendingImportName;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const resolveRouteModuleNoExt = async (aliasedNoExtPath: string) => {
|
|
347
|
+
const prefix = `${appContext.internalSrcAlias}/`;
|
|
348
|
+
let absNoExt: string;
|
|
349
|
+
if (aliasedNoExtPath.startsWith(prefix)) {
|
|
350
|
+
const rel = aliasedNoExtPath.slice(prefix.length);
|
|
351
|
+
absNoExt = path.join(appContext.srcDirectory, rel);
|
|
352
|
+
} else if (path.isAbsolute(aliasedNoExtPath)) {
|
|
353
|
+
absNoExt = aliasedNoExtPath;
|
|
354
|
+
} else {
|
|
355
|
+
// Unknown format; treat as already relative to src.
|
|
356
|
+
absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return resolveFileNoExt(absNoExt);
|
|
360
|
+
};
|
|
361
|
+
|
|
188
362
|
const getImportNamesForLoader = async (
|
|
189
363
|
aliasedNoExtPath: string,
|
|
190
364
|
inline: boolean,
|
|
@@ -201,19 +375,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
201
375
|
};
|
|
202
376
|
}
|
|
203
377
|
|
|
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);
|
|
378
|
+
const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
|
|
217
379
|
if (!resolvedNoExt) {
|
|
218
380
|
return null;
|
|
219
381
|
}
|
|
@@ -242,10 +404,49 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
242
404
|
return { loaderName: importName, actionName };
|
|
243
405
|
};
|
|
244
406
|
|
|
407
|
+
const getImportNameForSearchContract = async (
|
|
408
|
+
aliasedNoExtPath: string,
|
|
409
|
+
exportName: 'validateSearch' | 'loaderDeps',
|
|
410
|
+
) => {
|
|
411
|
+
const key = `${exportName}:${aliasedNoExtPath}`;
|
|
412
|
+
const existing = searchContractImportMap.get(key);
|
|
413
|
+
if (existing) {
|
|
414
|
+
return existing;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
|
|
418
|
+
if (!resolvedNoExt) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const relImport = normalizeRelativeImport(
|
|
423
|
+
path.relative(outDir, resolvedNoExt),
|
|
424
|
+
);
|
|
425
|
+
const importName =
|
|
426
|
+
exportName === 'validateSearch'
|
|
427
|
+
? `validateSearch_${validateSearchIndex++}`
|
|
428
|
+
: `loaderDeps_${loaderDepsIndex++}`;
|
|
429
|
+
imports.push(
|
|
430
|
+
`import { ${exportName} as ${importName} } from ${quote(relImport)};`,
|
|
431
|
+
);
|
|
432
|
+
searchContractImportMap.set(key, importName);
|
|
433
|
+
return importName;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const reserveRouteVarName = (preferred: string) => {
|
|
437
|
+
let candidate = preferred;
|
|
438
|
+
let suffix = 1;
|
|
439
|
+
while (usedRouteVarNames.has(candidate)) {
|
|
440
|
+
candidate = `${preferred}_${suffix++}`;
|
|
441
|
+
}
|
|
442
|
+
usedRouteVarNames.add(candidate);
|
|
443
|
+
return candidate;
|
|
444
|
+
};
|
|
445
|
+
|
|
245
446
|
const createRouteVarName = (route: NestedRouteForCli | PageRoute) => {
|
|
246
447
|
const id = (route as any).id as string | undefined;
|
|
247
448
|
const base = id ? makeLegalIdentifier(id) : `r_${routeIndex++}`;
|
|
248
|
-
return `route_${base}
|
|
449
|
+
return reserveRouteVarName(`route_${base}`);
|
|
249
450
|
};
|
|
250
451
|
|
|
251
452
|
const buildRoute = async (opts: {
|
|
@@ -267,12 +468,32 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
267
468
|
: null;
|
|
268
469
|
const loaderName = loaderImports?.loaderName || null;
|
|
269
470
|
const actionName = loaderImports?.actionName || null;
|
|
471
|
+
const searchContractInfo = pickRouteSearchContractModules(route);
|
|
472
|
+
const validateSearchName = searchContractInfo.validateSearchPath
|
|
473
|
+
? await getImportNameForSearchContract(
|
|
474
|
+
searchContractInfo.validateSearchPath,
|
|
475
|
+
'validateSearch',
|
|
476
|
+
)
|
|
477
|
+
: null;
|
|
478
|
+
const loaderDepsName = searchContractInfo.loaderDepsPath
|
|
479
|
+
? await getImportNameForSearchContract(
|
|
480
|
+
searchContractInfo.loaderDepsPath,
|
|
481
|
+
'loaderDeps',
|
|
482
|
+
)
|
|
483
|
+
: null;
|
|
270
484
|
|
|
271
485
|
const rawPath = (route as any).path as string | undefined;
|
|
272
486
|
const hasSplat = typeof rawPath === 'string' && rawPath.includes('*');
|
|
273
487
|
|
|
274
488
|
const routeOpts: string[] = [`getParentRoute: () => ${parentVar},`];
|
|
275
489
|
|
|
490
|
+
const componentName = await getImportNameForComponent(
|
|
491
|
+
(route as any)._component,
|
|
492
|
+
);
|
|
493
|
+
if (componentName) {
|
|
494
|
+
routeOpts.push(`component: ${componentName},`);
|
|
495
|
+
}
|
|
496
|
+
|
|
276
497
|
if (isPathlessLayout(route)) {
|
|
277
498
|
const id = (route as any).id as string | undefined;
|
|
278
499
|
routeOpts.push(`id: ${quote(id || 'pathless')},`);
|
|
@@ -286,6 +507,12 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
286
507
|
`loader: modernLoaderToTanstack({ hasSplat: ${hasSplat} }, ${loaderName}),`,
|
|
287
508
|
);
|
|
288
509
|
}
|
|
510
|
+
if (validateSearchName) {
|
|
511
|
+
routeOpts.push(`validateSearch: ${validateSearchName},`);
|
|
512
|
+
}
|
|
513
|
+
if (loaderDepsName) {
|
|
514
|
+
routeOpts.push(`loaderDeps: ${loaderDepsName},`);
|
|
515
|
+
}
|
|
289
516
|
|
|
290
517
|
const staticDataSnippet = createRouteStaticDataSnippet({
|
|
291
518
|
modernRouteId: (route as any).id as string | undefined,
|
|
@@ -296,18 +523,27 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
296
523
|
routeOpts.push(staticDataSnippet);
|
|
297
524
|
}
|
|
298
525
|
|
|
299
|
-
statements.push(
|
|
300
|
-
`const ${varName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
|
|
301
|
-
);
|
|
302
|
-
|
|
303
526
|
const children = (route as any).children as
|
|
304
527
|
| Array<NestedRouteForCli | PageRoute>
|
|
305
528
|
| undefined;
|
|
529
|
+
const hasChildren = Boolean(children && children.length > 0);
|
|
530
|
+
const routeCtorVarName = hasChildren
|
|
531
|
+
? reserveRouteVarName(`${varName}__base`)
|
|
532
|
+
: varName;
|
|
533
|
+
|
|
534
|
+
statements.push(
|
|
535
|
+
`const ${routeCtorVarName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
|
|
536
|
+
);
|
|
537
|
+
|
|
306
538
|
if (children && children.length > 0) {
|
|
307
539
|
const childVars = await Promise.all(
|
|
308
|
-
children.map(child =>
|
|
540
|
+
children.map(child =>
|
|
541
|
+
buildRoute({ parentVar: routeCtorVarName, route: child }),
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
statements.push(
|
|
545
|
+
`const ${varName} = ${routeCtorVarName}.addChildren([${childVars.join(', ')}]);`,
|
|
309
546
|
);
|
|
310
|
-
statements.push(`${varName}.addChildren([${childVars.join(', ')}]);`);
|
|
311
547
|
}
|
|
312
548
|
|
|
313
549
|
return varName;
|
|
@@ -326,17 +562,46 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
|
|
|
326
562
|
: null;
|
|
327
563
|
const rootLoaderName = rootLoaderImports?.loaderName || null;
|
|
328
564
|
const rootActionName = rootLoaderImports?.actionName || null;
|
|
565
|
+
const rootSearchContractInfo = rootModern
|
|
566
|
+
? pickRouteSearchContractModules(rootModern)
|
|
567
|
+
: null;
|
|
568
|
+
const rootValidateSearchName = rootSearchContractInfo?.validateSearchPath
|
|
569
|
+
? await getImportNameForSearchContract(
|
|
570
|
+
rootSearchContractInfo.validateSearchPath,
|
|
571
|
+
'validateSearch',
|
|
572
|
+
)
|
|
573
|
+
: null;
|
|
574
|
+
const rootLoaderDepsName = rootSearchContractInfo?.loaderDepsPath
|
|
575
|
+
? await getImportNameForSearchContract(
|
|
576
|
+
rootSearchContractInfo.loaderDepsPath,
|
|
577
|
+
'loaderDeps',
|
|
578
|
+
)
|
|
579
|
+
: null;
|
|
329
580
|
|
|
330
581
|
const topLevelVars = await Promise.all(
|
|
331
582
|
topLevel.map(route => buildRoute({ parentVar: 'rootRoute', route })),
|
|
332
583
|
);
|
|
333
584
|
|
|
334
585
|
const rootOpts: string[] = [];
|
|
586
|
+
|
|
587
|
+
const rootComponentName = await getImportNameForComponent(
|
|
588
|
+
(rootModern as any)?._component,
|
|
589
|
+
);
|
|
590
|
+
if (rootComponentName) {
|
|
591
|
+
rootOpts.push(`component: ${rootComponentName},`);
|
|
592
|
+
}
|
|
593
|
+
|
|
335
594
|
if (rootLoaderName) {
|
|
336
595
|
rootOpts.push(
|
|
337
596
|
`loader: modernLoaderToTanstack({ hasSplat: false }, ${rootLoaderName}),`,
|
|
338
597
|
);
|
|
339
598
|
}
|
|
599
|
+
if (rootValidateSearchName) {
|
|
600
|
+
rootOpts.push(`validateSearch: ${rootValidateSearchName},`);
|
|
601
|
+
}
|
|
602
|
+
if (rootLoaderDepsName) {
|
|
603
|
+
rootOpts.push(`loaderDeps: ${rootLoaderDepsName},`);
|
|
604
|
+
}
|
|
340
605
|
|
|
341
606
|
const routerGenTs = `/* eslint-disable */
|
|
342
607
|
// This file is auto-generated by Modern.js. Do not edit manually.
|
|
@@ -346,134 +611,12 @@ import {
|
|
|
346
611
|
createRootRouteWithContext,
|
|
347
612
|
createRoute,
|
|
348
613
|
createRouter,
|
|
349
|
-
|
|
350
|
-
|
|
614
|
+
createRouteStaticData,
|
|
615
|
+
type ModernRouterContext,
|
|
616
|
+
modernLoaderToTanstack,
|
|
617
|
+
modernTanstackRouterFastDefaults,
|
|
351
618
|
} from '@modern-js/plugin-tanstack/runtime';
|
|
352
619
|
|
|
353
|
-
type ModernRouterContext = {
|
|
354
|
-
request?: Request;
|
|
355
|
-
requestContext?: unknown;
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
function isResponse(value: unknown): value is Response {
|
|
359
|
-
return (
|
|
360
|
-
value != null &&
|
|
361
|
-
typeof value === 'object' &&
|
|
362
|
-
typeof (value as any).status === 'number' &&
|
|
363
|
-
typeof (value as any).headers === 'object'
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
|
|
368
|
-
function isRedirectResponse(res: Response) {
|
|
369
|
-
return redirectStatusCodes.has(res.status);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function throwTanstackRedirect(location: string) {
|
|
373
|
-
const target = location || '/';
|
|
374
|
-
try {
|
|
375
|
-
void new URL(target);
|
|
376
|
-
throw redirect({ href: target });
|
|
377
|
-
} catch {
|
|
378
|
-
throw redirect({ to: target });
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function mapParamsForModernLoader(params: Record<string, string>, hasSplat: boolean) {
|
|
383
|
-
if (!hasSplat) {
|
|
384
|
-
return params;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const { _splat, ...rest } = params as any;
|
|
388
|
-
if (typeof _splat !== 'undefined') {
|
|
389
|
-
return { ...rest, '*': _splat };
|
|
390
|
-
}
|
|
391
|
-
return rest;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function createRouteStaticData(opts: {
|
|
395
|
-
modernRouteId?: string;
|
|
396
|
-
modernRouteAction?: unknown;
|
|
397
|
-
modernRouteLoader?: unknown;
|
|
398
|
-
}) {
|
|
399
|
-
const staticData: Record<string, unknown> = {};
|
|
400
|
-
|
|
401
|
-
if (opts.modernRouteId) {
|
|
402
|
-
staticData.modernRouteId = opts.modernRouteId;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (opts.modernRouteLoader) {
|
|
406
|
-
staticData.modernRouteLoader = opts.modernRouteLoader;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (opts.modernRouteAction) {
|
|
410
|
-
staticData.modernRouteAction = opts.modernRouteAction;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return Object.keys(staticData).length > 0 ? staticData : undefined;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function modernLoaderToTanstack<TLoader extends (args: any) => any>(
|
|
417
|
-
opts: { hasSplat: boolean },
|
|
418
|
-
modernLoader: TLoader,
|
|
419
|
-
) {
|
|
420
|
-
type LoaderResult = Awaited<ReturnType<TLoader>>;
|
|
421
|
-
|
|
422
|
-
return async (ctx: any): Promise<LoaderResult> => {
|
|
423
|
-
try {
|
|
424
|
-
const signal: AbortSignal =
|
|
425
|
-
ctx?.abortController?.signal ||
|
|
426
|
-
ctx?.signal ||
|
|
427
|
-
new AbortController().signal;
|
|
428
|
-
const baseRequest: Request | undefined =
|
|
429
|
-
ctx?.context?.request instanceof Request ? ctx.context.request : undefined;
|
|
430
|
-
|
|
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
|
-
'';
|
|
438
|
-
|
|
439
|
-
const request = baseRequest
|
|
440
|
-
? new Request(baseRequest, { signal })
|
|
441
|
-
: new Request(href, { signal });
|
|
442
|
-
|
|
443
|
-
const params = mapParamsForModernLoader(ctx?.params || {}, opts.hasSplat);
|
|
444
|
-
|
|
445
|
-
const result = await (modernLoader as any)({
|
|
446
|
-
request,
|
|
447
|
-
params,
|
|
448
|
-
context: ctx?.context?.requestContext,
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
if (isResponse(result)) {
|
|
452
|
-
if (isRedirectResponse(result)) {
|
|
453
|
-
const location = result.headers.get('Location') || '/';
|
|
454
|
-
throwTanstackRedirect(location);
|
|
455
|
-
}
|
|
456
|
-
if (result.status === 404) {
|
|
457
|
-
throw notFound();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return result as LoaderResult;
|
|
462
|
-
} catch (err) {
|
|
463
|
-
if (isResponse(err)) {
|
|
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;
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
620
|
${imports.join('\n')}
|
|
478
621
|
|
|
479
622
|
export const rootRoute = createRootRouteWithContext<ModernRouterContext>()({
|
|
@@ -492,6 +635,7 @@ ${statements.join('\n\n')}
|
|
|
492
635
|
export const routeTree = rootRoute.addChildren([${topLevelVars.join(', ')}]);
|
|
493
636
|
|
|
494
637
|
export const router = createRouter({
|
|
638
|
+
...modernTanstackRouterFastDefaults,
|
|
495
639
|
routeTree,
|
|
496
640
|
history: createMemoryHistory({
|
|
497
641
|
initialEntries: ['/'],
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
createSyncHook<(context: TRuntimeContext) => void>();
|
|
9
|
-
const onBeforeCreateRouter =
|
|
10
|
-
createSyncHook<(context: RouterLifecycleContext) => void>();
|
|
11
|
-
const onAfterCreateRouter =
|
|
12
|
-
createSyncHook<(context: RouterLifecycleContext) => void>();
|
|
13
|
-
const onBeforeHydrateRouter =
|
|
14
|
-
createSyncHook<(context: RouterLifecycleContext) => void>();
|
|
15
|
-
const onAfterHydrateRouter =
|
|
16
|
-
createSyncHook<(context: RouterLifecycleContext) => void>();
|
|
17
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The router hooks are owned by @modern-js/runtime — this module re-exports
|
|
3
|
+
* the canonical instances through the `/context` seam, so the TanStack
|
|
4
|
+
* provider taps and calls the exact same hooks the built-in router wrapper
|
|
5
|
+
* registers. Creating separate hook instances here would silently split the
|
|
6
|
+
* hook registry between the wrapper and this provider.
|
|
7
|
+
*/
|
|
18
8
|
export {
|
|
19
9
|
modifyRoutes,
|
|
20
10
|
onAfterCreateRouter,
|
|
@@ -22,13 +12,6 @@ export {
|
|
|
22
12
|
onBeforeCreateRouter,
|
|
23
13
|
onBeforeCreateRoutes,
|
|
24
14
|
onBeforeHydrateRouter,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
modifyRoutes: typeof modifyRoutes;
|
|
29
|
-
onBeforeCreateRoutes: typeof onBeforeCreateRoutes;
|
|
30
|
-
onBeforeCreateRouter: typeof onBeforeCreateRouter;
|
|
31
|
-
onAfterCreateRouter: typeof onAfterCreateRouter;
|
|
32
|
-
onBeforeHydrateRouter: typeof onBeforeHydrateRouter;
|
|
33
|
-
onAfterHydrateRouter: typeof onAfterHydrateRouter;
|
|
34
|
-
};
|
|
15
|
+
type RouterExtendsHooks,
|
|
16
|
+
routerProviderRegistryHooks,
|
|
17
|
+
} from '@modern-js/runtime/context';
|
|
@@ -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
|
+
}
|