@tanstack/router-generator 1.147.1 → 1.149.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 +29 -6
- package/dist/cjs/config.cjs.map +1 -1
- package/dist/cjs/config.d.cts +94 -12
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs +50 -27
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -1
- package/dist/cjs/filesystem/physical/getRouteNodes.d.cts +12 -3
- package/dist/cjs/filesystem/virtual/getRouteNodes.cjs +12 -5
- package/dist/cjs/filesystem/virtual/getRouteNodes.cjs.map +1 -1
- package/dist/cjs/filesystem/virtual/getRouteNodes.d.cts +3 -2
- package/dist/cjs/generator.cjs +32 -7
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/generator.d.cts +20 -2
- package/dist/cjs/utils.cjs +47 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +6 -1
- package/dist/esm/config.d.ts +94 -12
- package/dist/esm/config.js +29 -6
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/filesystem/physical/getRouteNodes.d.ts +12 -3
- package/dist/esm/filesystem/physical/getRouteNodes.js +51 -28
- package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -1
- package/dist/esm/filesystem/virtual/getRouteNodes.d.ts +3 -2
- package/dist/esm/filesystem/virtual/getRouteNodes.js +12 -5
- package/dist/esm/filesystem/virtual/getRouteNodes.js.map +1 -1
- package/dist/esm/generator.d.ts +20 -2
- package/dist/esm/generator.js +33 -8
- package/dist/esm/generator.js.map +1 -1
- package/dist/esm/utils.d.ts +6 -1
- package/dist/esm/utils.js +47 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +58 -6
- package/src/filesystem/physical/getRouteNodes.ts +105 -43
- package/src/filesystem/virtual/getRouteNodes.ts +12 -2
- package/src/generator.ts +59 -9
- package/src/utils.ts +73 -1
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
} from '@tanstack/virtual-file-routes'
|
|
18
18
|
import type { GetRouteNodesResult, RouteNode } from '../../types'
|
|
19
19
|
import type { Config } from '../../config'
|
|
20
|
+
import type { TokenRegexBundle } from '../physical/getRouteNodes'
|
|
20
21
|
|
|
21
22
|
function ensureLeadingUnderScore(id: string) {
|
|
22
23
|
if (id.startsWith('_')) {
|
|
@@ -49,6 +50,7 @@ export async function getRouteNodes(
|
|
|
49
50
|
| 'routeToken'
|
|
50
51
|
>,
|
|
51
52
|
root: string,
|
|
53
|
+
tokenRegexes: TokenRegexBundle,
|
|
52
54
|
): Promise<GetRouteNodesResult> {
|
|
53
55
|
const fullDir = resolve(tsrConfig.routesDirectory)
|
|
54
56
|
if (tsrConfig.virtualRouteConfig === undefined) {
|
|
@@ -68,6 +70,7 @@ export async function getRouteNodes(
|
|
|
68
70
|
root,
|
|
69
71
|
fullDir,
|
|
70
72
|
virtualRouteConfig.children,
|
|
73
|
+
tokenRegexes,
|
|
71
74
|
)
|
|
72
75
|
const allNodes = flattenTree({
|
|
73
76
|
children,
|
|
@@ -134,7 +137,8 @@ export async function getRouteNodesRecursive(
|
|
|
134
137
|
>,
|
|
135
138
|
root: string,
|
|
136
139
|
fullDir: string,
|
|
137
|
-
nodes
|
|
140
|
+
nodes: Array<VirtualRouteNode> | undefined,
|
|
141
|
+
tokenRegexes: TokenRegexBundle,
|
|
138
142
|
parent?: RouteNode,
|
|
139
143
|
): Promise<{ children: Array<RouteNode>; physicalDirectories: Array<string> }> {
|
|
140
144
|
if (nodes === undefined) {
|
|
@@ -150,8 +154,12 @@ export async function getRouteNodesRecursive(
|
|
|
150
154
|
routesDirectory: resolve(fullDir, node.directory),
|
|
151
155
|
},
|
|
152
156
|
root,
|
|
157
|
+
tokenRegexes,
|
|
158
|
+
)
|
|
159
|
+
allPhysicalDirectories.push(
|
|
160
|
+
resolve(fullDir, node.directory),
|
|
161
|
+
...physicalDirectories,
|
|
153
162
|
)
|
|
154
|
-
allPhysicalDirectories.push(node.directory)
|
|
155
163
|
routeNodes.forEach((subtreeNode) => {
|
|
156
164
|
subtreeNode.variableName = routePathToVariable(
|
|
157
165
|
`${node.pathPrefix}/${removeExt(subtreeNode.filePath)}`,
|
|
@@ -229,6 +237,7 @@ export async function getRouteNodesRecursive(
|
|
|
229
237
|
root,
|
|
230
238
|
fullDir,
|
|
231
239
|
node.children,
|
|
240
|
+
tokenRegexes,
|
|
232
241
|
routeNode,
|
|
233
242
|
)
|
|
234
243
|
routeNode.children = children
|
|
@@ -275,6 +284,7 @@ export async function getRouteNodesRecursive(
|
|
|
275
284
|
root,
|
|
276
285
|
fullDir,
|
|
277
286
|
node.children,
|
|
287
|
+
tokenRegexes,
|
|
278
288
|
routeNode,
|
|
279
289
|
)
|
|
280
290
|
routeNode.children = children
|
package/src/generator.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createRouteNodesByFullPath,
|
|
21
21
|
createRouteNodesById,
|
|
22
22
|
createRouteNodesByTo,
|
|
23
|
+
createTokenRegex,
|
|
23
24
|
determineNodePath,
|
|
24
25
|
findParent,
|
|
25
26
|
format,
|
|
@@ -191,8 +192,26 @@ export class Generator {
|
|
|
191
192
|
private static routeGroupPatternRegex = /\(.+\)/
|
|
192
193
|
private physicalDirectories: Array<string> = []
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Token regexes are pre-compiled once here and reused throughout route processing.
|
|
197
|
+
* We need TWO types of regex for each token because they match against different inputs:
|
|
198
|
+
*
|
|
199
|
+
* 1. FILENAME regexes: Match token patterns within full file path strings.
|
|
200
|
+
* Example: For file "routes/dashboard.index.tsx", we want to detect ".index."
|
|
201
|
+
* Pattern: `[./](?:token)[.]` - matches token bounded by path separators/dots
|
|
202
|
+
* Used in: sorting route nodes by file path
|
|
203
|
+
*
|
|
204
|
+
* 2. SEGMENT regexes: Match token against a single logical route segment.
|
|
205
|
+
* Example: For segment "index" (extracted from path), match the whole segment
|
|
206
|
+
* Pattern: `^(?:token)$` - matches entire segment exactly
|
|
207
|
+
* Used in: route parsing, determining route types, escape detection
|
|
208
|
+
*
|
|
209
|
+
* We cannot reuse one for the other without false positives or missing matches.
|
|
210
|
+
*/
|
|
211
|
+
private indexTokenFilenameRegex: RegExp
|
|
212
|
+
private routeTokenFilenameRegex: RegExp
|
|
213
|
+
private indexTokenSegmentRegex: RegExp
|
|
214
|
+
private routeTokenSegmentRegex: RegExp
|
|
196
215
|
private static componentPieceRegex =
|
|
197
216
|
/[./](component|errorComponent|notFoundComponent|pendingComponent|loader|lazy)[.]/
|
|
198
217
|
|
|
@@ -207,8 +226,19 @@ export class Generator {
|
|
|
207
226
|
this.routesDirectoryPath = this.getRoutesDirectoryPath()
|
|
208
227
|
this.plugins.push(...(opts.config.plugins || []))
|
|
209
228
|
|
|
210
|
-
|
|
211
|
-
this.
|
|
229
|
+
// Create all token regexes once in constructor
|
|
230
|
+
this.indexTokenFilenameRegex = createTokenRegex(this.config.indexToken, {
|
|
231
|
+
type: 'filename',
|
|
232
|
+
})
|
|
233
|
+
this.routeTokenFilenameRegex = createTokenRegex(this.config.routeToken, {
|
|
234
|
+
type: 'filename',
|
|
235
|
+
})
|
|
236
|
+
this.indexTokenSegmentRegex = createTokenRegex(this.config.indexToken, {
|
|
237
|
+
type: 'segment',
|
|
238
|
+
})
|
|
239
|
+
this.routeTokenSegmentRegex = createTokenRegex(this.config.routeToken, {
|
|
240
|
+
type: 'segment',
|
|
241
|
+
})
|
|
212
242
|
|
|
213
243
|
for (const plugin of this.plugins) {
|
|
214
244
|
plugin.init?.({ generator: this })
|
|
@@ -330,9 +360,19 @@ export class Generator {
|
|
|
330
360
|
let getRouteNodesResult: GetRouteNodesResult
|
|
331
361
|
|
|
332
362
|
if (this.config.virtualRouteConfig) {
|
|
333
|
-
getRouteNodesResult = await virtualGetRouteNodes(this.config, this.root
|
|
363
|
+
getRouteNodesResult = await virtualGetRouteNodes(this.config, this.root, {
|
|
364
|
+
indexTokenSegmentRegex: this.indexTokenSegmentRegex,
|
|
365
|
+
routeTokenSegmentRegex: this.routeTokenSegmentRegex,
|
|
366
|
+
})
|
|
334
367
|
} else {
|
|
335
|
-
getRouteNodesResult = await physicalGetRouteNodes(
|
|
368
|
+
getRouteNodesResult = await physicalGetRouteNodes(
|
|
369
|
+
this.config,
|
|
370
|
+
this.root,
|
|
371
|
+
{
|
|
372
|
+
indexTokenSegmentRegex: this.indexTokenSegmentRegex,
|
|
373
|
+
routeTokenSegmentRegex: this.routeTokenSegmentRegex,
|
|
374
|
+
},
|
|
375
|
+
)
|
|
336
376
|
}
|
|
337
377
|
|
|
338
378
|
const {
|
|
@@ -354,9 +394,9 @@ export class Generator {
|
|
|
354
394
|
const preRouteNodes = multiSortBy(beforeRouteNodes, [
|
|
355
395
|
(d) => (d.routePath === '/' ? -1 : 1),
|
|
356
396
|
(d) => d.routePath?.split('/').length,
|
|
357
|
-
(d) => (d.filePath.match(this.
|
|
397
|
+
(d) => (d.filePath.match(this.indexTokenFilenameRegex) ? 1 : -1),
|
|
358
398
|
(d) => (d.filePath.match(Generator.componentPieceRegex) ? 1 : -1),
|
|
359
|
-
(d) => (d.filePath.match(this.
|
|
399
|
+
(d) => (d.filePath.match(this.routeTokenFilenameRegex) ? -1 : 1),
|
|
360
400
|
(d) => (d.routePath?.endsWith('/') ? -1 : 1),
|
|
361
401
|
(d) => d.routePath,
|
|
362
402
|
]).filter((d) => {
|
|
@@ -541,10 +581,20 @@ export class Generator {
|
|
|
541
581
|
|
|
542
582
|
const { rootRouteNode, acc } = opts
|
|
543
583
|
|
|
584
|
+
// Use pre-compiled regex if config hasn't been overridden, otherwise create new one
|
|
585
|
+
const indexTokenSegmentRegex =
|
|
586
|
+
config.indexToken === this.config.indexToken
|
|
587
|
+
? this.indexTokenSegmentRegex
|
|
588
|
+
: createTokenRegex(config.indexToken, { type: 'segment' })
|
|
589
|
+
|
|
544
590
|
const sortedRouteNodes = multiSortBy(acc.routeNodes, [
|
|
545
591
|
(d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
|
|
546
592
|
(d) => d.routePath?.split('/').length,
|
|
547
|
-
(d) =>
|
|
593
|
+
(d) => {
|
|
594
|
+
const segments = d.routePath?.split('/').filter(Boolean) ?? []
|
|
595
|
+
const last = segments[segments.length - 1] ?? ''
|
|
596
|
+
return indexTokenSegmentRegex.test(last) ? -1 : 1
|
|
597
|
+
},
|
|
548
598
|
(d) => d,
|
|
549
599
|
])
|
|
550
600
|
|
package/src/utils.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fsp from 'node:fs/promises'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import * as prettier from 'prettier'
|
|
5
5
|
import { rootPathId } from './filesystem/physical/rootPathId'
|
|
6
|
-
import type { Config } from './config'
|
|
6
|
+
import type { Config, TokenMatcher } from './config'
|
|
7
7
|
import type { ImportDeclaration, RouteNode } from './types'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -418,6 +418,78 @@ function escapeRegExp(s: string): string {
|
|
|
418
418
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
function sanitizeTokenFlags(flags?: string): string | undefined {
|
|
422
|
+
if (!flags) return flags
|
|
423
|
+
|
|
424
|
+
// Prevent stateful behavior with RegExp.prototype.test/exec
|
|
425
|
+
// g = global, y = sticky
|
|
426
|
+
return flags.replace(/[gy]/g, '')
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function createTokenRegex(
|
|
430
|
+
token: TokenMatcher,
|
|
431
|
+
opts: {
|
|
432
|
+
type: 'segment' | 'filename'
|
|
433
|
+
},
|
|
434
|
+
): RegExp {
|
|
435
|
+
// Defensive check: if token is undefined/null, throw a clear error
|
|
436
|
+
// (runtime safety for config loading edge cases)
|
|
437
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
438
|
+
if (token === undefined || token === null) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`createTokenRegex: token is ${token}. This usually means the config was not properly parsed with defaults.`,
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
if (typeof token === 'string') {
|
|
446
|
+
return opts.type === 'segment'
|
|
447
|
+
? new RegExp(`^${escapeRegExp(token)}$`)
|
|
448
|
+
: new RegExp(`[./]${escapeRegExp(token)}[.]`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (token instanceof RegExp) {
|
|
452
|
+
const flags = sanitizeTokenFlags(token.flags)
|
|
453
|
+
return opts.type === 'segment'
|
|
454
|
+
? new RegExp(`^(?:${token.source})$`, flags)
|
|
455
|
+
: new RegExp(`[./](?:${token.source})[.]`, flags)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Handle JSON regex object form: { regex: string, flags?: string }
|
|
459
|
+
if (typeof token === 'object' && 'regex' in token) {
|
|
460
|
+
const flags = sanitizeTokenFlags(token.flags)
|
|
461
|
+
return opts.type === 'segment'
|
|
462
|
+
? new RegExp(`^(?:${token.regex})$`, flags)
|
|
463
|
+
: new RegExp(`[./](?:${token.regex})[.]`, flags)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
throw new Error(
|
|
467
|
+
`createTokenRegex: invalid token type. Expected string, RegExp, or { regex, flags } object, got: ${typeof token}`,
|
|
468
|
+
)
|
|
469
|
+
} catch (e) {
|
|
470
|
+
if (e instanceof SyntaxError) {
|
|
471
|
+
const pattern =
|
|
472
|
+
typeof token === 'string'
|
|
473
|
+
? token
|
|
474
|
+
: token instanceof RegExp
|
|
475
|
+
? token.source
|
|
476
|
+
: token.regex
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Invalid regex pattern in token config: "${pattern}". ${e.message}`,
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
throw e
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function isBracketWrappedSegment(segment: string): boolean {
|
|
486
|
+
return segment.startsWith('[') && segment.endsWith(']')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function unwrapBracketWrappedSegment(segment: string): string {
|
|
490
|
+
return isBracketWrappedSegment(segment) ? segment.slice(1, -1) : segment
|
|
491
|
+
}
|
|
492
|
+
|
|
421
493
|
export function removeLeadingUnderscores(s: string, routeToken: string) {
|
|
422
494
|
if (!s) return s
|
|
423
495
|
|