@@ -182,4 +182,103 @@ describe("alias", () => {
182182 it ( "should resolve a wildcard alias with multiple targets correctly" , ( ) => {
183183 expect ( resolver . resolveSync ( { } , "/" , "shared/b" ) ) . toBe ( "/src/components/b" ) ;
184184 } ) ;
185+
186+ // Regression tests for the watch-mode fallback described in
187+ // https://github.com/webpack/enhanced-resolve/issues/395 and
188+ // https://github.com/webpack/enhanced-resolve/issues/250.
189+ //
190+ // When an alias maps to an array of target paths (used for
191+ // theme-override-style setups), a subsequent resolve after one of the
192+ // target files is deleted must gracefully fall back to the next target
193+ // in the array instead of holding on to the previously-resolved path.
194+ // Conversely, a newly created higher-priority file must be used on the
195+ // next resolve.
196+ describe ( "multi-target alias (theme override) watch-mode behavior" , ( ) => {
197+ const AliasPlugin = require ( "../lib/AliasPlugin" ) ;
198+
199+ /**
200+ * Builds a fresh resolver over an in-memory filesystem with a
201+ * `theme` alias that maps to two directories in priority order.
202+ * @param {Record<string, string> } files initial file contents keyed by absolute path
203+ * @returns {{ resolver: import("../").Resolver, fileSystem: import("memfs").Volume } } helpers
204+ */
205+ const createThemeResolver = ( files ) => {
206+ const fileSystem = Volume . fromJSON ( files , "/" ) ;
207+ const resolver = ResolverFactory . createResolver ( {
208+ extensions : [ ".js" ] ,
209+ useSyncFileSystemCalls : true ,
210+ // @ts -expect-error for tests
211+ fileSystem,
212+ plugins : [
213+ new AliasPlugin (
214+ "described-resolve" ,
215+ [ { name : "theme" , alias : [ "/fancy-theme" , "/default-theme" ] } ] ,
216+ "resolve" ,
217+ ) ,
218+ ] ,
219+ } ) ;
220+
221+ return { resolver, fileSystem } ;
222+ } ;
223+
224+ it ( "falls back to the next target once the preferred file is removed" , ( ) => {
225+ const { resolver, fileSystem } = createThemeResolver ( {
226+ "/fancy-theme/Hello.js" : "" ,
227+ "/default-theme/Hello.js" : "" ,
228+ } ) ;
229+
230+ expect ( resolver . resolveSync ( { } , "/" , "theme/Hello" ) ) . toBe (
231+ "/fancy-theme/Hello.js" ,
232+ ) ;
233+
234+ fileSystem . unlinkSync ( "/fancy-theme/Hello.js" ) ;
235+
236+ expect ( resolver . resolveSync ( { } , "/" , "theme/Hello" ) ) . toBe (
237+ "/default-theme/Hello.js" ,
238+ ) ;
239+ } ) ;
240+
241+ it ( "picks up a newly created higher-priority file" , ( ) => {
242+ const { resolver, fileSystem } = createThemeResolver ( {
243+ "/default-theme/Hello.js" : "" ,
244+ } ) ;
245+
246+ expect ( resolver . resolveSync ( { } , "/" , "theme/Hello" ) ) . toBe (
247+ "/default-theme/Hello.js" ,
248+ ) ;
249+
250+ fileSystem . mkdirSync ( "/fancy-theme" ) ;
251+ fileSystem . writeFileSync ( "/fancy-theme/Hello.js" , "" ) ;
252+
253+ expect ( resolver . resolveSync ( { } , "/" , "theme/Hello" ) ) . toBe (
254+ "/fancy-theme/Hello.js" ,
255+ ) ;
256+ } ) ;
257+
258+ it ( "reports a missing-higher-priority path as a missing dependency so watchers can invalidate" , ( done ) => {
259+ const { resolver } = createThemeResolver ( {
260+ "/default-theme/Hello.js" : "" ,
261+ } ) ;
262+
263+ const fileDependencies = new Set ( ) ;
264+ const missingDependencies = new Set ( ) ;
265+
266+ resolver . resolve (
267+ { } ,
268+ "/" ,
269+ "theme/Hello" ,
270+ { fileDependencies, missingDependencies } ,
271+ ( err , result ) => {
272+ if ( err ) return done ( err ) ;
273+ expect ( result ) . toBe ( "/default-theme/Hello.js" ) ;
274+ // The winning file is tracked so that deletions invalidate.
275+ expect ( fileDependencies . has ( "/default-theme/Hello.js" ) ) . toBe ( true ) ;
276+ // The non-existent higher-priority file is tracked so that
277+ // its creation triggers a re-resolve (see issue #250).
278+ expect ( missingDependencies . has ( "/fancy-theme/Hello.js" ) ) . toBe ( true ) ;
279+ done ( ) ;
280+ } ,
281+ ) ;
282+ } ) ;
283+ } ) ;
185284} ) ;
0 commit comments