@@ -32,16 +32,19 @@ export const DEFAULT_METADATA_ROUTE_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx']
32
32
33
33
// Match the file extension with the dynamic multi-routes extensions
34
34
// e.g. ([xml, js], null) -> can match `/sitemap.xml/route`, `sitemap.js/route`
35
- // e.g. ([png], [ts]) -> can match `/opengrapg -image.png`, `/opengraph-image.ts`
35
+ // e.g. ([png], [ts]) -> can match `/opengraph -image.png`, `/opengraph-image.ts`
36
36
export const getExtensionRegexString = (
37
37
staticExtensions : readonly string [ ] ,
38
38
dynamicExtensions : readonly string [ ] | null
39
39
) => {
40
+ let result : string
40
41
// If there's no possible multi dynamic routes, will not match any <name>[].<ext> files
41
42
if ( ! dynamicExtensions || dynamicExtensions . length === 0 ) {
42
- return `(\\.(?:${ staticExtensions . join ( '|' ) } ))`
43
+ result = `(\\.(?:${ staticExtensions . join ( '|' ) } ))`
44
+ } else {
45
+ result = `(?:\\.(${ staticExtensions . join ( '|' ) } )|(\\.(${ dynamicExtensions . join ( '|' ) } )))`
43
46
}
44
- return `(?:\\.( ${ staticExtensions . join ( '|' ) } )|(\\.( ${ dynamicExtensions . join ( '|' ) } )))`
47
+ return result
45
48
}
46
49
47
50
/**
@@ -53,79 +56,147 @@ export function isStaticMetadataFile(appDirRelativePath: string) {
53
56
return isMetadataRouteFile ( appDirRelativePath , [ ] , true )
54
57
}
55
58
56
- /**
57
- * Determine if the file is a metadata route file entry
58
- * @param appDirRelativePath the relative file path to app/
59
- * @param pageExtensions the js extensions, such as ['js', 'jsx', 'ts', 'tsx']
60
- * @param strictlyMatchExtensions if it's true, match the file with page extension, otherwise match the file with default corresponding extension
61
- * @returns if the file is a metadata route file
62
- */
63
- export function isMetadataRouteFile (
64
- appDirRelativePath : string ,
59
+ // Pre-compiled static regexes for common cases
60
+ const FAVICON_REGEX = / ^ [ \\ / ] f a v i c o n \. i c o $ /
61
+ const ROBOTS_TXT_REGEX = / ^ [ \\ / ] r o b o t s \. t x t $ /
62
+ const MANIFEST_JSON_REGEX = / ^ [ \\ / ] m a n i f e s t \. j s o n $ /
63
+ const MANIFEST_WEBMANIFEST_REGEX = / ^ [ \\ / ] m a n i f e s t \. w e b m a n i f e s t $ /
64
+ const SITEMAP_XML_REGEX = / [ \\ / ] s i t e m a p \. x m l $ /
65
+
66
+ // Cache for compiled regex patterns based on parameters
67
+ const compiledRegexCache = new Map < string , RegExp [ ] > ( )
68
+
69
+ // Fast path checks for common metadata files
70
+ function fastPathCheck ( normalizedPath : string ) : boolean | null {
71
+ // Check favicon.ico first (most common)
72
+ if ( FAVICON_REGEX . test ( normalizedPath ) ) return true
73
+
74
+ // Check other common static files
75
+ if ( ROBOTS_TXT_REGEX . test ( normalizedPath ) ) return true
76
+ if ( MANIFEST_JSON_REGEX . test ( normalizedPath ) ) return true
77
+ if ( MANIFEST_WEBMANIFEST_REGEX . test ( normalizedPath ) ) return true
78
+ if ( SITEMAP_XML_REGEX . test ( normalizedPath ) ) return true
79
+
80
+ // Quick negative check - if it doesn't contain any metadata keywords, skip
81
+ if (
82
+ ! normalizedPath . includes ( 'robots' ) &&
83
+ ! normalizedPath . includes ( 'manifest' ) &&
84
+ ! normalizedPath . includes ( 'sitemap' ) &&
85
+ ! normalizedPath . includes ( 'icon' ) &&
86
+ ! normalizedPath . includes ( 'apple-icon' ) &&
87
+ ! normalizedPath . includes ( 'opengraph-image' ) &&
88
+ ! normalizedPath . includes ( 'twitter-image' ) &&
89
+ ! normalizedPath . includes ( 'favicon' )
90
+ ) {
91
+ return false
92
+ }
93
+
94
+ return null // Continue with full regex matching
95
+ }
96
+
97
+ function getCompiledRegexes (
65
98
pageExtensions : PageExtensions ,
66
99
strictlyMatchExtensions : boolean
67
- ) {
68
- // End with the extension or optional to have the extension
69
- // When strictlyMatchExtensions is true, it's used for match file path;
70
- // When strictlyMatchExtensions, the dynamic extension is skipped but
71
- // static extension is kept, which is usually used for matching route path.
72
- const trailingMatcher = ( strictlyMatchExtensions ? '' : '?' ) + '$'
73
- // Match the optional variants like /opengraph-image2, /icon-a102f4.png, etc.
100
+ ) : RegExp [ ] {
101
+ // Create cache key
102
+ const cacheKey = `${ pageExtensions . join ( ',' ) } |${ strictlyMatchExtensions } `
103
+
104
+ const cached = compiledRegexCache . get ( cacheKey )
105
+ if ( cached ) {
106
+ return cached
107
+ }
108
+
109
+ // Pre-compute common strings
110
+ const trailingMatcher = strictlyMatchExtensions ? '$' : '?$'
74
111
const variantsMatcher = '\\d?'
75
- // The -\w{6} is the suffix that normalized from group routes;
76
112
const groupSuffix = strictlyMatchExtensions ? '' : '(-\\w{6})?'
113
+ const suffixMatcher = variantsMatcher + groupSuffix
77
114
78
- const suffixMatcher = `${ variantsMatcher } ${ groupSuffix } `
115
+ // Pre-compute extension arrays to avoid repeated concatenation
116
+ const robotsExts =
117
+ pageExtensions . length > 0 ? [ ...pageExtensions , 'txt' ] : [ 'txt' ]
118
+ const manifestExts =
119
+ pageExtensions . length > 0
120
+ ? [ ...pageExtensions , 'webmanifest' , 'json' ]
121
+ : [ 'webmanifest' , 'json' ]
79
122
80
- const metadataRouteFilesRegex = [
123
+ const regexes = [
81
124
new RegExp (
82
- `^[\\\\/]robots${ getExtensionRegexString (
83
- pageExtensions . concat ( 'txt' ) ,
84
- null
85
- ) } ${ trailingMatcher } `
125
+ `^[\\\\/]robots${ getExtensionRegexString ( robotsExts , null ) } ${ trailingMatcher } `
86
126
) ,
87
127
new RegExp (
88
- `^[\\\\/]manifest${ getExtensionRegexString (
89
- pageExtensions . concat ( 'webmanifest' , 'json' ) ,
90
- null
91
- ) } ${ trailingMatcher } `
128
+ `^[\\\\/]manifest${ getExtensionRegexString ( manifestExts , null ) } ${ trailingMatcher } `
92
129
) ,
93
- new RegExp ( `^[\\\\/]favicon\\.ico$` ) ,
130
+ // FAVICON_REGEX removed - already handled in fastPathCheck
94
131
new RegExp (
95
132
`[\\\\/]sitemap${ getExtensionRegexString ( [ 'xml' ] , pageExtensions ) } ${ trailingMatcher } `
96
133
) ,
97
134
new RegExp (
98
- `[\\\\/]${ STATIC_METADATA_IMAGES . icon . filename } ${ suffixMatcher } ${ getExtensionRegexString (
135
+ `[\\\\/]icon${ suffixMatcher } ${ getExtensionRegexString (
99
136
STATIC_METADATA_IMAGES . icon . extensions ,
100
137
pageExtensions
101
138
) } ${ trailingMatcher } `
102
139
) ,
103
140
new RegExp (
104
- `[\\\\/]${ STATIC_METADATA_IMAGES . apple . filename } ${ suffixMatcher } ${ getExtensionRegexString (
141
+ `[\\\\/]apple-icon ${ suffixMatcher } ${ getExtensionRegexString (
105
142
STATIC_METADATA_IMAGES . apple . extensions ,
106
143
pageExtensions
107
144
) } ${ trailingMatcher } `
108
145
) ,
109
146
new RegExp (
110
- `[\\\\/]${ STATIC_METADATA_IMAGES . openGraph . filename } ${ suffixMatcher } ${ getExtensionRegexString (
147
+ `[\\\\/]opengraph-image ${ suffixMatcher } ${ getExtensionRegexString (
111
148
STATIC_METADATA_IMAGES . openGraph . extensions ,
112
149
pageExtensions
113
150
) } ${ trailingMatcher } `
114
151
) ,
115
152
new RegExp (
116
- `[\\\\/]${ STATIC_METADATA_IMAGES . twitter . filename } ${ suffixMatcher } ${ getExtensionRegexString (
153
+ `[\\\\/]twitter-image ${ suffixMatcher } ${ getExtensionRegexString (
117
154
STATIC_METADATA_IMAGES . twitter . extensions ,
118
155
pageExtensions
119
156
) } ${ trailingMatcher } `
120
157
) ,
121
158
]
122
159
123
- const normalizedAppDirRelativePath = normalizePathSep ( appDirRelativePath )
124
- const matched = metadataRouteFilesRegex . some ( ( r ) =>
125
- r . test ( normalizedAppDirRelativePath )
126
- )
160
+ compiledRegexCache . set ( cacheKey , regexes )
161
+ return regexes
162
+ }
127
163
128
- return matched
164
+ /**
165
+ * Determine if the file is a metadata route file entry
166
+ * @param appDirRelativePath the relative file path to app/
167
+ * @param pageExtensions the js extensions, such as ['js', 'jsx', 'ts', 'tsx']
168
+ * @param strictlyMatchExtensions if it's true, match the file with page extension, otherwise match the file with default corresponding extension
169
+ * @returns if the file is a metadata route file
170
+ */
171
+ export function isMetadataRouteFile (
172
+ appDirRelativePath : string ,
173
+ pageExtensions : PageExtensions ,
174
+ strictlyMatchExtensions : boolean
175
+ ) : boolean {
176
+ // Early exit for empty or obviously non-metadata paths
177
+ if ( ! appDirRelativePath || appDirRelativePath . length < 2 ) {
178
+ return false
179
+ }
180
+
181
+ const normalizedPath = normalizePathSep ( appDirRelativePath )
182
+
183
+ // Fast path check for common cases
184
+ const fastResult = fastPathCheck ( normalizedPath )
185
+ if ( fastResult !== null ) {
186
+ return fastResult
187
+ }
188
+
189
+ // Get compiled regexes from cache
190
+ const regexes = getCompiledRegexes ( pageExtensions , strictlyMatchExtensions )
191
+
192
+ // Use for loop instead of .some() for better performance
193
+ for ( let i = 0 ; i < regexes . length ; i ++ ) {
194
+ if ( regexes [ i ] . test ( normalizedPath ) ) {
195
+ return true
196
+ }
197
+ }
198
+
199
+ return false
129
200
}
130
201
131
202
// Check if the route is a static metadata route, with /route suffix
0 commit comments