Skip to content

Commit 42b6620

Browse files
test: document multi-target alias watch-mode fallback (#395) (#531)
1 parent 990c604 commit 42b6620

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

lib/AliasPlugin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@
1212

1313
const { aliasResolveHandler, compileAliasOptions } = require("./AliasUtils");
1414

15+
/**
16+
* When `alias` is given as an array, the targets are tried in priority
17+
* order and the first matching one wins. Tried-and-failed higher-priority
18+
* targets are recorded on `resolveContext.missingDependencies` (via the
19+
* downstream `FileExistsPlugin`) so that a consumer's watcher can
20+
* invalidate the resolve once one of them appears. The winning target is
21+
* recorded on `resolveContext.fileDependencies`; its removal triggers
22+
* re-resolution, at which point the fallback target is returned.
23+
*
24+
* Callers that cache successful resolves (e.g. webpack's `unsafeCache`)
25+
* are responsible for invalidating those entries when the tracked
26+
* dependencies change -- otherwise a stale path may survive across
27+
* rebuilds even though this plugin itself would return the correct
28+
* fallback on a fresh resolve.
29+
*/
1530
module.exports = class AliasPlugin {
1631
/**
1732
* @param {string | ResolveStepHook} source source

test/alias.test.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)