mirror of https://github.com/webpack/webpack.git
				
				
				
			
		
			
				
	
	
		
			482 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			482 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
/*
 | 
						|
	MIT License http://www.opensource.org/licenses/mit-license.php
 | 
						|
	Author Tobias Koppers @sokra
 | 
						|
*/
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
const asyncLib = require("neo-async");
 | 
						|
const { AsyncSeriesWaterfallHook, SyncWaterfallHook } = require("tapable");
 | 
						|
const ContextModule = require("./ContextModule");
 | 
						|
const ModuleFactory = require("./ModuleFactory");
 | 
						|
const ContextElementDependency = require("./dependencies/ContextElementDependency");
 | 
						|
const LazySet = require("./util/LazySet");
 | 
						|
const { cachedSetProperty } = require("./util/cleverMerge");
 | 
						|
const { createFakeHook } = require("./util/deprecation");
 | 
						|
const { join } = require("./util/fs");
 | 
						|
 | 
						|
/** @typedef {import("./ContextModule").ContextModuleOptions} ContextModuleOptions */
 | 
						|
/** @typedef {import("./ContextModule").ResolveDependenciesCallback} ResolveDependenciesCallback */
 | 
						|
/** @typedef {import("./Module")} Module */
 | 
						|
/** @typedef {import("./ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */
 | 
						|
/** @typedef {import("./ModuleFactory").ModuleFactoryResult} ModuleFactoryResult */
 | 
						|
/** @typedef {import("./ResolverFactory")} ResolverFactory */
 | 
						|
/** @typedef {import("./dependencies/ContextDependency")} ContextDependency */
 | 
						|
/** @typedef {import("enhanced-resolve").ResolveRequest} ResolveRequest */
 | 
						|
/**
 | 
						|
 * @template T
 | 
						|
 * @typedef {import("./util/deprecation").FakeHook<T>} FakeHook<T>
 | 
						|
 */
 | 
						|
/** @typedef {import("./util/fs").IStats} IStats */
 | 
						|
/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
 | 
						|
/** @typedef {{ context: string, request: string }} ContextAlternativeRequest */
 | 
						|
 | 
						|
const EMPTY_RESOLVE_OPTIONS = {};
 | 
						|
 | 
						|
