7
7
*/
8
8
9
9
import * as path from 'path' ;
10
- import { CompilerOptions , MapLike } from 'typescript' ;
10
+ import { CompilerOptions } from 'typescript' ;
11
11
import type { Configuration } from 'webpack' ;
12
12
13
13
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -16,21 +16,98 @@ export interface TypeScriptPathsPluginOptions extends Pick<CompilerOptions, 'pat
16
16
// Extract Resolver type from Webpack types since it is not directly exported
17
17
type Resolver = Exclude < Exclude < Configuration [ 'resolve' ] , undefined > [ 'resolver' ] , undefined > ;
18
18
19
+ interface PathPattern {
20
+ starIndex : number ;
21
+ prefix : string ;
22
+ suffix ?: string ;
23
+ potentials : { hasStar : boolean ; prefix : string ; suffix ?: string } [ ] ;
24
+ }
25
+
19
26
export class TypeScriptPathsPlugin {
20
- constructor ( private options ?: TypeScriptPathsPluginOptions ) { }
27
+ private baseUrl ?: string ;
28
+ private patterns ?: PathPattern [ ] ;
29
+
30
+ constructor ( options ?: TypeScriptPathsPluginOptions ) {
31
+ if ( options ) {
32
+ this . update ( options ) ;
33
+ }
34
+ }
21
35
36
+ /**
37
+ * Update the plugin with new path mapping option values.
38
+ * The options will also be preprocessed to reduce the overhead of individual resolve actions
39
+ * during a build.
40
+ *
41
+ * @param options The `paths` and `baseUrl` options from TypeScript's `CompilerOptions`.
42
+ */
22
43
update ( options : TypeScriptPathsPluginOptions ) : void {
23
- this . options = options ;
44
+ this . baseUrl = options . baseUrl ;
45
+ this . patterns = undefined ;
46
+
47
+ if ( options . paths ) {
48
+ for ( const [ pattern , potentials ] of Object . entries ( options . paths ) ) {
49
+ // Ignore any entries that would not result in a new mapping
50
+ if ( potentials . length === 0 || potentials . every ( ( potential ) => potential === '*' ) ) {
51
+ continue ;
52
+ }
53
+
54
+ const starIndex = pattern . indexOf ( '*' ) ;
55
+ let prefix = pattern ;
56
+ let suffix ;
57
+ if ( starIndex > - 1 ) {
58
+ prefix = pattern . slice ( 0 , starIndex ) ;
59
+ if ( starIndex < pattern . length - 1 ) {
60
+ suffix = pattern . slice ( starIndex + 1 ) ;
61
+ }
62
+ }
63
+
64
+ this . patterns ??= [ ] ;
65
+ this . patterns . push ( {
66
+ starIndex,
67
+ prefix,
68
+ suffix,
69
+ potentials : potentials . map ( ( potential ) => {
70
+ const potentialStarIndex = potential . indexOf ( '*' ) ;
71
+ if ( potentialStarIndex === - 1 ) {
72
+ return { hasStar : false , prefix : potential } ;
73
+ }
74
+
75
+ return {
76
+ hasStar : true ,
77
+ prefix : potential . slice ( 0 , potentialStarIndex ) ,
78
+ suffix :
79
+ potentialStarIndex < potential . length - 1
80
+ ? potential . slice ( potentialStarIndex + 1 )
81
+ : undefined ,
82
+ } ;
83
+ } ) ,
84
+ } ) ;
85
+ }
86
+
87
+ // Sort patterns so that exact matches take priority then largest prefix match
88
+ this . patterns ?. sort ( ( a , b ) => {
89
+ if ( a . starIndex === - 1 ) {
90
+ return - 1 ;
91
+ } else if ( b . starIndex === - 1 ) {
92
+ return 1 ;
93
+ } else {
94
+ return b . starIndex - a . starIndex ;
95
+ }
96
+ } ) ;
97
+ }
24
98
}
25
99
26
100
apply ( resolver : Resolver ) : void {
27
101
const target = resolver . ensureHook ( 'resolve' ) ;
28
102
103
+ // To support synchronous resolvers this hook cannot be promise based.
104
+ // Webpack supports synchronous resolution with `tap` and `tapAsync` hooks.
29
105
resolver . getHook ( 'described-resolve' ) . tapAsync (
30
106
'TypeScriptPathsPlugin' ,
31
107
// eslint-disable-next-line @typescript-eslint/no-explicit-any
32
108
( request : any , resolveContext , callback ) => {
33
- if ( ! this . options ) {
109
+ // Preprocessing of the options will ensure that `patterns` is either undefined or has elements to check
110
+ if ( ! this . patterns ) {
34
111
callback ( ) ;
35
112
36
113
return ;
@@ -50,39 +127,45 @@ export class TypeScriptPathsPlugin {
50
127
}
51
128
52
129
// Only work on Javascript/TypeScript issuers.
53
- if ( ! request . context . issuer || ! request . context . issuer . match ( / \. [ j t ] s x ? $ / ) ) {
130
+ if ( ! request . context . issuer || ! request . context . issuer . match ( / \. [ c m ] ? [ j t ] s x ? $ / ) ) {
54
131
callback ( ) ;
55
132
56
133
return ;
57
134
}
58
135
59
- // Relative or absolute requests are not mapped
60
- if ( originalRequest . startsWith ( '.' ) || originalRequest . startsWith ( '/' ) ) {
61
- callback ( ) ;
62
-
63
- return ;
64
- }
65
-
66
- // Ignore all webpack special requests
67
- if ( originalRequest . startsWith ( '!!' ) ) {
68
- callback ( ) ;
136
+ switch ( originalRequest [ 0 ] ) {
137
+ case '.' :
138
+ case '/' :
139
+ // Relative or absolute requests are not mapped
140
+ callback ( ) ;
69
141
70
- return ;
142
+ return ;
143
+ case '!' :
144
+ // Ignore all webpack special requests
145
+ if ( originalRequest . length > 1 && originalRequest [ 1 ] === '!' ) {
146
+ callback ( ) ;
147
+
148
+ return ;
149
+ }
150
+ break ;
71
151
}
72
152
73
- const replacements = findReplacements ( originalRequest , this . options . paths || { } ) ;
153
+ // A generator is used to limit the amount of replacements that need to be created.
154
+ // For example, if the first one resolves, any others are not needed and do not need
155
+ // to be created.
156
+ const replacements = findReplacements ( originalRequest , this . patterns ) ;
74
157
75
158
const tryResolve = ( ) => {
76
- const potential = replacements . shift ( ) ;
77
- if ( ! potential ) {
159
+ const next = replacements . next ( ) ;
160
+ if ( next . done ) {
78
161
callback ( ) ;
79
162
80
163
return ;
81
164
}
82
165
83
166
const potentialRequest = {
84
167
...request ,
85
- request : path . resolve ( this . options ?. baseUrl || '' , potential ) ,
168
+ request : path . resolve ( this . baseUrl ?? '' , next . value ) ,
86
169
typescriptPathMapped : true ,
87
170
} ;
88
171
@@ -110,89 +193,52 @@ export class TypeScriptPathsPlugin {
110
193
}
111
194
}
112
195
113
- function findReplacements ( originalRequest : string , paths : MapLike < string [ ] > ) : string [ ] {
196
+ function * findReplacements (
197
+ originalRequest : string ,
198
+ patterns : PathPattern [ ] ,
199
+ ) : IterableIterator < string > {
114
200
// check if any path mapping rules are relevant
115
- const pathMapOptions = [ ] ;
116
- for ( const pattern in paths ) {
117
- // get potentials and remove duplicates; JS Set maintains insertion order
118
- const potentials = Array . from ( new Set ( paths [ pattern ] ) ) ;
119
- if ( potentials . length === 0 ) {
120
- // no potential replacements so skip
121
- continue ;
122
- }
201
+ for ( const { starIndex, prefix, suffix, potentials } of patterns ) {
202
+ let partial ;
123
203
124
- // can only contain zero or one
125
- const starIndex = pattern . indexOf ( '*' ) ;
126
204
if ( starIndex === - 1 ) {
127
- if ( pattern === originalRequest ) {
128
- pathMapOptions . push ( {
129
- starIndex,
130
- partial : '' ,
131
- potentials,
132
- } ) ;
133
- }
134
- } else if ( starIndex === 0 && pattern . length === 1 ) {
135
- if ( potentials . length === 1 && potentials [ 0 ] === '*' ) {
136
- // identity mapping -> noop
137
- continue ;
205
+ // No star means an exact match is required
206
+ if ( prefix === originalRequest ) {
207
+ partial = '' ;
138
208
}
139
- pathMapOptions . push ( {
140
- starIndex,
141
- partial : originalRequest ,
142
- potentials,
143
- } ) ;
144
- } else if ( starIndex === pattern . length - 1 ) {
145
- if ( originalRequest . startsWith ( pattern . slice ( 0 , - 1 ) ) ) {
146
- pathMapOptions . push ( {
147
- starIndex,
148
- partial : originalRequest . slice ( pattern . length - 1 ) ,
149
- potentials,
150
- } ) ;
209
+ } else if ( starIndex === 0 && ! suffix ) {
210
+ // Everything matches a single wildcard pattern ("*")
211
+ partial = originalRequest ;
212
+ } else if ( ! suffix ) {
213
+ // No suffix means the star is at the end of the pattern
214
+ if ( originalRequest . startsWith ( prefix ) ) {
215
+ partial = originalRequest . slice ( prefix . length ) ;
151
216
}
152
217
} else {
153
- const [ prefix , suffix ] = pattern . split ( '*' ) ;
218
+ // Star was in the middle of the pattern
154
219
if ( originalRequest . startsWith ( prefix ) && originalRequest . endsWith ( suffix ) ) {
155
- pathMapOptions . push ( {
156
- starIndex,
157
- partial : originalRequest . slice ( prefix . length ) . slice ( 0 , - suffix . length ) ,
158
- potentials,
159
- } ) ;
220
+ partial = originalRequest . substring ( prefix . length , originalRequest . length - suffix . length ) ;
160
221
}
161
222
}
162
- }
163
-
164
- if ( pathMapOptions . length === 0 ) {
165
- return [ ] ;
166
- }
167
223
168
- // exact matches take priority then largest prefix match
169
- pathMapOptions . sort ( ( a , b ) => {
170
- if ( a . starIndex === - 1 ) {
171
- return - 1 ;
172
- } else if ( b . starIndex === - 1 ) {
173
- return 1 ;
174
- } else {
175
- return b . starIndex - a . starIndex ;
224
+ // If request was not matched, move on to the next pattern
225
+ if ( partial === undefined ) {
226
+ continue ;
176
227
}
177
- } ) ;
178
-
179
- const replacements : string [ ] = [ ] ;
180
- pathMapOptions . forEach ( ( option ) => {
181
- for ( const potential of option . potentials ) {
182
- let replacement ;
183
- const starIndex = potential . indexOf ( '*' ) ;
184
- if ( starIndex === - 1 ) {
185
- replacement = potential ;
186
- } else if ( starIndex === potential . length - 1 ) {
187
- replacement = potential . slice ( 0 , - 1 ) + option . partial ;
188
- } else {
189
- const [ prefix , suffix ] = potential . split ( '*' ) ;
190
- replacement = prefix + option . partial + suffix ;
228
+
229
+ // Create the full replacement values based on the original request and the potentials
230
+ // for the successfully matched pattern.
231
+ for ( const { hasStar, prefix, suffix } of potentials ) {
232
+ let replacement = prefix ;
233
+
234
+ if ( hasStar ) {
235
+ replacement += partial ;
236
+ if ( suffix ) {
237
+ replacement += suffix ;
238
+ }
191
239
}
192
240
193
- replacements . push ( replacement ) ;
241
+ yield replacement ;
194
242
}
195
- } ) ;
196
-
197
- return replacements ;
243
+ }
198
244
}
0 commit comments