webpack 格式化模块工厂 第 一 节

这段代码是 Webpack 用于处理 module.rules 规则、loader 解析、模块创建配置合并的核心工具集合,是构建模块解析与加载链路的基础设施。

核心类型定义(Typedefs)

  • ModuleSettings:提取了模块规则中会影响模块构建的一些设置项(如 type、parser、resolve 等)。

  • CreateData:模块创建时传入的数据结构,包括构建配置等。

  • ResolveData:描述模块解析过程所需的上下文、依赖、请求、配置等完整数据。

  • ResourceData / ResourceDataWithData:封装资源路径及其附加信息(如 query、fragment)。

  • ParsedLoaderRequest:描述 loader 请求字符串解析后的结构。

工具函数

  • loaderToIdent:将 loader + options 转换为唯一标识字符串(便于缓存等用途)。

  • stringifyLoadersAndResource:将 loader 链与资源路径拼接为完整的 Webpack 请求格式。

  • needCalls:返回一个函数,调用指定次数后才执行回调,用于等待多个异步结果。

  • mergeGlobalOptions:按层级合并全局配置与局部配置(支持按 type 路径向上查找)。

  • deprecationChangedHookMessage:提示 hook 类型变更(瀑布钩子改为跳板钩子)的警告信息。

js 复制代码
/**
 * @typedef {Pick<RuleSetRule, 'type' | 'sideEffects' | 'parser' | 'generator' | 'resolve' | 'layer'>} ModuleSettings
 * 表示模块配置中与构建相关的选项子集,比如模块类型、是否有副作用、解析器、生成器、自定义解析选项等
 */

/**
 * @typedef {Partial<NormalModuleCreateData & { settings: ModuleSettings }>} CreateData
 * 用于模块工厂创建模块时传入的数据结构,是 NormalModuleCreateData 的可选子集,并包含可选的 settings 字段
 */

/**
 * @typedef {object} ResolveData
 * 模块解析过程中使用的完整数据结构
 * @property {ModuleFactoryCreateData["contextInfo"]} contextInfo 上下文信息
 * @property {ModuleFactoryCreateData["resolveOptions"]} resolveOptions 解析选项
 * @property {string} context 当前模块所在目录
 * @property {string} request 请求字符串
 * @property {Record<string, any> | undefined} assertions import 断言对象
 * @property {ModuleDependency[]} dependencies 模块依赖
 * @property {string} dependencyType 依赖类型
 * @property {CreateData} createData 创建模块时附加的数据
 * @property {LazySet<string>} fileDependencies 文件依赖
 * @property {LazySet<string>} missingDependencies 缺失的依赖
 * @property {LazySet<string>} contextDependencies 上下文依赖
 * @property {Module=} ignoredModule 被忽略的模块(可选)
 * @property {boolean} cacheable 是否允许使用缓存
 */

/**
 * @typedef {object} ResourceData
 * 表示单个资源路径的信息(不含 loader)
 * @property {string} resource 资源路径
 * @property {string=} path 文件路径部分
 * @property {string=} query 查询字符串部分
 * @property {string=} fragment 片段部分
 * @property {string=} context 资源上下文
 */

/** @typedef {ResourceData & { data: Record<string, any> }} ResourceDataWithData
 * 在 ResourceData 的基础上加上额外数据字段,用于内部传递分析结果等
 */

/**
 * @typedef {object} ParsedLoaderRequest
 * 表示对单个 loader 请求字符串解析后的结果
 * @property {string} loader loader 名称
 * @property {string|undefined} options loader 参数(可能是字符串)
 */

/**
 * @template T
 * @callback Callback
 * 标准回调函数定义
 * @param {(Error | null)=} err 错误信息
 * @param {T=} stats 结果数据
 * @returns {void}
 */

// 空配置常量
const EMPTY_RESOLVE_OPTIONS = {};      // 空解析配置
const EMPTY_PARSER_OPTIONS = {};       // 空 parser 配置
const EMPTY_GENERATOR_OPTIONS = {};    // 空生成器配置
const EMPTY_ELEMENTS = [];             // 空 loader 数组

// 正则表达式常量
const MATCH_RESOURCE_REGEX = /^([^!]+)!=!/;  // 匹配形如 "resource!=!" 的格式
const LEADING_DOT_EXTENSION_REGEX = /^[^.]/; // 匹配不是以 "." 开头的扩展名(如文件名无扩展)

/**
 * 将 loader 对象转换为唯一的字符串标识符(用于缓存等)
 * @param {LoaderItem} data loader 信息
 * @returns {string} 标识字符串
 */
const loaderToIdent = data => {
	if (!data.options) {
		return data.loader;
	}
	if (typeof data.options === "string") {
		return `${data.loader}?${data.options}`;
	}
	if (typeof data.options !== "object") {
		throw new Error("loader options must be string or object");
	}
	if (data.ident) {
		return `${data.loader}??${data.ident}`;
	}
	return `${data.loader}?${JSON.stringify(data.options)}`;
};

/**
 * 将 loader 数组与资源路径拼接成完整的 Webpack 请求格式(如 a-loader?x!b-loader!src/index.js)
 * @param {LoaderItem[]} loaders 所有 loader
 * @param {string} resource 资源路径
 * @returns {string} 拼接后的完整请求字符串
 */
