1
1
import type { Token } from 'next/dist/compiled/path-to-regexp'
2
- import { parse , tokensToRegexp } from 'next/dist/compiled/path-to-regexp'
2
+ import {
3
+ parse ,
4
+ tokensToRegexp ,
5
+ pathToRegexp ,
6
+ compile ,
7
+ regexpToFunction ,
8
+ } from 'next/dist/compiled/path-to-regexp'
3
9
import { parse as parseURL } from 'url'
4
10
import isError from './is-error'
5
11
@@ -34,6 +40,208 @@ function reportError({ route, parsedPath }: ParseResult, err: any) {
34
40
}
35
41
}
36
42
43
+ /**
44
+ * Fixes tokens that have repeating modifiers (* or +) but empty prefix and suffix.
45
+ * This is needed to work around path-to-regexp 6.3.0+ which doesn't allow such tokens.
46
+ */
47
+ function fixTokensForRegexp ( tokens : Token [ ] ) : Token [ ] {
48
+ return tokens . map ( ( token ) => {
49
+ if (
50
+ typeof token === 'object' &&
51
+ token !== null &&
52
+ 'modifier' in token &&
53
+ ( token . modifier === '*' || token . modifier === '+' ) &&
54
+ 'prefix' in token &&
55
+ 'suffix' in token &&
56
+ token . prefix === '' &&
57
+ token . suffix === ''
58
+ ) {
59
+ // For tokens with repeating modifiers but no prefix/suffix,
60
+ // we need to provide a minimal prefix to satisfy path-to-regexp
61
+ return {
62
+ ...token ,
63
+ prefix : '/' ,
64
+ }
65
+ }
66
+ return token
67
+ } )
68
+ }
69
+
70
+ /**
71
+ * Fixes route patterns that have adjacent parameters without text between them.
72
+ * This is needed to work around path-to-regexp 6.3.0+ validation.
73
+ * We use a special marker that can be stripped out later to avoid parameter pollution.
74
+ */
75
+ function fixAdjacentParameters ( route : string ) : string {
76
+ let fixed = route
77
+
78
+ // Use a special marker that users would never add themselves
79
+ // and that we can strip out before it leaks into params
80
+ const PARAM_SEPARATOR = '__NEXT_SEP__'
81
+
82
+ // The issue is (.):param - add our special separator
83
+ fixed = fixed . replace ( / ( \( [ ^ ) ] * \) ) : ( [ ^ / \s ] + ) / g, `$1${ PARAM_SEPARATOR } :$2` )
84
+
85
+ // Handle other basic adjacent parameter patterns conservatively
86
+ fixed = fixed . replace ( / : ( [ ^ : / \s ) ] + ) (? = : ) / g, `:$1${ PARAM_SEPARATOR } ` )
87
+
88
+ return fixed
89
+ }
90
+
91
+ /**
92
+ * Detects if a route pattern has issues that would cause path-to-regexp 6.3.0+ validation to fail
93
+ */
94
+ function hasAdjacentParameterIssues ( route : string ) : boolean {
95
+ if ( typeof route !== 'string' ) return false
96
+
97
+ // Check for interception route markers followed immediately by parameters
98
+ // Pattern: (.):param, (..):param, etc.
99
+ if ( / \( [ ^ ) ] * \) : [ ^ / \s ] + / . test ( route ) ) {
100
+ return true
101
+ }
102
+
103
+ // Check for adjacent parameters without separators
104
+ // Pattern: :param1:param2
105
+ if ( / : ( [ ^ : / \s ) ] + ) : / . test ( route ) ) {
106
+ return true
107
+ }
108
+
109
+ return false
110
+ }
111
+
112
+ /**
113
+ * Safe wrapper around pathToRegexp that handles path-to-regexp 6.3.0+ validation errors.
114
+ * This includes both "Can not repeat without prefix/suffix" and "Must have text between parameters" errors.
115
+ */
116
+ export function safePathToRegexp (
117
+ route : string | RegExp | Array < string | RegExp > ,
118
+ keys ?: any [ ] ,
119
+ options ?: any
120
+ ) : RegExp {
121
+ // Proactively fix known problematic patterns
122
+ if ( typeof route === 'string' && hasAdjacentParameterIssues ( route ) ) {
123
+ const fixedRoute = fixAdjacentParameters ( route )
124
+ return pathToRegexp ( fixedRoute , keys , options )
125
+ }
126
+
127
+ try {
128
+ return pathToRegexp ( route , keys , options )
129
+ } catch ( error ) {
130
+ // For any remaining edge cases, try the fix as fallback
131
+ if ( isError ( error ) && typeof route === 'string' ) {
132
+ try {
133
+ const fixedRoute = fixAdjacentParameters ( route )
134
+ return pathToRegexp ( fixedRoute , keys , options )
135
+ } catch ( retryError ) {
136
+ // If that doesn't work, fall back to original error
137
+ throw error
138
+ }
139
+ }
140
+ throw error
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Safe wrapper around compile that handles path-to-regexp 6.3.0+ validation errors.
146
+ */
147
+ export function safeCompile ( route : string , options ?: any ) {
148
+ // Proactively fix known problematic patterns
149
+ if ( hasAdjacentParameterIssues ( route ) ) {
150
+ const fixedRoute = fixAdjacentParameters ( route )
151
+ return compile ( fixedRoute , options )
152
+ }
153
+
154
+ try {
155
+ return compile ( route , options )
156
+ } catch ( error ) {
157
+ // For any remaining edge cases, try the fix as fallback
158
+ if ( isError ( error ) ) {
159
+ try {
160
+ const fixedRoute = fixAdjacentParameters ( route )
161
+ return compile ( fixedRoute , options )
162
+ } catch ( retryError ) {
163
+ // If that doesn't work, fall back to original error
164
+ throw error
165
+ }
166
+ }
167
+ throw error
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Safe wrapper around tokensToRegexp that handles path-to-regexp 6.3.0+ validation errors.
173
+ */
174
+ function safeTokensToRegexp ( tokens : Token [ ] ) : RegExp {
175
+ try {
176
+ return tokensToRegexp ( tokens )
177
+ } catch ( error ) {
178
+ if ( isError ( error ) ) {
179
+ // Try to fix tokens with repeating modifiers but no prefix/suffix
180
+ const fixedTokens = fixTokensForRegexp ( tokens )
181
+ return tokensToRegexp ( fixedTokens )
182
+ }
183
+ throw error
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Strips the special parameter separator from extracted route parameters.
189
+ * This is internal to the safe wrappers and should not be used elsewhere.
190
+ */
191
+ function stripParameterSeparators (
192
+ params : Record < string , any >
193
+ ) : Record < string , any > {
194
+ const PARAM_SEPARATOR = '__NEXT_SEP__'
195
+ const cleaned : Record < string , any > = { }
196
+
197
+ for ( const [ key , value ] of Object . entries ( params ) ) {
198
+ if ( typeof value === 'string' ) {
199
+ // Remove the separator if it appears at the start of parameter values
200
+ cleaned [ key ] = value . replace ( new RegExp ( `^${ PARAM_SEPARATOR } ` ) , '' )
201
+ } else {
202
+ cleaned [ key ] = value
203
+ }
204
+ }
205
+
206
+ return cleaned
207
+ }
208
+
209
+ /**
210
+ * Safe wrapper around regexpToFunction that automatically cleans parameters.
211
+ */
212
+ export function safeRegexpToFunction < T = object > (
213
+ regexp : RegExp ,
214
+ keys ?: any [ ]
215
+ ) : ( pathname : string ) => { params : T } | false {
216
+ const originalMatcher = regexpToFunction < T > ( regexp , keys )
217
+
218
+ return ( pathname : string ) => {
219
+ const result = originalMatcher ( pathname )
220
+ if ( ! result ) return false
221
+
222
+ // Clean parameters before returning
223
+ return {
224
+ ...result ,
225
+ params : stripParameterSeparators ( result . params as any ) as T ,
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Safe wrapper for route matcher functions that automatically cleans interception markers.
232
+ */
233
+ export function safeRouteMatcher < T extends Record < string , any > > (
234
+ matcherFn : ( pathname : string ) => false | T
235
+ ) : ( pathname : string ) => false | T {
236
+ return ( pathname : string ) => {
237
+ const result = matcherFn ( pathname )
238
+ if ( ! result ) return false
239
+
240
+ // Clean parameters before returning
241
+ return stripParameterSeparators ( result ) as T
242
+ }
243
+ }
244
+
37
245
/**
38
246
* Attempts to parse a given route with `path-to-regexp` and returns an object
39
247
* with the result. Whenever an error happens on parse, it will print an error
@@ -55,7 +263,11 @@ export function tryToParsePath(
55
263
}
56
264
57
265
result . tokens = parse ( result . parsedPath )
58
- result . regexStr = tokensToRegexp ( result . tokens ) . source
266
+
267
+ // Use safe wrapper instead of proactive detection
268
+ if ( result . tokens ) {
269
+ result . regexStr = safeTokensToRegexp ( result . tokens ) . source
270
+ }
59
271
} catch ( err ) {
60
272
reportError ( result , err )
61
273
result . error = err
0 commit comments