add ability to set resolve options per dependency category

- byDependency option
- create resolver per dependency category
This commit is contained in:
Ivan Kopeykin 2020-05-28 01:59:23 +03:00
parent 0dd9aaf25b
commit d4b1819749
9 changed files with 131 additions and 73 deletions

View File

@ -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).
*/

View File

@ -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
);
}
}

View File

@ -29,13 +29,19 @@ const EMPTY_RESOLVE_OPTIONS = {};
module.exports = class ResolverFactory {
constructor() {
this.hooks = Object.freeze({
/** @type {HookMap<SyncWaterfallHook<[Object]>>} */
/** @type {HookMap<SyncWaterfallHook<[Object, string]>>} */
resolveOptions: new HookMap(
() => new SyncWaterfallHook(["resolveOptions"])
() => new SyncWaterfallHook(["resolveOptions", "category"])
),
/** @type {HookMap<SyncHook<[Resolver, Object, Object]>>} */
/** @type {HookMap<SyncHook<[Resolver, Object, Object, string]>>} */
resolver: new HookMap(
() => new SyncHook(["resolver", "resolveOptions", "userResolveOptions"])
() =>
new SyncHook([
"resolver",
"resolveOptions",
"userResolveOptions",
"category"
])
)
});
/** @type {Map<string, ResolverCache>} */
@ -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;
}
};

View File

@ -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);

View File

@ -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
)}`;

View File

@ -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", "..."]
}
}));
};
/**

View File

@ -17,6 +17,10 @@ class RequireEnsureItemDependency extends ModuleDependency {
get type() {
return "require.ensure item";
}
get category() {
return "commonjs";
}
}
makeSerializable(

View File

@ -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"

17
types.d.ts vendored
View File

@ -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<SyncWaterfallHook<[any]>>;
resolver: HookMap<SyncHook<[Resolver, any, any], void>>;
resolveOptions: HookMap<SyncWaterfallHook<[any, string]>>;
resolver: HookMap<SyncHook<[Resolver, any, any, string], void>>;
}>;
cache: Map<string, ResolverCache>;
get(type: string, resolveOptions?: any): Resolver & WithOptions;
get(
type: string,
resolveOptions?: any,
category?: string
): Resolver & WithOptions;
}
declare interface RuleSet {
/**