@tanstack/router-generator 1.141.9 → 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/src/utils.ts CHANGED
@@ -140,23 +140,26 @@ export function removeTrailingSlash(s: string) {
140
140
  const BRACKET_CONTENT_RE = /\[(.*?)\]/g
141
141
  const SPLIT_REGEX = /(?<!\[)\.(?!\])/g
142
142
 
143
- export function determineInitialRoutePath(routePath: string) {
144
- const DISALLOWED_ESCAPE_CHARS = new Set([
145
- '/',
146
- '\\',
147
- '?',
148
- '#',
149
- ':',
150
- '*',
151
- '<',
152
- '>',
153
- '|',
154
- '!',
155
- '$',
156
- '%',
157
- '_',
158
- ])
143
+ /**
144
+ * Characters that cannot be escaped in square brackets.
145
+ * These are characters that would cause issues in URLs or file systems.
146
+ */
147
+ const DISALLOWED_ESCAPE_CHARS = new Set([
148
+ '/',
149
+ '\\',
150
+ '?',
151
+ '#',
152
+ ':',
153
+ '*',
154
+ '<',
155
+ '>',
156
+ '|',
157
+ '!',
158
+ '$',
159
+ '%',
160
+ ])
159
161
 
162
+ export function determineInitialRoutePath(routePath: string) {
160
163
  const originalRoutePath =
161
164
  cleanPath(
162
165
  `/${(cleanPath(routePath) || '').split(SPLIT_REGEX).join('/')}`,
@@ -201,6 +204,47 @@ export function determineInitialRoutePath(routePath: string) {
201
204
  }
202
205
  }
203
206
 
207
+ /**
208
+ * Checks if a segment is fully escaped (entirely wrapped in brackets with no nested brackets).
209
+ * E.g., "[index]" -> true, "[_layout]" -> true, "foo[.]bar" -> false, "index" -> false
210
+ */
211
+ function isFullyEscapedSegment(originalSegment: string): boolean {
212
+ return (
213
+ originalSegment.startsWith('[') &&
214
+ originalSegment.endsWith(']') &&
215
+ !originalSegment.slice(1, -1).includes('[') &&
216
+ !originalSegment.slice(1, -1).includes(']')
217
+ )
218
+ }
219
+
220
+ /**
221
+ * Checks if the leading underscore in a segment is escaped.
222
+ * Returns true if:
223
+ * - Segment starts with [_] pattern: "[_]layout" -> "_layout"
224
+ * - Segment is fully escaped and content starts with _: "[_1nd3x]" -> "_1nd3x"
225
+ */
226
+ export function hasEscapedLeadingUnderscore(originalSegment: string): boolean {
227
+ // Pattern: [_]something or [_something]
228
+ return (
229
+ originalSegment.startsWith('[_]') ||
230
+ (originalSegment.startsWith('[_') && isFullyEscapedSegment(originalSegment))
231
+ )
232
+ }
233
+
234
+ /**
235
+ * Checks if the trailing underscore in a segment is escaped.
236
+ * Returns true if:
237
+ * - Segment ends with [_] pattern: "blog[_]" -> "blog_"
238
+ * - Segment is fully escaped and content ends with _: "[_r0ut3_]" -> "_r0ut3_"
239
+ */
240
+ export function hasEscapedTrailingUnderscore(originalSegment: string): boolean {
241
+ // Pattern: something[_] or [something_]
242
+ return (
243
+ originalSegment.endsWith('[_]') ||
244
+ (originalSegment.endsWith('_]') && isFullyEscapedSegment(originalSegment))
245
+ )
246
+ }
247
+
204
248
  const backslashRegex = /\\/g
205
249
 
206
250
  export function replaceBackslash(s: string) {
@@ -271,6 +315,92 @@ export function removeUnderscores(s?: string) {
271
315
  .replace(underscoreSlashRegex, '/')
272
316
  }
273
317
 
318
+ /**
319
+ * Removes underscores from a path, but preserves underscores that were escaped
320
+ * in the original path (indicated by [_] syntax).
321
+ *
322
+ * @param routePath - The path with brackets removed
323
+ * @param originalPath - The original path that may contain [_] escape sequences
324
+ * @returns The path with non-escaped underscores removed
325
+ */
326
+ export function removeUnderscoresWithEscape(
327
+ routePath?: string,
328
+ originalPath?: string,
329
+ ): string {
330
+ if (!routePath) return ''
331
+ if (!originalPath) return removeUnderscores(routePath) ?? ''
332
+
333
+ const routeSegments = routePath.split('/')
334
+ const originalSegments = originalPath.split('/')
335
+
336
+ const newSegments = routeSegments.map((segment, i) => {
337
+ const originalSegment = originalSegments[i] || ''
338
+
339
+ // Check if leading underscore is escaped
340
+ const leadingEscaped = hasEscapedLeadingUnderscore(originalSegment)
341
+ // Check if trailing underscore is escaped
342
+ const trailingEscaped = hasEscapedTrailingUnderscore(originalSegment)
343
+
344
+ let result = segment
345
+
346
+ // Remove leading underscore only if not escaped
347
+ if (result.startsWith('_') && !leadingEscaped) {
348
+ result = result.slice(1)
349
+ }
350
+
351
+ // Remove trailing underscore only if not escaped
352
+ if (result.endsWith('_') && !trailingEscaped) {
353
+ result = result.slice(0, -1)
354
+ }
355
+
356
+ return result
357
+ })
358
+
359
+ return newSegments.join('/')
360
+ }
361
+
362
+ /**
363
+ * Removes layout segments (segments starting with underscore) from a path,
364
+ * but preserves segments where the underscore was escaped.
365
+ *
366
+ * @param routePath - The path with brackets removed
367
+ * @param originalPath - The original path that may contain [_] escape sequences
368
+ * @returns The path with non-escaped layout segments removed
369
+ */
370
+ export function removeLayoutSegmentsWithEscape(
371
+ routePath: string = '/',
372
+ originalPath?: string,
373
+ ): string {
374
+ if (!originalPath) return removeLayoutSegments(routePath)
375
+
376
+ const routeSegments = routePath.split('/')
377
+ const originalSegments = originalPath.split('/')
378
+
379
+ // Keep segments that are NOT pathless (i.e., don't start with unescaped underscore)
380
+ const newSegments = routeSegments.filter((segment, i) => {
381
+ const originalSegment = originalSegments[i] || ''
382
+ return !isSegmentPathless(segment, originalSegment)
383
+ })
384
+
385
+ return newSegments.join('/')
386
+ }
387
+
388
+ /**
389
+ * Checks if a segment should be treated as a pathless/layout segment.
390
+ * A segment is pathless if it starts with underscore and the underscore is not escaped.
391
+ *
392
+ * @param segment - The segment from routePath (brackets removed)
393
+ * @param originalSegment - The segment from originalRoutePath (may contain brackets)
394
+ * @returns true if the segment is pathless (has non-escaped leading underscore)
395
+ */
396
+ export function isSegmentPathless(
397
+ segment: string,
398
+ originalSegment: string,
399
+ ): boolean {
400
+ if (!segment.startsWith('_')) return false
401
+ return !hasEscapedLeadingUnderscore(originalSegment)
402
+ }
403
+
274
404
  function escapeRegExp(s: string): string {
275
405
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
276
406
  }
@@ -495,7 +625,13 @@ export const inferPath = (routeNode: RouteNode): string => {
495
625
  */
496
626
  export const inferFullPath = (routeNode: RouteNode): string => {
497
627
  const fullPath = removeGroups(
498
- removeUnderscores(removeLayoutSegments(routeNode.routePath)) ?? '',
628
+ removeUnderscoresWithEscape(
629
+ removeLayoutSegmentsWithEscape(
630
+ routeNode.routePath,
631
+ routeNode.originalRoutePath,
632
+ ),
633
+ routeNode.originalRoutePath,
634
+ ),
499
635
  )
500
636
 
501
637
  return routeNode.cleanedPath === '/' ? fullPath : fullPath.replace(/\/$/, '')