1 | import browserslist from 'browserslist';
|
2 | import * as caniuse from 'caniuse-lite';
|
3 |
|
4 | import FEATURES from '../data/features.js';
|
5 | import { formatBrowserName } from '../utils/util.js';
|
6 |
|
7 | /**
|
8 | * @template T
|
9 | * @typedef {{missing: T, partial: T}} Filter
|
10 | */
|
11 |
|
12 | /**
|
13 | * @typedef {Object} MissingSupportResultStats
|
14 | * @prop {string} title
|
15 | * @prop {caniuse.Feature} caniuseData
|
16 | * @prop {string} [missing]
|
17 | * @prop {Record<string, Record<string, string>>} [missingData]
|
18 | * @prop {string} [partial]
|
19 | * @prop {Record<string, Record<string, string>>} [partialData]
|
20 | */
|
21 |
|
22 | /**
|
23 | * @typedef {Partial<Record<keyof FEATURES, MissingSupportResultStats>>} BrowserSupportStats
|
24 | */
|
25 |
|
26 | /**
|
27 | * @typedef {Object} MissingSupportResult
|
28 | * @prop {string[][]} browsers
|
29 | * @prop {Partial<Record<keyof FEATURES, MissingSupportResultStats>>} features
|
30 | */
|
31 |
|
32 | export default class BrowserSelection {
|
33 | #list;
|
34 |
|
35 | /**
|
36 | * @param {string | string[]} [query]
|
37 | * @param {string | false} [from]
|
38 | */
|
39 | constructor(query, from) {
|
40 | this.browsersRequest = query;
|
41 | const options = from ? { path: from } : {};
|
42 | this.#list = browserslist(this.browsersRequest, options).map((browser) => browser.split(' '));
|
43 | }
|
44 |
|
45 | /**
|
46 | * @param {string} browser
|
47 | * @param {string} version
|
48 | * @return {string[]|undefined}
|
49 | */
|
50 | test(browser, version) {
|
51 | const versions = version.split('-');
|
52 |
|
53 | if (versions.length === 1) {
|
54 | versions.push(versions[0]);
|
55 | }
|
56 |
|
57 | return this.#list.find(([b, v]) => b === browser && v >= versions[0] && v <= versions[1]);
|
58 | }
|
59 |
|
60 | list() {
|
61 | return [...this.#list];
|
62 | }
|
63 |
|
64 | /**
|
65 | * @param {Record<string, Record<string, string>>} browserStats
|
66 | * @return {string}
|
67 | */
|
68 | static lackingBrowsers(browserStats) {
|
69 | const result = [];
|
70 | for (const browser of Object.keys(browserStats)) {
|
71 | result.push(formatBrowserName(browser, Object.keys(browserStats[browser])));
|
72 | }
|
73 | return result.join(', ');
|
74 | }
|
75 |
|
76 | /**
|
77 | * @param {import('caniuse-lite').StatsByAgentID} stats
|
78 | * @return {Filter<Record<string,Record<string, string>>>}
|
79 | */
|
80 | filterStats(stats) {
|
81 | /** @type {Filter<Record<string,Record<string, string>>>} */
|
82 | const result = { missing: {}, partial: {} };
|
83 | for (const [browser, versions] of Object.entries(stats)) {
|
84 | /** @type {Filter<Record<string,string>>} */
|
85 | const feature = { missing: {}, partial: {} };
|
86 | for (const [version, support] of Object.entries(versions)) {
|
87 | const selected = this.test(browser, version);
|
88 |
|
89 | // check if browser is NOT fully (i.e., don't have 'y' in their stats) supported
|
90 | if (!selected) continue;
|
91 | if ((/(^|\s)y($|\s)/.test(support))) continue;
|
92 | // when it's not partially supported ('a'), it's missing
|
93 | const type = (/(^|\s)a($|\s)/.test(support) ? 'partial' : 'missing');
|
94 |
|
95 | if (!feature[type]) {
|
96 | feature[type] = {};
|
97 | }
|
98 |
|
99 | feature[type][selected[1]] = support;
|
100 | }
|
101 | if (Object.keys(feature.missing).length > 0) {
|
102 | result.missing[browser] = feature.missing;
|
103 | }
|
104 |
|
105 | if (Object.keys(feature.partial).length > 0) {
|
106 | result.partial[browser] = feature.partial;
|
107 | }
|
108 | }
|
109 | return result;
|
110 | }
|
111 |
|
112 | /**
|
113 | * Get data on CSS features not supported by the given autoprefixer-like
|
114 | * browser selection.
|
115 | * @return {BrowserSupportStats} `features` is an array of:
|
116 | * ```
|
117 | * {
|
118 | * 'feature-name': {
|
119 | * title: 'Title of feature'
|
120 | * missing: "IE (8), Chrome (31)"
|
121 | * missingData: {
|
122 | * // map of browser -> version -> (lack of)support code
|
123 | * ie: { '8': 'n' },
|
124 | * chrome: { '31': 'n' }
|
125 | * }
|
126 | * partialData: {
|
127 | * // map of browser -> version -> (partial)support code
|
128 | * ie: { '7': 'a' },
|
129 | * ff: { '29': 'a #1' }
|
130 | * }
|
131 | * caniuseData: {
|
132 | * // caniuse-db json data for this feature
|
133 | * }
|
134 | * },
|
135 | * 'feature-name-2': {} // etc.
|
136 | * }
|
137 | * ```
|
138 | *
|
139 | * `feature-name` is a caniuse-db slug.
|
140 | */
|
141 | compileBrowserSupport() {
|
142 | /** @type {Partial<Record<keyof FEATURES, MissingSupportResultStats>>} */
|
143 | const result = {};
|
144 |
|
145 | for (const key of Object.keys(FEATURES)) {
|
146 | const feature = /** @type {keyof FEATURES} */ (key);
|
147 | const packedFeature = caniuse.features[feature];
|
148 | const featureData = caniuse.feature(packedFeature);
|
149 | const lackData = this.filterStats(featureData.stats);
|
150 | const missingData = lackData.missing;
|
151 | const partialData = lackData.partial;
|
152 | // browsers with missing or partial support for this feature
|
153 | const missing = BrowserSelection.lackingBrowsers(missingData);
|
154 | const partial = BrowserSelection.lackingBrowsers(partialData);
|
155 |
|
156 | if (missing.length > 0 || partial.length > 0) {
|
157 | result[feature] = {
|
158 | title: featureData.title,
|
159 | caniuseData: featureData,
|
160 | ...(missing.length > 0 ? { missingData, missing } : null),
|
161 | ...(partial.length > 0 ? { partialData, partial } : null),
|
162 | };
|
163 | }
|
164 | }
|
165 |
|
166 | return result;
|
167 | }
|
168 |
|
169 | /**
|
170 | * @see BrowserSelection.compileBrowserSupport
|
171 | * @param {ConstructorParameters<typeof this>} constructorParameters
|
172 | * @return {MissingSupportResult} `{browsers, features}`
|
173 | */
|
174 | static missingSupport(...constructorParameters) {
|
175 | const selection = new BrowserSelection(...constructorParameters);
|
176 | return {
|
177 | browsers: selection.list(),
|
178 | features: selection.compileBrowserSupport(),
|
179 | };
|
180 | }
|
181 | }
|