From fcf26211bf08b52ac9a5876b9d48d85edea1dfa0 Mon Sep 17 00:00:00 2001 From: Cameron Moon Date: Mon, 4 Mar 2024 21:23:32 +0000 Subject: [PATCH 1/4] cache missingSupport --- lib/BrowserSelection.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/BrowserSelection.js b/lib/BrowserSelection.js index 1746754..6cdd703 100644 --- a/lib/BrowserSelection.js +++ b/lib/BrowserSelection.js @@ -166,16 +166,29 @@ export default class BrowserSelection { return result; } + /** @readonly */ + static #missingSupportCache = new Map(); + /** * @see BrowserSelection.compileBrowserSupport * @param {ConstructorParameters} constructorParameters * @return {MissingSupportResult} `{browsers, features}` */ static missingSupport(...constructorParameters) { - const selection = new BrowserSelection(...constructorParameters); - return { + const [query, path] = constructorParameters; + // browserslist only uses `path` if `query` is not provided. + const key = query ? JSON.stringify(query) : path; + + if (this.#missingSupportCache.has(key)) { + return this.#missingSupportCache.get(key); + } + + const selection = new BrowserSelection(query, path); + const result = { browsers: selection.list(), features: selection.compileBrowserSupport(), }; + this.#missingSupportCache.set(key, result); + return result; } } From dd50a44289a818dfd5452dd0eadf0235596c8245 Mon Sep 17 00:00:00 2001 From: Cameron Moon Date: Mon, 4 Mar 2024 21:23:48 +0000 Subject: [PATCH 2/4] refactor Detector --- lib/Detector.js | 82 +++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 34 deletions(-) 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); } From f08f937ca7fcfa23b838edc0c2b79181e435e95d Mon Sep 17 00:00:00 2001 From: Cameron Moon Date: Wed, 20 Mar 2024 16:15:08 +0000 Subject: [PATCH 3/4] Use map instead of tuple array --- lib/Detector.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/Detector.js b/lib/Detector.js index 6d845fc..7d82c6d 100644 --- a/lib/Detector.js +++ b/lib/Detector.js @@ -73,13 +73,14 @@ export default class Detector { * @param {(FeatureKeys & string)[]} featureList an array of feature slugs (see caniuse-db) */ constructor(featureList) { - /** @type {[FeatureKeys, RuleCheck][]} */ - this.features = featureList - .filter((featureName) => FEATURES[featureName] != null) - .map((featureName) => { - const feature = FEATURES[featureName]; - return [featureName, normaliseFeature(feature)]; - }); + /** @type {Map} */ + this.features = new Map(); + for (const featureName of featureList) { + const feature = FEATURES[featureName]; + if (feature != null) { + this.features.set(featureName, normaliseFeature(feature)); + } + } /** @type {(FeatureKeys & string)[]} */ this.ignore = []; } @@ -98,7 +99,7 @@ export default class Detector { switch (option) { case DISABLE_FEATURE_COMMENT: { if (value === '') { - this.ignore = this.features.map(([featureName]) => featureName); + this.ignore = [...this.features.keys()]; } else { for (const feat of value.split(',')) { /** @type {any} */ @@ -135,9 +136,10 @@ export default class Detector { return; } - const detectedFeatures = this.features.filter(([, ruleCheck]) => ruleCheck(child)); - for (const [featureName] of detectedFeatures) { - callback({ usage: child, feature: featureName, ignore: this.ignore }); + for (const [feature, ruleCheck] of this.features) { + if (ruleCheck(child)) { + callback({ usage: child, feature, ignore: this.ignore }); + } } if (child.type !== 'decl') { From 3fb3244f1020bc8b8a2c9e9caaac796c0447d350 Mon Sep 17 00:00:00 2001 From: Cameron Moon Date: Wed, 20 Mar 2024 16:15:35 +0000 Subject: [PATCH 4/4] add type to missingSupportCache --- lib/BrowserSelection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/BrowserSelection.js b/lib/BrowserSelection.js index 6cdd703..4dbe1e6 100644 --- a/lib/BrowserSelection.js +++ b/lib/BrowserSelection.js @@ -166,7 +166,7 @@ export default class BrowserSelection { return result; } - /** @readonly */ + /** @type {Map} @readonly */ static #missingSupportCache = new Map(); /** @@ -179,7 +179,7 @@ export default class BrowserSelection { // browserslist only uses `path` if `query` is not provided. const key = query ? JSON.stringify(query) : path; - if (this.#missingSupportCache.has(key)) { + if (key && this.#missingSupportCache.has(key)) { return this.#missingSupportCache.get(key); } @@ -188,7 +188,7 @@ export default class BrowserSelection { browsers: selection.list(), features: selection.compileBrowserSupport(), }; - this.#missingSupportCache.set(key, result); + if (key) this.#missingSupportCache.set(key, result); return result; } }