@@ -7,35 +7,50 @@ const extensionPattern = /\.[^./]+$/;
77
88/**
99 * Extract wildcard match from file path based on pattern
10- * @param filePath - File path with extension
10+ * @param pathWithoutExtension - File path without extension
1111 * @param patternParts - Parts between wildcards (e.g., [""] or ["/index", ""])
1212 * @returns Captured wildcard value, or undefined if no match
1313 */
1414const extractWildcardMatch = (
15- filePath : string ,
15+ pathWithoutExtension : string ,
1616 patternParts : string [ ] ,
1717) : string | undefined => {
18- const pathWithoutExtension = filePath . replace ( extensionPattern , '' ) ;
19-
2018 // Early validation: check if path ends with the last pattern part
2119 const lastPart = patternParts . at ( - 1 ) ! ;
2220 if ( ! pathWithoutExtension . endsWith ( lastPart ) ) {
2321 return ;
2422 }
2523
26- // Multiple wildcards: find the repeated capture value by trying different capture lengths
27- // Example: "./*" → "./dist/*/*/index.js" with "a/b/a/b/index" captures "a/b"
28- // Example: "./*" → "./dist/*/_/*/_/*.mjs" with "foo/_/foo/_/foo" captures "foo"
29- const segments = pathWithoutExtension . split ( '/' ) . filter ( Boolean ) ;
24+ // Detect if this is a path-based pattern (contains '/') or filename-based pattern
25+ const isPathPattern = patternParts . some ( part => part . includes ( '/' ) ) ;
26+
27+ if ( isPathPattern ) {
28+ // Path-based patterns: split by '/' and try different segment lengths
29+ // Example: "./*" → "./dist/*/*/index.js" with "a/b/a/b/index" captures "a/b"
30+ // Example: "./*" → "./dist/*/_/*/_/*.mjs" with "foo/_/foo/_/foo" captures "foo"
31+ const segments = pathWithoutExtension . split ( '/' ) . filter ( Boolean ) ;
3032
31- // Try different capture lengths (from 1 segment up to total length)
32- for ( let captureLength = 1 ; captureLength <= segments . length ; captureLength += 1 ) {
33- const captureValue = segments . slice ( 0 , captureLength ) . join ( '/' ) ;
33+ // Try different capture lengths (from 1 segment up to total length)
34+ for ( let captureLength = 1 ; captureLength <= segments . length ; captureLength += 1 ) {
35+ const captureValue = segments . slice ( 0 , captureLength ) . join ( '/' ) ;
3436
35- // Build expected pattern by joining parts with the capture value
36- const expectedPath = captureValue + patternParts . join ( captureValue ) ;
37- if ( expectedPath === pathWithoutExtension ) {
38- return expectedPath ;
37+ // Build expected pattern by joining parts with the capture value
38+ const expectedPath = captureValue + patternParts . join ( captureValue ) ;
39+ if ( expectedPath === pathWithoutExtension ) {
40+ return expectedPath ;
41+ }
42+ }
43+ } else {
44+ // Filename-based patterns: try different character lengths
45+ // Example: "x.*.y.*" with "foo.y.foo" should capture "foo"
46+ for ( let captureLength = 1 ; captureLength <= pathWithoutExtension . length ; captureLength += 1 ) {
47+ const captureValue = pathWithoutExtension . slice ( 0 , captureLength ) ;
48+
49+ // Build expected pattern by joining parts with the capture value
50+ const expectedPath = captureValue + patternParts . join ( captureValue ) ;
51+ if ( expectedPath === pathWithoutExtension ) {
52+ return expectedPath ;
53+ }
3954 }
4055 }
4156} ;
@@ -80,9 +95,77 @@ export const expandBuildOutputWildcards = async (
8095 // Split by wildcards to get prefix and pattern parts
8196 const [ outputPrefix , ...patternParts ] = outputPathWithoutExtension . split ( '*' ) ;
8297
83- // Find the matching src:dist pair (most specific first)
98+ // Separate directory path from filename prefix
99+ // Find the last '/' to split directory from filename component
100+ const lastSlashIndex = outputPrefix . lastIndexOf ( '/' ) ;
101+ const directoryPrefix = lastSlashIndex === - 1
102+ ? outputPrefix
103+ : outputPrefix . slice ( 0 , lastSlashIndex + 1 ) ;
104+ const filenamePrefix = lastSlashIndex >= 0
105+ ? outputPrefix . slice ( lastSlashIndex + 1 )
106+ : '' ;
107+
108+ // Helper function to check if a file matches the wildcard pattern
109+ const matchesPattern = ( filePath : string ) : boolean => {
110+ // Strip source extension for pattern matching
111+ const filePathWithoutExtension = filePath . replace ( extensionPattern , '' ) ;
112+ const fileName = filePathWithoutExtension . split ( '/' ) . at ( - 1 ) || '' ;
113+
114+ // Check filename prefix
115+ if ( filenamePrefix && ! fileName . startsWith ( filenamePrefix ) ) {
116+ return false ;
117+ }
118+
119+ // For single wildcard patterns, check suffix
120+ if ( patternParts . length === 1 ) {
121+ const suffix = patternParts [ 0 ] ;
122+ if ( suffix ) {
123+ // If suffix contains path separators, check against full path
124+ // Otherwise, check against filename only
125+ const checkTarget = suffix . includes ( '/' ) ? filePathWithoutExtension : fileName ;
126+ if ( ! checkTarget . endsWith ( suffix ) ) {
127+ return false ;
128+ }
129+ }
130+ return true ;
131+ }
132+
133+ // For multiple wildcard patterns, use extractWildcardMatch
134+ // This validates that all wildcards capture the same value
135+ // For filename patterns with prefix, strip it before matching
136+ const matchTarget = filenamePrefix
137+ ? fileName . slice ( filenamePrefix . length )
138+ : filePathWithoutExtension ;
139+ return ! ! extractWildcardMatch ( matchTarget , patternParts ) ;
140+ } ;
141+
142+ // Handle root-level patterns (directoryPrefix is './')
143+ if ( directoryPrefix === './' ) {
144+ // Expand for each srcdist pair
145+ const allExpansions = await Promise . all (
146+ sortedByDistLength . map ( async ( srcdist ) => {
147+ const srcPath = srcdist . srcResolved ;
148+ const allFiles = await getDirectoryFiles ( srcPath ) ;
149+ return allFiles . flatMap ( ( filePath ) => {
150+ if ( ! matchesPattern ( filePath ) ) {
151+ return [ ] ;
152+ }
153+
154+ // Strip source extension before appending output extension
155+ const filePathWithoutExtension = filePath . replace ( extensionPattern , '' ) ;
156+ return [ {
157+ ...output ,
158+ outputPath : srcdist . dist + filePathWithoutExtension + matchedExtension ,
159+ } ] ;
160+ } ) ;
161+ } ) ,
162+ ) ;
163+ return allExpansions . flat ( ) ;
164+ }
165+
166+ // Non-root-level patterns: find matching src:dist pair based on directory prefix
84167 const srcdistMatch = sortedByDistLength . find (
85- ( { dist } ) => outputPrefix === dist || outputPrefix . startsWith ( dist ) ,
168+ ( { dist } ) => directoryPrefix === dist || directoryPrefix . startsWith ( dist ) ,
86169 ) ;
87170
88171 if ( ! srcdistMatch ) {
@@ -91,19 +174,32 @@ export const expandBuildOutputWildcards = async (
91174
92175 const srcPath = path . posix . join (
93176 srcdistMatch . srcResolved ,
94- outputPrefix . slice ( srcdistMatch . dist . length ) ,
177+ directoryPrefix . slice ( srcdistMatch . dist . length ) ,
95178 ) ;
179+
96180 const allFiles = await getDirectoryFiles ( srcPath ) ;
97181 return allFiles . flatMap ( ( filePath ) => {
98- const match = extractWildcardMatch ( filePath , patternParts ) ;
99- if ( match ) {
182+ if ( ! matchesPattern ( filePath ) ) {
183+ return [ ] ;
184+ }
185+
186+ // Strip source extension before appending output extension
187+ const filePathWithoutExtension = filePath . replace ( extensionPattern , '' ) ;
188+
189+ // For multiple wildcard patterns, use the matched path from extractWildcardMatch
190+ if ( patternParts . length > 1 ) {
191+ const match = extractWildcardMatch ( filePathWithoutExtension , patternParts ) ;
100192 return [ {
101193 ...output ,
102- outputPath : outputPrefix + match + matchedExtension ,
194+ outputPath : directoryPrefix + match + matchedExtension ,
103195 } ] ;
104196 }
105197
106- return [ ] ;
198+ // For single wildcard patterns, preserve the full relative path
199+ return [ {
200+ ...output ,
201+ outputPath : directoryPrefix + filePathWithoutExtension + matchedExtension ,
202+ } ] ;
107203 } ) ;
108204 } ) ,
109205 ) ;
0 commit comments