summaryrefslogtreecommitdiff
path: root/node_modules/doiuse/lib/DoIUse.js
blob: 78db53c8c596879c112302ef79c2125049ee23c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import multimatch from 'multimatch';

import BrowserSelection from './BrowserSelection.js';
import Detector from './Detector.js';

/** @typedef {import('../data/features.js').FeatureKeys} FeatureKeys */

/**
 * @typedef {Object} OnFeatureUsageArguments
 * @prop {FeatureKeys} feature
 * @prop {import('./BrowserSelection.js').MissingSupportResultStats|undefined} featureData
 * @prop {import('postcss').Node} usage
 * @prop {string} message
 */

/**
 * @callback OnFeatureUsage
 * @param {OnFeatureUsageArguments} result
 * @return {any}
 */

/**
 * @typedef {Object} DoIUseOptions
 * @prop {ConstructorParameters<typeof BrowserSelection>[0]} [browsers]
 * @prop {FeatureKeys[]} [ignore]
 * @prop {OnFeatureUsage} [onFeatureUsage]
 * @prop {string[]} [ignoreFiles]
 */

export default class DoIUse {
  static default = null;

  /**
   * @param {DoIUseOptions} [optionsOrBrowserQuery]
   */
  constructor(optionsOrBrowserQuery) {
    const options = (typeof optionsOrBrowserQuery === 'string')
      ? { browsers: optionsOrBrowserQuery }
      : { ...optionsOrBrowserQuery };
    this.browserQuery = options.browsers;
    this.onFeatureUsage = options.onFeatureUsage;
    this.ignoreOptions = options.ignore;
    this.ignoreFiles = options.ignoreFiles;
    this.info = this.info.bind(this);
    this.postcss = this.postcss.bind(this);
  }

  /**
   * @param {Object} [options]
   * @param {ConstructorParameters<typeof BrowserSelection>[1]} [options.from]
   */
  info(options = {}) {
    const { browsers, features } = BrowserSelection.missingSupport(this.browserQuery, options.from);

    return {
      browsers,
      features,
    };
  }

  /** @type {import('postcss').TransformCallback} */
  postcss(css, result) {
    let from;
    if (css.source && css.source.input) {
      from = css.source.input.file;
    }
    const { features } = BrowserSelection.missingSupport(this.browserQuery, from);
    // @ts-expect-error Needs cast
    const detector = new Detector(Object.keys(features));

    return detector.process(css, ({ feature, usage, ignore }) => {
      if (ignore && ignore.includes(feature)) {
        return;
      }

      if (this.ignoreOptions && this.ignoreOptions.includes(feature)) {
        return;
      }

      if (!usage.source) {
        throw new Error('No source?');
      }
      if (this.ignoreFiles && multimatch(usage.source.input.from, this.ignoreFiles).length > 0) {
        return;
      }

      const data = features[feature];
      if (!data) {
        throw new Error('No feature data?');
      }
      const messages = [];
      if (data.missing) {
        messages.push(`not supported by: ${data.missing}`);
      }
      if (data.partial) {
        messages.push(`only partially supported by: ${data.partial}`);
      }

      let message = `${data.title} ${messages.join(' and ')} (${feature})`;

      result.warn(message, { node: usage, plugin: 'doiuse' });

      if (this.onFeatureUsage) {
        if (!usage.source) {
          throw new Error('No usage source?');
        }
        const { start, input } = usage.source;
        if (!start) {
          throw new Error('No usage source start?');
        }

        const map = css.source && css.source.input.map;
        const mappedStart = map && map.consumer().originalPositionFor(start);

        const file = (mappedStart && mappedStart.source) || input.file || input.from;
        const line = (mappedStart && mappedStart.line) || start.line;
        const column = (mappedStart && mappedStart.column) || start.column;

        message = `${file}:${line}:${column}: ${message}`;

        this.onFeatureUsage({
          feature,
          featureData: features[feature],
          usage,
          message,
        });
      }
    });
  }
}