UNPKG

4.34 kBJavaScriptView Raw
1import FEATURES from '../data/features.js';
2import { performFeatureCheck } from '../utils/util.js';
3
4/**
5 * @typedef DetectorCallbackArgument
6 * @prop {!import('postcss').ChildNode} usage
7 * @prop {keyof FEATURES} feature
8 * @prop {(keyof FEATURES & string)[]} ignore
9 */
10
11/**
12 * @callback DetectorCallback
13 * @param {DetectorCallbackArgument} result
14 * @return {any}
15 */
16
17const PLUGIN_OPTION_COMMENT = 'doiuse-';
18const DISABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}disable`;
19const ENABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}enable`;
20
21/**
22 * Strip the contents of url literals so they aren't matched
23 * by our naive substring matching.
24 * @param {string} input
25 * @return {string}
26 */
27function stripUrls(input) {
28 return input.replaceAll(/url\([^)]*\)/g, 'url()');
29}
30
31/**
32 * Detect the use of any of a given list of CSS features.
33 * ```
34 * var detector = new Detector(featureList)
35 * detector.process(css, cb)
36 * ```
37 *
38 * `featureList`: an array of feature slugs (see caniuse-db)
39 * `cb`: a callback that gets called for each usage of one of the given features,
40 * called with an argument like:
41 * ```
42 * {
43 * usage: {} // postcss node where usage was found
44 * feature: {} // caniuse-db feature slug
45 * ignore: {} // caniuse-db feature to ignore in current file
46 * }
47 * ```
48 */
49export default class Detector {
50 /**
51 * @param {(keyof FEATURES & string)[]} featureList an array of feature slugs (see caniuse-db)
52 */
53 constructor(featureList) {
54 /** @type {Partial<FEATURES>} */
55 this.features = {};
56 for (const feature of featureList) {
57 if (FEATURES[feature]) {
58 this.features[feature] = FEATURES[feature];
59 }
60 }
61 /** @type {(keyof FEATURES & string)[]} */
62 this.ignore = [];
63 }
64
65 /**
66 * @param {import('postcss').Comment} comment
67 * @return {void}
68 */
69 comment(comment) {
70 const text = comment.text.toLowerCase();
71
72 if (!text.startsWith(PLUGIN_OPTION_COMMENT)) return;
73 const option = text.split(' ', 1)[0];
74 const value = text.replace(option, '').trim();
75
76 switch (option) {
77 case DISABLE_FEATURE_COMMENT: {
78 if (value === '') {
79 // @ts-expect-error Skip cast
80 this.ignore = Object.keys(this.features);
81 } else {
82 for (const feat of value.split(',')) {
83 /** @type {any} */
84 const f = feat.trim();
85 if (!this.ignore.includes(f)) {
86 this.ignore.push(f);
87 }
88 }
89 }
90 break;
91 }
92 case ENABLE_FEATURE_COMMENT: {
93 if (value === '') {
94 this.ignore = [];
95 } else {
96 const without = new Set(value.split(',').map((feat) => feat.trim()));
97 this.ignore = this.ignore.filter((index) => !without.has(index));
98 }
99 break;
100 }
101 default:
102 }
103 }
104
105 /**
106 * @param {import('postcss').Container} node
107 * @param {DetectorCallback} callback
108 * @return {void}
109 */
110 node(node, callback) {
111 node.each((child) => {
112 if (child.type === 'comment') {
113 this.comment(child);
114 return;
115 }
116
117 for (const [feat] of Object.entries(this.features).filter(([, featValue]) => {
118 if (!featValue) return false;
119 if (typeof featValue === 'function') {
120 return featValue(child);
121 }
122 if (Array.isArray(featValue)) {
123 return featValue.some((function_) => function_(child));
124 }
125 if (child.type !== 'decl') {
126 return false;
127 }
128
129 return Object.entries(featValue).some(([property, value]) => {
130 if (property !== '' && property !== child.prop) return false;
131 if (value === true) return true;
132 if (value === false) return false;
133 return performFeatureCheck(value, stripUrls(child.value));
134 });
135 })) {
136 const feature = /** @type {keyof FEATURES} */ (feat);
137 callback({ usage: child, feature, ignore: this.ignore });
138 }
139 if (child.type !== 'decl') {
140 this.node(child, callback);
141 }
142 });
143 }
144
145 /**
146 * @param {import('postcss').Root} node
147 * @param {DetectorCallback} callback
148 * @return {void}
149 */
150 process(node, callback) {
151 // Reset ignoring rules specified by inline comments per each file
152 this.ignore = [];
153
154 // Recursively walk nodes in file
155 this.node(node, callback);
156 }
157}