From d4b1819749143daeb73ad26d1b7fc28e0c4ac441 Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Thu, 28 May 2020 01:59:23 +0300 Subject: [PATCH] add ability to set resolve options per dependency category - byDependency option - create resolver per dependency category --- declarations/WebpackOptions.d.ts | 9 +++ lib/NormalModuleFactory.js | 55 ++++--------------- lib/ResolverFactory.js | 35 ++++++++---- lib/WebpackOptionsApply.js | 44 ++++++++++++--- lib/cache/ResolverCachePlugin.js | 5 +- lib/config/defaults.js | 23 ++++++-- .../RequireEnsureItemDependency.js | 4 ++ schemas/WebpackOptions.json | 12 ++++ types.d.ts | 17 ++++-- 9 files changed, 131 insertions(+), 73 deletions(-) diff --git a/declarations/WebpackOptions.d.ts b/declarations/WebpackOptions.d.ts index 22b2fced3..d265893d6 100644 --- a/declarations/WebpackOptions.d.ts +++ b/declarations/WebpackOptions.d.ts @@ -1111,6 +1111,15 @@ export interface ResolveOptions { * Fields in the description file (usually package.json) which are used to redirect requests inside the module. */ aliasFields?: (string[] | string)[]; + /** + * Extra resolve options per dependency category. Typical categories are "commonjs", "amd", "esm". + */ + byDependency?: { + /** + * Options object for resolving requests. + */ + [k: string]: ResolveOptions; + }; /** * Enable caching of successfully resolved requests (cache entries are revalidated). */ diff --git a/lib/NormalModuleFactory.js b/lib/NormalModuleFactory.js index 37ceafe6e..b4944ad2b 100644 --- a/lib/NormalModuleFactory.js +++ b/lib/NormalModuleFactory.js @@ -44,8 +44,6 @@ const { join } = require("./util/fs"); */ const EMPTY_OBJECT = {}; -const REQUIRE_CONDITION_NAME = { conditionNames: ["require", "..."] }; -const IMPORT_CONDITION_NAME = { conditionNames: ["import", "..."] }; const MATCH_RESOURCE_REGEX = /^([^!]+)!=!/; @@ -234,57 +232,22 @@ class NormalModuleFactory extends ModuleFactory { stage: 100 }, (data, callback) => { - let { resolveOptions } = data; const { contextInfo, context, dependencies, request, + resolveOptions, fileDependencies, missingDependencies, contextDependencies } = data; - const category = - dependencies.length > 0 ? dependencies[0].category : undefined; + const loaderResolver = this.getResolver("loader"); - - if (category === "commonjs") { - if (!resolveOptions) { - resolveOptions = REQUIRE_CONDITION_NAME; - } else { - if (Array.isArray(resolveOptions.conditionNames)) { - if (resolveOptions.conditionNames.indexOf("require") === -1) { - resolveOptions = { ...resolveOptions }; - resolveOptions.conditionNames = REQUIRE_CONDITION_NAME.conditionNames.slice(); - } - } else { - resolveOptions = { - ...resolveOptions, - conditionNames: REQUIRE_CONDITION_NAME.conditionNames.slice() - }; - } - } - } else if (category === "esm") { - if (!resolveOptions) { - resolveOptions = IMPORT_CONDITION_NAME; - } else { - if (Array.isArray(resolveOptions.conditionNames)) { - if (resolveOptions.conditionNames.indexOf("import") === -1) { - resolveOptions = { ...resolveOptions }; - resolveOptions.conditionNames = IMPORT_CONDITION_NAME.conditionNames.slice(); - } - } else { - resolveOptions = { - ...resolveOptions, - conditionNames: IMPORT_CONDITION_NAME.conditionNames.slice() - }; - } - } - } - const normalResolver = this.getResolver( "normal", - resolveOptions || EMPTY_OBJECT + resolveOptions, + dependencies.length > 0 ? dependencies[0].category : undefined ); /** @type {string} */ @@ -544,7 +507,7 @@ class NormalModuleFactory extends ModuleFactory { if (cacheEntry) return callback(null, cacheEntry); } const context = data.context || this.context; - const resolveOptions = data.resolveOptions; + const resolveOptions = data.resolveOptions || EMPTY_OBJECT; const dependency = dependencies[0]; const request = dependency.request; const contextInfo = data.contextInfo; @@ -727,8 +690,12 @@ class NormalModuleFactory extends ModuleFactory { return generator; } - getResolver(type, resolveOptions) { - return this.resolverFactory.get(type, resolveOptions || EMPTY_OBJECT); + getResolver(type, resolveOptions, category) { + return this.resolverFactory.get( + type, + resolveOptions || EMPTY_OBJECT, + category + ); } } diff --git a/lib/ResolverFactory.js b/lib/ResolverFactory.js index 2cec7ef55..d7ab958bc 100644 --- a/lib/ResolverFactory.js +++ b/lib/ResolverFactory.js @@ -29,13 +29,19 @@ const EMPTY_RESOLVE_OPTIONS = {}; module.exports = class ResolverFactory { constructor() { this.hooks = Object.freeze({ - /** @type {HookMap>} */ + /** @type {HookMap>} */ resolveOptions: new HookMap( - () => new SyncWaterfallHook(["resolveOptions"]) + () => new SyncWaterfallHook(["resolveOptions", "category"]) ), - /** @type {HookMap>} */ + /** @type {HookMap>} */ resolver: new HookMap( - () => new SyncHook(["resolver", "resolveOptions", "userResolveOptions"]) + () => + new SyncHook([ + "resolver", + "resolveOptions", + "userResolveOptions", + "category" + ]) ) }); /** @type {Map} */ @@ -45,16 +51,18 @@ module.exports = class ResolverFactory { /** * @param {string} type type of resolver * @param {Object=} resolveOptions options + * @param {string=} category dependency category if any * @returns {ResolverWithOptions} the resolver */ - get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS) { - let typedCaches = this.cache.get(type); + get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS, category = "unknown") { + const typedCacheId = category === "esm" ? type : `${type}-${category}`; + let typedCaches = this.cache.get(typedCacheId); if (!typedCaches) { typedCaches = { direct: new WeakMap(), stringified: new Map() }; - this.cache.set(type, typedCaches); + this.cache.set(typedCacheId, typedCaches); } const cachedResolver = typedCaches.direct.get(resolveOptions); if (cachedResolver) { @@ -66,7 +74,7 @@ module.exports = class ResolverFactory { typedCaches.direct.set(resolveOptions, resolver); return resolver; } - const newResolver = this._create(type, resolveOptions); + const newResolver = this._create(type, resolveOptions, category); typedCaches.direct.set(resolveOptions, newResolver); typedCaches.stringified.set(ident, newResolver); return newResolver; @@ -75,11 +83,14 @@ module.exports = class ResolverFactory { /** * @param {string} type type of resolver * @param {Object} resolveOptions options + * @param {string} category category * @returns {ResolverWithOptions} the resolver */ - _create(type, resolveOptions) { + _create(type, resolveOptions, category) { const originalResolveOptions = { ...resolveOptions }; - resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions); + resolveOptions = this.hooks.resolveOptions + .for(type) + .call(resolveOptions, category); const resolver = /** @type {ResolverWithOptions} */ (Factory.createResolver( resolveOptions )); @@ -92,13 +103,13 @@ module.exports = class ResolverFactory { const cacheEntry = childCache.get(options); if (cacheEntry !== undefined) return cacheEntry; const mergedOptions = cachedCleverMerge(originalResolveOptions, options); - const resolver = this.get(type, mergedOptions); + const resolver = this.get(type, mergedOptions, category); childCache.set(options, resolver); return resolver; }; this.hooks.resolver .for(type) - .call(resolver, resolveOptions, originalResolveOptions); + .call(resolver, resolveOptions, originalResolveOptions, category); return resolver; } }; diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index 911fa9cf7..07677642b 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -44,7 +44,7 @@ const DefaultStatsFactoryPlugin = require("./stats/DefaultStatsFactoryPlugin"); const DefaultStatsPresetPlugin = require("./stats/DefaultStatsPresetPlugin"); const DefaultStatsPrinterPlugin = require("./stats/DefaultStatsPrinterPlugin"); -const { cachedCleverMerge } = require("./util/cleverMerge"); +const { cachedCleverMerge, cleverMerge } = require("./util/cleverMerge"); /** @typedef {import("../declarations/WebpackOptions").WebpackOptionsNormalized} WebpackOptions */ /** @typedef {import("./Compiler")} Compiler */ @@ -593,29 +593,59 @@ class WebpackOptionsApply extends OptionsApply { if (!compiler.inputFileSystem) { throw new Error("No input filesystem provided"); } + + const resolveOptionsByCategory = new Map(); + const baseResolveOptions = { + ...options.resolve + }; + resolveOptionsByCategory.set("unknown", baseResolveOptions); + + if (baseResolveOptions.byDependency) { + const categories = Object.keys(baseResolveOptions.byDependency); + delete baseResolveOptions.byDependency; + for (const category of categories) { + resolveOptionsByCategory.set( + category, + cleverMerge( + baseResolveOptions, + options.resolve.byDependency[category] + ) + ); + } + } + compiler.resolverFactory.hooks.resolveOptions .for("normal") - .tap("WebpackOptionsApply", resolveOptions => { + .tap("WebpackOptionsApply", (resolveOptions, category) => { return { fileSystem: compiler.inputFileSystem, - ...cachedCleverMerge(options.resolve, resolveOptions) + ...cachedCleverMerge( + resolveOptionsByCategory.get(category) || baseResolveOptions, + resolveOptions + ) }; }); compiler.resolverFactory.hooks.resolveOptions .for("context") - .tap("WebpackOptionsApply", resolveOptions => { + .tap("WebpackOptionsApply", (resolveOptions, category) => { return { fileSystem: compiler.inputFileSystem, resolveToContext: true, - ...cachedCleverMerge(options.resolve, resolveOptions) + ...cachedCleverMerge( + resolveOptionsByCategory.get(category) || baseResolveOptions, + resolveOptions + ) }; }); compiler.resolverFactory.hooks.resolveOptions .for("loader") - .tap("WebpackOptionsApply", resolveOptions => { + .tap("WebpackOptionsApply", (resolveOptions, category) => { return { fileSystem: compiler.inputFileSystem, - ...cachedCleverMerge(options.resolveLoader, resolveOptions) + ...cachedCleverMerge( + resolveOptionsByCategory.get(category) || baseResolveOptions, + resolveOptions + ) }; }); compiler.hooks.afterResolvers.call(compiler); diff --git a/lib/cache/ResolverCachePlugin.js b/lib/cache/ResolverCachePlugin.js index 717be14f2..1320de743 100644 --- a/lib/cache/ResolverCachePlugin.js +++ b/lib/cache/ResolverCachePlugin.js @@ -201,9 +201,10 @@ class ResolverCachePlugin { * @param {Resolver} resolver the resolver * @param {Object} options resolve options * @param {Object} userOptions resolve options passed by the user + * @param {string} category category * @returns {void} */ - (resolver, options, userOptions) => { + (resolver, options, userOptions, category) => { if (options.cache !== true) return; const optionsIdent = objectToString(userOptions, false); const cacheWithContext = @@ -219,7 +220,7 @@ class ResolverCachePlugin { if (request._ResolverCachePluginCacheMiss || !fileSystemInfo) { return callback(); } - const identifier = `/resolve/${type}${optionsIdent}${objectToString( + const identifier = `/resolve/${type}${category}${optionsIdent}${objectToString( request, !cacheWithContext )}`; diff --git a/lib/config/defaults.js b/lib/config/defaults.js index c3914249b..d55344c8a 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -608,10 +608,17 @@ const applyResolveDefaults = ( return conditions; } - conditions.push("node"); - - if (target === "electron-main" || target === "electron-preload") { - conditions.push("electron"); + switch (target) { + case "node": + case "async-node": + case "node-webkit": + conditions.push("node"); + break; + case "electron-main": + case "electron-preload": + conditions.push("node"); + conditions.push("electron"); + break; } return conditions; @@ -628,6 +635,14 @@ const applyResolveDefaults = ( F(resolve, "mainFields", () => webTarget ? ["browser", "module", "main"] : ["module", "main"] ); + F(resolve, "byDependency", () => ({ + commonjs: { + conditionNames: ["require", "..."] + }, + esm: { + conditionNames: ["import", "..."] + } + })); }; /** diff --git a/lib/dependencies/RequireEnsureItemDependency.js b/lib/dependencies/RequireEnsureItemDependency.js index d7a6b20c2..70d2df1f0 100644 --- a/lib/dependencies/RequireEnsureItemDependency.js +++ b/lib/dependencies/RequireEnsureItemDependency.js @@ -17,6 +17,10 @@ class RequireEnsureItemDependency extends ModuleDependency { get type() { return "require.ensure item"; } + + get category() { + return "commonjs"; + } } makeSerializable( diff --git a/schemas/WebpackOptions.json b/schemas/WebpackOptions.json index 9d3f420bf..662c4eba7 100644 --- a/schemas/WebpackOptions.json +++ b/schemas/WebpackOptions.json @@ -2115,6 +2115,18 @@ ] } }, + "byDependency": { + "description": "Extra resolve options per dependency category. Typical categories are \"commonjs\", \"amd\", \"esm\".", + "type": "object", + "additionalProperties": { + "description": "Options object for resolving requests.", + "oneOf": [ + { + "$ref": "#/definitions/ResolveOptions" + } + ] + } + }, "cache": { "description": "Enable caching of successfully resolved requests (cache entries are revalidated).", "type": "boolean" diff --git a/types.d.ts b/types.d.ts index 76b4fffca..f8f7d1ded 100644 --- a/types.d.ts +++ b/types.d.ts @@ -4361,7 +4361,7 @@ declare abstract class NormalModuleFactory extends ModuleFactory { createParser(type?: any, parserOptions?: {}): any; getGenerator(type?: any, generatorOptions?: {}): Generator; createGenerator(type?: any, generatorOptions?: {}): any; - getResolver(type?: any, resolveOptions?: any): any; + getResolver(type?: any, resolveOptions?: any, category?: any): any; } declare class NormalModuleReplacementPlugin { /** @@ -5646,6 +5646,11 @@ declare interface ResolveOptions { */ aliasFields?: LibraryExport[]; + /** + * Extra resolve options per dependency category. Typical categories are "commonjs", "amd", "esm". + */ + byDependency?: { [index: string]: ResolveOptions }; + /** * Enable caching of successfully resolved requests (cache entries are revalidated). */ @@ -5762,11 +5767,15 @@ declare interface ResolverCache { } declare abstract class ResolverFactory { hooks: Readonly<{ - resolveOptions: HookMap>; - resolver: HookMap>; + resolveOptions: HookMap>; + resolver: HookMap>; }>; cache: Map; - get(type: string, resolveOptions?: any): Resolver & WithOptions; + get( + type: string, + resolveOptions?: any, + category?: string + ): Resolver & WithOptions; } declare interface RuleSet { /**