module.exports = class ContextModuleFactory extends ModuleFactory {
 | 
						|
	/**
 | 
						|
	 * @param {ResolverFactory} resolverFactory resolverFactory
 | 
						|
	 */
 | 
						|
	constructor(resolverFactory) {
 | 
						|
		super();
 | 
						|
		/** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[], ContextModuleOptions]>} */
 | 
						|
		const alternativeRequests = new AsyncSeriesWaterfallHook([
 | 
						|
			"modules",
 | 
						|
			"options"
 | 
						|
		]);
 | 
						|
		this.hooks = Object.freeze({
 | 
						|
			/** @type {AsyncSeriesWaterfallHook<[TODO]>} */
 | 
						|
			beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
 | 
						|
			/** @type {AsyncSeriesWaterfallHook<[TODO]>} */
 | 
						|
			afterResolve: new AsyncSeriesWaterfallHook(["data"]),
 | 
						|
			/** @type {SyncWaterfallHook<[string[]]>} */
 | 
						|
			contextModuleFiles: new SyncWaterfallHook(["files"]),
 | 
						|
			/** @type {FakeHook<Pick<AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>, "tap" | "tapAsync" | "tapPromise" | "name">>} */
 | 
						|
			alternatives: createFakeHook(
 | 
						|
				{
 | 
						|
					name: "alternatives",
 | 
						|
					/** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["intercept"]} */
 | 
						|
					intercept: interceptor => {
 | 
						|
						throw new Error(
 | 
						|
							"Intercepting fake hook ContextModuleFactory.hooks.alternatives is not possible, use ContextModuleFactory.hooks.alternativeRequests instead"
 | 
						|
						);
 | 
						|
					},
 | 
						|
					/** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tap"]} */
 | 
						|
					tap: (options, fn) => {
 | 
						|
						alternativeRequests.tap(options, fn);
 | 
						|
					},
 | 
						|
					/** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tapAsync"]} */
 | 
						|
					tapAsync: (options, fn) => {
 | 
						|
						alternativeRequests.tapAsync(options, (items, _options, callback) =>
 | 
						|
							fn(items, callback)
 | 
						|
						);
 | 
						|
					},
 | 
						|
					/** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tapPromise"]} */
 | 
						|
					tapPromise: (options, fn) => {
 | 
						|
						alternativeRequests.tapPromise(options, fn);
 | 
						|
					}
 | 
						|
				},
 | 
						|
				"ContextModuleFactory.hooks.alternatives has deprecated in favor of ContextModuleFactory.hooks.alternativeRequests with an additional options argument.",
 | 
						|
				"DEP_WEBPACK_CONTEXT_MODULE_FACTORY_ALTERNATIVES"
 | 
						|
			),
 | 
						|
			alternativeRequests
 | 
						|
		});
 | 
						|
		this.resolverFactory = resolverFactory;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {ModuleFactoryCreateData} data data object
 | 
						|
	 * @param {function((Error | null)=, ModuleFactoryResult=): void} callback callback
 | 
						|
	 * @returns {void}
 | 
						|
	 */
 | 
						|
	create(data, callback) {
 | 
						|
		const context = data.context;
 | 
						|
		const dependencies = data.dependencies;
 | 
						|
		const resolveOptions = data.resolveOptions;
 | 
						|
		const dependency = /** @type {ContextDependency} */ (dependencies[0]);
 | 
						|
		const fileDependencies = new LazySet();
 | 
						|
		const missingDependencies = new LazySet();
 | 
						|
		const contextDependencies = new LazySet();
 | 
						|
		this.hooks.beforeResolve.callAsync(
 | 
						|
			{
 | 
						|
				context,
 | 
						|
				dependencies,
 | 
						|
				layer: data.contextInfo.issuerLayer,
 | 
						|
				resolveOptions,
 | 
						|
				fileDependencies,
 | 
						|
				missingDependencies,
 | 
						|
				contextDependencies,
 | 
						|
				...dependency.options
 | 
						|
			},
 | 
						|
			(err, beforeResolveResult) => {
 | 
						|
				if (err) {
 | 
						|
					return callback(err, {
 | 
						|
						fileDependencies,
 | 
						|
						missingDependencies,
 | 
						|
						contextDependencies
 | 
						|
					});
 | 
						|
				}
 | 
						|
 | 
						|
				// Ignored
 | 
						|
				if (!beforeResolveResult) {
 | 
						|
					return callback(null, {
 | 
						|
						fileDependencies,
 | 
						|
						missingDependencies,
 | 
						|
						contextDependencies
 | 
						|
					});
 | 
						|
				}
 | 
						|
 | 
						|
				const context = beforeResolveResult.context;
 | 
						|
				const request = beforeResolveResult.request;
 | 
						|
				const resolveOptions = beforeResolveResult.resolveOptions;
 | 
						|
 | 
						|
				let loaders;
 | 
						|
				let resource;
 | 
						|
				let loadersPrefix = "";
 | 
						|
				const idx = request.lastIndexOf("!");
 | 
						|
				if (idx >= 0) {
 | 
						|
					let loadersRequest = request.slice(0, idx + 1);
 | 
						|
					let i;
 | 
						|
					for (
 | 
						|
						i = 0;
 | 
						|
						i < loadersRequest.length && loadersRequest[i] === "!";
 | 
						|
						i++
 | 
						|
					) {
 | 
						|
						loadersPrefix += "!";
 | 
						|
					}
 | 
						|
					loadersRequest = loadersRequest
 | 
						|
						.slice(i)
 | 
						|
						.replace(/!+$/, "")
 | 
						|
						.replace(/!!+/g, "!");
 | 
						|
					loaders = loadersRequest === "" ? [] : loadersRequest.split("!");
 | 
						|
					resource = request.slice(idx + 1);
 | 
						|
				} else {
 | 
						|
					loaders = [];
 | 
						|
					resource = request;
 | 
						|
				}
 | 
						|
 | 
						|
				const contextResolver = this.resolverFactory.get(
 | 
						|
					"context",
 | 
						|
					dependencies.length > 0
 | 
						|
						? cachedSetProperty(
 | 
						|
								resolveOptions || EMPTY_RESOLVE_OPTIONS,
 | 
						|
								"dependencyType",
 | 
						|
								dependencies[0].category
 | 
						|
							)
 | 
						|
						: resolveOptions
 | 
						|
				);
 | 
						|
				const loaderResolver = this.resolverFactory.get("loader");
 | 
						|
 | 
						|
				asyncLib.parallel(
 | 
						|
					[
 | 
						|
						callback => {
 | 
						|
							const results = /** @type ResolveRequest[] */ ([]);
 | 
						|
							/**
 | 
						|
							 * @param {ResolveRequest} obj obj
 | 
						|
							 * @returns {void}
 | 
						|
							 */
 | 
						|
							const yield_ = obj => {
 | 
						|
								results.push(obj);
 | 
						|
							};
 | 
						|
 | 
						|
							contextResolver.resolve(
 | 
						|
								{},
 | 
						|
								context,
 | 
						|
								resource,
 | 
						|
								{
 | 
						|
									fileDependencies,
 | 
						|
									missingDependencies,
 | 
						|
									contextDependencies,
 | 
						|
									yield: yield_
 | 
						|
								},
 | 
						|
								err => {
 | 
						|
									if (err) return callback(err);
 | 
						|
									callback(null, results);
 | 
						|
								}
 | 
						|
							);
 | 
						|
						},
 | 
						|
						callback => {
 | 
						|
							asyncLib.map(
 | 
						|
								loaders,
 | 
						|
								(loader, callback) => {
 | 
						|
									loaderResolver.resolve(
 | 
						|
										{},
 | 
						|
										context,
 | 
						|
										loader,
 | 
						|
										{
 | 
						|
											fileDependencies,
 | 
						|
											missingDependencies,
 | 
						|
											contextDependencies
 | 
						|
										},
 | 
						|
										(err, result) => {
 | 
						|
											if (err) return callback(err);
 | 
						|
											callback(null, /** @type {string} */ (result));
 | 
						|
										}
 | 
						|
									);
 | 
						|
								},
 | 
						|
								callback
 | 
						|
							);
 | 
						|
						}
 | 
						|
					],
 | 
						|
					(err, result) => {
 | 
						|
						if (err) {
 | 
						|
							return callback(err, {
 | 
						|
								fileDependencies,
 | 
						|
								missingDependencies,
 | 
						|
								contextDependencies
 | 
						|
							});
 | 
						|
						}
 | 
						|
						let [contextResult, loaderResult] =
 | 
						|
							/** @type {[ResolveRequest[], string[]]} */ (result);
 | 
						|
						if (contextResult.length > 1) {
 | 
						|
							const first = contextResult[0];
 | 
						|
							contextResult = contextResult.filter(r => r.path);
 | 
						|
							if (contextResult.length === 0) contextResult.push(first);
 | 
						|
						}
 | 
						|
						this.hooks.afterResolve.callAsync(
 | 
						|
							{
 | 
						|
								addon:
 | 
						|
									loadersPrefix +
 | 
						|
									loaderResult.join("!") +
 | 
						|
									(loaderResult.length > 0 ? "!" : ""),
 | 
						|
								resource:
 | 
						|
									contextResult.length > 1
 | 
						|
										? contextResult.map(r => r.path)
 | 
						|
										: contextResult[0].path,
 | 
						|
								resolveDependencies: this.resolveDependencies.bind(this),
 | 
						|
								resourceQuery: contextResult[0].query,
 | 
						|
								resourceFragment: contextResult[0].fragment,
 | 
						|
								...beforeResolveResult
 | 
						|
							},
 | 
						|
							(err, result) => {
 | 
						|
								if (err) {
 | 
						|
									return callback(err, {
 | 
						|
										fileDependencies,
 | 
						|
										missingDependencies,
 | 
						|
										contextDependencies
 | 
						|
									});
 | 
						|
								}
 | 
						|
 | 
						|
								// Ignored
 | 
						|
								if (!result) {
 | 
						|
									return callback(null, {
 | 
						|
										fileDependencies,
 | 
						|
										missingDependencies,
 | 
						|
										contextDependencies
 | 
						|
									});
 | 
						|
								}
 | 
						|
 | 
						|
								return callback(null, {
 | 
						|
									module: new ContextModule(result.resolveDependencies, result),
 | 
						|
									fileDependencies,
 | 
						|
									missingDependencies,
 | 
						|
									contextDependencies
 | 
						|
								});
 | 
						|
							}
 | 
						|
						);
 | 
						|
					}
 | 
						|
				);
 | 
						|
			}
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {InputFileSystem} fs file system
 | 
						|
	 * @param {ContextModuleOptions} options options
 | 
						|
	 * @param {ResolveDependenciesCallback} callback callback function
 | 
						|
	 * @returns {void}
 | 
						|
	 */
 | 
						|
	resolveDependencies(fs, options, callback) {
 | 
						|
		const cmf = this;
 | 
						|
		const {
 | 
						|
			resource,
 | 
						|
			resourceQuery,
 | 
						|
			resourceFragment,
 | 
						|
			recursive,
 | 
						|
			regExp,
 | 
						|
			include,
 | 
						|
			exclude,
 | 
						|
			referencedExports,
 | 
						|
			category,
 | 
						|
			typePrefix,
 | 
						|
			attributes
 | 
						|
		} = options;
 | 
						|
		if (!regExp || !resource) return callback(null, []);
 | 
						|
 | 
						|
		/**
 | 
						|
		 * @param {string} ctx context
 | 
						|
		 * @param {string} directory directory
 | 
						|
		 * @param {Set<string>} visited visited
 | 
						|
		 * @param {ResolveDependenciesCallback} callback callback
 | 
						|
		 */
 | 
						|
		const addDirectoryChecked = (ctx, directory, visited, callback) => {
 | 
						|
			/** @type {NonNullable<InputFileSystem["realpath"]>} */
 | 
						|
			(fs.realpath)(directory, (err, _realPath) => {
 | 
						|
				if (err) return callback(err);
 | 
						|
				const realPath = /** @type {string} */ (_realPath);
 | 
						|
				if (visited.has(realPath)) return callback(null, []);
 | 
						|
				/** @type {Set<string> | undefined} */
 | 
						|
				let recursionStack;
 | 
						|
				addDirectory(
 | 
						|
					ctx,
 | 
						|
					directory,
 | 
						|
					(_, dir, callback) => {
 | 
						|
						if (recursionStack === undefined) {
 | 
						|
							recursionStack = new Set(visited);
 | 
						|
							recursionStack.add(realPath);
 | 
						|
						}
 | 
						|
						addDirectoryChecked(ctx, dir, recursionStack, callback);
 | 
						|
					},
 | 
						|
					callback
 | 
						|
				);
 | 
						|
			});
 | 
						|
		};
 | 
						|
 | 
						|
		/**
 | 
						|
		 * @param {string} ctx context
 | 
						|
		 * @param {string} directory directory
 | 
						|
		 * @param {function(string, string, function(): void): void} addSubDirectory addSubDirectoryFn
 | 
						|
		 * @param {ResolveDependenciesCallback} callback callback
 | 
						|
		 */
 | 
						|
		const addDirectory = (ctx, directory, addSubDirectory, callback) => {
 | 
						|
			fs.readdir(directory, (err, files) => {
 | 
						|
				if (err) return callback(err);
 | 
						|
				const processedFiles = cmf.hooks.contextModuleFiles.call(
 | 
						|
					/** @type {string[]} */ (files).map(file => file.normalize("NFC"))
 | 
						|
				);
 | 
						|
				if (!processedFiles || processedFiles.length === 0)
 | 
						|
					return callback(null, []);
 | 
						|
				asyncLib.map(
 | 
						|
					processedFiles.filter(p => p.indexOf(".") !== 0),
 | 
						|
					(segment, callback) => {
 | 
						|
						const subResource = join(fs, directory, segment);
 | 
						|
 | 
						|
						if (!exclude || !subResource.match(exclude)) {
 | 
						|
							fs.stat(subResource, (err, _stat) => {
 | 
						|
								if (err) {
 | 
						|
									if (err.code === "ENOENT") {
 | 
						|
										// ENOENT is ok here because the file may have been deleted between
 | 
						|
										// the readdir and stat calls.
 | 
						|
										return callback();
 | 
						|
									}
 | 
						|
									return callback(err);
 | 
						|
								}
 | 
						|
 | 
						|
								const stat = /** @type {IStats} */ (_stat);
 | 
						|
 | 
						|
								if (stat.isDirectory()) {
 | 
						|
									if (!recursive) return callback();
 | 
						|
									addSubDirectory(ctx, subResource, callback);
 | 
						|
								} else if (
 | 
						|
									stat.isFile() &&
 | 
						|
									(!include || subResource.match(include))
 | 
						|
								) {
 | 
						|
									/** @type {{ context: string, request: string }} */
 | 
						|
									const obj = {
 | 
						|
										context: ctx,
 | 
						|
										request: `.${subResource.slice(ctx.length).replace(/\\/g, "/")}`
 | 
						|
									};
 | 
						|
 | 
						|
									this.hooks.alternativeRequests.callAsync(
 | 
						|
										[obj],
 | 
						|
										options,
 | 
						|
										(err, alternatives) => {
 | 
						|
											if (err) return callback(err);
 | 
						|
											callback(
 | 
						|
												null,
 | 
						|
												/** @type {ContextAlternativeRequest[]} */
 | 
						|
												(alternatives)
 | 
						|
													.filter(obj =>
 | 
						|
														regExp.test(/** @type {string} */ (obj.request))
 | 
						|
													)
 | 
						|
													.map(obj => {
 | 
						|
														const dep = new ContextElementDependency(
 | 
						|
															`${obj.request}${resourceQuery}${resourceFragment}`,
 | 
						|
															obj.request,
 | 
						|
															typePrefix,
 | 
						|
															/** @type {string} */
 | 
						|
															(category),
 | 
						|
															referencedExports,
 | 
						|
															/** @type {TODO} */
 | 
						|
															(obj.context),
 | 
						|
															attributes
 | 
						|
														);
 | 
						|
														dep.optional = true;
 | 
						|
														return dep;
 | 
						|
													})
 | 
						|
											);
 | 
						|
										}
 | 
						|
									);
 | 
						|
								} else {
 | 
						|
									callback();
 | 
						|
								}
 | 
						|
							});
 | 
						|
						} else {
 | 
						|
							callback();
 | 
						|
						}
 | 
						|
					},
 | 
						|
					(err, result) => {
 | 
						|
						if (err) return callback(err);
 | 
						|
 | 
						|
						if (!result) return callback(null, []);
 | 
						|
 | 
						|
						const flattenedResult = [];
 | 
						|
 | 
						|
						for (const item of result) {
 | 
						|
							if (item) flattenedResult.push(...item);
 | 
						|
						}
 | 
						|
 | 
						|
						callback(null, flattenedResult);
 | 
						|
					}
 | 
						|
				);
 | 
						|
			});
 | 
						|
		};
 | 
						|
 | 
						|
		/**
 | 
						|
		 * @param {string} ctx context
 | 
						|
		 * @param {string} dir dir
 | 
						|
		 * @param {ResolveDependenciesCallback} callback callback
 | 
						|
		 * @returns {void}
 | 
						|
		 */
 | 
						|
		const addSubDirectory = (ctx, dir, callback) =>
 | 
						|
			addDirectory(ctx, dir, addSubDirectory, callback);
 | 
						|
 | 
						|
		/**
 | 
						|
		 * @param {string} resource resource
 | 
						|
		 * @param {ResolveDependenciesCallback} callback callback
 | 
						|
		 */
 | 
						|
		const visitResource = (resource, callback) => {
 | 
						|
			if (typeof fs.realpath === "function") {
 | 
						|
				addDirectoryChecked(resource, resource, new Set(), callback);
 | 
						|
			} else {
 | 
						|
				addDirectory(resource, resource, addSubDirectory, callback);
 | 
						|
			}
 | 
						|
		};
 | 
						|
 | 
						|
		if (typeof resource === "string") {
 | 
						|
			visitResource(resource, callback);
 | 
						|
		} else {
 | 
						|
			asyncLib.map(resource, visitResource, (err, _result) => {
 | 
						|
				if (err) return callback(err);
 | 
						|
				const result = /** @type {ContextElementDependency[][]} */ (_result);
 | 
						|
 | 
						|
				// result dependencies should have unique userRequest
 | 
						|
				// ordered by resolve result
 | 
						|
				/** @type {Set<string>} */
 | 
						|
				const temp = new Set();
 | 
						|
				/** @type {ContextElementDependency[]} */
 | 
						|
				const res = [];
 | 
						|
				for (let i = 0; i < result.length; i++) {
 | 
						|
					const inner = result[i];
 | 
						|
					for (const el of inner) {
 | 
						|
						if (temp.has(el.userRequest)) continue;
 | 
						|
						res.push(el);
 | 
						|
						temp.add(el.userRequest);
 | 
						|
					}
 | 
						|
				}
 | 
						|
				callback(null, res);
 | 
						|
			});
 | 
						|
		}
 | 
						|
	}
 | 
						|
};
 |