diff --git a/schemas/collector-report.md b/schemas/collector-report.md deleted file mode 100644 index 0e0444d208da1f..00000000000000 --- a/schemas/collector-report.md +++ /dev/null @@ -1,61 +0,0 @@ -# The collector report JSON schema - -This document details the structure of reports produced by https://mdn-bcd-collector.appspot.com/, https://mdn-bcd-collector.gooborg.com/, and potentially other tooling. - -## JSON structure - -Below is an example of the report data: - -```json -{ - "__version": "9.3.1", - "results": { - "https://mdn-bcd-collector.example/tests/": [ - { - "exposure": "ServiceWorker", - "name": "api.AbortController.AbortController", - "result": false - }, - { - "exposure": "Window", - "message": "threw TypeError: Array.isArray is not a function", - "name": "api.AbortController.AbortController", - "result": null - } - ] - }, - "userAgent": "Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8) Gecko/20051111 Firefox/1.5" -} -``` - -## Properties - -### `exposure` - -The `exposure` string is a required property. It must be one of four values whether the test was executed in a window (`"Window"`), dedicated worker (`"Worker"`), shared worker (`"SharedWorker"`) or service worker (`"ServiceWorker"`). - -### `message` - -The `message` string is a required property if `result` is `null` and optional if `result` is a `boolean`. - -The message can be an error message from the browser or from the test framework, for example "threw TypeError: Failed to construct 'Notification': Illegal constructor." or "Browser does not support detection methods" respectively. - -### `name` - -The `name` string is a required property and represents the feature identifier the result is for. The identifier is a dot-separated path commonly used in BCD. For example, "javascript.builtins.Array.at" refers to the feature at `{"javascript": {"builtins": {"Array": {"at": {"__compat": ...}}}}}`. - -### `result` - -The `result` boolean or `null` value is a required property indicating if the tested feature under the given `exposure` is present, not present, or cannot be determined by the test. - -### `__version` - -The `__version` string is a required property and the version of mdn-bcd-collector that collected the report. - -### `results` - -The `results` object is a required property and maps the collection page endpoint to an array of test results. There must be at least one key in the map. - -### `userAgent` - -The `userAgent` string is a required property and states the user agent of the tested browser the results were collected from. diff --git a/schemas/collector-report.schema.json b/schemas/collector-report.schema.json deleted file mode 100644 index 3595b15073bf80..00000000000000 --- a/schemas/collector-report.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - - "definitions": { - "exposure": { - "type": "string", - "enum": ["ServiceWorker", "SharedWorker", "Window", "Worker"] - }, - - "feature_name": { - "type": "string", - "pattern": "^[a-zA-Z_0-9-$@.]*$", - "description": "Dot separated identifier path.\n\n@example \"javascript.builtins.Array.at\"" - }, - - "result_message": { - "type": "string", - "description": "An error message from the browser or from the test framework.\n\n@example \"threw TypeError: Failed to construct 'Notification': Illegal constructor.\"\n@example \"Testing CanvasRenderingContext2D in workers is not yet implemented\"\n@example \"Browser does not support detection methods\"" - }, - - "collector_result": { - "oneOf": [ - { - "type": "object", - "properties": { - "exposure": { "$ref": "#/definitions/exposure" }, - "message": { "$ref": "#/definitions/result_message" }, - "name": { "$ref": "#/definitions/feature_name" }, - "result": { "type": "boolean" } - }, - "additionalProperties": false, - "required": ["exposure", "name", "result"] - }, - { - "type": "object", - "properties": { - "exposure": { "$ref": "#/definitions/exposure" }, - "message": { "$ref": "#/definitions/result_message" }, - "name": { "$ref": "#/definitions/feature_name" }, - "result": { "type": "null" } - }, - "additionalProperties": false, - "required": ["exposure", "message", "name", "result"] - } - ] - } - }, - - "title": "CollectorReport", - "type": "object", - "properties": { - "__version": { - "type": "string", - "pattern": "^[0-9.]+$", - "description": "Version of mdn-bcd-collector that collected the report." - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - }, - "results": { - "type": "object", - "patternProperties": { - "^.*://.*$": { - "type": "array", - "items": { - "$ref": "#/definitions/collector_result" - } - } - }, - "minProperties": 1 - }, - "userAgent": { - "type": "string", - "description": "User agent of tested browser." - } - }, - "required": ["__version", "results", "userAgent"], - "additionalProperties": false -} diff --git a/scripts/update.test.ts b/scripts/update.test.ts deleted file mode 100644 index 01fccd167813dc..00000000000000 --- a/scripts/update.test.ts +++ /dev/null @@ -1,1851 +0,0 @@ -/* This file is a part of @mdn/browser-compat-data - * See LICENSE file for more information. */ - -import assert from 'node:assert'; - -import sinon from 'sinon'; -import _minimatch from 'minimatch'; - -import { Browsers, CompatData, Identifier } from '../types/types'; - -import { - ManualOverride, - Report, - findEntry, - getSupportMap, - getSupportMatrix, - inferSupportStatements, - logger, - splitRange, - update, -} from './update.js'; - -const { Minimatch } = _minimatch; - -const clone = (value) => JSON.parse(JSON.stringify(value)); -const chromeAndroid86UaString = - 'Mozilla/5.0 (Linux; Android 10; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.5112.97 Mobile Safari/537.36'; -const firefox92UaString = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:92.0) Gecko/20100101 Firefox/92.0'; - -const overrides = [ - ['css.properties.font-family', 'safari', '5.1', false], - ['css.properties.font-family', 'chrome', '83', false], - ['css.properties.font-face', 'chrome', '*', null], - ['css.properties.font-style', 'chrome', '82-84', false], -] as ManualOverride[]; - -const bcd = { - api: { - AbortController: { - __compat: { - support: { - chrome: { version_added: '80' }, - safari: { version_added: null }, - }, - }, - AbortController: { - __compat: { support: { chrome: { version_added: null } } }, - }, - abort: { - __compat: { support: { chrome: { version_added: '85' } } }, - }, - dummy: { - __compat: { support: { chrome: { version_added: null } } }, - }, - signal: { - __compat: { support: { chrome: { version_added: null } } }, - }, - }, - AudioContext: { - __compat: { - support: { - chrome: [ - { - version_added: null, - }, - { - version_added: '1', - prefix: 'webkit', - }, - ], - }, - }, - close: { - __compat: { support: {} }, - }, - }, - DeprecatedInterface: { - __compat: { support: { chrome: { version_added: null } } }, - }, - DummyAPI: { - __compat: { support: { chrome: { version_added: null } } }, - dummy: { - __compat: { support: { chrome: { version_added: null } } }, - }, - }, - ExperimentalInterface: { - __compat: { - support: { - chrome: [ - { - version_added: '70', - notes: 'Not supported on Windows XP.', - }, - { - version_added: '64', - version_removed: '70', - flags: {}, - notes: 'Not supported on Windows XP.', - }, - { - version_added: '50', - version_removed: '70', - alternative_name: 'TryingOutInterface', - notes: 'Not supported on Windows XP.', - }, - ], - }, - }, - }, - UnflaggedInterface: { - __compat: { - support: { - chrome: [ - { - version_added: '83', - flags: {}, - notes: 'Not supported on Windows XP.', - }, - ], - }, - }, - }, - UnprefixedInterface: { - __compat: { - support: { - chrome: [ - { - version_added: '83', - prefix: 'webkit', - notes: 'Not supported on Windows XP.', - }, - ], - }, - }, - }, - NullAPI: { - __compat: { support: { chrome: { version_added: '80' } } }, - }, - RemovedInterface: { - __compat: { support: { chrome: { version_added: null } } }, - }, - SuperNewInterface: { - __compat: { support: { chrome: { version_added: '100' } } }, - }, - }, - browsers: { - chrome: { name: 'Chrome', releases: { 82: {}, 83: {}, 84: {}, 85: {} } }, - chrome_android: { name: 'Chrome Android', releases: { 85: {} } }, - edge: { name: 'Edge', releases: { 16: {}, 84: {} } }, - safari: { name: 'Safari', releases: { 13: {}, 13.1: {}, 14: {} } }, - safari_ios: { - name: 'iOS Safari', - releases: { 13: {}, 13.3: {}, 13.4: {}, 14: {} }, - }, - samsunginternet_android: { - name: 'Samsung Internet', - releases: { - '10.0': {}, - 10.2: {}, - '11.0': {}, - 11.2: {}, - '12.0': {}, - 12.1: {}, - }, - }, - }, - css: { - properties: { - 'font-family': { - __compat: { support: { chrome: { version_added: null } } }, - }, - 'font-face': { - __compat: { support: { chrome: { version_added: null } } }, - }, - 'font-style': { - __compat: { support: { chrome: { version_added: null } } }, - }, - }, - }, - javascript: { - builtins: { - Array: { - __compat: { support: { chrome: { version_added: null } } }, - }, - Date: { - __compat: { support: { chrome: { version_added: null } } }, - }, - }, - }, -} as any as CompatData; - -const reports: Report[] = [ - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - { - name: 'api.AbortController.abort', - exposure: 'Window', - result: null, - }, - { - name: 'api.AbortController.AbortController', - exposure: 'Window', - result: false, - }, - { - name: 'api.AudioContext', - exposure: 'Window', - result: false, - }, - { - name: 'api.AudioContext.close', - exposure: 'Window', - result: null, - message: 'threw ReferenceError: AbortController is not defined', - }, - { - name: 'api.DeprecatedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.ExperimentalInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.UnflaggedInterface', - exposure: 'Window', - result: null, - }, - { - name: 'api.UnprefixedInterface', - exposure: 'Window', - result: null, - }, - { - name: 'api.NullAPI', - exposure: 'Window', - result: null, - }, - { - name: 'api.RemovedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.SuperNewInterface', - exposure: 'Window', - result: false, - }, - { - name: 'css.properties.font-family', - exposure: 'Window', - result: true, - }, - { - name: 'css.properties.font-face', - exposure: 'Window', - result: true, - }, - { - name: 'css.properties.font-style', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36', - }, - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - { - name: 'api.AbortController.abort', - exposure: 'Window', - result: false, - }, - { - name: 'api.AbortController.abort', - exposure: 'Worker', - result: true, - }, - { - name: 'api.AbortController.AbortController', - exposure: 'Window', - result: false, - }, - { - name: 'api.AudioContext', - exposure: 'Window', - result: false, - }, - { - name: 'api.AudioContext.close', - exposure: 'Window', - result: null, - message: 'threw ReferenceError: AbortController is not defined', - }, - { - name: 'api.DeprecatedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.ExperimentalInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.UnflaggedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.UnprefixedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.NewInterfaceNotInBCD', - exposure: 'Window', - result: false, - }, - { - name: 'api.NullAPI', - exposure: 'Window', - result: null, - }, - { - name: 'api.RemovedInterface', - exposure: 'Window', - result: false, - }, - { - name: 'api.SuperNewInterface', - exposure: 'Window', - result: false, - }, - { - name: 'css.properties.font-family', - exposure: 'Window', - result: true, - }, - { - name: 'css.properties.font-face', - exposure: 'Window', - result: true, - }, - { - name: 'css.properties.font-style', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36', - }, - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - { - name: 'api.AbortController.abort', - exposure: 'Window', - result: true, - }, - { - name: 'api.AbortController.AbortController', - exposure: 'Window', - result: true, - }, - { - name: 'api.AudioContext', - exposure: 'Window', - result: true, - }, - { - name: 'api.AudioContext.close', - exposure: 'Window', - result: true, - }, - { - name: 'api.DeprecatedInterface', - exposure: 'Window', - result: false, - }, - { - name: 'api.ExperimentalInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.UnflaggedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.UnprefixedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.NewInterfaceNotInBCD', - exposure: 'Window', - result: true, - }, - { - name: 'api.NullAPI', - exposure: 'Window', - result: null, - }, - { - name: 'api.RemovedInterface', - exposure: 'Window', - result: true, - }, - { - name: 'api.SuperNewInterface', - exposure: 'Window', - result: false, - }, - { - name: 'css.properties.font-family', - exposure: 'Window', - result: true, - }, - { - name: 'css.properties.font-face', - exposure: 'Window', - result: true, - }, - { - name: 'css.properties.font-style', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36', - }, - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: false, - }, - ], - }, - userAgent: - 'Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 YaBrowser/17.6.1.749 Yowser/2.5 Safari/537.36', - }, - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: false, - }, - ], - }, - userAgent: - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1000.1.4183.83 Safari/537.36', - }, - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15', - }, - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: false, - }, - ], - }, - userAgent: 'node-superagent/1.2.3', - }, -]; - -describe('BCD updater', () => { - describe('findEntry', () => { - it('equal', () => { - assert.strictEqual( - findEntry(bcd as any, 'api.AbortController'), - bcd.api.AbortController, - ); - }); - - it('no path', () => { - assert.strictEqual(findEntry(bcd as any, ''), null); - }); - - it('invalid path', () => { - assert.strictEqual(findEntry(bcd as any, 'api.MissingAPI'), undefined); - }); - }); - - describe('getSupportMap', () => { - it('normal', () => { - assert.deepEqual( - getSupportMap(reports[0]), - new Map([ - ['api.AbortController', true], - ['api.AbortController.abort', null], - ['api.AbortController.AbortController', false], - ['api.AudioContext', false], - ['api.AudioContext.close', false], - ['api.DeprecatedInterface', true], - ['api.ExperimentalInterface', true], - ['api.UnflaggedInterface', null], - ['api.UnprefixedInterface', null], - ['api.NullAPI', null], - ['api.RemovedInterface', true], - ['api.SuperNewInterface', false], - ['css.properties.font-family', true], - ['css.properties.font-face', true], - ['css.properties.font-style', true], - ]), - ); - }); - - it('support in only one exposure', () => { - assert.deepEqual( - getSupportMap(reports[1]), - new Map([ - ['api.AbortController', true], - ['api.AbortController.abort', true], - ['api.AbortController.AbortController', false], - ['api.AudioContext', false], - ['api.AudioContext.close', false], - ['api.DeprecatedInterface', true], - ['api.ExperimentalInterface', true], - ['api.UnflaggedInterface', true], - ['api.UnprefixedInterface', true], - ['api.NewInterfaceNotInBCD', false], - ['api.NullAPI', null], - ['api.RemovedInterface', false], - ['api.SuperNewInterface', false], - ['css.properties.font-family', true], - ['css.properties.font-face', true], - ['css.properties.font-style', true], - ]), - ); - }); - - it('no results', () => { - assert.throws( - () => { - getSupportMap({ - __version: 'test', - results: {}, - userAgent: 'abc/1.2.3-beta', - }); - }, - { message: 'Report for "abc/1.2.3-beta" has no results!' }, - ); - }); - }); - - describe('getSupportMatrix', () => { - beforeEach(() => { - sinon.stub(logger, 'warn'); - }); - - it('normal', () => { - assert.deepEqual( - getSupportMatrix(reports, bcd.browsers, overrides), - new Map([ - [ - 'api.AbortController', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', true], - ['84', true], - ['85', true], - ]), - ], - [ - 'safari', - new Map([ - ['13', null], - ['13.1', true], - ['14', null], - ]), - ], - ]), - ], - [ - 'api.AbortController.abort', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', null], - ['84', true], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.AbortController.AbortController', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', false], - ['84', false], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.AudioContext', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', false], - ['84', false], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.AudioContext.close', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', false], - ['84', false], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.DeprecatedInterface', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', true], - ['84', true], - ['85', false], - ]), - ], - ]), - ], - [ - 'api.ExperimentalInterface', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', true], - ['84', true], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.UnflaggedInterface', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', null], - ['84', true], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.UnprefixedInterface', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', null], - ['84', true], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.NewInterfaceNotInBCD', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', null], - ['84', false], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.NullAPI', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', null], - ['84', null], - ['85', null], - ]), - ], - ]), - ], - [ - 'api.RemovedInterface', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', true], - ['84', false], - ['85', true], - ]), - ], - ]), - ], - [ - 'api.SuperNewInterface', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', false], - ['84', false], - ['85', false], - ]), - ], - ]), - ], - [ - 'css.properties.font-family', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', false], - ['84', true], - ['85', true], - ]), - ], - ]), - ], - [ - 'css.properties.font-face', - new Map([ - [ - 'chrome', - new Map([ - ['82', null], - ['83', null], - ['84', null], - ['85', null], - ]), - ], - ]), - ], - [ - 'css.properties.font-style', - new Map([ - [ - 'chrome', - new Map([ - ['82', false], - ['83', false], - ['84', false], - ['85', true], - ]), - ], - ]), - ], - ]), - ); - - assert.ok( - (logger.warn as any).calledWith( - 'Ignoring unknown browser Yandex 17.6 (Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 YaBrowser/17.6.1.749 Yowser/2.5 Safari/537.36)', - ), - ); - assert.ok( - (logger.warn as any).calledWith( - 'Ignoring unknown Chrome version 1000.1 (Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1000.1.4183.83 Safari/537.36)', - ), - ); - assert.ok( - (logger.warn as any).calledWith( - 'Unable to parse browser from UA node-superagent/1.2.3', - ), - ); - }); - - it('Invalid results', () => { - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: 87 as any, - }, - ], - }, - userAgent: - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36', - }; - - assert.throws( - () => { - getSupportMatrix([report], bcd.browsers, overrides); - }, - { message: 'result not true/false/null; got 87' }, - ); - }); - - afterEach(() => { - (logger.warn as any).restore(); - }); - }); - - describe('inferSupportStatements', () => { - const expectedResults = { - 'api.AbortController': { - chrome: [{ version_added: '≤83' }], - safari: [{ version_added: '≤13.1' }], - }, - 'api.AbortController.abort': { chrome: [{ version_added: '≤84' }] }, - 'api.AbortController.AbortController': { - chrome: [{ version_added: '85' }], - }, - 'api.AudioContext': { chrome: [{ version_added: '85' }] }, - 'api.AudioContext.close': { chrome: [{ version_added: '85' }] }, - 'api.DeprecatedInterface': { - chrome: [{ version_added: '≤83', version_removed: '85' }], - }, - 'api.ExperimentalInterface': { chrome: [{ version_added: '≤83' }] }, - 'api.UnflaggedInterface': { chrome: [{ version_added: '≤84' }] }, - 'api.UnprefixedInterface': { chrome: [{ version_added: '≤84' }] }, - 'api.NewInterfaceNotInBCD': { chrome: [{ version_added: '85' }] }, - 'api.NullAPI': { chrome: [] }, - 'api.RemovedInterface': { - chrome: [ - { version_added: '≤83', version_removed: '84' }, - { version_added: '85' }, - ], - }, - 'api.SuperNewInterface': { - chrome: [{ version_added: false }], - }, - 'css.properties.font-family': { chrome: [{ version_added: '84' }] }, - 'css.properties.font-face': { chrome: [] }, - 'css.properties.font-style': { chrome: [{ version_added: '85' }] }, - }; - - const supportMatrix = getSupportMatrix(reports, bcd.browsers, overrides); - for (const [path, browserMap] of supportMatrix.entries()) { - for (const [browser, versionMap] of browserMap.entries()) { - it(`${path}: ${browser}`, () => { - assert.deepEqual( - inferSupportStatements(versionMap), - expectedResults[path][browser], - ); - }); - } - } - - it('Invalid results', () => { - const versionMap = new Map([ - ['82', null], - ['83', 87 as any], - ['84', true], - ['85', true], - ]); - - assert.throws( - () => { - inferSupportStatements(versionMap); - }, - { message: 'result not true/false/null; got 87' }, - ); - }); - - it('non-contiguous data, support added', () => { - const versionMap = new Map([ - ['82', false], - ['83', null], - ['84', true], - ]); - - assert.deepEqual(inferSupportStatements(versionMap), [ - { - version_added: '82> ≤84', - }, - ]); - }); - - it('non-contiguous data, support removed', () => { - const versionMap = new Map([ - ['82', true], - ['83', null], - ['84', false], - ]); - - assert.deepEqual(inferSupportStatements(versionMap), [ - { - version_added: '82', - version_removed: '82> ≤84', - }, - ]); - }); - }); - - describe('splitRange', () => { - it('fails for single versions', () => { - assert.throws( - () => { - splitRange('23'); - }, - { message: 'Unrecognized version range value: "23"' }, - ); - }); - }); - - describe('update', () => { - const supportMatrix = getSupportMatrix(reports, bcd.browsers, overrides); - let bcdCopy; - - beforeEach(() => { - bcdCopy = clone(bcd); - }); - - it('normal', () => { - update(bcdCopy, supportMatrix, {}); - assert.deepEqual(bcdCopy, { - api: { - AbortController: { - __compat: { - support: { - chrome: { version_added: '80' }, - safari: { version_added: '≤13.1' }, - }, - }, - AbortController: { - __compat: { support: { chrome: { version_added: '85' } } }, - }, - abort: { - __compat: { support: { chrome: { version_added: '≤84' } } }, - }, - dummy: { - __compat: { support: { chrome: { version_added: null } } }, - }, - signal: { - __compat: { support: { chrome: { version_added: null } } }, - }, - }, - AudioContext: { - __compat: { - support: { - chrome: [ - { - version_added: '85', - }, - { - version_added: '1', - prefix: 'webkit', - }, - ], - }, - }, - close: { - __compat: { support: { chrome: { version_added: '85' } } }, - }, - }, - DeprecatedInterface: { - __compat: { - support: { - chrome: { - version_added: '≤83', - version_removed: '85', - }, - }, - }, - }, - DummyAPI: { - __compat: { support: { chrome: { version_added: null } } }, - dummy: { - __compat: { support: { chrome: { version_added: null } } }, - }, - }, - ExperimentalInterface: { - __compat: { - support: { - chrome: [ - { - version_added: '70', - notes: 'Not supported on Windows XP.', - }, - { - version_added: '64', - version_removed: '70', - flags: {}, - notes: 'Not supported on Windows XP.', - }, - { - version_added: '50', - version_removed: '70', - alternative_name: 'TryingOutInterface', - notes: 'Not supported on Windows XP.', - }, - ], - }, - }, - }, - UnflaggedInterface: { - __compat: { - support: { - chrome: { - version_added: '≤84', - }, - }, - }, - }, - UnprefixedInterface: { - __compat: { - support: { - chrome: [ - { - version_added: '≤84', - }, - { - version_added: '83', - prefix: 'webkit', - notes: 'Not supported on Windows XP.', - }, - ], - }, - }, - }, - NullAPI: { - __compat: { support: { chrome: { version_added: '80' } } }, - }, - RemovedInterface: { - // TODO: handle more complicated scenarios - // __compat: {support: {chrome: [ - // {version_added: '85'}, - // {version_added: '≤83', version_removed: '84'} - // ]}} - __compat: { support: { chrome: { version_added: null } } }, - }, - SuperNewInterface: { - __compat: { support: { chrome: { version_added: '100' } } }, - }, - }, - browsers: { - chrome: { - name: 'Chrome', - releases: { 82: {}, 83: {}, 84: {}, 85: {} }, - }, - chrome_android: { name: 'Chrome Android', releases: { 85: {} } }, - edge: { name: 'Edge', releases: { 16: {}, 84: {} } }, - safari: { name: 'Safari', releases: { 13: {}, 13.1: {}, 14: {} } }, - safari_ios: { - name: 'iOS Safari', - releases: { 13: {}, 13.3: {}, 13.4: {}, 14: {} }, - }, - samsunginternet_android: { - name: 'Samsung Internet', - releases: { - '10.0': {}, - 10.2: {}, - '11.0': {}, - 11.2: {}, - '12.0': {}, - 12.1: {}, - }, - }, - }, - css: { - properties: { - 'font-family': { - __compat: { support: { chrome: { version_added: '84' } } }, - }, - 'font-face': { - __compat: { support: { chrome: { version_added: null } } }, - }, - 'font-style': { - __compat: { support: { chrome: { version_added: '85' } } }, - }, - }, - }, - javascript: { - builtins: { - Array: { - __compat: { support: { chrome: { version_added: null } } }, - }, - Date: { - __compat: { support: { chrome: { version_added: null } } }, - }, - }, - }, - }); - }); - - it('limit browsers', () => { - update(bcdCopy, supportMatrix, { browser: ['chrome'] }); - assert.deepEqual(bcdCopy.api.AbortController.__compat.support.safari, { - version_added: null, - }); - }); - - describe('mirror', () => { - const chromeAndroid86UaString = - 'Mozilla/5.0 (Linux; Android 10; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.5112.97 Mobile Safari/537.36'; - const browsers: any = { - chrome: { name: 'Chrome', releases: { 85: {}, 86: {} } }, - chrome_android: { - name: 'Chrome Android', - upstream: 'chrome', - releases: { 86: {} }, - }, - }; - - const bcdFromSupport = (support) => ({ - api: { FakeInterface: { __compat: { support } } }, - }); - - /** - * Create a BCD data structure for an arbitrary web platform feature - * based on support data for Chrome and Chrome Android and test result - * data for Chrome Android. This utility invokes the `update` function - * and is designed to observe the behavior of the "mirror" support value. - * - * @returns {Identifier} - */ - const mirroringCase = ({ support, downstreamResult }): Identifier => { - const reports: Report[] = [ - { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.FakeInterface', - exposure: 'Window', - result: downstreamResult, - }, - ], - }, - userAgent: chromeAndroid86UaString, - }, - ]; - const supportMatrix = getSupportMatrix(reports, browsers, []); - const bcd = bcdFromSupport(support); - update(bcd, supportMatrix, {}); - return bcd; - }; - - describe('supported upstream (without flags)', () => { - it('supported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: '86' }, - chrome_android: 'mirror', - }, - downstreamResult: true, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: '86' }, - chrome_android: 'mirror', - }), - ); - }); - - it('unsupported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: '85' }, - chrome_android: 'mirror', - }, - downstreamResult: false, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: '85' }, - chrome_android: { version_added: false }, - }), - ); - }); - - it('omitted from downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: '85' }, - chrome_android: 'mirror', - }, - downstreamResult: null, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: '85' }, - chrome_android: 'mirror', - }), - ); - }); - }); - - describe('supported upstream (with flags)', () => { - it('supported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: '85', flags: [{}] }, - chrome_android: 'mirror', - }, - downstreamResult: true, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: '85', flags: [{}] }, - chrome_android: { version_added: '86' }, - }), - ); - }); - - it('unsupported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: '85', flags: [{}] }, - chrome_android: 'mirror', - }, - downstreamResult: false, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: '85', flags: [{}] }, - chrome_android: 'mirror', - }), - ); - }); - - it('omitted from downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: '85', flags: [{}] }, - chrome_android: 'mirror', - }, - downstreamResult: null, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: '85', flags: [{}] }, - chrome_android: 'mirror', - }), - ); - }); - }); - - describe('partially supported upstream', () => { - it('supported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { - version_added: '85', - partial_implementation: true, - notes: 'This only works on Tuesdays', - }, - chrome_android: 'mirror', - }, - downstreamResult: true, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { - version_added: '85', - partial_implementation: true, - notes: 'This only works on Tuesdays', - }, - chrome_android: 'mirror', - }), - ); - }); - - it('unsupported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: [ - { - version_added: '85', - partial_implementation: true, - impl_url: 'http://zombo.com', - notes: 'This only works on Wednesdays', - }, - { version_added: '84', flags: [{}] }, - ], - chrome_android: 'mirror', - }, - downstreamResult: false, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: [ - { - version_added: '85', - partial_implementation: true, - impl_url: 'http://zombo.com', - notes: 'This only works on Wednesdays', - }, - { version_added: '84', flags: [{}] }, - ], - chrome_android: { - version_added: false, - }, - }), - ); - }); - - it('omitted from downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { - version_added: '85', - partial_implementation: true, - notes: 'This only works on Thursdays', - }, - chrome_android: 'mirror', - }, - downstreamResult: null, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { - version_added: '85', - partial_implementation: true, - notes: 'This only works on Thursdays', - }, - chrome_android: 'mirror', - }), - ); - }); - }); - - describe('unsupported upstream', () => { - it('supported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: false }, - chrome_android: 'mirror', - }, - downstreamResult: true, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: false }, - chrome_android: { version_added: '86' }, - }), - ); - }); - - it('unsupported in downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: false }, - chrome_android: 'mirror', - }, - downstreamResult: false, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: false }, - chrome_android: 'mirror', - }), - ); - }); - - it('omitted from downstream test results', () => { - const actual = mirroringCase({ - support: { - chrome: { version_added: false }, - chrome_android: 'mirror', - }, - downstreamResult: null, - }); - assert.deepEqual( - actual, - bcdFromSupport({ - chrome: { version_added: false }, - chrome_android: 'mirror', - }), - ); - }); - }); - }); - - it('does not report a modification when results corroborate existing data', () => { - const firefox92UaString = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:92.0) Gecko/20100101 Firefox/92.0'; - const initialBcd = { - api: { - AbortController: { - __compat: { - support: { - firefox: { version_added: '92' }, - }, - }, - }, - }, - browsers: { - firefox: { name: 'Firefox', releases: { 92: {} } }, - } as unknown as Browsers, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: firefox92UaString, - }; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert.equal(modified, false, 'modified'); - assert.deepEqual(finalBcd, initialBcd); - }); - - it('retains flag data for unsupported features', () => { - const initialBcd = { - api: { - AbortController: { - __compat: { - support: { - firefox: { version_added: '91', flags: [{}] }, - }, - }, - }, - }, - browsers: { - firefox: { name: 'Firefox', releases: { 91: {}, 92: {}, 93: {} } }, - } as unknown as Browsers, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: false, - }, - ], - }, - userAgent: firefox92UaString, - }; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert.equal(modified, false, 'modified'); - assert.deepEqual(finalBcd, initialBcd); - }); - - it('no update given partial confirmation of complex support scenario', () => { - const initialBcd: any = { - api: { - AbortController: { - __compat: { - support: { - firefox: [ - { version_added: '92' }, - { - version_added: '91', - partial_implementation: true, - notes: '', - }, - ], - }, - }, - }, - }, - browsers: { - firefox: { name: 'Firefox', releases: { 91: {}, 92: {}, 93: {} } }, - }, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: false, - }, - ], - }, - userAgent: firefox92UaString, - }; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert.equal(modified, false, 'modified'); - assert.deepEqual(finalBcd, initialBcd); - }); - - it('skips complex support scenarios', () => { - const initialBcd: any = { - api: { - AbortController: { - __compat: { - support: { - firefox: [ - { version_added: '94' }, - { - version_added: '93', - partial_implementation: true, - notes: '', - }, - ], - }, - }, - }, - }, - browsers: { - firefox: { - name: 'Firefox', - releases: { 91: {}, 92: {}, 93: {}, 94: {} }, - }, - }, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: false, - }, - ], - }, - userAgent: firefox92UaString, - }; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert.equal(modified, false, 'modified'); - assert.deepEqual(finalBcd, initialBcd); - }); - - it('skips removed features', () => { - const initialBcd: any = { - api: { - AbortController: { - __compat: { - support: { - firefox: { version_added: '90', version_removed: '91' }, - }, - }, - }, - }, - browsers: { - firefox: { name: 'Firefox', releases: { 90: {}, 91: {}, 92: {} } }, - }, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: firefox92UaString, - }; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert.equal(modified, false, 'modified'); - assert.deepEqual(finalBcd, initialBcd); - }); - - it('persists non-default statements', () => { - const initialBcd: any = { - api: { - AbortController: { - __compat: { - support: { - firefox: { version_added: '91', prefix: 'moz' }, - }, - }, - }, - }, - browsers: { - firefox: { name: 'Firefox', releases: { 91: {}, 92: {}, 93: {} } }, - }, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: firefox92UaString, - }; - const expectedBcd = clone(initialBcd); - expectedBcd.api.AbortController.__compat.support.firefox = [ - { - version_added: '≤92', - }, - { - prefix: 'moz', - version_added: '91', - }, - ]; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert(modified, 'modified'); - assert.deepEqual(finalBcd, expectedBcd); - }); - - it('overrides existing support information in response to negative test results', () => { - const initialBcd: any = { - api: { - AbortController: { - __compat: { - support: { - firefox: { version_added: '91' }, - }, - }, - }, - }, - browsers: { - firefox: { name: 'Firefox', releases: { 91: {}, 92: {}, 93: {} } }, - }, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: false, - }, - ], - }, - userAgent: firefox92UaString, - }; - const expectedBcd = clone(initialBcd); - expectedBcd.api.AbortController.__compat.support.firefox.version_added = - false; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert(modified, 'modified'); - assert.deepEqual(finalBcd, expectedBcd); - }); - - describe('filtering', () => { - let expectedBcd; - beforeEach(() => { - expectedBcd = clone(bcd); - }); - - it('path', () => { - const filter = { - path: new Minimatch('css.properties.*'), - }; - expectedBcd.css.properties[ - 'font-family' - ].__compat.support.chrome.version_added = '84'; - expectedBcd.css.properties[ - 'font-style' - ].__compat.support.chrome.version_added = '85'; - - const modified = update(bcdCopy, supportMatrix, filter); - - assert(modified, 'modified'); - assert.deepEqual(bcdCopy, expectedBcd); - }); - - it('release', () => { - const filter = { release: '84' }; - expectedBcd.css.properties[ - 'font-family' - ].__compat.support.chrome.version_added = '84'; - - const modified = update(bcdCopy, supportMatrix, filter); - - assert(modified, 'modified'); - assert.deepEqual(bcdCopy, expectedBcd); - }); - }); - - it('persists "mirror" when test results align with support data', () => { - const initialBcd = { - api: { - AbortController: { - __compat: { - support: { - chrome: { version_added: '86' }, - chrome_android: 'mirror', - }, - }, - }, - }, - browsers: { - chrome: { name: 'Chrome', releases: { 85: {}, 86: {} } }, - chrome_android: { - name: 'Chrome Android', - upstream: 'chrome', - releases: { 86: {} }, - }, - } as unknown as Browsers, - }; - const finalBcd = clone(initialBcd); - const report: Report = { - __version: '0.3.1', - results: { - 'https://mdn-bcd-collector.gooborg.com/tests/': [ - { - name: 'api.AbortController', - exposure: 'Window', - result: true, - }, - ], - }, - userAgent: chromeAndroid86UaString, - }; - - const sm = getSupportMatrix([report], initialBcd.browsers, []); - - const modified = update(finalBcd, sm, {}); - - assert.equal(modified, false, 'modified'); - assert.deepEqual(finalBcd, initialBcd); - }); - }); -}); diff --git a/scripts/update.ts b/scripts/update.ts deleted file mode 100644 index 6a66678bcd8df6..00000000000000 --- a/scripts/update.ts +++ /dev/null @@ -1,755 +0,0 @@ -/* This file is a part of @mdn/browser-compat-data - * See LICENSE file for more information. */ - -import assert from 'node:assert'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import fs from 'node:fs/promises'; - -import { - compare as compareVersions, - compareVersions as compareVersionsSort, -} from 'compare-versions'; -import esMain from 'es-main'; -import _minimatch from 'minimatch'; -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import chalk from 'chalk-template'; -import { fdir } from 'fdir'; - -import { - Browsers, - BrowserName, - SimpleSupportStatement, - Identifier, - SupportStatement, -} from '../types/types.js'; -import { parseUA } from '../utils/ua-parser.js'; - -const { Minimatch } = _minimatch; - -type Exposure = 'Window' | 'Worker' | 'SharedWorker' | 'ServiceWorker'; - -type TestResultValue = boolean | null; - -interface TestResult { - exposure: Exposure; - name: string; - result: TestResultValue; - message?: string; -} - -interface TestResults { - [key: string]: TestResult[]; -} - -export interface Report { - __version: string; - results: TestResults; - userAgent: string; -} - -type BrowserSupportMap = Map; -type SupportMap = Map; -type SupportMatrix = Map; - -export type ManualOverride = [string, string, string, TestResultValue]; - -type Overrides = Array; - -type InternalSupportStatement = SupportStatement | 'mirror'; - -const BASE_DIR = new URL('..', import.meta.url); - -const BCD_DIR = process.env.BCD_DIR - ? path.resolve(process.env.BCD_DIR) - : fileURLToPath(BASE_DIR); - -const MDN_COLLECTOR_DIR = process.env.MDN_COLLECTOR_DIR - ? path.resolve(process.env.MDN_COLLECTOR_DIR) - : fileURLToPath(new URL('../mdn-bcd-collector', BASE_DIR)); - -const CATEGORIES = ['api', 'css.properties', 'javascript.builtins']; - -const { default: mirror } = await import( - `${BCD_DIR}/scripts/release/mirror.js` -); - -export const logger = { - warn: (message: string) => { - console.warn(chalk`{yellow warn}: ${message}`); - }, - info: (message: string) => { - console.info(chalk`{green warn}: ${message}`); - }, -}; - -export const findEntry = ( - bcd: Identifier, - ident: string, -): Identifier | null => { - if (!ident) { - return null; - } - const keys: string[] = ident.split('.'); - let entry: any = bcd; - while (entry && keys.length) { - entry = entry[keys.shift() as string]; - } - return entry; -}; - -const clone = (value) => JSON.parse(JSON.stringify(value)); - -const combineResults = (results: TestResultValue[]): TestResultValue => { - let supported: TestResultValue = null; - for (const result of results) { - if (result === true) { - // If any result is true, the flattened support should be true. There - // can be contradictory results with multiple exposure scopes, but here - // we treat support in any scope as support of the feature. - return true; - } else if (result === false) { - // This may yet be overruled by a later result (above). - supported = false; - } else if (result === null) { - // Leave supported as it is. - } else { - throw new Error(`result not true/false/null; got ${result}`); - } - } - return supported; -}; - -// Create a string represenation of a version range, optimized for human -// legibility. -const joinRange = (lower: string, upper: string) => - lower === '0' ? `≤${upper}` : `${lower}> ≤${upper}`; - -// Parse a version range string produced by `joinRange` into a lower and upper -// boundary. -export const splitRange = (range: string) => { - const match = range.match(/(?:(.*)> )?(?:≤(.*))/); - if (!match) { - throw new Error(`Unrecognized version range value: "${range}"`); - } - return { lower: match[1] || '0', upper: match[2] }; -}; - -// Get support map from BCD path to test result (null/true/false) for a single -// report. -export const getSupportMap = (report: Report): BrowserSupportMap => { - // Transform `report` to map from test name (BCD path) to array of results. - const testMap = new Map(); - for (const tests of Object.values(report.results)) { - for (const test of tests) { - // TODO: If test.exposure.endsWith('Worker'), then map this to a - // worker_support feature. - const tests = testMap.get(test.name) || []; - tests.push(test.result); - testMap.set(test.name, tests); - } - } - - if (testMap.size === 0) { - throw new Error(`Report for "${report.userAgent}" has no results!`); - } - - // Transform `testMap` to map from test name (BCD path) to flattened support. - const supportMap = new Map(); - for (const [name, results] of testMap.entries()) { - let supported = combineResults(results); - - if (supported === null) { - // If the parent feature support is false, copy that. - // TODO: This assumes that the parent feature came first when iterating - // the report, which isn't guaranteed. Move this to a second phase. - const parentName = name.split('.').slice(0, -1).join('.'); - const parentSupport = supportMap.get(parentName); - if (parentSupport === false) { - supported = false; - } - } - - supportMap.set(name, supported); - } - return supportMap; -}; - -// Load all reports and build a map from BCD path to browser + version -// and test result (null/true/false) for that version. -export const getSupportMatrix = ( - reports: Report[], - browsers: Browsers, - overrides: ManualOverride[], -): SupportMatrix => { - const supportMatrix = new Map(); - - for (const report of reports) { - const { browser, version, inBcd } = parseUA(report.userAgent, browsers); - if (!inBcd) { - if (inBcd === false) { - logger.warn( - `Ignoring unknown ${browser.name} version ${version} (${report.userAgent})`, - ); - } else if (browser.name) { - logger.warn( - `Ignoring unknown browser ${browser.name} ${version} (${report.userAgent})`, - ); - } else { - logger.warn(`Unable to parse browser from UA ${report.userAgent}`); - } - - continue; - } - - const supportMap = getSupportMap(report); - - // Merge `supportMap` into `supportMatrix`. - for (const [name, supported] of supportMap.entries()) { - let browserMap = supportMatrix.get(name); - if (!browserMap) { - browserMap = new Map(); - supportMatrix.set(name, browserMap); - } - let versionMap = browserMap.get(browser.id); - if (!versionMap) { - versionMap = new Map(); - for (const browserVersion of Object.keys( - browsers[browser.id].releases, - )) { - versionMap.set(browserVersion, null); - } - browserMap.set(browser.id, versionMap); - } - assert(versionMap.has(version), `${browser.id} ${version} missing`); - - // In case of multiple reports for a single version it's possible we - // already have (non-null) support information. Combine results to deal - // with this possibility. - const combined = combineResults([supported, versionMap.get(version)]); - versionMap.set(version, combined); - } - } - - // apply manual overrides - for (const [path, browser, version, supported] of overrides) { - const browserMap = supportMatrix.get(path); - if (!browserMap) { - continue; - } - const versionMap = browserMap.get(browser); - if (!versionMap) { - continue; - } - - if (version === '*') { - // All versions of a browser - for (const v of versionMap.keys()) { - versionMap.set(v, supported); - } - } else if (version.includes('+')) { - // Browser versions from x onwards (inclusive) - for (const v of versionMap.keys()) { - if (compareVersions(version.replace('+', ''), v, '<=')) { - versionMap.set(v, supported); - } - } - } else if (version.includes('-')) { - // Browser versions between x and y (inclusive) - const versions = version.split('-'); - for (const v of versionMap.keys()) { - if ( - compareVersions(versions[0], v, '<=') && - compareVersions(versions[1], v, '>=') - ) { - versionMap.set(v, supported); - } - } - } else { - // Single browser versions - versionMap.set(version, supported); - } - } - - return supportMatrix; -}; - -export const inferSupportStatements = ( - versionMap: BrowserSupportMap, -): SimpleSupportStatement[] => { - const versions = Array.from(versionMap.keys()).sort(compareVersionsSort); - - const statements: SimpleSupportStatement[] = []; - const lastKnown: { version: string; support: TestResultValue } = { - version: '0', - support: null, - }; - let lastWasNull = false; - - for (const version of versions) { - const supported = versionMap.get(version); - const lastStatement = statements[statements.length - 1]; - - if (supported === true) { - if (!lastStatement) { - statements.push({ - version_added: - lastWasNull || lastKnown.support === false - ? joinRange(lastKnown.version, version) - : version, - }); - } else if (!lastStatement.version_added) { - lastStatement.version_added = lastWasNull - ? joinRange(lastKnown.version, version) - : version; - } else if (lastStatement.version_removed) { - // added back again - statements.push({ - version_added: version, - }); - } - - lastKnown.version = version; - lastKnown.support = true; - lastWasNull = false; - } else if (supported === false) { - if ( - lastStatement && - lastStatement.version_added && - !lastStatement.version_removed - ) { - lastStatement.version_removed = lastWasNull - ? joinRange(lastKnown.version, version) - : version; - } else if (!lastStatement) { - statements.push({ version_added: false }); - } - - lastKnown.version = version; - lastKnown.support = false; - lastWasNull = false; - } else if (supported === null) { - lastWasNull = true; - // TODO - } else { - throw new Error(`result not true/false/null; got ${supported}`); - } - } - - return statements; -}; - -export const update = ( - bcd: Identifier, - supportMatrix: SupportMatrix, - filter: any, -): boolean => { - let modified = false; - - for (const [path, browserMap] of supportMatrix.entries()) { - if (filter.path) { - if (filter.path.constructor === Minimatch) { - if (!filter.path.match(path)) { - // If filter.path does not match glob - continue; - } - } else if (path !== filter.path && !path.startsWith(`${filter.path}.`)) { - continue; - } - } - - const entry = findEntry(bcd, path); - if (!entry || !entry.__compat) { - continue; - } - - const support = entry.__compat.support; - // Stringified then parsed to deep clone the support statements - const originalSupport = clone(support); - - for (const [browser, versionMap] of browserMap.entries()) { - if ( - filter.browser && - filter.browser.length && - !filter.browser.includes(browser) - ) { - continue; - } - const inferredStatements = inferSupportStatements(versionMap); - if (inferredStatements.length !== 1) { - // TODO: handle more complicated scenarios - logger.warn( - `${path} skipped for ${browser} due to multiple inferred statements`, - ); - continue; - } - - const inferredStatement = inferredStatements[0]; - - // If there's a version number filter - if (filter.release || filter.release === false) { - const filterMatch = - filter.release && filter.release.match(/([\d.]+)-([\d.]+)/); - if (filterMatch) { - if (typeof inferredStatement.version_added !== 'string') { - // If the version_added is not a string, it must be false and won't - // match our - continue; - } - if ( - compareVersions( - inferredStatement.version_added.replace(/(([\d.]+)> )?≤/, ''), - filterMatch[1], - '<', - ) || - compareVersions( - inferredStatement.version_added.replace(/(([\d.]+)> )?≤/, ''), - filterMatch[2], - '>', - ) - ) { - // If version_added is outside of filter range - continue; - } - if ( - typeof inferredStatement.version_removed === 'string' && - (compareVersions( - inferredStatement.version_removed.replace(/(([\d.]+)> )?≤/, ''), - filterMatch[1], - '<', - ) || - compareVersions( - inferredStatement.version_removed.replace(/(([\d.]+)> )?≤/, ''), - filterMatch[2], - '>', - )) - ) { - // If version_removed and it's outside of filter range - continue; - } - } else { - if (filter.release !== inferredStatement.version_added) { - // If version_added doesn't match filter - continue; - } - if ( - inferredStatement.version_removed && - filter.release !== inferredStatement.version_removed - ) { - // If version_removed and it doesn't match filter - continue; - } - } - } - - // Update the support data with a new value. - const persist = (statements: SimpleSupportStatement[]) => { - // Check for ranges and ignore them if we specify `exact-only` argument - if (filter.exactOnly) { - for (const statement of statements) { - if ( - (typeof statement.version_added === 'string' && - statement.version_added.includes('≤')) || - (typeof statement.version_removed === 'string' && - statement.version_removed.includes('≤')) - ) { - return; - } - } - } - - support[browser] = statements.length === 1 ? statements[0] : statements; - modified = true; - }; - - let allStatements = - (support[browser] as InternalSupportStatement) === 'mirror' - ? mirror(browser, originalSupport) - : // Although non-mirrored support data could be modified in-place, - // working with a cloned version forces the subsequent code to - // explicitly assign it back to the originating data structure. - // This reduces the likelihood of inconsistencies in the handling - // of mirrored and non-mirrored support data. - clone(support[browser] || null); - - if (!allStatements) { - allStatements = []; - } else if (!Array.isArray(allStatements)) { - allStatements = [allStatements]; - } - - // Filter to the statements representing the feature being enabled by - // default under the default name and no flags. - const defaultStatements = allStatements.filter((statement) => { - if ('flags' in statement) { - return false; - } - if ('prefix' in statement || 'alternative_name' in statement) { - // TODO: map the results for aliases to these statements. - return false; - } - return true; - }); - - if (defaultStatements.length === 0) { - // Prepend |inferredStatement| to |allStatements|, since there were no - // relevant statements to begin with... - if (inferredStatement.version_added === false) { - // ... but not if the new statement just claims no support, since - // that is implicit in no statement. - continue; - } - // Remove flag data for features which are enabled by default. - // - // See https://github.com/mdn/browser-compat-data/pull/16637 - const nonFlagStatements = allStatements.filter( - (statement) => !('flags' in statement), - ); - persist([inferredStatement, ...nonFlagStatements]); - - continue; - } - - if (defaultStatements.length !== 1) { - // TODO: handle more complicated scenarios - logger.warn( - `${path} skipped for ${browser} due to multiple default statements`, - ); - continue; - } - - const simpleStatement = defaultStatements[0]; - - if (simpleStatement.version_removed) { - // TODO: handle updating existing added+removed entries. - logger.warn( - `${path} skipped for ${browser} due to added+removed statement`, - ); - continue; - } - - // If we infer no support but BCD currently has a version number, check to make sure - // our data is not older than BCD (ex. BCD says 79 but we have results for 40-78) - if ( - inferredStatement.version_added === false && - typeof simpleStatement.version_added === 'string' - ) { - let latestNonNullVersion = ''; - - for (const [version, result] of Array.from( - versionMap.entries(), - ).reverse()) { - if (result === null) { - // Ignore null values - continue; - } - - if ( - !latestNonNullVersion || - compareVersions(version, latestNonNullVersion, '>') - ) { - latestNonNullVersion = version; - } - } - - if ( - simpleStatement.version_added === 'preview' || - compareVersions( - latestNonNullVersion, - simpleStatement.version_added.replace('≤', ''), - '<', - ) - ) { - logger.warn( - `${path} skipped for ${browser}; BCD says support was added in a version newer than there are results for`, - ); - continue; - } - } - - if ( - typeof simpleStatement.version_added === 'string' && - typeof inferredStatement.version_added === 'string' && - inferredStatement.version_added.includes('≤') - ) { - const { lower, upper } = splitRange(inferredStatement.version_added); - const simpleAdded = simpleStatement.version_added.replace('≤', ''); - if ( - simpleStatement.version_added === 'preview' || - compareVersions(simpleAdded, lower, '<=') || - compareVersions(simpleAdded, upper, '>') - ) { - simpleStatement.version_added = inferredStatement.version_added; - persist(allStatements); - } - } else if ( - !( - typeof simpleStatement.version_added === 'string' && - inferredStatement.version_added === true - ) && - simpleStatement.version_added !== inferredStatement.version_added - ) { - // When a "mirrored" statement will be replaced with a statement - // documenting lack of support, notes describing partial implementation - // status are no longer relevant. - if ( - !inferredStatement.version_added && - simpleStatement.partial_implementation - ) { - persist([{ version_added: false }]); - - // Positive test results do not conclusively indicate that a partial - // implementation has been completed. - } else if (!simpleStatement.partial_implementation) { - simpleStatement.version_added = inferredStatement.version_added; - persist(allStatements); - } - } - - if (typeof inferredStatement.version_removed === 'string') { - simpleStatement.version_removed = inferredStatement.version_removed; - persist(allStatements); - } - } - } - - return modified; -}; - -/* c8 ignore start */ -/** - * Read a file and parse it as JSON. - * @param {string} file Path to json file - * @returns {Promise} Parsed JSON object - */ -const readJson = async (file: string): Promise => - JSON.parse(await fs.readFile(file, 'utf8')); - -// |paths| can be files or directories. Returns an object mapping -// from (absolute) path to the parsed file content. -export const loadJsonFiles = async ( - paths: string[], -): Promise<{ [filename: string]: any }> => { - const jsonCrawler = new fdir() - .withFullPaths() - .filter((item) => { - // Ignores .DS_Store, .git, etc. - const basename = path.basename(item); - return basename === '.' || basename[0] !== '.'; - }) - .filter((item) => item.endsWith('.json')); - - const jsonFiles: string[] = []; - - for (const p of paths) { - for (const item of await jsonCrawler.crawl(p).withPromise()) { - jsonFiles.push(item); - } - } - - const entries: [string, JSON][] = []; - - for (const file of jsonFiles) { - entries.push([file, await readJson(file)]); - } - - return Object.fromEntries(entries); -}; - -export const main = async ( - reportPaths: string[], - filter: any, - browsers: Browsers, - overrides: Overrides, -): Promise => { - // Replace filter.path with a minimatch object. - if (filter.path && filter.path.includes('*')) { - filter.path = new Minimatch(filter.path); - } - - if (filter.release === 'false') { - filter.release = false; - } - - const bcdFiles = (await loadJsonFiles( - filter.addNewFeatures - ? [path.join(BCD_DIR, '__missing')] - : CATEGORIES.map((cat) => path.join(BCD_DIR, ...cat.split('.'))), - )) as { [key: string]: Identifier }; - - const reports = Object.values(await loadJsonFiles(reportPaths)) as Report[]; - const supportMatrix = getSupportMatrix( - reports, - browsers, - overrides.filter( - Array.isArray as (item: unknown) => item is ManualOverride, - ), - ); - - // Should match https://github.com/mdn/browser-compat-data/blob/f10bf2cc7d1b001a390e70b7854cab9435ffb443/test/linter/test-style.js#L63 - // TODO: https://github.com/mdn/browser-compat-data/issues/3617 - for (const [file, data] of Object.entries(bcdFiles)) { - const modified = update(data, supportMatrix, filter); - if (!modified) { - continue; - } - logger.info(`Updating ${path.relative(BCD_DIR, file)}`); - const json = JSON.stringify(data, null, ' ') + '\n'; - await fs.writeFile(file, json); - } -}; - -if (esMain(import.meta)) { - const { - default: { browsers }, - } = await import(`${BCD_DIR}/index.js`); - const overrides = await readJson( - path.join(MDN_COLLECTOR_DIR, 'custom/overrides.json'), - ); - - const { argv }: { argv: any } = yargs(hideBin(process.argv)).command( - '$0 [reports..]', - 'Update BCD from a specified set of report files', - (yargs) => { - yargs - .positional('reports', { - describe: 'The report files to update from (also accepts folders)', - type: 'string', - array: true, - default: ['../mdn-bcd-results/'], - }) - .option('path', { - alias: 'p', - describe: - 'The BCD path to update (includes children, ex. "api.Document" will also update "api.Document.body")', - type: 'string', - default: null, - }) - .option('browser', { - alias: 'b', - describe: 'The browser to update', - type: 'array', - choices: Object.keys(browsers), - default: [], - }) - .option('release', { - alias: 'r', - describe: - 'Only update when version_added or version_removed is set to the given value (can be an inclusive range, ex. xx-yy, or `false` for changes that set no support)', - type: 'string', - default: null, - }) - .option('exact-only', { - alias: 'e', - describe: - 'Only update when versions are a specific number (or "false"), disallowing ranges', - type: 'boolean', - default: false, - }); - }, - ); - - await main(argv.reports, argv, browsers, overrides); -} -/* c8 ignore stop */