diff --git a/lib/Detector.js b/lib/Detector.js index 83dab43..6d845fc 100644 --- a/lib/Detector.js +++ b/lib/Detector.js @@ -1,11 +1,14 @@ import FEATURES from '../data/features.js'; import { performFeatureCheck, stripUrls } from '../utils/util.js'; +/** @typedef {import('../data/features.js').FeatureKeys} FeatureKeys */ +/** @typedef {import('../data/features.js').RuleCheck} RuleCheck */ + /** * @typedef DetectorCallbackArgument * @prop {!import('postcss').ChildNode} usage - * @prop {keyof FEATURES} feature - * @prop {(keyof FEATURES & string)[]} ignore + * @prop {FeatureKeys} feature + * @prop {(FeatureKeys & string)[]} ignore */ /** @@ -18,6 +21,35 @@ const PLUGIN_OPTION_COMMENT = 'doiuse-'; const DISABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}disable`; const ENABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}enable`; +/** + * Normalise a Feature into a RuleCheck function. + * @param {import('../data/features.js').Feature} feature + * @return {RuleCheck} + */ +function normaliseFeature(feature) { + if (typeof feature === 'function') { + return feature; + } + if (Array.isArray(feature)) { + return (child) => feature.some((function_) => function_(child)); + } + if (typeof feature === 'object') { + const properties = Object.entries(feature); + return (child) => { + if (child.type !== 'decl') { + return false; + } + return properties.some(([property, value]) => { + if (property !== '' && property !== child.prop) return false; + if (value === true) return true; + if (value === false) return false; + return performFeatureCheck(value, stripUrls(child.value)); + }); + }; + } + throw new TypeError(`Invalid feature definition: ${feature}`); +} + /** * Detect the use of any of a given list of CSS features. * ``` @@ -38,17 +70,17 @@ const ENABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}enable`; */ export default class Detector { /** - * @param {(keyof FEATURES & string)[]} featureList an array of feature slugs (see caniuse-db) + * @param {(FeatureKeys & string)[]} featureList an array of feature slugs (see caniuse-db) */ constructor(featureList) { - /** @type {Partial} */ - this.features = {}; - for (const feature of featureList) { - if (FEATURES[feature]) { - this.features[feature] = FEATURES[feature]; - } - } - /** @type {(keyof FEATURES & string)[]} */ + /** @type {[FeatureKeys, RuleCheck][]} */ + this.features = featureList + .filter((featureName) => FEATURES[featureName] != null) + .map((featureName) => { + const feature = FEATURES[featureName]; + return [featureName, normaliseFeature(feature)]; + }); + /** @type {(FeatureKeys & string)[]} */ this.ignore = []; } @@ -66,8 +98,7 @@ export default class Detector { switch (option) { case DISABLE_FEATURE_COMMENT: { if (value === '') { - // @ts-expect-error Skip cast - this.ignore = Object.keys(this.features); + this.ignore = this.features.map(([featureName]) => featureName); } else { for (const feat of value.split(',')) { /** @type {any} */ @@ -104,28 +135,11 @@ export default class Detector { return; } - for (const [feat] of Object.entries(this.features).filter(([, featValue]) => { - if (!featValue) return false; - if (typeof featValue === 'function') { - return featValue(child); - } - if (Array.isArray(featValue)) { - return featValue.some((function_) => function_(child)); - } - if (child.type !== 'decl') { - return false; - } - - return Object.entries(featValue).some(([property, value]) => { - if (property !== '' && property !== child.prop) return false; - if (value === true) return true; - if (value === false) return false; - return performFeatureCheck(value, stripUrls(child.value)); - }); - })) { - const feature = /** @type {keyof FEATURES} */ (feat); - callback({ usage: child, feature, ignore: this.ignore }); + const detectedFeatures = this.features.filter(([, ruleCheck]) => ruleCheck(child)); + for (const [featureName] of detectedFeatures) { + callback({ usage: child, feature: featureName, ignore: this.ignore }); } + if (child.type !== 'decl') { this.node(child, callback); }