| /* This file is a part of @mdn/browser-compat-data |
| * See LICENSE file for more information. */ |
| |
| import { styleText } from 'node:util'; |
| |
| import { compare, validate } from 'compare-versions'; |
| |
| import bcd from '../../index.js'; |
| |
| /** @import {Linter, LinterData} from '../types.js' */ |
| /** @import {Logger} from '../utils.js' */ |
| /** @import {BrowserName, InternalCompatStatement, InternalSimpleSupportStatement, VersionValue} from '../../types/index.js' */ |
| /** @import {InternalSupportBlock, InternalSupportStatement} from '../../types/index.js' */ |
| |
| /* The latest date a range's release can correspond to */ |
| const rangeCutoffDate = '2020-05-19'; |
| |
| /** @type {Record<string, string>} */ |
| const browserTips = { |
| 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.', |
| }; |
| |
| /** |
| * Test to see if the browser allows for the specified version |
| * @param {BrowserName} browser The browser to check |
| * @param {string} category The category of the data |
| * @param {VersionValue} version The version to test |
| * @returns {boolean} Whether the browser allows that version |
| */ |
| const isValidVersion = (browser, category, version) => { |
| if (typeof version === 'string') { |
| if (version === 'preview') { |
| return !!bcd.browsers[browser].preview_name; |
| } |
| return Object.hasOwn( |
| bcd.browsers[browser].releases, |
| version.replace('≤', ''), |
| ); |
| } |
| 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 {InternalSimpleSupportStatement} statement The statement to test |
| * @returns {boolean | null} Whether the version added was earlier than the version removed |
| */ |
| const addedBeforeRemoved = (statement) => { |
| 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 {InternalSupportBlock} supportData The data to test |
| * @param {string} category The category the data |
| * @param {Logger} logger The logger to output errors to |
| * @returns {void} |
| */ |
| const checkVersions = (supportData, category, logger) => { |
| const browsersToCheck = /** @type {BrowserName[]} */ ( |
| Object.keys(bcd.browsers).filter((b) => |
| category === 'webextensions' |
| ? bcd.browsers[b].accepts_webextensions |
| : !!b, |
| ) |
| ); |
| |
| for (const browser of browsersToCheck) { |
| /** @type {InternalSupportStatement | undefined} */ |
| const supportStatement = supportData[browser]; |
| |
| if (!supportStatement) { |
| 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 (!bcd.browsers[browser].upstream) { |
| logger.error( |
| `${styleText('bold', browser)} is set to mirror, however ${styleText('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( |
| `${styleText('bold', `${property}: "${version}"`)} is ${styleText('bold', 'NOT')} a valid version number for ${styleText('bold', browser)}\n Valid ${styleText('bold', browser)} versions are: ${Object.keys(bcd.browsers[browser].releases).join(', ')}, false`, |
| { tip: browserTips[browser] }, |
| ); |
| } |
| |
| if (typeof version === 'string' && version.startsWith('≤')) { |
| const releaseData = |
| bcd.browsers[browser].releases[version.replace('≤', '')]; |
| if ( |
| !releaseData || |
| !releaseData.release_date || |
| releaseData.release_date > rangeCutoffDate |
| ) { |
| logger.error( |
| `${styleText('bold', `${property}: "${version}"`)} is ${styleText('bold', 'NOT')} a valid version number for ${styleText('bold', browser)}\n Ranged values are only allowed for browser versions released on or before ${rangeCutoffDate}. (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( |
| `${styleText('bold', `version_removed: "${statement.version_removed}"`)} must be greater than ${styleText('bold', `version_added: "${statement.version_added}"`)}`, |
| ); |
| } |
| } |
| |
| if ('flags' in statement && !bcd.browsers[browser].accepts_flags) { |
| logger.error( |
| `This browser (${styleText('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( |
| `The data for (${styleText('bold', browser)}) says no support, but contains additional properties that suggest support.`, |
| ); |
| } |
| } |
| |
| if ( |
| Array.isArray(supportStatement) && |
| statement.version_added === false |
| ) { |
| logger.error( |
| `${styleText('bold', browser)} cannot have a ${styleText('bold', 'version_added: false')} in an array of statements.`, |
| ); |
| } |
| |
| if ('version_last' in statement) { |
| logger.error( |
| `${styleText('bold', 'version_last')} is automatically generated and should not be defined manually.`, |
| ); |
| } |
| } |
| } |
| }; |
| |
| /** @type {Linter} */ |
| export default { |
| name: 'Versions', |
| description: 'Test the version numbers of support statements', |
| scope: 'feature', |
| /** |
| * Test the data |
| * @param {Logger} logger The logger to output errors to |
| * @param {LinterData} root The data to test |
| */ |
| check: (logger, { data, path: { category } }) => { |
| checkVersions( |
| /** @type {InternalCompatStatement} */ (data).support, |
| category, |
| logger, |
| ); |
| }, |
| }; |