UNPKG

3.75 kBJavaScriptView Raw
1import multimatch from 'multimatch';
2
3import BrowserSelection from './BrowserSelection.js';
4import Detector from './Detector.js';
5
6/** @typedef {import('../data/features.js').FeatureKeys} FeatureKeys */
7
8/**
9 * @typedef {Object} OnFeatureUsageArguments
10 * @prop {FeatureKeys} feature
11 * @prop {import('./BrowserSelection.js').MissingSupportResultStats|undefined} featureData
12 * @prop {import('postcss').Node} usage
13 * @prop {string} message
14 */
15
16/**
17 * @callback OnFeatureUsage
18 * @param {OnFeatureUsageArguments} result
19 * @return {any}
20 */
21
22/**
23 * @typedef {Object} DoIUseOptions
24 * @prop {ConstructorParameters<typeof BrowserSelection>[0]} [browsers]
25 * @prop {FeatureKeys[]} [ignore]
26 * @prop {OnFeatureUsage} [onFeatureUsage]
27 * @prop {string[]} [ignoreFiles]
28 */
29
30export default class DoIUse {
31 static default = null;
32
33 /**
34 * @param {DoIUseOptions} [optionsOrBrowserQuery]
35 */
36 constructor(optionsOrBrowserQuery) {
37 const options = (typeof optionsOrBrowserQuery === 'string')
38 ? { browsers: optionsOrBrowserQuery }
39 : { ...optionsOrBrowserQuery };
40 this.browserQuery = options.browsers;
41 this.onFeatureUsage = options.onFeatureUsage;
42 this.ignoreOptions = options.ignore;
43 this.ignoreFiles = options.ignoreFiles;
44 this.info = this.info.bind(this);
45 this.postcss = this.postcss.bind(this);
46 }
47
48 /**
49 * @param {Object} [options]
50 * @param {ConstructorParameters<typeof BrowserSelection>[1]} [options.from]
51 */
52 info(options = {}) {
53 const { browsers, features } = BrowserSelection.missingSupport(this.browserQuery, options.from);
54
55 return {
56 browsers,
57 features,
58 };
59 }
60
61 /** @type {import('postcss').TransformCallback} */
62 postcss(css, result) {
63 let from;
64 if (css.source && css.source.input) {
65 from = css.source.input.file;
66 }
67 const { features } = BrowserSelection.missingSupport(this.browserQuery, from);
68 // @ts-expect-error Needs cast
69 const detector = new Detector(Object.keys(features));
70
71 return detector.process(css, ({ feature, usage, ignore }) => {
72 if (ignore && ignore.includes(feature)) {
73 return;
74 }
75
76 if (this.ignoreOptions && this.ignoreOptions.includes(feature)) {
77 return;
78 }
79
80 if (!usage.source) {
81 throw new Error('No source?');
82 }
83 if (this.ignoreFiles && multimatch(usage.source.input.from, this.ignoreFiles).length > 0) {
84 return;
85 }
86
87 const data = features[feature];
88 if (!data) {
89 throw new Error('No feature data?');
90 }
91 const messages = [];
92 if (data.missing) {
93 messages.push(`not supported by: ${data.missing}`);
94 }
95 if (data.partial) {
96 messages.push(`only partially supported by: ${data.partial}`);
97 }
98
99 let message = `${data.title} ${messages.join(' and ')} (${feature})`;
100
101 result.warn(message, { node: usage, plugin: 'doiuse' });
102
103 if (this.onFeatureUsage) {
104 if (!usage.source) {
105 throw new Error('No usage source?');
106 }
107 const { start, input } = usage.source;
108 if (!start) {
109 throw new Error('No usage source start?');
110 }
111
112 const map = css.source && css.source.input.map;
113 const mappedStart = map && map.consumer().originalPositionFor(start);
114
115 const file = (mappedStart && mappedStart.source) || input.file || input.from;
116 const line = (mappedStart && mappedStart.line) || start.line;
117 const column = (mappedStart && mappedStart.column) || start.column;
118
119 message = `${file}:${line}:${column}: ${message}`;
120
121 this.onFeatureUsage({
122 feature,
123 featureData: features[feature],
124 usage,
125 message,
126 });
127 }
128 });
129 }
130}