@tanstack/router-generator 1.141.8 → 1.142.0
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/config.cjs +1 -3
- package/dist/cjs/config.cjs.map +1 -1
- package/dist/cjs/config.d.cts +10 -15
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs +69 -37
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -1
- package/dist/cjs/filesystem/physical/getRouteNodes.d.cts +4 -3
- package/dist/cjs/generator.cjs +29 -20
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/types.d.cts +0 -1
- package/dist/cjs/utils.cjs +80 -144
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +48 -25
- package/dist/esm/config.d.ts +10 -15
- package/dist/esm/config.js +1 -3
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/filesystem/physical/getRouteNodes.d.ts +4 -3
- package/dist/esm/filesystem/physical/getRouteNodes.js +70 -38
- package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -1
- package/dist/esm/generator.js +30 -21
- package/dist/esm/generator.js.map +1 -1
- package/dist/esm/types.d.ts +0 -1
- package/dist/esm/utils.d.ts +48 -25
- package/dist/esm/utils.js +80 -144
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/config.ts +0 -2
- package/src/filesystem/physical/getRouteNodes.ts +120 -42
- package/src/generator.ts +36 -27
- package/src/types.ts +0 -1
- package/src/utils.ts +163 -203
|
@@ -2,6 +2,7 @@ import path from 'node:path'
|
|
|
2
2
|
import * as fsp from 'node:fs/promises'
|
|
3
3
|
import {
|
|
4
4
|
determineInitialRoutePath,
|
|
5
|
+
hasEscapedLeadingUnderscore,
|
|
5
6
|
removeExt,
|
|
6
7
|
replaceBackslash,
|
|
7
8
|
routePathToVariable,
|
|
@@ -34,7 +35,6 @@ export async function getRouteNodes(
|
|
|
34
35
|
| 'disableLogging'
|
|
35
36
|
| 'routeToken'
|
|
36
37
|
| 'indexToken'
|
|
37
|
-
| 'experimental'
|
|
38
38
|
>,
|
|
39
39
|
root: string,
|
|
40
40
|
): Promise<GetRouteNodesResult> {
|
|
@@ -135,8 +135,7 @@ export async function getRouteNodes(
|
|
|
135
135
|
const {
|
|
136
136
|
routePath: initialRoutePath,
|
|
137
137
|
originalRoutePath: initialOriginalRoutePath,
|
|
138
|
-
|
|
139
|
-
} = determineInitialRoutePath(filePathNoExt, config)
|
|
138
|
+
} = determineInitialRoutePath(filePathNoExt)
|
|
140
139
|
|
|
141
140
|
let routePath = initialRoutePath
|
|
142
141
|
let originalRoutePath = initialOriginalRoutePath
|
|
@@ -155,7 +154,7 @@ export async function getRouteNodes(
|
|
|
155
154
|
throw new Error(errorMessage)
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
const meta = getRouteMeta(routePath, config)
|
|
157
|
+
const meta = getRouteMeta(routePath, originalRoutePath, config)
|
|
159
158
|
const variableName = meta.variableName
|
|
160
159
|
let routeType: FsRouteType = meta.fsRouteType
|
|
161
160
|
|
|
@@ -166,7 +165,14 @@ export async function getRouteNodes(
|
|
|
166
165
|
|
|
167
166
|
// this check needs to happen after the lazy route has been cleaned up
|
|
168
167
|
// since the routePath is used to determine if a route is pathless
|
|
169
|
-
if (
|
|
168
|
+
if (
|
|
169
|
+
isValidPathlessLayoutRoute(
|
|
170
|
+
routePath,
|
|
171
|
+
originalRoutePath,
|
|
172
|
+
routeType,
|
|
173
|
+
config,
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
170
176
|
routeType = 'pathless_layout'
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -191,44 +197,70 @@ export async function getRouteNodes(
|
|
|
191
197
|
})
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
'',
|
|
206
|
-
|
|
200
|
+
// Get the last segment of originalRoutePath to check for escaping
|
|
201
|
+
const originalSegments = originalRoutePath.split('/').filter(Boolean)
|
|
202
|
+
const lastOriginalSegmentForSuffix =
|
|
203
|
+
originalSegments[originalSegments.length - 1] || ''
|
|
204
|
+
|
|
205
|
+
// List of special suffixes that can be escaped
|
|
206
|
+
const specialSuffixes = [
|
|
207
|
+
'component',
|
|
208
|
+
'errorComponent',
|
|
209
|
+
'notFoundComponent',
|
|
210
|
+
'pendingComponent',
|
|
211
|
+
'loader',
|
|
212
|
+
config.routeToken,
|
|
213
|
+
'lazy',
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
// Only strip the suffix if it wasn't escaped (not wrapped in brackets)
|
|
217
|
+
const suffixToStrip = specialSuffixes.find((suffix) => {
|
|
218
|
+
const endsWithSuffix = routePath.endsWith(`/${suffix}`)
|
|
219
|
+
const isEscaped = lastOriginalSegmentForSuffix === `[${suffix}]`
|
|
220
|
+
return endsWithSuffix && !isEscaped
|
|
221
|
+
})
|
|
207
222
|
|
|
208
|
-
if (
|
|
209
|
-
routePath = '
|
|
223
|
+
if (suffixToStrip) {
|
|
224
|
+
routePath = routePath.replace(new RegExp(`/${suffixToStrip}$`), '')
|
|
225
|
+
originalRoutePath = originalRoutePath.replace(
|
|
226
|
+
new RegExp(`/${suffixToStrip}$`),
|
|
227
|
+
'',
|
|
228
|
+
)
|
|
210
229
|
}
|
|
211
230
|
|
|
212
|
-
if
|
|
213
|
-
|
|
231
|
+
// Check if the index token should be treated specially or as a literal path
|
|
232
|
+
// If it's escaped (wrapped in brackets in originalRoutePath), it should be literal
|
|
233
|
+
const lastOriginalSegment =
|
|
234
|
+
originalRoutePath.split('/').filter(Boolean).pop() || ''
|
|
235
|
+
const isIndexEscaped =
|
|
236
|
+
lastOriginalSegment === `[${config.indexToken}]`
|
|
237
|
+
|
|
238
|
+
if (!isIndexEscaped) {
|
|
239
|
+
if (routePath === config.indexToken) {
|
|
240
|
+
routePath = '/'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (originalRoutePath === config.indexToken) {
|
|
244
|
+
originalRoutePath = '/'
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
routePath =
|
|
248
|
+
routePath.replace(new RegExp(`/${config.indexToken}$`), '/') ||
|
|
249
|
+
'/'
|
|
250
|
+
|
|
251
|
+
originalRoutePath =
|
|
252
|
+
originalRoutePath.replace(
|
|
253
|
+
new RegExp(`/${config.indexToken}$`),
|
|
254
|
+
'/',
|
|
255
|
+
) || '/'
|
|
214
256
|
}
|
|
215
257
|
|
|
216
|
-
routePath =
|
|
217
|
-
routePath.replace(new RegExp(`/${config.indexToken}$`), '/') || '/'
|
|
218
|
-
|
|
219
|
-
originalRoutePath =
|
|
220
|
-
originalRoutePath.replace(
|
|
221
|
-
new RegExp(`/${config.indexToken}$`),
|
|
222
|
-
'/',
|
|
223
|
-
) || '/'
|
|
224
|
-
|
|
225
258
|
routeNodes.push({
|
|
226
259
|
filePath,
|
|
227
260
|
fullPath,
|
|
228
261
|
routePath,
|
|
229
262
|
variableName,
|
|
230
263
|
_fsRouteType: routeType,
|
|
231
|
-
_isExperimentalNonNestedRoute: isExperimentalNonNestedRoute,
|
|
232
264
|
originalRoutePath,
|
|
233
265
|
})
|
|
234
266
|
}
|
|
@@ -269,12 +301,14 @@ export async function getRouteNodes(
|
|
|
269
301
|
/**
|
|
270
302
|
* Determines the metadata for a given route path based on the provided configuration.
|
|
271
303
|
*
|
|
272
|
-
* @param routePath - The determined initial routePath.
|
|
304
|
+
* @param routePath - The determined initial routePath (with brackets removed).
|
|
305
|
+
* @param originalRoutePath - The original route path (may contain brackets for escaped content).
|
|
273
306
|
* @param config - The user configuration object.
|
|
274
307
|
* @returns An object containing the type of the route and the variable name derived from the route path.
|
|
275
308
|
*/
|
|
276
309
|
export function getRouteMeta(
|
|
277
310
|
routePath: string,
|
|
311
|
+
originalRoutePath: string,
|
|
278
312
|
config: Pick<Config, 'routeToken' | 'indexToken'>,
|
|
279
313
|
): {
|
|
280
314
|
// `__root` is can be more easily determined by filtering down to routePath === /${rootPathId}
|
|
@@ -295,25 +329,50 @@ export function getRouteMeta(
|
|
|
295
329
|
} {
|
|
296
330
|
let fsRouteType: FsRouteType = 'static'
|
|
297
331
|
|
|
298
|
-
|
|
332
|
+
// Get the last segment from the original path to check for escaping
|
|
333
|
+
const originalSegments = originalRoutePath.split('/').filter(Boolean)
|
|
334
|
+
const lastOriginalSegment =
|
|
335
|
+
originalSegments[originalSegments.length - 1] || ''
|
|
336
|
+
|
|
337
|
+
// Helper to check if a specific suffix is escaped
|
|
338
|
+
const isSuffixEscaped = (suffix: string): boolean => {
|
|
339
|
+
return lastOriginalSegment === `[${suffix}]`
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
routePath.endsWith(`/${config.routeToken}`) &&
|
|
344
|
+
!isSuffixEscaped(config.routeToken)
|
|
345
|
+
) {
|
|
299
346
|
// layout routes, i.e `/foo/route.tsx` or `/foo/_layout/route.tsx`
|
|
300
347
|
fsRouteType = 'layout'
|
|
301
|
-
} else if (routePath.endsWith('/lazy')) {
|
|
348
|
+
} else if (routePath.endsWith('/lazy') && !isSuffixEscaped('lazy')) {
|
|
302
349
|
// lazy routes, i.e. `/foo.lazy.tsx`
|
|
303
350
|
fsRouteType = 'lazy'
|
|
304
|
-
} else if (routePath.endsWith('/loader')) {
|
|
351
|
+
} else if (routePath.endsWith('/loader') && !isSuffixEscaped('loader')) {
|
|
305
352
|
// loader routes, i.e. `/foo.loader.tsx`
|
|
306
353
|
fsRouteType = 'loader'
|
|
307
|
-
} else if (
|
|
354
|
+
} else if (
|
|
355
|
+
routePath.endsWith('/component') &&
|
|
356
|
+
!isSuffixEscaped('component')
|
|
357
|
+
) {
|
|
308
358
|
// component routes, i.e. `/foo.component.tsx`
|
|
309
359
|
fsRouteType = 'component'
|
|
310
|
-
} else if (
|
|
360
|
+
} else if (
|
|
361
|
+
routePath.endsWith('/pendingComponent') &&
|
|
362
|
+
!isSuffixEscaped('pendingComponent')
|
|
363
|
+
) {
|
|
311
364
|
// pending component routes, i.e. `/foo.pendingComponent.tsx`
|
|
312
365
|
fsRouteType = 'pendingComponent'
|
|
313
|
-
} else if (
|
|
366
|
+
} else if (
|
|
367
|
+
routePath.endsWith('/errorComponent') &&
|
|
368
|
+
!isSuffixEscaped('errorComponent')
|
|
369
|
+
) {
|
|
314
370
|
// error component routes, i.e. `/foo.errorComponent.tsx`
|
|
315
371
|
fsRouteType = 'errorComponent'
|
|
316
|
-
} else if (
|
|
372
|
+
} else if (
|
|
373
|
+
routePath.endsWith('/notFoundComponent') &&
|
|
374
|
+
!isSuffixEscaped('notFoundComponent')
|
|
375
|
+
) {
|
|
317
376
|
// not found component routes, i.e. `/foo.notFoundComponent.tsx`
|
|
318
377
|
fsRouteType = 'notFoundComponent'
|
|
319
378
|
}
|
|
@@ -326,11 +385,14 @@ export function getRouteMeta(
|
|
|
326
385
|
/**
|
|
327
386
|
* Used to validate if a route is a pathless layout route
|
|
328
387
|
* @param normalizedRoutePath Normalized route path, i.e `/foo/_layout/route.tsx` and `/foo._layout.route.tsx` to `/foo/_layout/route`
|
|
388
|
+
* @param originalRoutePath Original route path with brackets for escaped content
|
|
389
|
+
* @param routeType The route type determined from file extension
|
|
329
390
|
* @param config The `router-generator` configuration object
|
|
330
391
|
* @returns Boolean indicating if the route is a pathless layout route
|
|
331
392
|
*/
|
|
332
393
|
function isValidPathlessLayoutRoute(
|
|
333
394
|
normalizedRoutePath: string,
|
|
395
|
+
originalRoutePath: string,
|
|
334
396
|
routeType: FsRouteType,
|
|
335
397
|
config: Pick<Config, 'routeToken' | 'indexToken'>,
|
|
336
398
|
): boolean {
|
|
@@ -339,13 +401,18 @@ function isValidPathlessLayoutRoute(
|
|
|
339
401
|
}
|
|
340
402
|
|
|
341
403
|
const segments = normalizedRoutePath.split('/').filter(Boolean)
|
|
404
|
+
const originalSegments = originalRoutePath.split('/').filter(Boolean)
|
|
342
405
|
|
|
343
406
|
if (segments.length === 0) {
|
|
344
407
|
return false
|
|
345
408
|
}
|
|
346
409
|
|
|
347
410
|
const lastRouteSegment = segments[segments.length - 1]!
|
|
411
|
+
const lastOriginalSegment =
|
|
412
|
+
originalSegments[originalSegments.length - 1] || ''
|
|
348
413
|
const secondToLastRouteSegment = segments[segments.length - 2]
|
|
414
|
+
const secondToLastOriginalSegment =
|
|
415
|
+
originalSegments[originalSegments.length - 2]
|
|
349
416
|
|
|
350
417
|
// If segment === __root, then exit as false
|
|
351
418
|
if (lastRouteSegment === rootPathId) {
|
|
@@ -355,14 +422,25 @@ function isValidPathlessLayoutRoute(
|
|
|
355
422
|
// If segment === config.routeToken and secondToLastSegment is a string that starts with _, then exit as true
|
|
356
423
|
// Since the route is actually a configuration route for a layout/pathless route
|
|
357
424
|
// i.e. /foo/_layout/route.tsx === /foo/_layout.tsx
|
|
425
|
+
// But if the underscore is escaped, it's not a pathless layout
|
|
358
426
|
if (
|
|
359
427
|
lastRouteSegment === config.routeToken &&
|
|
360
|
-
typeof secondToLastRouteSegment === 'string'
|
|
428
|
+
typeof secondToLastRouteSegment === 'string' &&
|
|
429
|
+
typeof secondToLastOriginalSegment === 'string'
|
|
361
430
|
) {
|
|
431
|
+
// Check if the underscore is escaped
|
|
432
|
+
if (hasEscapedLeadingUnderscore(secondToLastOriginalSegment)) {
|
|
433
|
+
return false
|
|
434
|
+
}
|
|
362
435
|
return secondToLastRouteSegment.startsWith('_')
|
|
363
436
|
}
|
|
364
437
|
|
|
365
|
-
// Segment starts with _
|
|
438
|
+
// Segment starts with _ but check if it's escaped
|
|
439
|
+
// If the original segment has [_] at the start, the underscore is escaped and it's not a pathless layout
|
|
440
|
+
if (hasEscapedLeadingUnderscore(lastOriginalSegment)) {
|
|
441
|
+
return false
|
|
442
|
+
}
|
|
443
|
+
|
|
366
444
|
return (
|
|
367
445
|
lastRouteSegment !== config.indexToken &&
|
|
368
446
|
lastRouteSegment !== config.routeToken &&
|
package/src/generator.ts
CHANGED
|
@@ -28,15 +28,15 @@ import {
|
|
|
28
28
|
getResolvedRouteNodeVariableName,
|
|
29
29
|
hasParentRoute,
|
|
30
30
|
isRouteNodeValidForAugmentation,
|
|
31
|
+
isSegmentPathless,
|
|
31
32
|
mergeImportDeclarations,
|
|
32
33
|
multiSortBy,
|
|
33
34
|
removeExt,
|
|
34
35
|
removeGroups,
|
|
35
36
|
removeLastSegmentFromPath,
|
|
36
|
-
|
|
37
|
-
removeLeadingUnderscores,
|
|
37
|
+
removeLayoutSegmentsWithEscape,
|
|
38
38
|
removeTrailingSlash,
|
|
39
|
-
|
|
39
|
+
removeUnderscoresWithEscape,
|
|
40
40
|
replaceBackslash,
|
|
41
41
|
trimPathLeft,
|
|
42
42
|
} from './utils'
|
|
@@ -822,11 +822,8 @@ export class Generator {
|
|
|
822
822
|
let fileRoutesByFullPath = ''
|
|
823
823
|
|
|
824
824
|
if (!config.disableTypes) {
|
|
825
|
-
const routeNodesByFullPath = createRouteNodesByFullPath(
|
|
826
|
-
|
|
827
|
-
config,
|
|
828
|
-
)
|
|
829
|
-
const routeNodesByTo = createRouteNodesByTo(acc.routeNodes, config)
|
|
825
|
+
const routeNodesByFullPath = createRouteNodesByFullPath(acc.routeNodes)
|
|
826
|
+
const routeNodesByTo = createRouteNodesByTo(acc.routeNodes)
|
|
830
827
|
const routeNodesById = createRouteNodesById(acc.routeNodes)
|
|
831
828
|
|
|
832
829
|
fileRoutesByFullPath = [
|
|
@@ -1360,38 +1357,37 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
|
|
|
1360
1357
|
prefixMap: RoutePrefixMap,
|
|
1361
1358
|
config?: Config,
|
|
1362
1359
|
) {
|
|
1363
|
-
const
|
|
1364
|
-
config?.experimental?.nonNestedRoutes ?? false
|
|
1365
|
-
|
|
1366
|
-
const parentRoute = hasParentRoute(
|
|
1367
|
-
prefixMap,
|
|
1368
|
-
node,
|
|
1369
|
-
node.routePath,
|
|
1370
|
-
node.originalRoutePath,
|
|
1371
|
-
)
|
|
1360
|
+
const parentRoute = hasParentRoute(prefixMap, node, node.routePath)
|
|
1372
1361
|
|
|
1373
1362
|
if (parentRoute) node.parent = parentRoute
|
|
1374
1363
|
|
|
1375
1364
|
node.path = determineNodePath(node)
|
|
1376
1365
|
|
|
1377
1366
|
const trimmedPath = trimPathLeft(node.path ?? '')
|
|
1367
|
+
const trimmedOriginalPath = trimPathLeft(
|
|
1368
|
+
node.originalRoutePath?.replace(
|
|
1369
|
+
node.parent?.originalRoutePath ?? '',
|
|
1370
|
+
'',
|
|
1371
|
+
) ?? '',
|
|
1372
|
+
)
|
|
1378
1373
|
|
|
1379
1374
|
const split = trimmedPath.split('/')
|
|
1375
|
+
const originalSplit = trimmedOriginalPath.split('/')
|
|
1380
1376
|
const lastRouteSegment = split[split.length - 1] ?? trimmedPath
|
|
1377
|
+
const lastOriginalSegment =
|
|
1378
|
+
originalSplit[originalSplit.length - 1] ?? trimmedOriginalPath
|
|
1381
1379
|
|
|
1380
|
+
// A segment is non-path if it starts with underscore AND the underscore is not escaped
|
|
1382
1381
|
node.isNonPath =
|
|
1383
|
-
lastRouteSegment
|
|
1382
|
+
isSegmentPathless(lastRouteSegment, lastOriginalSegment) ||
|
|
1384
1383
|
split.every((part) => this.routeGroupPatternRegex.test(part))
|
|
1385
1384
|
|
|
1386
|
-
//
|
|
1387
|
-
// TODO with new major we can remove check and only remove leading underscores
|
|
1385
|
+
// Use escape-aware functions to compute cleanedPath
|
|
1388
1386
|
node.cleanedPath = removeGroups(
|
|
1389
|
-
(
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
)
|
|
1394
|
-
: removeUnderscores(removeLayoutSegments(node.path))) ?? '',
|
|
1387
|
+
removeUnderscoresWithEscape(
|
|
1388
|
+
removeLayoutSegmentsWithEscape(node.path, node.originalRoutePath),
|
|
1389
|
+
node.originalRoutePath,
|
|
1390
|
+
),
|
|
1395
1391
|
)
|
|
1396
1392
|
|
|
1397
1393
|
if (
|
|
@@ -1450,6 +1446,8 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
|
|
|
1450
1446
|
if (!node.isVirtual && isPathlessLayoutWithPath) {
|
|
1451
1447
|
const immediateParentPath =
|
|
1452
1448
|
removeLastSegmentFromPath(node.routePath) || '/'
|
|
1449
|
+
const immediateParentOriginalPath =
|
|
1450
|
+
removeLastSegmentFromPath(node.originalRoutePath) || '/'
|
|
1453
1451
|
let searchPath = immediateParentPath
|
|
1454
1452
|
|
|
1455
1453
|
// Find nearest real (non-virtual, non-index) parent
|
|
@@ -1461,8 +1459,19 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
|
|
|
1461
1459
|
node.routePath?.replace(candidate.routePath ?? '', '') || '/'
|
|
1462
1460
|
const pathRelativeToParent =
|
|
1463
1461
|
immediateParentPath.replace(candidate.routePath ?? '', '') || '/'
|
|
1462
|
+
const originalPathRelativeToParent =
|
|
1463
|
+
immediateParentOriginalPath.replace(
|
|
1464
|
+
candidate.originalRoutePath ?? '',
|
|
1465
|
+
'',
|
|
1466
|
+
) || '/'
|
|
1464
1467
|
node.cleanedPath = removeGroups(
|
|
1465
|
-
|
|
1468
|
+
removeUnderscoresWithEscape(
|
|
1469
|
+
removeLayoutSegmentsWithEscape(
|
|
1470
|
+
pathRelativeToParent,
|
|
1471
|
+
originalPathRelativeToParent,
|
|
1472
|
+
),
|
|
1473
|
+
originalPathRelativeToParent,
|
|
1474
|
+
),
|
|
1466
1475
|
)
|
|
1467
1476
|
break
|
|
1468
1477
|
}
|