/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v5.109.2
lib/ContextModuleFactory.js
656 строк
20 KB
Alexander Akait
feat: extend import.meta.glob with caseSensitive option and fix parity gaps (#21385)
11 июл 2026, 20:07
Не верифицирован
11 июл 2026, 20:07
2ae49ff
Код
Авторство
О чём код?
/* 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"); const { globPatternBaseReachesDir, globUserRequest, isNonExhaustiveImportMetaGlobSkippedDir, resolveContextModuleGlobPattern } = require("./util/globUtils"); /** @typedef {import("./util/globUtils").ResolvedContextModuleGlobPattern} ResolvedContextModuleGlobPattern */ /** @typedef {(context: string, subResource: string, callback: () => void, resolvedGlobPatterns?: ResolvedContextModuleGlobPattern[]) => void} AddSubDirectoryFn */ /** @typedef {import("enhanced-resolve").ResolveRequest} ResolveRequest */ /** @typedef {import("./Compilation").FileSystemDependencies} FileSystemDependencies */ /** @typedef {import("./ContextModule").ContextModuleOptions} ContextModuleOptions */ /** @typedef {import("./ContextModule").ResolveDependenciesCallback} ResolveDependenciesCallback */ /** @typedef {import("./ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */ /** @typedef {import("./ModuleFactory").ModuleFactoryCallback} ModuleFactoryCallback */ /** @typedef {import("./ResolverFactory")} ResolverFactory */ /** @typedef {import("./dependencies/ContextDependency")} ContextDependency */ /** @typedef {import("./dependencies/ContextDependency").ContextOptions} ContextOptions */ /** * Defines the shared type used by this module. * @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 */ /** * Defines the context resolve data type used by this module. * @typedef {object} ContextResolveData * @property {string} context * @property {string} request * @property {ModuleFactoryCreateData["resolveOptions"]} resolveOptions * @property {FileSystemDependencies} fileDependencies * @property {FileSystemDependencies} missingDependencies * @property {FileSystemDependencies} contextDependencies * @property {ContextDependency[]} dependencies */ /** @typedef {ContextResolveData & ContextOptions} BeforeContextResolveData */ /** @typedef {BeforeContextResolveData & { resource: string | string[], resourceQuery: string | undefined, resourceFragment: string | undefined, resolveDependencies: ContextModuleFactory["resolveDependencies"] }} AfterContextResolveData */ const EMPTY_RESOLVE_OPTIONS = {}; class ContextModuleFactory extends ModuleFactory { /** * Creates an instance of ContextModuleFactory. * @param {ResolverFactory} resolverFactory resolverFactory */ constructor(resolverFactory) { super(); /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[], ContextModuleOptions]>} */ const alternativeRequests = new AsyncSeriesWaterfallHook([ "modules", "options" ]); this.hooks = Object.freeze({ /** @type {AsyncSeriesWaterfallHook<[BeforeContextResolveData], BeforeContextResolveData | false | void>} */ beforeResolve: new AsyncSeriesWaterfallHook(["data"]), /** @type {AsyncSeriesWaterfallHook<[AfterContextResolveData], AfterContextResolveData | false | void>} */ 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 }); /** @type {ResolverFactory} */ this.resolverFactory = resolverFactory; } /** * Processes the provided data. * @param {ModuleFactoryCreateData} data data object * @param {ModuleFactoryCallback} callback callback * @returns {void} */ create(data, callback) { const context = data.context; const dependencies = /** @type {ContextDependency[]} */ (data.dependencies); const resolveOptions = data.resolveOptions; const dependency = dependencies[0]; /** @type {FileSystemDependencies} */ const fileDependencies = new LazySet(); /** @type {FileSystemDependencies} */ const missingDependencies = new LazySet(); /** @type {FileSystemDependencies} */ 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; /** @type {undefined | string[]} */ let loaders; /** @type {undefined | string} */ let resource; let loadersPrefix = ""; const idx = request.lastIndexOf("!"); if (idx >= 0) { let loadersRequest = request.slice(0, idx + 1); /** @type {number} */ let i; for ( i = 0; i < loadersRequest.length && loadersRequest[i] === "!"; i++ ) { loadersPrefix += "!"; } loadersRequest = loadersRequest .slice(i) .replace(/!+$/, "") .replace(/!{2,}/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[]} */ ([]); /** * Processes the provided obj. * @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, 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 ? /** @type {string[]} */ (contextResult.map((r) => r.path)) : /** @type {string} */ (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 }); } ); } ); } ); } /** * Resolves dependencies. * @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, patterns, requestContext, exhaustive, caseSensitive, include, exclude, referencedExports, category, typePrefix, attributes } = options; const isImportMetaGlob = Boolean(patterns && requestContext); if ((!regExp && !isImportMetaGlob) || !resource) return callback(null, []); /** * Adds directory checked. * @param {string} ctx context * @param {string} directory directory * @param {Set<string>} visited visited * @param {ResolveDependenciesCallback} callback callback * @param {ResolvedContextModuleGlobPattern[] | undefined} resolvedGlobPatterns resolved glob patterns */ const addDirectoryChecked = ( ctx, directory, visited, callback, resolvedGlobPatterns ) => { /** @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, resolvedGlobPatterns ); }, callback, resolvedGlobPatterns ); }); }; /** * Adds the provided ctx to the context module factory. * @param {string} ctx context * @param {string} directory directory * @param {AddSubDirectoryFn} addSubDirectory addSubDirectoryFn * @param {ResolveDependenciesCallback} callback callback * @param {ResolvedContextModuleGlobPattern[] | undefined} resolvedGlobPatterns resolved glob patterns * @returns {void} */ const addDirectory = ( ctx, directory, addSubDirectory, callback, resolvedGlobPatterns ) => { 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, []); } /** @type {ContextAlternativeRequest[]} */ const fileObjs = []; /** @type {ContextElementDependency[]} */ const globDeps = []; /** @type {Set<string>} */ const globUserRequests = new Set(); asyncLib.map( isImportMetaGlob ? processedFiles : processedFiles.filter((p) => p.indexOf(".") !== 0), (segment, callback) => { const subResource = join(fs, directory, segment); if (!exclude || !exclude.test(subResource)) { 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(); if ( isImportMetaGlob && !exhaustive && isNonExhaustiveImportMetaGlobSkippedDir(segment) && !( resolvedGlobPatterns && globPatternBaseReachesDir( resolvedGlobPatterns, subResource ) ) ) { return callback(); } addSubDirectory( ctx, subResource, callback, resolvedGlobPatterns ); } else if ( stat.isFile() && (!include || include.test(subResource)) ) { if ( isImportMetaGlob && patterns && requestContext && resolvedGlobPatterns ) { const relativePath = `.${subResource .slice(ctx.length) .replace(/\\/g, "/")}`; const exposedUserRequest = globUserRequest( resolvedGlobPatterns, subResource, exhaustive === true, caseSensitive !== false ); if ( exposedUserRequest && !globUserRequests.has(exposedUserRequest) ) { globUserRequests.add(exposedUserRequest); const dep = new ContextElementDependency( `${relativePath}${resourceQuery}${resourceFragment}`, exposedUserRequest, typePrefix, /** @type {string} */ (category), referencedExports, ctx, attributes ); dep.optional = true; globDeps.push(dep); } return callback(); } // Collect for a single batched alternativeRequests call // per directory below. Calling the hook once per file // would pay per-call overhead (closure, resolverFactory // lookup, array allocations) for every file in the // context — which is the bulk of work on rebuilds. fileObjs.push({ context: ctx, request: `.${subResource.slice(ctx.length).replace(/\\/g, "/")}` }); callback(); } else { callback(); } }); } else { callback(); } }, (err, result) => { if (err) return callback(err); /** @type {ContextElementDependency[]} */ const flattenedResult = []; if (result) { for (const item of result) { if (item) flattenedResult.push(...item); } } if (isImportMetaGlob) { /** @type {Set<string>} */ const mergedUserRequests = new Set(); /** @type {ContextElementDependency[]} */ const merged = []; for (const dep of [...flattenedResult, ...globDeps]) { if (mergedUserRequests.has(dep.userRequest)) continue; mergedUserRequests.add(dep.userRequest); merged.push(dep); } return callback(null, merged); } if (fileObjs.length === 0) { return callback(null, flattenedResult); } this.hooks.alternativeRequests.callAsync( fileObjs, options, (err, alternatives) => { if (err) return callback(err); for (const alt of /** @type {ContextAlternativeRequest[]} */ ( alternatives )) { if ( !(regExp instanceof RegExp) || !regExp.test(/** @type {string} */ (alt.request)) ) { continue; } const dep = new ContextElementDependency( `${alt.request}${resourceQuery}${resourceFragment}`, alt.request, typePrefix, /** @type {string} */ (category), referencedExports, alt.context, attributes ); dep.optional = true; flattenedResult.push(dep); } callback(null, flattenedResult); } ); } ); }); }; /** * Adds sub directory. * @param {string} ctx context * @param {string} dir dir * @param {ResolveDependenciesCallback} callback callback * @param {ResolvedContextModuleGlobPattern[] | undefined} resolvedGlobPatterns resolved glob patterns * @returns {void} */ const addSubDirectory = (ctx, dir, callback, resolvedGlobPatterns) => addDirectory(ctx, dir, addSubDirectory, callback, resolvedGlobPatterns); /** * Processes the provided resource. * @param {string} resource resource * @param {ResolveDependenciesCallback} callback callback */ const visitResource = (resource, callback) => { /** @type {ResolvedContextModuleGlobPattern[] | undefined} */ const resolvedGlobPatterns = isImportMetaGlob && patterns && requestContext ? patterns.map((pattern) => resolveContextModuleGlobPattern(pattern, requestContext, resource) ) : undefined; if (typeof fs.realpath === "function") { addDirectoryChecked( resource, resource, /** @type {Set<string>} */ new Set(), callback, resolvedGlobPatterns ); } else { addDirectory( resource, resource, addSubDirectory, callback, resolvedGlobPatterns ); } }; 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); }); } } } module.exports = ContextModuleFactory;