UNPKG

5.43 kBJavaScriptView Raw
1import browserslist from 'browserslist';
2import * as caniuse from 'caniuse-lite';
3
4import FEATURES from '../data/features.js';
5import { 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
32export 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}