/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/rules/RuleSetCompiler.js
580 строк
17 KB
Alexander Akait
feat: default experiments.css/html/asyncWebAssembly to "auto" (#21365)
08 июл 2026, 23:22
Не верифицирован
08 июл 2026, 23:22
a3773f5
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { SyncHook } = require("tapable"); /** @typedef {import("enhanced-resolve").ResolveRequest} ResolveRequest */ /** @typedef {import("../../declarations/WebpackOptions").Falsy} Falsy */ /** @typedef {import("../../declarations/WebpackOptions").RuleSetUseItem} RuleSetUseItem */ /** @typedef {import("../../declarations/WebpackOptions").RuleSetLoaderOptions} RuleSetLoaderOptions */ /** @typedef {import("../../declarations/WebpackOptions").RuleSetRule} RuleSetRule */ /** @typedef {import("../../declarations/WebpackOptions").RuleSetConditionAbsolute} RuleSetConditionAbsolute */ /** @typedef {(Falsy | RuleSetRule)[]} RuleSetRules */ /** * Defines the rule condition function type used by this module. * @typedef {(value: EffectData[keyof EffectData]) => boolean} RuleConditionFunction */ /** * Defines the rule condition type used by this module. * @typedef {object} RuleCondition * @property {string | string[]} property * @property {boolean} matchWhenEmpty * @property {RuleConditionFunction} fn */ /** * Defines the condition type used by this module. * @typedef {object} Condition * @property {boolean} matchWhenEmpty * @property {RuleConditionFunction} fn */ /** * Defines the effect data type used by this module. * @typedef {object} EffectData * @property {string=} resource * @property {string=} realResource * @property {string=} resourceQuery * @property {string=} resourceFragment * @property {string=} scheme * @property {ImportAttributes=} attributes * @property {string=} mimetype * @property {string} dependency * @property {ResolveRequest["descriptionFileData"]=} descriptionData * @property {string=} compiler * @property {string} issuer * @property {string} issuerLayer * @property {string=} phase */ /** * Defines the compiled rule type used by this module. * @typedef {object} CompiledRule * @property {RuleCondition[]} conditions * @property {(Effect | ((effectData: EffectData) => Effect[]))[]} effects * @property {CompiledRule[]=} rules * @property {CompiledRule[]=} oneOf */ /** @typedef {"use" | "use-pre" | "use-post"} EffectUseType */ /** * Defines the effect use type used by this module. * @typedef {object} EffectUse * @property {EffectUseType} type * @property {{ loader: string, options?: string | null | Record<string, EXPECTED_ANY>, ident?: string }} value */ /** * Defines the effect basic type used by this module. * @typedef {object} EffectBasic * @property {string} type * @property {EXPECTED_ANY} value */ /** @typedef {EffectUse | EffectBasic} Effect */ /** @typedef {Map<string, RuleSetLoaderOptions>} References */ /** * Defines the rule set type used by this module. * @typedef {object} RuleSet * @property {References} references map of references in the rule set (may grow over time) * @property {(effectData: EffectData) => Effect[]} exec execute the rule set */ /** * Defines the keys of types type used by this module. * @template T * @template {T[keyof T]} V * @typedef {({ [key in keyof Required<T>]: Required<T>[key] extends V ? key : never })[keyof T]} KeysOfTypes */ /** @typedef {Set<string>} UnhandledProperties */ /** @typedef {(data: EffectData) => (RuleSetUseItem | (Falsy | RuleSetUseItem)[])} RuleSetUseFn */ /** @typedef {(value: string) => boolean} RuleSetConditionFn */ /** @typedef {{ apply: (ruleSetCompiler: RuleSetCompiler) => void }} RuleSetPlugin */ class RuleSetCompiler { /** * Creates an instance of RuleSetCompiler. * @param {RuleSetPlugin[]} plugins plugins */ constructor(plugins) { this.hooks = Object.freeze({ /** @type {SyncHook<[string, RuleSetRule, UnhandledProperties, CompiledRule, References]>} */ rule: new SyncHook([ "path", "rule", "unhandledProperties", "compiledRule", "references" ]) }); if (plugins) { for (const plugin of plugins) { plugin.apply(this); } } } /** * Returns compiled RuleSet. * @param {RuleSetRules} ruleSet raw user provided rules * @returns {RuleSet} compiled RuleSet */ compile(ruleSet) { /** @type {References} */ const refs = new Map(); const rules = this.compileRules("ruleSet", ruleSet, refs); /** * Returns true, if the rule has matched. * @param {EffectData} data data passed in * @param {CompiledRule} rule the compiled rule * @param {Effect[]} effects an array where effects are pushed to * @returns {boolean} true, if the rule has matched */ const execRule = (data, rule, effects) => { for (const condition of rule.conditions) { const p = condition.property; if (Array.isArray(p)) { /** @type {EXPECTED_ANY} */ let current = data; for (const subProperty of p) { if ( current && typeof current === "object" && Object.prototype.hasOwnProperty.call(current, subProperty) ) { current = current[/** @type {keyof EffectData} */ (subProperty)]; } else { current = undefined; break; } } if (current !== undefined) { if (!condition.fn(current)) return false; continue; } } else if (p in data) { const value = data[/** @type {keyof EffectData} */ (p)]; if (value !== undefined) { if (!condition.fn(value)) return false; continue; } } if (!condition.matchWhenEmpty) { return false; } } for (const effect of rule.effects) { if (typeof effect === "function") { const returnedEffects = effect(data); for (const effect of returnedEffects) { effects.push(effect); } } else { effects.push(effect); } } if (rule.rules) { for (const childRule of rule.rules) { execRule(data, childRule, effects); } } if (rule.oneOf) { for (const childRule of rule.oneOf) { if (execRule(data, childRule, effects)) { break; } } } return true; }; return { references: refs, exec: (data) => { /** @type {Effect[]} */ const effects = []; for (const rule of rules) { execRule(data, rule, effects); } return effects; } }; } /** * Returns rules. * @param {string} path current path * @param {RuleSetRules} rules the raw rules provided by user * @param {References} refs references * @returns {CompiledRule[]} rules */ compileRules(path, rules, refs) { return rules .filter(Boolean) .map((rule, i) => this.compileRule( `${path}[${i}]`, /** @type {RuleSetRule} */ (rule), refs ) ); } /** * Returns normalized and compiled rule for processing. * @param {string} path current path * @param {RuleSetRule} rule the raw rule provided by user * @param {References} refs references * @returns {CompiledRule} normalized and compiled rule for processing */ compileRule(path, rule, refs) { /** @type {UnhandledProperties} */ const unhandledProperties = new Set( Object.keys(rule).filter( (key) => rule[/** @type {keyof RuleSetRule} */ (key)] !== undefined ) ); /** @type {CompiledRule} */ const compiledRule = { conditions: [], effects: [], rules: undefined, oneOf: undefined }; this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs); if (unhandledProperties.has("rules")) { unhandledProperties.delete("rules"); const rules = rule.rules; if (!Array.isArray(rules)) { throw this.error(path, rules, "Rule.rules must be an array of rules"); } compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs); } if (unhandledProperties.has("oneOf")) { unhandledProperties.delete("oneOf"); const oneOf = rule.oneOf; if (!Array.isArray(oneOf)) { throw this.error(path, oneOf, "Rule.oneOf must be an array of rules"); } compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs); } if (unhandledProperties.size > 0) { throw this.error( path, rule, `Properties ${[...unhandledProperties].join(", ")} are unknown` ); } return compiledRule; } /** * Returns compiled condition. * @param {string} path current path * @param {RuleSetLoaderOptions} condition user provided condition value * @returns {Condition} compiled condition */ compileCondition(path, condition) { if (condition === "") { return { matchWhenEmpty: true, fn: (str) => str === "" }; } if (!condition) { throw this.error( path, condition, "Expected condition but got falsy value" ); } if (typeof condition === "string") { return { matchWhenEmpty: condition.length === 0, fn: (str) => typeof str === "string" && str.startsWith(condition) }; } if (typeof condition === "function") { try { return { matchWhenEmpty: condition(""), fn: /** @type {RuleConditionFunction} */ (condition) }; } catch (_err) { throw this.error( path, condition, "Evaluation of condition function threw error" ); } } if (condition instanceof RegExp) { return { matchWhenEmpty: condition.test(""), fn: (v) => typeof v === "string" && condition.test(v) }; } if (Array.isArray(condition)) { const items = condition.map((c, i) => this.compileCondition(`${path}[${i}]`, c) ); return this.combineConditionsOr(items); } if (typeof condition !== "object") { throw this.error( path, condition, `Unexpected ${typeof condition} when condition was expected` ); } /** @type {Condition[]} */ const conditions = []; for (const key of Object.keys(condition)) { const value = condition[key]; switch (key) { case "or": if (value) { if (!Array.isArray(value)) { throw this.error( `${path}.or`, condition.or, "Expected array of conditions" ); } conditions.push(this.compileCondition(`${path}.or`, value)); } break; case "and": if (value) { if (!Array.isArray(value)) { throw this.error( `${path}.and`, condition.and, "Expected array of conditions" ); } let i = 0; for (const item of value) { conditions.push(this.compileCondition(`${path}.and[${i}]`, item)); i++; } } break; case "not": if (value) { const matcher = this.compileCondition(`${path}.not`, value); const fn = matcher.fn; conditions.push({ matchWhenEmpty: !matcher.matchWhenEmpty, fn: /** @type {RuleConditionFunction} */ ((v) => !fn(v)) }); } break; default: throw this.error( `${path}.${key}`, condition[key], `Unexpected property ${key} in condition` ); } } if (conditions.length === 0) { throw this.error( path, condition, "Expected condition, but got empty thing" ); } return this.combineConditionsAnd(conditions); } /** * Combine conditions or. * @param {Condition[]} conditions some conditions * @returns {Condition} merged condition */ combineConditionsOr(conditions) { if (conditions.length === 0) { return { matchWhenEmpty: false, fn: () => false }; } else if (conditions.length === 1) { return conditions[0]; } return { matchWhenEmpty: conditions.some((c) => c.matchWhenEmpty), fn: (v) => conditions.some((c) => c.fn(v)) }; } /** * Combine conditions and. * @param {Condition[]} conditions some conditions * @returns {Condition} merged condition */ combineConditionsAnd(conditions) { if (conditions.length === 0) { return { matchWhenEmpty: false, fn: () => false }; } else if (conditions.length === 1) { return conditions[0]; } return { matchWhenEmpty: conditions.every((c) => c.matchWhenEmpty), fn: (v) => conditions.every((c) => c.fn(v)) }; } /** * Returns an error object. * @param {string} path current path * @param {EXPECTED_ANY} value value at the error location * @param {string} message message explaining the problem * @returns {Error} an error object */ error(path, value, message) { return new Error( `Compiling RuleSet failed: ${message} (at ${path}: ${value})` ); } /** * Best-effort detection of a user-registered loader (or explicit module type) * for a resource, without building the whole rule set. Used to resolve the * `experiments.css`/`experiments.html`/`experiments.asyncWebAssembly` "auto" * defaults: when the user already handles these files the built-in module type * stays off, so enabling it by default is non-breaking. * * `include`/`exclude` are treated leniently — a matching `test`/`resource`/ * `include` with a loader counts even when another condition would narrow it — * so a loader scoped to e.g. `include: /src/` still keeps the built-in type off * and those files are not double-processed. `enforce: "pre"`/`"post"` loaders * are ignored: they don't establish the module type (e.g. a stylelint pre-loader * must not suppress the built-in css type). A `test` regexp that references the * extension (e.g. `/source\.css$/`) also counts even if the sample path itself * doesn't match, so a loader scoped to specific filenames is still detected. * @param {import("../../declarations/WebpackOptions").RuleSetRules | undefined} rules user `module.rules` * @param {string} resource sample resource path (e.g. `"/file.css"`) * @param {boolean=} inherited whether an enclosing rule already matched the resource * @returns {boolean} whether a rule assigns a loader or module type to the resource */ static hasRuleForResource(rules, resource, inherited = false) { if (!rules) return false; // `\.css` for `/file.css`, `\.wasm` for `/file.wasm`, … const extProbe = `\\.${resource.slice(resource.lastIndexOf(".") + 1)}`; for (const rule of rules) { if (!rule || typeof rule !== "object") continue; const matched = inherited || matchesResource(rule.test, resource, extProbe) || matchesResource(rule.resource, resource, extProbe) || matchesResource(rule.include, resource, extProbe); const establishesType = rule.type !== undefined || ((rule.use !== undefined || rule.loader !== undefined) && rule.enforce === undefined); if (matched && establishesType) return true; if ( rule.oneOf && RuleSetCompiler.hasRuleForResource(rule.oneOf, resource, matched) ) { return true; } if ( rule.rules && RuleSetCompiler.hasRuleForResource(rule.rules, resource, matched) ) { return true; } } return false; } } // Bare compiler reused to match single conditions via `compileCondition` // (no plugins needed); created lazily on first use. /** @type {RuleSetCompiler | undefined} */ let conditionCompiler; /** * Matches a single rule condition against a sample string, reusing the ruleset * condition logic (string/regexp/function/array/and/or/not). * @param {RuleSetConditionAbsolute | undefined} condition condition * @param {string} resource sample resource path * @returns {boolean} whether the condition matches */ const matchRuleSetCondition = (condition, resource) => { if (condition === undefined) return false; if (conditionCompiler === undefined) { conditionCompiler = new RuleSetCompiler([]); } try { return conditionCompiler .compileCondition("ruleSet", condition) .fn(resource); } catch (_err) { return false; } }; /** * Whether any regexp in the condition references the extension probe (e.g. * `\.css`), i.e. the rule clearly targets that extension even if the generic * sample path doesn't match its (possibly filename-scoped) pattern. * @param {EXPECTED_ANY} condition condition * @param {string} extProbe escaped extension probe (e.g. `"\\.css"`) * @returns {boolean} whether a regexp in the condition targets the extension */ const conditionReferencesExtension = (condition, extProbe) => { if (condition instanceof RegExp) return condition.source.includes(extProbe); if (Array.isArray(condition)) { return condition.some((c) => conditionReferencesExtension(c, extProbe)); } if (condition && typeof condition === "object") { return ( conditionReferencesExtension(condition.and, extProbe) || conditionReferencesExtension(condition.or, extProbe) ); } return false; }; /** * A condition counts as targeting the resource if it matches the sample path or * references the extension via a regexp. * @param {RuleSetConditionAbsolute | undefined} condition condition * @param {string} resource sample resource path * @param {string} extProbe escaped extension probe (e.g. `"\\.css"`) * @returns {boolean} whether the condition targets the resource's extension */ const matchesResource = (condition, resource, extProbe) => condition !== undefined && (matchRuleSetCondition(condition, resource) || conditionReferencesExtension(condition, extProbe)); module.exports = RuleSetCompiler;