RuleSetCompiler.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { SyncHook } = require("tapable");
  7. const { createPathGlobMatcher } = require("../util/globUtils");
  8. /** @import { ResolveRequest } from "enhanced-resolve" */
  9. /**
  10. * @import {
  11. * Falsy,
  12. * RuleSetUseItem,
  13. * RuleSetLoaderOptions,
  14. * RuleSetRule,
  15. * RuleSetConditionAbsolute
  16. * } from "../../declarations/WebpackOptions"
  17. */
  18. /** @typedef {(Falsy | RuleSetRule)[]} RuleSetRules */
  19. /**
  20. * Defines the rule condition function type used by this module.
  21. * @typedef {(value: EffectData[keyof EffectData]) => boolean} RuleConditionFunction
  22. */
  23. /**
  24. * Defines the rule condition type used by this module.
  25. * @typedef {object} RuleCondition
  26. * @property {string | string[]} property
  27. * @property {boolean} matchWhenEmpty
  28. * @property {RuleConditionFunction} fn
  29. */
  30. /**
  31. * Defines the condition type used by this module.
  32. * @typedef {object} Condition
  33. * @property {boolean} matchWhenEmpty
  34. * @property {RuleConditionFunction} fn
  35. */
  36. /**
  37. * Defines the effect data type used by this module.
  38. * @typedef {object} EffectData
  39. * @property {string=} resource
  40. * @property {string=} realResource
  41. * @property {string=} resourceQuery
  42. * @property {string=} resourceFragment
  43. * @property {string=} scheme
  44. * @property {ImportAttributes=} attributes
  45. * @property {string=} mimetype
  46. * @property {string} dependency
  47. * @property {ResolveRequest["descriptionFileData"]=} descriptionData
  48. * @property {ResolveRequest["relativePath"]=} descriptionRelativePath
  49. * @property {string=} compiler
  50. * @property {string} issuer
  51. * @property {string} issuerLayer
  52. * @property {string=} phase
  53. */
  54. /**
  55. * Defines the compiled rule type used by this module.
  56. * @typedef {object} CompiledRule
  57. * @property {string} path
  58. * @property {RuleSetRule} raw
  59. * @property {boolean} used
  60. * @property {RuleCondition[]} conditions
  61. * @property {(Effect | ((effectData: EffectData) => Effect[]))[]} effects
  62. * @property {CompiledRule[]=} rules
  63. * @property {CompiledRule[]=} oneOf
  64. */
  65. /** @typedef {"use" | "use-pre" | "use-post"} EffectUseType */
  66. /**
  67. * Defines the effect use type used by this module.
  68. * @typedef {object} EffectUse
  69. * @property {EffectUseType} type
  70. * @property {{ loader: string, options?: string | null | Record<string, EXPECTED_ANY>, ident?: string }} value
  71. */
  72. /**
  73. * Defines the effect basic type used by this module.
  74. * @typedef {object} EffectBasic
  75. * @property {string} type
  76. * @property {EXPECTED_ANY} value
  77. */
  78. /** @typedef {EffectUse | EffectBasic} Effect */
  79. /** @typedef {Map<string, RuleSetLoaderOptions>} References */
  80. /**
  81. * Defines the rule set type used by this module.
  82. * @typedef {object} RuleSet
  83. * @property {References} references map of references in the rule set (may grow over time)
  84. * @property {(effectData: EffectData) => Effect[]} exec execute the rule set
  85. * @property {() => CompiledRule[]} unusedRules the rules that never matched, outermost first
  86. */
  87. /**
  88. * Defines the keys of types type used by this module.
  89. * @template T
  90. * @template {T[keyof T]} V
  91. * @typedef {({ [key in keyof Required<T>]: Required<T>[key] extends V ? key : never })[keyof T]} KeysOfTypes
  92. */
  93. /** @typedef {Set<string>} UnhandledProperties */
  94. /** @typedef {(str: string) => boolean} GlobMatcher */
  95. /** @typedef {(data: EffectData) => (RuleSetUseItem | (Falsy | RuleSetUseItem)[])} RuleSetUseFn */
  96. /** @typedef {(value: string) => boolean} RuleSetConditionFn */
  97. /** @typedef {{ apply: (ruleSetCompiler: RuleSetCompiler) => void }} RuleSetPlugin */
  98. /**
  99. * @param {GlobMatcher[]} positive patterns that select
  100. * @param {GlobMatcher[]} negative patterns that subtract
  101. * @returns {GlobMatcher} matcher
  102. */
  103. const combineGlobMatchers = (positive, negative) => {
  104. /** @type {GlobMatcher} */
  105. const matchPositive =
  106. positive.length === 0
  107. ? () => true
  108. : positive.length === 1
  109. ? positive[0]
  110. : (str) => positive.some((match) => match(str));
  111. if (negative.length === 0) return matchPositive;
  112. /** @type {GlobMatcher} */
  113. const matchNegative =
  114. negative.length === 1
  115. ? negative[0]
  116. : (str) => negative.some((match) => match(str));
  117. return (str) => matchPositive(str) && !matchNegative(str);
  118. };
  119. class RuleSetCompiler {
  120. /**
  121. * Creates an instance of RuleSetCompiler.
  122. * @param {RuleSetPlugin[]} plugins plugins
  123. */
  124. constructor(plugins) {
  125. this.hooks = Object.freeze({
  126. /** @type {SyncHook<[string, RuleSetRule, UnhandledProperties, CompiledRule, References]>} */
  127. rule: new SyncHook([
  128. "path",
  129. "rule",
  130. "unhandledProperties",
  131. "compiledRule",
  132. "references"
  133. ])
  134. });
  135. if (plugins) {
  136. for (const plugin of plugins) {
  137. plugin.apply(this);
  138. }
  139. }
  140. }
  141. /**
  142. * Returns compiled RuleSet.
  143. * @param {RuleSetRules} ruleSet raw user provided rules
  144. * @returns {RuleSet} compiled RuleSet
  145. */
  146. compile(ruleSet) {
  147. /** @type {References} */
  148. const refs = new Map();
  149. const rules = this.compileRules("ruleSet", ruleSet, refs);
  150. /**
  151. * Returns true, if the rule has matched.
  152. * @param {EffectData} data data passed in
  153. * @param {CompiledRule} rule the compiled rule
  154. * @param {Effect[]} effects an array where effects are pushed to
  155. * @returns {boolean} true, if the rule has matched
  156. */
  157. const execRule = (data, rule, effects) => {
  158. for (const condition of rule.conditions) {
  159. const p = condition.property;
  160. if (Array.isArray(p)) {
  161. /** @type {EXPECTED_ANY} */
  162. let current = data;
  163. for (const subProperty of p) {
  164. if (
  165. current &&
  166. typeof current === "object" &&
  167. Object.prototype.hasOwnProperty.call(current, subProperty)
  168. ) {
  169. current = current[/** @type {keyof EffectData} */ (subProperty)];
  170. } else {
  171. current = undefined;
  172. break;
  173. }
  174. }
  175. if (current !== undefined) {
  176. if (!condition.fn(current)) return false;
  177. continue;
  178. }
  179. } else if (p in data) {
  180. const value = data[/** @type {keyof EffectData} */ (p)];
  181. if (value !== undefined) {
  182. if (!condition.fn(value)) return false;
  183. continue;
  184. }
  185. }
  186. if (!condition.matchWhenEmpty) {
  187. return false;
  188. }
  189. }
  190. rule.used = true;
  191. for (const effect of rule.effects) {
  192. if (typeof effect === "function") {
  193. const returnedEffects = effect(data);
  194. for (const effect of returnedEffects) {
  195. effects.push(effect);
  196. }
  197. } else {
  198. effects.push(effect);
  199. }
  200. }
  201. if (rule.rules) {
  202. for (const childRule of rule.rules) {
  203. execRule(data, childRule, effects);
  204. }
  205. }
  206. if (rule.oneOf) {
  207. for (const childRule of rule.oneOf) {
  208. if (execRule(data, childRule, effects)) {
  209. break;
  210. }
  211. }
  212. }
  213. return true;
  214. };
  215. return {
  216. references: refs,
  217. exec: (data) => {
  218. /** @type {Effect[]} */
  219. const effects = [];
  220. for (const rule of rules) {
  221. execRule(data, rule, effects);
  222. }
  223. return effects;
  224. },
  225. unusedRules: () => {
  226. /** @type {CompiledRule[]} */
  227. const unused = [];
  228. collectUnusedRules(rules, unused);
  229. return unused;
  230. }
  231. };
  232. }
  233. /**
  234. * Returns rules.
  235. * @param {string} path current path
  236. * @param {RuleSetRules} rules the raw rules provided by user
  237. * @param {References} refs references
  238. * @returns {CompiledRule[]} rules
  239. */
  240. compileRules(path, rules, refs) {
  241. return rules
  242. .filter(Boolean)
  243. .map((rule, i) =>
  244. this.compileRule(
  245. `${path}[${i}]`,
  246. /** @type {RuleSetRule} */ (rule),
  247. refs
  248. )
  249. );
  250. }
  251. /**
  252. * Returns normalized and compiled rule for processing.
  253. * @param {string} path current path
  254. * @param {RuleSetRule} rule the raw rule provided by user
  255. * @param {References} refs references
  256. * @returns {CompiledRule} normalized and compiled rule for processing
  257. */
  258. compileRule(path, rule, refs) {
  259. /** @type {UnhandledProperties} */
  260. const unhandledProperties = new Set(
  261. Object.keys(rule).filter(
  262. (key) => rule[/** @type {keyof RuleSetRule} */ (key)] !== undefined
  263. )
  264. );
  265. /** @type {CompiledRule} */
  266. const compiledRule = {
  267. path,
  268. raw: rule,
  269. used: false,
  270. conditions: [],
  271. effects: [],
  272. rules: undefined,
  273. oneOf: undefined
  274. };
  275. this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
  276. if (unhandledProperties.has("rules")) {
  277. unhandledProperties.delete("rules");
  278. const rules = rule.rules;
  279. if (!Array.isArray(rules)) {
  280. throw this.error(path, rules, "Rule.rules must be an array of rules");
  281. }
  282. compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
  283. }
  284. if (unhandledProperties.has("oneOf")) {
  285. unhandledProperties.delete("oneOf");
  286. const oneOf = rule.oneOf;
  287. if (!Array.isArray(oneOf)) {
  288. throw this.error(path, oneOf, "Rule.oneOf must be an array of rules");
  289. }
  290. compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
  291. }
  292. if (unhandledProperties.size > 0) {
  293. throw this.error(
  294. path,
  295. rule,
  296. `Properties ${[...unhandledProperties].join(", ")} are unknown`
  297. );
  298. }
  299. return compiledRule;
  300. }
  301. /**
  302. * Returns compiled condition.
  303. * @param {string} path current path
  304. * @param {RuleSetLoaderOptions} condition user provided condition value
  305. * @returns {Condition} compiled condition
  306. */
  307. compileCondition(path, condition) {
  308. if (condition === "") {
  309. return {
  310. matchWhenEmpty: true,
  311. fn: (str) => str === ""
  312. };
  313. }
  314. if (!condition) {
  315. throw this.error(
  316. path,
  317. condition,
  318. "Expected condition but got falsy value"
  319. );
  320. }
  321. if (typeof condition === "string") {
  322. return {
  323. matchWhenEmpty: condition.length === 0,
  324. fn: (str) => typeof str === "string" && str.startsWith(condition)
  325. };
  326. }
  327. if (typeof condition === "function") {
  328. try {
  329. return {
  330. matchWhenEmpty: condition(""),
  331. fn: /** @type {RuleConditionFunction} */ (condition)
  332. };
  333. } catch (_err) {
  334. throw this.error(
  335. path,
  336. condition,
  337. "Evaluation of condition function threw error"
  338. );
  339. }
  340. }
  341. if (condition instanceof RegExp) {
  342. return {
  343. matchWhenEmpty: condition.test(""),
  344. fn: (v) => typeof v === "string" && condition.test(v)
  345. };
  346. }
  347. if (Array.isArray(condition)) {
  348. const items = condition.map((c, i) =>
  349. this.compileCondition(`${path}[${i}]`, c)
  350. );
  351. return this.combineConditionsOr(items);
  352. }
  353. if (typeof condition !== "object") {
  354. throw this.error(
  355. path,
  356. condition,
  357. `Unexpected ${typeof condition} when condition was expected`
  358. );
  359. }
  360. /** @type {Condition[]} */
  361. const conditions = [];
  362. for (const key of Object.keys(condition)) {
  363. const value = condition[key];
  364. switch (key) {
  365. case "or":
  366. if (value) {
  367. if (!Array.isArray(value)) {
  368. throw this.error(
  369. `${path}.or`,
  370. condition.or,
  371. "Expected array of conditions"
  372. );
  373. }
  374. conditions.push(this.compileCondition(`${path}.or`, value));
  375. }
  376. break;
  377. case "and":
  378. if (value) {
  379. if (!Array.isArray(value)) {
  380. throw this.error(
  381. `${path}.and`,
  382. condition.and,
  383. "Expected array of conditions"
  384. );
  385. }
  386. let i = 0;
  387. for (const item of value) {
  388. conditions.push(this.compileCondition(`${path}.and[${i}]`, item));
  389. i++;
  390. }
  391. }
  392. break;
  393. case "glob":
  394. if (value) {
  395. conditions.push(this.compileGlobCondition(`${path}.glob`, value));
  396. }
  397. break;
  398. case "not":
  399. if (value) {
  400. const matcher = this.compileCondition(`${path}.not`, value);
  401. const fn = matcher.fn;
  402. conditions.push({
  403. matchWhenEmpty: !matcher.matchWhenEmpty,
  404. fn: /** @type {RuleConditionFunction} */ ((v) => !fn(v))
  405. });
  406. }
  407. break;
  408. default:
  409. throw this.error(
  410. `${path}.${key}`,
  411. condition[key],
  412. `Unexpected property ${key} in condition`
  413. );
  414. }
  415. }
  416. if (conditions.length === 0) {
  417. throw this.error(
  418. path,
  419. condition,
  420. "Expected condition, but got empty thing"
  421. );
  422. }
  423. return this.combineConditionsAnd(conditions);
  424. }
  425. /**
  426. * Returns a compiled glob condition. Unlike a regexp it matches the same way
  427. * on every OS: `\` is a path separator in both the pattern and the tested
  428. * value, and a relative pattern matches at any depth. Patterns are OR-ed,
  429. * a `!` prefix subtracts, and a list of only `!` patterns subtracts from
  430. * everything.
  431. * @param {string} path current path
  432. * @param {string | string[]} glob user provided glob pattern(s)
  433. * @returns {Condition} compiled condition
  434. */
  435. compileGlobCondition(path, glob) {
  436. const globs = Array.isArray(glob) ? glob : [glob];
  437. if (globs.length === 0) {
  438. throw this.error(path, glob, "Expected glob pattern, but got empty list");
  439. }
  440. /** @type {GlobMatcher[]} */
  441. const positive = [];
  442. /** @type {GlobMatcher[]} */
  443. const negative = [];
  444. for (let i = 0; i < globs.length; i++) {
  445. const pattern = globs[i];
  446. const patternPath = Array.isArray(glob) ? `${path}[${i}]` : path;
  447. if (typeof pattern !== "string") {
  448. throw this.error(
  449. patternPath,
  450. pattern,
  451. `Unexpected ${typeof pattern} when glob pattern was expected`
  452. );
  453. }
  454. const negated = pattern.startsWith("!");
  455. const match = createPathGlobMatcher(negated ? pattern.slice(1) : pattern);
  456. if (match === null) {
  457. throw this.error(patternPath, pattern, "Invalid glob pattern");
  458. }
  459. (negated ? negative : positive).push(match);
  460. }
  461. const match = combineGlobMatchers(positive, negative);
  462. return {
  463. matchWhenEmpty: match(""),
  464. fn: (v) => typeof v === "string" && match(v)
  465. };
  466. }
  467. /**
  468. * Combine conditions or.
  469. * @param {Condition[]} conditions some conditions
  470. * @returns {Condition} merged condition
  471. */
  472. combineConditionsOr(conditions) {
  473. if (conditions.length === 0) {
  474. return {
  475. matchWhenEmpty: false,
  476. fn: () => false
  477. };
  478. } else if (conditions.length === 1) {
  479. return conditions[0];
  480. }
  481. return {
  482. matchWhenEmpty: conditions.some((c) => c.matchWhenEmpty),
  483. fn: (v) => conditions.some((c) => c.fn(v))
  484. };
  485. }
  486. /**
  487. * Combine conditions and.
  488. * @param {Condition[]} conditions some conditions
  489. * @returns {Condition} merged condition
  490. */
  491. combineConditionsAnd(conditions) {
  492. if (conditions.length === 0) {
  493. return {
  494. matchWhenEmpty: false,
  495. fn: () => false
  496. };
  497. } else if (conditions.length === 1) {
  498. return conditions[0];
  499. }
  500. return {
  501. matchWhenEmpty: conditions.every((c) => c.matchWhenEmpty),
  502. fn: (v) => conditions.every((c) => c.fn(v))
  503. };
  504. }
  505. /**
  506. * Returns an error object.
  507. * @param {string} path current path
  508. * @param {EXPECTED_ANY} value value at the error location
  509. * @param {string} message message explaining the problem
  510. * @returns {Error} an error object
  511. */
  512. error(path, value, message) {
  513. return new Error(
  514. `Compiling RuleSet failed: ${message} (at ${path}: ${value})`
  515. );
  516. }
  517. /**
  518. * Best-effort detection of a user-registered loader (or explicit module type)
  519. * for a resource, without building the whole rule set. Used to resolve the
  520. * `experiments.css`/`experiments.html`/`experiments.asyncWebAssembly` "auto"
  521. * defaults: when the user already handles these files the built-in module type
  522. * stays off, so enabling it by default is non-breaking.
  523. *
  524. * `include`/`exclude` are treated leniently — a matching `test`/`resource`/
  525. * `include` with a loader counts even when another condition would narrow it —
  526. * so a loader scoped to e.g. `include: /src/` still keeps the built-in type off
  527. * and those files are not double-processed. `enforce: "pre"`/`"post"` loaders
  528. * are ignored: they don't establish the module type (e.g. a stylelint pre-loader
  529. * must not suppress the built-in css type). A `test` regexp that references the
  530. * extension (e.g. `/source\.css$/`) also counts even if the sample path itself
  531. * doesn't match, so a loader scoped to specific filenames is still detected; a
  532. * `glob` condition naming the extension counts the same way.
  533. * @param {import("../../declarations/WebpackOptions").RuleSetRules | undefined} rules user `module.rules`
  534. * @param {string} resource sample resource path (e.g. `"/file.css"`)
  535. * @param {boolean=} inherited whether an enclosing rule already matched the resource
  536. * @returns {boolean} whether a rule assigns a loader or module type to the resource
  537. */
  538. static hasRuleForResource(rules, resource, inherited = false) {
  539. if (!rules) return false;
  540. // `\.css` for `/file.css`, `\.wasm` for `/file.wasm`, …
  541. const extProbe = `\\.${resource.slice(resource.lastIndexOf(".") + 1)}`;
  542. for (const rule of rules) {
  543. if (!rule || typeof rule !== "object") continue;
  544. const matched =
  545. inherited ||
  546. matchesResource(rule.test, resource, extProbe) ||
  547. matchesResource(rule.resource, resource, extProbe) ||
  548. matchesResource(rule.include, resource, extProbe) ||
  549. // the rule-level `glob` selects the resource as well
  550. matchesResource(
  551. rule.glob === undefined ? undefined : { glob: rule.glob },
  552. resource,
  553. extProbe
  554. );
  555. const establishesType =
  556. rule.type !== undefined ||
  557. ((rule.use !== undefined || rule.loader !== undefined) &&
  558. rule.enforce === undefined);
  559. if (matched && establishesType) return true;
  560. if (
  561. rule.oneOf &&
  562. RuleSetCompiler.hasRuleForResource(rule.oneOf, resource, matched)
  563. ) {
  564. return true;
  565. }
  566. if (
  567. rule.rules &&
  568. RuleSetCompiler.hasRuleForResource(rule.rules, resource, matched)
  569. ) {
  570. return true;
  571. }
  572. }
  573. return false;
  574. }
  575. }
  576. // Bare compiler reused to match single conditions via `compileCondition`
  577. // (no plugins needed); created lazily on first use.
  578. /** @type {RuleSetCompiler | undefined} */
  579. let conditionCompiler;
  580. /**
  581. * Matches a single rule condition against a sample string, reusing the ruleset
  582. * condition logic (string/regexp/function/array/and/or/not).
  583. * @param {RuleSetConditionAbsolute | undefined} condition condition
  584. * @param {string} resource sample resource path
  585. * @returns {boolean} whether the condition matches
  586. */
  587. const matchRuleSetCondition = (condition, resource) => {
  588. if (condition === undefined) return false;
  589. if (conditionCompiler === undefined) {
  590. conditionCompiler = new RuleSetCompiler([]);
  591. }
  592. try {
  593. return conditionCompiler
  594. .compileCondition("ruleSet", condition)
  595. .fn(resource);
  596. } catch (_err) {
  597. return false;
  598. }
  599. };
  600. /**
  601. * @param {EXPECTED_ANY} glob glob pattern(s)
  602. * @param {string} extension extension including the dot (e.g. `".css"`)
  603. * @returns {boolean} whether a pattern targets the extension
  604. */
  605. const globReferencesExtension = (glob, extension) => {
  606. // a `!` pattern subtracts the extension instead of targeting it
  607. if (typeof glob === "string") {
  608. return !glob.startsWith("!") && glob.includes(extension);
  609. }
  610. return (
  611. Array.isArray(glob) &&
  612. glob.some((pattern) => globReferencesExtension(pattern, extension))
  613. );
  614. };
  615. /**
  616. * Whether any regexp or glob in the condition references the extension probe
  617. * (e.g. `\.css`), i.e. the rule clearly targets that extension even if the
  618. * generic sample path doesn't match its (possibly filename-scoped) pattern.
  619. * @param {EXPECTED_ANY} condition condition
  620. * @param {string} extProbe escaped extension probe (e.g. `"\\.css"`)
  621. * @returns {boolean} whether a regexp in the condition targets the extension
  622. */
  623. const conditionReferencesExtension = (condition, extProbe) => {
  624. if (condition instanceof RegExp) return condition.source.includes(extProbe);
  625. if (Array.isArray(condition)) {
  626. return condition.some((c) => conditionReferencesExtension(c, extProbe));
  627. }
  628. if (condition && typeof condition === "object") {
  629. return (
  630. // `\.css` → `.css`
  631. globReferencesExtension(condition.glob, extProbe.slice(1)) ||
  632. conditionReferencesExtension(condition.and, extProbe) ||
  633. conditionReferencesExtension(condition.or, extProbe)
  634. );
  635. }
  636. return false;
  637. };
  638. /**
  639. * A condition counts as targeting the resource if it matches the sample path or
  640. * references the extension via a regexp or glob.
  641. * @param {RuleSetConditionAbsolute | undefined} condition condition
  642. * @param {string} resource sample resource path
  643. * @param {string} extProbe escaped extension probe (e.g. `"\\.css"`)
  644. * @returns {boolean} whether the condition targets the resource's extension
  645. */
  646. const matchesResource = (condition, resource, extProbe) =>
  647. condition !== undefined &&
  648. (matchRuleSetCondition(condition, resource) ||
  649. conditionReferencesExtension(condition, extProbe));
  650. /**
  651. * Collects the outermost rules that never matched. A rule below an unused one is
  652. * unused only because of its parent, so its subtree is not descended into.
  653. * @param {CompiledRule[]} rules compiled rules
  654. * @param {CompiledRule[]} result where unused rules are collected
  655. * @returns {void}
  656. */
  657. const collectUnusedRules = (rules, result) => {
  658. for (const rule of rules) {
  659. if (!rule.used) {
  660. result.push(rule);
  661. continue;
  662. }
  663. if (rule.rules) collectUnusedRules(rule.rules, result);
  664. if (rule.oneOf) collectUnusedRules(rule.oneOf, result);
  665. }
  666. };
  667. module.exports = RuleSetCompiler;