const stringifyLoadersAndResource = (loaders, resource) => {
	let str = "";
	for (const loader of loaders) {
		str += `${loaderToIdent(loader)}!`;
	}
	return str + resource;
};

/**
 * 返回一个函数,需被调用 `times` 次后才执行 callback,用于等待多个异步完成后统一处理
 * @param {number} times 剩余调用次数
 * @param {(err?: null | Error) => void} callback 完成回调
 * @returns {(err?: null | Error) => void}
 */
const needCalls = (times, callback) => err => {
	if (--times === 0) {
		return callback(err);
	}
	if (err && times > 0) {
		times = Number.NaN; // 使函数失效,防止多次回调
		return callback(err);
	}
};

/**
 * 合并全局和局部配置,允许根据 type 字符串逐层查找全局配置并合并到局部配置
 * @template T
 * @template O
 * @param {T} globalOptions 全局配置(嵌套结构)
 * @param {string} type 类型路径,例如 "javascript/auto"
 * @param {O} localOptions 局部配置
 * @returns {T & O | T | O} 合并后的配置对象
 */
const mergeGlobalOptions = (globalOptions, type, localOptions) => {
	const parts = type.split("/");
	let result;
	let current = "";
	for (const part of parts) {
		current = current ? `${current}/${part}` : part;
		const options = globalOptions[current];
		if (typeof options === "object") {
			result = result === undefined ? options : cachedCleverMerge(result, options);
		}
	}
	if (result === undefined) {
		return localOptions;
	}
	return cachedCleverMerge(result, localOptions);
};

// TODO webpack 6 remove
/**
 * 输出警告信息,提示某个 hook 已从"瀑布钩子"变成"跳板钩子"
 * @param {string} name hook 名称
 * @param {TODO} hook 具体 hook 实例
 * @returns {string} 提示信息
 */
const deprecationChangedHookMessage = (name, hook) => {
	const names = hook.taps.map(tapped => tapped.name).join(", ");
	return (
		`NormalModuleFactory.${name} (${names}) is no longer a waterfall hook, but a bailing hook instead. ` +
		"Do not return the passed object, but modify it instead. " +
		"Returning false will ignore the request and results in no module created."
	);
};

/**
 * 创建 RuleSetCompiler 实例,并注册所有匹配规则插件与效果插件
 * 这些插件会用于处理 webpack 配置中的 module.rules
 */
const ruleSetCompiler = new RuleSetCompiler([
	// 条件匹配器
	new BasicMatcherRulePlugin("test", "resource"),
	new BasicMatcherRulePlugin("scheme"),
	new BasicMatcherRulePlugin("mimetype"),
	new BasicMatcherRulePlugin("dependency"),
	new BasicMatcherRulePlugin("include", "resource"),
	new BasicMatcherRulePlugin("exclude", "resource", true),
	new BasicMatcherRulePlugin("resource"),
	new BasicMatcherRulePlugin("resourceQuery"),
	new BasicMatcherRulePlugin("resourceFragment"),
	new BasicMatcherRulePlugin("realResource"),
	new BasicMatcherRulePlugin("issuer"),
	new BasicMatcherRulePlugin("compiler"),
	new BasicMatcherRulePlugin("issuerLayer"),

	// assert 匹配器:兼容旧式 `_isLegacyAssert` 和现代断言语法
	new ObjectMatcherRulePlugin("assert", "assertions", value => {
		if (value) {
			return value._isLegacyAssert !== undefined;
		}
		return false;
	}),
	new ObjectMatcherRulePlugin("with", "assertions", value => {
		if (value) {
			return !(value._isLegacyAssert);
		}
		return false;
	}),
	new ObjectMatcherRulePlugin("descriptionData"),

	// 效果插件:匹配后对模块应用的变更项
	new BasicEffectRulePlugin("type"),
	new BasicEffectRulePlugin("sideEffects"),
	new BasicEffectRulePlugin("parser"),
	new BasicEffectRulePlugin("resolve"),
	new BasicEffectRulePlugin("generator"),
	new BasicEffectRulePlugin("layer"),
	new UseEffectRulePlugin()
]);
相关推荐
夜寒花碎8 分钟前
前端自动化测试一jest基础使用
前端·单元测试·jest
小徐_233311 分钟前
uni-app工程实战:基于vue-i18n和i18n-ally的国际化方案
前端·微信小程序·uni-app
前端小菜鸟一枚s15 分钟前
`ConstantPositionProperty` 的使用与应用
前端·javascript·cesium
JohnsonXin15 分钟前
怎么使用vue3实现一个优雅的不定高虚拟列表
前端·javascript·css·html5
17Knight17 分钟前
我的个性化 VSCode
前端
前端小菜鸟一枚s21 分钟前
`ConstantProperty` 的使用与应用
前端·javascript·cesium
狠狠的学习27 分钟前
antd表格行hover效果性能处理
前端·css
在下小航31 分钟前
前端本地大模型 window.ai 最新教程
前端·人工智能
一颗奇趣蛋32 分钟前
vue哪些情况称作“销毁组件”
前端·vue.js
Ody32 分钟前
循环滚动列表浅析
前端