-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
Copy pathtest-versions.ts
266 lines (240 loc) · 8.58 KB
/
test-versions.ts
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/* This file is a part of @mdn/browser-compat-data
* See LICENSE file for more information. */
import { compare, validate } from 'compare-versions';
import chalk from 'chalk-template';
import { Linter, Logger, LinterData } from '../utils.js';
import {
BrowserName,
SimpleSupportStatement,
VersionValue,
} from '../../types/types.js';
import {
InternalSupportBlock,
InternalSupportStatement,
} from '../../types/index';
import bcd from '../../index.js';
const { browsers } = bcd;
const now = new Date();
/* The latest date a range's release can correspond to */
const rangeCutoffDate = new Date(
now.getFullYear() - 4,
now.getMonth(),
now.getDate(),
);
const browserTips: Record<string, string> = {
nodejs:
'BCD does not record every individual version of Node.js, only the releases that update V8 engine versions or add a new feature. You may need to add the release to browsers/nodejs.json.',
safari_ios:
'The version numbers for Safari for iOS are based upon the iOS version number rather than the Safari version number. Maybe you are trying to use the desktop version number?',
opera_android:
'Blink editions of Opera Android and Opera desktop were the Chrome version number minus 13, up until Opera Android 43 when they began skipping Chrome versions. Please double-check browsers/opera_android.json to make sure you are using the correct versions.',
};
const realValuesTargetBrowsers = [
'chrome',
'chrome_android',
'edge',
'firefox',
'firefox_android',
'opera',
'opera_android',
'safari',
'safari_ios',
'samsunginternet_android',
'webview_android',
];
const realValuesRequired: Record<string, string[]> = {
api: realValuesTargetBrowsers,
css: realValuesTargetBrowsers,
html: realValuesTargetBrowsers,
http: realValuesTargetBrowsers,
svg: realValuesTargetBrowsers,
javascript: [...realValuesTargetBrowsers, 'nodejs', 'deno'],
manifests: realValuesTargetBrowsers,
mathml: realValuesTargetBrowsers,
webassembly: realValuesTargetBrowsers,
webdriver: realValuesTargetBrowsers,
webextensions: [],
};
/**
* Test to see if the browser allows for the specified version
* @param browser The browser to check
* @param category The category of the data
* @param version The version to test
* @returns Whether the browser allows that version
*/
const isValidVersion = (
browser: BrowserName,
category: string,
version: VersionValue,
): boolean => {
if (typeof version === 'string') {
if (version === 'preview') {
return !!browsers[browser].preview_name;
}
return Object.hasOwn(browsers[browser].releases, version.replace('≤', ''));
} else if (
realValuesRequired[category].includes(browser) &&
version !== false
) {
return false;
}
return true;
};
/**
* Checks if the version number of version_removed is greater than or equal to
* that of version_added, assuming they are both version strings. If either one
* is not a valid version string, return null.
* @param statement The statement to test
* @returns Whether the version added was earlier than the version removed
*/
const addedBeforeRemoved = (
statement: SimpleSupportStatement,
): boolean | null => {
if (
typeof statement.version_added !== 'string' ||
typeof statement.version_removed !== 'string'
) {
return false;
}
// In order to ensure that the versions could be displayed without the "≤"
// markers and still make sense, compare the versions without them. This
// means that combinations like version_added: "≤37" + version_removed: "37"
// are not allowed, even though this can be technically correct.
const added = statement.version_added.replace('≤', '');
const removed = statement.version_removed.replace('≤', '');
if (!validate(added) || !validate(removed)) {
return null;
}
if (added === 'preview' && removed === 'preview') {
return false;
}
if (added === 'preview' && removed !== 'preview') {
return false;
}
if (added !== 'preview' && removed === 'preview') {
return true;
}
return compare(added, removed, '<');
};
/**
* Check the data for any errors in provided versions
* @param supportData The data to test
* @param category The category the data
* @param logger The logger to output errors to
*/
const checkVersions = (
supportData: InternalSupportBlock,
category: string,
logger: Logger,
): void => {
const browsersToCheck = Object.keys(browsers).filter((b) =>
category === 'webextensions' ? browsers[b].accepts_webextensions : !!b,
) as BrowserName[];
for (const browser of browsersToCheck) {
const supportStatement: InternalSupportStatement | undefined =
supportData[browser];
if (!supportStatement) {
if (realValuesRequired[category].includes(browser)) {
logger.error(chalk`{red {bold ${browser}} must be defined}`);
}
continue;
}
for (const statement of Array.isArray(supportStatement)
? supportStatement
: [supportStatement]) {
if (statement === 'mirror') {
// If the data is to be mirrored, make sure it is mirrorable
if (!browsers[browser].upstream) {
logger.error(
chalk`{bold ${browser}} is set to mirror, however {bold ${browser}} does not have an upstream browser.`,
);
}
continue;
}
for (const property of ['version_added', 'version_removed']) {
const version = statement[property];
if (property == 'version_removed' && version === undefined) {
// version_removed is optional.
continue;
}
if (!isValidVersion(browser, category, version)) {
logger.error(
chalk`{bold ${property}: "${version}"} is {bold NOT} a valid version number for {bold ${browser}}\n Valid {bold ${browser}} versions are: ${Object.keys(
browsers[browser].releases,
).join(', ')}`,
{ tip: browserTips[browser] },
);
}
if (typeof version === 'string' && version.startsWith('≤')) {
const releaseData =
browsers[browser].releases[version.replace('≤', '')];
if (
!releaseData ||
!releaseData.release_date ||
new Date(releaseData.release_date) > rangeCutoffDate
) {
logger.error(
chalk`{bold ${property}: "${version}"} is {bold NOT} a valid version number for {bold ${browser}}\n Ranged values are only allowed for browser versions released on or before ${rangeCutoffDate.toDateString()}. (Ranged values are also not allowed for browser versions without a known release date.)`,
);
}
}
}
if ('version_added' in statement && 'version_removed' in statement) {
if (
typeof statement.version_added === 'string' &&
typeof statement.version_removed === 'string' &&
addedBeforeRemoved(statement) === false
) {
logger.error(
chalk`{bold version_removed: "${statement.version_removed}"} must be greater than {bold version_added: "${statement.version_added}"}`,
);
}
}
if ('flags' in statement && !browsers[browser].accepts_flags) {
logger.error(
chalk`This browser ({bold ${browser}}) does not support flags, so support cannot be behind a flag for this feature.`,
);
}
if (statement.version_added === false) {
if (
Object.keys(statement).some(
(k) => !['version_added', 'notes', 'impl_url'].includes(k),
)
) {
logger.error(
chalk`The data for ({bold ${browser}}) says no support, but contains additional properties that suggest support.`,
);
}
}
if (
Array.isArray(supportStatement) &&
statement.version_added === false
) {
logger.error(
chalk`{bold ${browser}} cannot have a {bold version_added: false} in an array of statements.`,
);
}
if ('version_last' in statement) {
logger.error(
chalk`{bold version_last} is automatically generated and should not be defined manually.`,
);
}
}
}
};
export default {
name: 'Versions',
description: 'Test the version numbers of support statements',
scope: 'feature',
/**
* Test the data
* @param logger The logger to output errors to
* @param root The data to test
* @param root.data The data to test
* @param root.path The path of the data
* @param root.path.category The category the data belongs to
*/
check: (logger: Logger, { data, path: { category } }: LinterData) => {
checkVersions(data.support, category, logger);
},
} as Linter;