diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5304fe872f446a..ae48a3f6f0dd32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,6 @@ on: branches: - main pull_request: - merge_group: permissions: contents: read @@ -18,11 +17,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + - name: Setup Node + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: npm @@ -40,11 +41,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + - name: Setup Node + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: npm @@ -58,11 +61,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + - name: Setup Node + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: npm @@ -71,3 +76,64 @@ jobs: - run: npm ci - run: npm run unittest + + diff-build: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }}-diff-build + cancel-in-progress: true + + steps: + - name: Determine base ref + id: base + run: | + if [[ "$GITHUB_HEAD_REF" == "release" ]]; then + VERSION=$(npm view @mdn/browser-compat-data version) + echo "ref=v$VERSION" >> "$GITHUB_OUTPUT" + echo "Comparing release branch against published v$VERSION" + else + echo "ref=$GITHUB_BASE_REF" >> "$GITHUB_OUTPUT" + echo "Comparing against base branch: $GITHUB_BASE_REF" + fi + + # Base + + - name: Checkout (base) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.base.outputs.ref }} + path: base + persist-credentials: false + + - name: Checkout (PR) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + path: pr + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version-file: pr/.nvmrc + cache: npm + cache-dependency-path: | + base/package-lock.json + pr/package-lock.json + package-manager-cache: true + + - name: Install + run: | + cd base && npm ci | sed "s/^/[base] /" & + cd pr && npm ci | sed "s/^/[pr] /" & + wait + + - name: Format + run: | + cd base/build && npx prettier --write . | sed "s/^/[base] /" & + cd pr/build && npx prettier --write . | sed "s/^/[pr] /" & + wait + + - name: Diff + run: git diff --color=always --no-index base/build/ pr/build/ || true diff --git a/.gitignore b/.gitignore index 678f4bcd8b5159..403e515d024c13 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ yarn.lock .nyc_output/ coverage.lcov coverage/ -types/types.d.ts +/types/internal.d.ts +/types/public.d.ts .DS_Store diff --git a/.prettierignore b/.prettierignore index ccb78a52b7b309..01b486c0727703 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,4 +11,5 @@ LICENSE /build/ coverage/ .features.json -types.d.ts +/types/internal.d.ts +/types/public.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 44873e54892587..2fda6fed29b1fa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,5 +31,6 @@ ], "url": "/schemas/browsers.schema.json" } - ] + ], + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/eslint.config.js b/eslint.config.js index 0dc3870b4cdd4c..483c6a9d240522 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,7 +34,8 @@ export default [ 'CODE_OF_CONDUCT.md', 'build/', '**/coverage/', - '**/types.d.ts', + 'types/internal.d.ts', + 'types/public.d.ts', ], }, ...fixupConfigRules( @@ -77,9 +78,7 @@ export default [ rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/no-unused-vars': 'off', - - 'no-unused-vars': [ + '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', @@ -166,6 +165,7 @@ export default [ 'no-return-assign': 'error', 'no-self-compare': 'error', 'no-unused-expressions': 'error', + 'no-unused-vars': 'off', // Using @typescript-eslint/no-unused-vars instead. 'no-useless-call': 'error', 'prefer-arrow-functions/prefer-arrow-functions': [ diff --git a/index.js b/index.js index 090a2943213788..870d67d340b54e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatData} from './types/types.js' */ +/** @import {InternalCompatData} from './types/index.js' */ import fs from 'node:fs/promises'; import path from 'node:path'; @@ -17,10 +17,12 @@ const dirname = fileURLToPath(new URL('.', import.meta.url)); /** * Recursively load one or more directories passed as arguments. - * @param {...string} dirs The directories to load - * @returns {Promise} All of the browser compatibility data + * @template {keyof InternalCompatData} Dir + * @param {...Dir} dirs The directories to load + * @returns {Promise>} All of the browser compatibility data */ const load = async (...dirs) => { + /** @type {Partial>} */ const result = {}; for (const dir of dirs) { @@ -35,12 +37,13 @@ const load = async (...dirs) => { for (const fp of paths) { try { const rawcontents = await fs.readFile(fp); - /** @type {CompatData} */ + /** @type {InternalCompatData} */ const contents = JSON.parse(rawcontents.toString('utf8')); // Add source_file props const walker = walk(undefined, contents); for (const { compat } of walker) { + // @ts-expect-error Need to better reflect transition from internal to public data. compat.source_file = normalizePath(path.relative(dirname, fp)); } @@ -54,7 +57,10 @@ const load = async (...dirs) => { } } - return /** @type {CompatData} */ (result); + return /** @type {Pick} */ (result); }; -export default await load(...dataFolders); +/** @type {InternalCompatData} */ +const bcd = await load(...dataFolders); + +export default bcd; diff --git a/index.test.js b/index.test.js index 83a90c0144fd6c..e8b5e7219b7c54 100644 --- a/index.test.js +++ b/index.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement} from './types/types.js' */ +/** @import {InternalCompatStatement} from './types/index.js' */ import assert from 'node:assert/strict'; @@ -9,13 +9,13 @@ import bcd from './index.js'; describe('Using BCD', () => { it('subscript notation', () => { - /** @type {CompatStatement | undefined} */ + /** @type {InternalCompatStatement | undefined} */ const data = bcd['api']['AbortController']['__compat']; assert.ok(data); }); it('dot notation', () => { - /** @type {CompatStatement | undefined} */ + /** @type {InternalCompatStatement | undefined} */ const data = bcd.api.AbortController.__compat; assert.ok(data); }); diff --git a/lint/common/overlap.js b/lint/common/overlap.js index 625409e82be6c5..6da689e517a245 100644 --- a/lint/common/overlap.js +++ b/lint/common/overlap.js @@ -9,15 +9,15 @@ import { createStatementGroupKey } from '../utils.js'; import compareStatements from '../../scripts/lib/compare-statements.js'; /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, SimpleSupportStatement, SupportStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalSimpleSupportStatement, InternalSupportStatement} from '../../types/index.js' */ /** * Groups statements by group key. - * @param {SimpleSupportStatement[]} data The support statements to group. - * @returns {Map} the statement groups + * @param {InternalSimpleSupportStatement[]} data The support statements to group. + * @returns {Map} the statement groups */ const groupByStatementKey = (data) => { - /** @type {Map} */ + /** @type {Map} */ const groups = new Map(); for (const support of data) { @@ -34,7 +34,7 @@ const groupByStatementKey = (data) => { /** * Formats a support statement as a simplified JSON-like version range. - * @param {SimpleSupportStatement} support The statement to format + * @param {InternalSimpleSupportStatement} support The statement to format * @returns {string} The formatted range */ const formatRange = (support) => { @@ -52,12 +52,12 @@ const formatRange = (support) => { /** * Process data and check to make sure there aren't support statements whose version ranges overlap. - * @param {SupportStatement} data The data to test + * @param {InternalSupportStatement} data The data to test * @param {BrowserName} browser The name of the browser * @param {object} options The check options * @param {Logger} [options.logger] The logger to output errors to * @param {boolean} [options.fix] Whether the statements should be fixed (if possible) - * @returns {SupportStatement} the data (with fixes, if specified) + * @returns {InternalSupportStatement} the data (with fixes, if specified) */ export const checkOverlap = (data, browser, { logger, fix = false }) => { if (!Array.isArray(data)) { @@ -73,8 +73,12 @@ export const checkOverlap = (data, browser, { logger, fix = false }) => { const statements = groupData.slice().sort(compareStatements).reverse(); for (let i = 0; i < statements.length - 1; i++) { - const current = /** @type {SimpleSupportStatement} */ (statements.at(i)); - const next = /** @type {SimpleSupportStatement} */ (statements.at(i + 1)); + const current = /** @type {InternalSimpleSupportStatement} */ ( + statements.at(i) + ); + const next = /** @type {InternalSimpleSupportStatement} */ ( + statements.at(i + 1) + ); if (!statementsOverlap(current, next)) { continue; @@ -106,8 +110,8 @@ export const checkOverlap = (data, browser, { logger, fix = false }) => { /** * Checks if the support statements overlap in terms of their version ranges. - * @param {SimpleSupportStatement} current the current statement. - * @param {SimpleSupportStatement} next the chronologically following statement. + * @param {InternalSimpleSupportStatement} current the current statement. + * @param {InternalSimpleSupportStatement} next the chronologically following statement. * @returns {boolean} Whether the support statements overlap. */ const statementsOverlap = (current, next) => { diff --git a/lint/fixer/browser-order.js b/lint/fixer/browser-order.js index 013a0ba5864f53..37f558c3316fa2 100644 --- a/lint/fixer/browser-order.js +++ b/lint/fixer/browser-order.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {BrowserName, CompatStatement, SupportBlock} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement, InternalSupportBlock} from '../../types/index.js' */ /** * Return a new "support_block" object whose first-level properties @@ -10,8 +10,8 @@ * guaranteed "own" property ordering, which is insertion order for * non-integer keys (which is our case). * @param {string} key The key of the object - * @param {CompatStatement} value The value of the key - * @returns {CompatStatement} Value with sorting applied + * @param {InternalCompatStatement} value The value of the key + * @returns {InternalCompatStatement} Value with sorting applied */ export const orderSupportBlock = (key, value) => { if (key === '__compat') { @@ -21,15 +21,15 @@ export const orderSupportBlock = (key, value) => { .sort() .reduce( /** - * @param {SupportBlock} result + * @param {InternalSupportBlock} result * @param {BrowserName} key - * @returns {SupportBlock} + * @returns {InternalSupportBlock} */ (result, key) => { result[key] = value.support[key]; return result; }, - /** @type {SupportBlock} */ ({}), + /** @type {InternalSupportBlock} */ ({}), ); } return value; diff --git a/lint/fixer/common-errors.js b/lint/fixer/common-errors.js index 5e88244d1ac9e2..3188c5b033eaed 100644 --- a/lint/fixer/common-errors.js +++ b/lint/fixer/common-errors.js @@ -3,33 +3,33 @@ import { walk } from '../../utils/index.js'; -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalSupportBlock} from '../../types/index.js' */ /** - * Fixes common errors in CompatStatements. + * Fixes common errors in an InternalSupportBlock. * * - Replaces `browser: { version_added: "mirror" }` with `browser: "mirror"` * - Wraps `browser: false` with `browser: `{ version_added: false }` - * @param {CompatStatement} compat The compat statement to fix + * @param {InternalSupportBlock} support The support block to fix * @returns {void} */ -export const fixCommonErrorsInCompatStatement = (compat) => { - for (const browser of Object.keys(compat.support)) { - if (compat.support[browser] === false) { - compat.support[browser] = { +export const fixCommonErrorsInSupportBlock = (support) => { + for (const browser of Object.keys(support)) { + if (support[browser] === false) { + support[browser] = { version_added: false, }; } else if ( - typeof compat.support[browser] === 'object' && - JSON.stringify(compat.support[browser]) === '{"version_added":"mirror"}' + typeof support[browser] === 'object' && + JSON.stringify(support[browser]) === '{"version_added":"mirror"}' ) { - compat.support[browser] = 'mirror'; + support[browser] = 'mirror'; } if ( browser == 'ie' && - JSON.stringify(compat.support[browser]) === '{"version_added":false}' + JSON.stringify(support[browser]) === '{"version_added":false}' ) { - Reflect.deleteProperty(compat.support, browser); + Reflect.deleteProperty(support, browser); } } }; @@ -48,7 +48,7 @@ const fixCommonErrors = (filename, actual) => { const bcd = JSON.parse(actual); for (const { compat } of walk(undefined, bcd)) { - fixCommonErrorsInCompatStatement(compat); + fixCommonErrorsInSupportBlock(compat.support); } return JSON.stringify(bcd, null, 2); diff --git a/lint/fixer/common-errors.test.js b/lint/fixer/common-errors.test.js index e3e01140ef6ecb..45fb97b0261541 100644 --- a/lint/fixer/common-errors.test.js +++ b/lint/fixer/common-errors.test.js @@ -3,13 +3,13 @@ import assert from 'node:assert/strict'; -import { fixCommonErrorsInCompatStatement } from './common-errors.js'; +import { fixCommonErrorsInSupportBlock } from './common-errors.js'; /** * @import { InternalSupportBlock } from '../../types/index.js' */ -/** @type {{ input: any; output?: InternalSupportBlock }[]} */ +/** @type {{ input: any; output: InternalSupportBlock }[]} */ const tests = [ // Replace unwrapped "false". { @@ -40,14 +40,10 @@ describe('fix -> common errors', () => { let i = 1; for (const test of tests) { it(`Test #${i}`, () => { - const input = { - support: test.input, - }; - const output = { - support: test.output ?? test.input, - }; - - fixCommonErrorsInCompatStatement(input); + const input = test.input; + const output = test.output ?? test.input; + + fixCommonErrorsInSupportBlock(input); assert.deepStrictEqual(input, output); }); diff --git a/lint/fixer/feature-order.js b/lint/fixer/feature-order.js index d4b6838da8dd5c..fba8d6a2c213d8 100644 --- a/lint/fixer/feature-order.js +++ b/lint/fixer/feature-order.js @@ -3,7 +3,7 @@ import compareFeatures from '../../scripts/lib/compare-features.js'; -/** @import {Identifier} from '../../types/types.js' */ +/** @import {InternalIdentifier} from '../../types/index.js' */ /** * Return a new feature object whose first-level properties have been @@ -12,8 +12,8 @@ import compareFeatures from '../../scripts/lib/compare-features.js'; * property ordering, which is insertion order for non-integer keys * (which is our case). * @param {string} _ The key in the object - * @param {Identifier} value The value of the key - * @returns {Identifier} The new value + * @param {InternalIdentifier} value The value of the key + * @returns {InternalIdentifier} The new value */ export const orderFeatures = (_, value) => { if (value instanceof Object && '__compat' in value) { @@ -21,15 +21,15 @@ export const orderFeatures = (_, value) => { .sort(compareFeatures) .reduce( /** - * @param {Identifier} result + * @param {InternalIdentifier} result * @param {string} key - * @returns {Identifier} + * @returns {InternalIdentifier} */ (result, key) => { result[key] = value[key]; return result; }, - /** @type {Identifier} */ ({}), + /** @type {InternalIdentifier} */ ({}), ); } return value; diff --git a/lint/fixer/flags.js b/lint/fixer/flags.js index 8f6bc8b09876ac..9c9f299caf9d87 100644 --- a/lint/fixer/flags.js +++ b/lint/fixer/flags.js @@ -7,12 +7,12 @@ import testFlags, { } from '../linter/test-flags.js'; import walk from '../../utils/walk.js'; -/** @import {BrowserName, CompatStatement, SupportStatement, SimpleSupportStatement, Identifier} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement, InternalSupportStatement, InternalSimpleSupportStatement, InternalIdentifier} from '../../types/index.js' */ /** * Removes irrelevant flags from the compatibility data - * @param {SupportStatement} supportData The compatibility statement to test - * @returns {SupportStatement} The compatibility statement with all of the flags removed + * @param {InternalSupportStatement} supportData The compatibility statement to test + * @returns {InternalSupportStatement} The compatibility statement with all of the flags removed */ export const removeIrrelevantFlags = (supportData) => { if (typeof supportData === 'string') { @@ -27,7 +27,7 @@ export const removeIrrelevantFlags = (supportData) => { return supportData; } - /** @type {SimpleSupportStatement[]} */ + /** @type {InternalSimpleSupportStatement[]} */ const result = []; const basicSupport = getBasicSupportStatement(supportData); @@ -43,7 +43,7 @@ export const removeIrrelevantFlags = (supportData) => { if (result.length == 1) { return result[0]; } - return /** @type {[SimpleSupportStatement, SimpleSupportStatement, ...SimpleSupportStatement[]]} */ ( + return /** @type {[InternalSimpleSupportStatement, InternalSimpleSupportStatement, ...InternalSimpleSupportStatement[]]} */ ( result ); }; @@ -68,12 +68,14 @@ const fixFlags = (filename, actual) => { } const featureData = - /** @type {Identifier & {__compat: CompatStatement}} */ (feature.data); + /** @type {InternalIdentifier & {__compat: InternalCompatStatement}} */ ( + feature.data + ); for (const [ browser, supportData, - ] of /** @type {[BrowserName, SupportStatement][]} */ ( + ] of /** @type {[BrowserName, InternalSupportStatement][]} */ ( Object.entries(feature.compat.support) )) { featureData.__compat.support[browser] = diff --git a/lint/fixer/flags.test.js b/lint/fixer/flags.test.js index 24790f5af0c055..8fcad51d4f4f5b 100644 --- a/lint/fixer/flags.test.js +++ b/lint/fixer/flags.test.js @@ -1,13 +1,13 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {SupportStatement} from '../../types/types.js' */ +/** @import {InternalSupportStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; import { removeIrrelevantFlags } from './flags.js'; -/** @type {{ input: SupportStatement; output: SupportStatement }[]} */ +/** @type {{ input: InternalSupportStatement; output: InternalSupportStatement }[]} */ const tests = [ { input: [ diff --git a/lint/fixer/mirror.js b/lint/fixer/mirror.js index 5ef3fc15aaaae8..f92cba01163198 100644 --- a/lint/fixer/mirror.js +++ b/lint/fixer/mirror.js @@ -7,12 +7,14 @@ import bcd from '../../index.js'; import { walk } from '../../utils/index.js'; import mirrorSupport from '../../scripts/build/mirror.js'; -/** @import {CompatData, BrowserName} from '../../types/types.js' */ +/** @import {InternalCompatData, BrowserName} from '../../types/index.js' */ /** @import {InternalSupportStatement, InternalSupportBlock} from '../../types/index.js' */ -const downstreamBrowsers = /** @type {(keyof typeof bcd.browsers)[]} */ ( - Object.keys(bcd.browsers) -).filter((browser) => bcd.browsers[browser].upstream); +const downstreamBrowsers = /** @type {BrowserName[]} */ ( + Object.entries(bcd.browsers).flatMap(([browser, stmt]) => + stmt.upstream ? [browser] : [], + ) +); /** * Check to see if the statement is equal to the mirrored statement @@ -46,6 +48,7 @@ export const isMirrorEquivalent = (support, browser) => { */ export const isMirrorRequired = (supportData, browser) => { const current = bcd.browsers[browser]; + /** @type {BrowserName | undefined} */ const upstream = current.upstream; @@ -69,7 +72,7 @@ export const isMirrorRequired = (supportData, browser) => { /** * Set the support statement for each browser to mirror if it matches mirroring - * @param {CompatData} bcd The compat data to update + * @param {InternalCompatData} bcd The compat data to update * @returns {void} */ export const mirrorIfEquivalent = (bcd) => { diff --git a/lint/fixer/overlap.js b/lint/fixer/overlap.js index e500ad83c325f3..692b50b7c6c7d4 100644 --- a/lint/fixer/overlap.js +++ b/lint/fixer/overlap.js @@ -3,14 +3,14 @@ import { checkOverlap as checkOverlap } from '../common/overlap.js'; -/** @import {BrowserName, CompatStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement} from '../../types/index.js' */ /** * Return a new "support_block" object whose support statements have * been updated to avoid overlapping version ranges. * @param {string} key The key in the object - * @param {CompatStatement} value The value of the key - * @returns {CompatStatement} The new value + * @param {InternalCompatStatement} value The value of the key + * @returns {InternalCompatStatement} The new value */ export const processStatement = (key, value) => { if (key === '__compat') { diff --git a/lint/fixer/overlap.test.js b/lint/fixer/overlap.test.js index 932bff58d90ee7..c36dec11672f7d 100644 --- a/lint/fixer/overlap.test.js +++ b/lint/fixer/overlap.test.js @@ -1,13 +1,13 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {SupportStatement} from '../../types/types.js' */ +/** @import {InternalSupportStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; import { checkOverlap } from '../common/overlap.js'; -/** @type {{ input: SupportStatement; output?: SupportStatement }[]} */ +/** @type {{ input: InternalSupportStatement; output?: InternalSupportStatement }[]} */ const tests = [ // Use version_added from following as version_removed of previous. { diff --git a/lint/fixer/statement-order.js b/lint/fixer/statement-order.js index a72df08e29ca9f..734ad5a1385554 100644 --- a/lint/fixer/statement-order.js +++ b/lint/fixer/statement-order.js @@ -3,15 +3,15 @@ import compareStatements from '../../scripts/lib/compare-statements.js'; -/** @import {BrowserName, CompatStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement} from '../../types/index.js' */ /** * Return a new "support_block" object whose support statements have * been ordered in reverse chronological order, moving statements * with flags, partial support, prefixes, or alternative names lower. * @param {string} key The key in the object - * @param {CompatStatement} value The value of the key - * @returns {CompatStatement} The new value + * @param {InternalCompatStatement} value The value of the key + * @returns {InternalCompatStatement} The new value */ export const orderStatements = (key, value) => { if (key === '__compat') { diff --git a/lint/fixer/status.js b/lint/fixer/status.js index 0cf22b5b49a1a0..a6cfea1c786fa7 100644 --- a/lint/fixer/status.js +++ b/lint/fixer/status.js @@ -4,12 +4,12 @@ import { checkExperimental } from '../linter/test-status.js'; import walk from '../../utils/walk.js'; -/** @import {Identifier} from '../../types/types.js' */ +/** @import {InternalCompatStatement, InternalIdentifier} from '../../types/index.js' */ /** * Fix the status values - * @param {Identifier} value The value to update - * @returns {Identifier} The updated value + * @param {InternalIdentifier} value The value to update + * @returns {InternalIdentifier} The updated value */ export const fixStatusValue = (value) => { const compat = value?.__compat; @@ -69,11 +69,7 @@ const fixStatusFromFile = (filename, actual) => { } return JSON.stringify( - JSON.parse( - actual, - (/** @type {string} */ _key, /** @type {Identifier} */ value) => - fixStatusValue(value), - ), + JSON.parse(actual, (_key, value) => fixStatusValue(value)), null, 2, ); diff --git a/lint/fixer/status.test.js b/lint/fixer/status.test.js index e5f179ca46c471..50bcd3467f725a 100644 --- a/lint/fixer/status.test.js +++ b/lint/fixer/status.test.js @@ -1,17 +1,13 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement, Identifier} from '../../types/types.js' */ - -/** - * @typedef {Record} TestValue - */ - import assert from 'node:assert/strict'; import { fixStatusValue } from './status.js'; -/** @type {{ name: string; input: TestValue; output: TestValue }[]} */ +/** @import {InternalIdentifier} from '../../types/internal.js' */ + +/** @type {{ name: string; input: InternalIdentifier; output: InternalIdentifier }[]} */ const tests = [ { name: 'should unset experimental when feature is deprecated', @@ -109,7 +105,7 @@ const tests = [ deprecated: true, }, }, - subfeature: /** @type {Identifier} */ ({ + subfeature: { __compat: { support: {}, status: { @@ -118,7 +114,7 @@ const tests = [ deprecated: false, }, }, - }), + }, }, output: { __compat: { @@ -129,7 +125,7 @@ const tests = [ deprecated: true, }, }, - subfeature: /** @type {Identifier} */ ({ + subfeature: { __compat: { support: {}, status: { @@ -138,7 +134,7 @@ const tests = [ deprecated: true, }, }, - }), + }, }, }, ]; @@ -146,7 +142,7 @@ const tests = [ describe('fixStatus', () => { for (const test of tests) { it(test.name, () => { - const result = fixStatusValue(/** @type {Identifier} */ (test.input)); + const result = fixStatusValue(test.input); assert.deepStrictEqual(result, test.output); }); diff --git a/lint/lint.js b/lint/lint.js index 81a23b2f28db9e..a1a8e0da76f67f 100644 --- a/lint/lint.js +++ b/lint/lint.js @@ -19,7 +19,7 @@ import * as linterModules from './linter/index.js'; import { Linters } from './utils.js'; /** @import {Stats} from 'node:fs' */ -/** @import {BrowserName, CompatData} from '../types/types.js' */ +/** @import {BrowserName, InternalCompatData} from '../types/index.js' */ /** @import {LinterMessage, LinterMessageLevel, LinterPath} from './types.js' */ const dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -51,7 +51,7 @@ const normalizeAndCategorizeFilePath = (file) => { /** * Recursively load * @param {...string} files The files to test - * @returns {Promise} The data from the loaded files + * @returns {Promise} The data from the loaded files */ const loadAndCheckFiles = async (...files) => { const data = {}; @@ -105,7 +105,7 @@ const loadAndCheckFiles = async (...files) => { } } - return /** @type {CompatData} */ (data); + return /** @type {InternalCompatData} */ (data); }; /** diff --git a/lint/linter/test-browsers-data.js b/lint/linter/test-browsers-data.js index f9d837f3196ba7..fc9e045fdf9d62 100644 --- a/lint/linter/test-browsers-data.js +++ b/lint/linter/test-browsers-data.js @@ -8,7 +8,7 @@ const { browsers } = bcd; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserStatement, BrowserName} from '../../types/types.js' */ +/** @import {BrowserStatement, BrowserName} from '../../types/index.js' */ /** * Process and test the data diff --git a/lint/linter/test-browsers-data.test.js b/lint/linter/test-browsers-data.test.js index 89e47c124bd777..6236ca5790edb1 100644 --- a/lint/linter/test-browsers-data.test.js +++ b/lint/linter/test-browsers-data.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {BrowserStatement} from '../../types/types.js' */ +/** @import {BrowserStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; diff --git a/lint/linter/test-browsers-presence.js b/lint/linter/test-browsers-presence.js index 4a91db063d0504..26f63eac475fd5 100644 --- a/lint/linter/test-browsers-presence.js +++ b/lint/linter/test-browsers-presence.js @@ -4,15 +4,14 @@ import { styleText } from 'node:util'; import bcd from '../../index.js'; -const { browsers } = bcd; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ /** * Check the data for any disallowed browsers or if it's missing required browsers - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {string} category The category the data belongs to. * @param {Logger} logger The logger to output errors to. * @returns {void} @@ -22,10 +21,8 @@ const processData = (data, category, logger) => { const support = data.support; const definedBrowsers = Object.keys(support); - const displayBrowsers = /** @type {(keyof typeof browsers)[]} */ ( - Object.keys(browsers) - ).filter( - (b) => + const displayBrowsers = Object.entries(bcd.browsers).flatMap( + ([name, browser]) => [ 'desktop', 'mobile', @@ -33,20 +30,20 @@ const processData = (data, category, logger) => { ...(['api', 'javascript', 'webassembly'].includes(category) ? ['server'] : []), - ].includes(browsers[b].type) && - (category !== 'webextensions' || browsers[b].accepts_webextensions), + ].includes(browser.type) && + (category !== 'webextensions' || browser.accepts_webextensions) + ? [name] + : [], ); - const requiredBrowsers = /** @type {(keyof typeof browsers)[]} */ ( - Object.keys(browsers) - ).filter( + const requiredBrowsers = Object.keys(bcd.browsers).filter( (b) => !['ie'].includes(b) && - ['desktop', 'mobile'].includes(browsers[b].type) && - (category !== 'webextensions' || browsers[b].accepts_webextensions), + ['desktop', 'mobile'].includes(bcd.browsers[b].type) && + (category !== 'webextensions' || bcd.browsers[b].accepts_webextensions), ); const undefEntries = definedBrowsers.filter( - (value) => !(value in browsers), + (value) => !(value in bcd.browsers), ); if (undefEntries.length > 0) { logger.error( @@ -54,9 +51,9 @@ const processData = (data, category, logger) => { ); } - const invalidEntries = /** @type {(keyof typeof support)[]} */ ( - Object.keys(support) - ).filter((value) => !displayBrowsers.includes(value)); + const invalidEntries = Object.keys(support).filter( + (value) => !displayBrowsers.includes(value), + ); if (invalidEntries.length > 0) { logger.error( `Has the following browsers, which are invalid for ${styleText('bold', category)} compat data: ${styleText('bold', invalidEntries.join(', '))}`, @@ -86,6 +83,10 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data, path: { category } }) => { - processData(/** @type {CompatStatement} */ (data), category, logger); + processData( + /** @type {InternalCompatStatement} */ (data), + category, + logger, + ); }, }; diff --git a/lint/linter/test-consistency.js b/lint/linter/test-consistency.js index 4644ce6b093c0b..5ee119e2a29b9f 100644 --- a/lint/linter/test-consistency.js +++ b/lint/linter/test-consistency.js @@ -11,8 +11,7 @@ import bcd from '../../index.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatData, CompatStatement, Identifier, SimpleSupportStatement, VersionValue} from '../../types/types.js' */ -/** @import {DataType, InternalSupportBlock, InternalSupportStatement} from '../../types/index.js' */ +/** @import {BrowserName, InternalCompatData, InternalCompatStatement, InternalIdentifier, InternalSimpleSupportStatement, InternalSupportBlock, InternalSupportStatement, VersionValue} from '../../types/index.js' */ /** * @typedef {'unsupported' | 'subfeature_earlier_implementation'} ErrorType @@ -41,17 +40,17 @@ import bcd from '../../index.js'; export class ConsistencyChecker { /** * Checks the data for any errors - * @param {CompatData} data The data to test + * @param {InternalCompatData} data The data to test * @returns {ConsistencyError[]} Any errors found within the data */ check(data) { - const { browsers: _browsers, __meta, ...rest } = data; + const { browsers: _browsers, ...rest } = data; return this.checkSubfeatures(rest); } /** * Recursively checks the data for any errors - * @param {Identifier} data The data to test + * @param {InternalIdentifier} data The data to test * @param {string[]} [path] The path of the data * @returns {ConsistencyError[]} Any errors found within the data */ @@ -75,10 +74,10 @@ export class ConsistencyChecker { this.getSubfeatures(data).forEach((key) => { allErrors = [ ...allErrors, - ...this.checkSubfeatures(/** @type {Identifier} */ (query(key, data)), [ - ...path, - ...key.split('.'), - ]), + ...this.checkSubfeatures( + /** @type {InternalIdentifier} */ (query(key, data)), + [...path, ...key.split('.')], + ), ]; }); @@ -86,8 +85,8 @@ export class ConsistencyChecker { } /** - * Get the subfeatures of an identifier - * @param {Identifier} data The identifier + * Get the subfeatures of an Internalidentifier + * @param {InternalIdentifier} data The Internalidentifier * @returns {string[]} The subfeatures */ getSubfeatures(data) { @@ -115,7 +114,7 @@ export class ConsistencyChecker { /** * Checks a specific feature for errors - * @param {Identifier} data The data to test + * @param {InternalIdentifier} data The data to test * @returns {FeatureError[]} Any errors found within the data */ checkFeature(data) { @@ -132,7 +131,7 @@ export class ConsistencyChecker { subfeatures.forEach((subfeature) => { const unsupportedInChild = this.extractUnsupportedBrowsers( - /** @type {Identifier} */ (query(subfeature, data)).__compat, + query(subfeature, data).__compat, ); const browsers = /** @type {BrowserName[]} */ ( @@ -145,7 +144,7 @@ export class ConsistencyChecker { browser, ); const subfeature_value = this.getVersionAdded( - /** @type {Identifier} */ (query(subfeature, data)).__compat?.support, + query(subfeature, data).__compat?.support, browser, ); if (feature_value === subfeature_value) { @@ -185,9 +184,7 @@ export class ConsistencyChecker { for (const subfeature of subfeatures) { for (const browser of supportInParent) { - const subfeatureData = /** @type {Identifier} */ ( - query(subfeature, data) - ); + const subfeatureData = query(subfeature, data); if ( subfeatureData.__compat?.support[browser] != undefined && this.isVersionAddedGreater( @@ -232,8 +229,8 @@ export class ConsistencyChecker { /** * Checks if the data is a feature - * @param {Identifier} data The data to test - * @returns {data is Identifier & {__compat: CompatStatement}} If the data is a feature statement + * @param {InternalIdentifier} data The data to test + * @returns {data is InternalIdentifier & {__compat: InternalCompatStatement}} If the data is a feature statement */ isFeature(data) { return '__compat' in data; @@ -241,12 +238,12 @@ export class ConsistencyChecker { /** * Get all of the unsupported browsers in a feature - * @param {CompatStatement} [compatData] The compat data to process + * @param {InternalCompatStatement} [InternalcompatData] The compat data to process * @returns {BrowserName[]} The list of browsers marked as unsupported */ - extractUnsupportedBrowsers(compatData) { + extractUnsupportedBrowsers(InternalcompatData) { return this.extractBrowsers( - compatData, + InternalcompatData, (data) => data.version_added === false || typeof data.version_removed !== 'undefined', @@ -255,13 +252,13 @@ export class ConsistencyChecker { /** * Get all of the browsers with a version number in a feature. - * @param {CompatStatement} [compatData] The compat data to process + * @param {InternalCompatStatement} [InternalcompatData] The compat data to process * @returns {BrowserName[]} The list of browsers with an exact version number */ - extractSupportedBrowsersWithVersion(compatData) { + extractSupportedBrowsersWithVersion(InternalcompatData) { return this.extractBrowsers( - compatData, - (/** @type {SimpleSupportStatement} */ data) => + InternalcompatData, + (/** @type {InternalSimpleSupportStatement} */ data) => typeof data.version_added === 'string', ); } @@ -291,7 +288,7 @@ export class ConsistencyChecker { /** * A convenience function to squash preview and flag support into `false` - * @param {SimpleSupportStatement} statement The statement to use + * @param {InternalSimpleSupportStatement} statement The statement to use * @returns {VersionValue} The version number or `false` */ const resolveVersionAddedValue = (statement) => @@ -374,28 +371,28 @@ export class ConsistencyChecker { /** * Get all of the browsers within the data and pass the data to the callback. - * @param {CompatStatement | undefined} compatData The compat data to process - * @param {(browserData: SimpleSupportStatement) => boolean} callback The function to pass the data to + * @param {InternalCompatStatement | undefined} InternalcompatData The compat data to process + * @param {(browserData: InternalSimpleSupportStatement) => boolean} callback The function to pass the data to * @returns {BrowserName[]} The list of browsers using the callback as a filter */ - extractBrowsers(compatData, callback) { - if (!compatData) { + extractBrowsers(InternalcompatData, callback) { + if (!InternalcompatData) { return []; } return /** @type {BrowserName[]} */ (Object.keys(bcd.browsers)).filter( (browser) => { - if (!(browser in compatData.support)) { + if (!(browser in InternalcompatData.support)) { return callback({ version_added: false }); } let browserData = /** @type {InternalSupportStatement | undefined} */ ( - compatData.support[browser] + InternalcompatData.support[browser] ); if ( /** @type {InternalSupportStatement} */ (browserData) === 'mirror' ) { - browserData = mirrorSupport(browser, compatData.support); + browserData = mirrorSupport(browser, InternalcompatData.support); } if (Array.isArray(browserData)) { @@ -421,7 +418,7 @@ export default { */ check: (logger, { data }) => { const checker = new ConsistencyChecker(); - const allErrors = checker.check(/** @type {CompatData} */ (data)); + const allErrors = checker.check(data); for (const { path, errors } of allErrors) { for (const { type, browser, parentValue, subfeatures } of errors) { diff --git a/lint/linter/test-descriptions.js b/lint/linter/test-descriptions.js index 81178416cd02be..0fa189f18e30b8 100644 --- a/lint/linter/test-descriptions.js +++ b/lint/linter/test-descriptions.js @@ -7,7 +7,7 @@ import { validateHTML } from './test-notes.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ /** * @typedef {object} DescriptionError @@ -21,7 +21,7 @@ import { validateHTML } from './test-notes.js'; * Check for errors in the description of a specified statement's description and return whether there's an error and log as such * @param {string} ruleName The name of the error * @param {string} path The feature path - * @param {CompatStatement} compat The compat data to test + * @param {InternalCompatStatement} compat The compat data to test * @param {string} expected Expected description * @param {(DescriptionError | string)[]} errors The array of errors to push to * @returns {void} @@ -40,7 +40,7 @@ const checkDescription = (ruleName, path, compat, expected, errors) => { /** * Process API data and check for any incorrect descriptions in said data, logging any errors - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {string} path The path of the feature * @param {(DescriptionError | string)[]} errors The array of errors to push to * @returns {void} @@ -94,7 +94,7 @@ const processApiData = (data, path, errors) => { /** * Process data and check for any incorrect descriptions in said data, logging any errors - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {string} category The feature category * @param {string} path The path of the feature * @returns {(DescriptionError | string)[]} The errors caught in the file @@ -135,7 +135,7 @@ export default { */ check: (logger, { data, path: { full, category } }) => { const errors = processData( - /** @type {CompatStatement} */ (data), + /** @type {InternalCompatStatement} */ (data), category, full, ); diff --git a/lint/linter/test-descriptions.test.js b/lint/linter/test-descriptions.test.js index ba4449e8adcd14..be04426de2704f 100644 --- a/lint/linter/test-descriptions.test.js +++ b/lint/linter/test-descriptions.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ /** @import {DescriptionError} from './test-descriptions.js' */ import assert from 'node:assert/strict'; @@ -12,7 +12,7 @@ describe('test-descriptions', () => { describe('API data', () => { it('should ignore anything that is not an interface subfeature', () => { const path = 'api.Interface.feature.subfeature'; - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: {}, }; @@ -23,7 +23,7 @@ describe('test-descriptions', () => { it('should check description for constructor', () => { const path = 'api.Interface.Interface'; - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { description: '', support: {}, @@ -39,7 +39,7 @@ describe('test-descriptions', () => { it('should check description for event', () => { const path = 'api.Interface.click_event'; - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { description: '', support: {}, @@ -55,7 +55,7 @@ describe('test-descriptions', () => { it('should check description for permission', () => { const path = 'api.Interface.geolocation_permission'; - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { description: '', support: {}, @@ -71,7 +71,7 @@ describe('test-descriptions', () => { it('should check description for secure context required', () => { const path = 'api.Interface.secure_context_required'; - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { description: '', support: {}, @@ -87,7 +87,7 @@ describe('test-descriptions', () => { it('should check description for worker support', () => { const path = 'api.Interface.worker_support'; - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { description: '', support: {}, @@ -103,7 +103,7 @@ describe('test-descriptions', () => { it('should check for redundant description', () => { const path = 'css.properties.width.auto'; - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { description: '`auto`', support: {}, diff --git a/lint/linter/test-filename.js b/lint/linter/test-filename.js index 2f2491735fa862..4822d31ae4b720 100644 --- a/lint/linter/test-filename.js +++ b/lint/linter/test-filename.js @@ -1,13 +1,13 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {Identifier} from '../../types/types.js' */ +/** @import {InternalIdentifier} from '../../types/index.js' */ /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ /** * Test the filename based on the identifier - * @param {Identifier} data The identifier + * @param {InternalIdentifier} data The identifier * @param {string[]} pathParts Parts of the path * @param {string} currentPath The current path traversed * @returns {string | false} A string with the error message if the lint failed, or false if it passed @@ -33,7 +33,7 @@ const testFilename = (data, pathParts, currentPath) => { /** * Process the data to make sure it defines the features appropriate to the file's name - * @param {Identifier} data The raw contents of the file to test + * @param {InternalIdentifier} data The raw contents of the file to test * @param {string} filepath The file path * @param {Logger} logger The logger to output errors to * @returns {void} @@ -64,6 +64,6 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data, path: { full } }) => { - processData(/** @type {Identifier} */ (data), full, logger); + processData(/** @type {InternalIdentifier} */ (data), full, logger); }, }; diff --git a/lint/linter/test-filename.test.js b/lint/linter/test-filename.test.js index fe34b39d84747b..973d6d75875b5d 100644 --- a/lint/linter/test-filename.test.js +++ b/lint/linter/test-filename.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {Identifier} from '../../types/types.js' */ +/** @import {InternalIdentifier} from '../../types/index.js' */ import assert from 'node:assert/strict'; @@ -12,7 +12,7 @@ import test from './test-filename.js'; describe('test-filename', () => { /** @type {Logger} */ let logger; - /** @type {Identifier} */ + /** @type {InternalIdentifier} */ let data; beforeEach(() => { logger = new Logger('test', 'test'); diff --git a/lint/linter/test-flags.js b/lint/linter/test-flags.js index 2eae63618b71e2..bd8da9773ad204 100644 --- a/lint/linter/test-flags.js +++ b/lint/linter/test-flags.js @@ -7,7 +7,7 @@ import { compare } from 'compare-versions'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatStatement, BrowserName, SupportStatement, SimpleSupportStatement, FlagStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement, BrowserName, InternalSupportStatement, InternalSimpleSupportStatement, FlagStatement} from '../../types/index.js' */ /** * @typedef {object} FlagError @@ -17,8 +17,8 @@ import { compare } from 'compare-versions'; /** * Get the support statement with basic, non-aliased and non-flagged support - * @param {SimpleSupportStatement[]} supportData The statements to check - * @returns {SimpleSupportStatement | undefined} The support statement with basic, non-aliased and non-flagged support + * @param {InternalSimpleSupportStatement[]} supportData The statements to check + * @returns {InternalSimpleSupportStatement | undefined} The support statement with basic, non-aliased and non-flagged support */ export const getBasicSupportStatement = (supportData) => supportData.find((statement) => { @@ -33,8 +33,8 @@ export const getBasicSupportStatement = (supportData) => /** * Determines if a support statement is for irrelevant flag data - * @param {SimpleSupportStatement} statement The statement to check - * @param {SimpleSupportStatement | undefined} basicSupport The support statement for the same browser that has no alt. name, prefix or flag + * @param {InternalSimpleSupportStatement} statement The statement to check + * @param {InternalSimpleSupportStatement | undefined} basicSupport The support statement for the same browser that has no alt. name, prefix or flag * @returns {boolean} Whether the support statement is irrelevant */ export const isIrrelevantFlagData = (statement, basicSupport) => { @@ -83,7 +83,7 @@ export const isIrrelevantFlagData = (statement, basicSupport) => { }; /** * Process data and check for any irrelevant flag data - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @returns {FlagError[]} The errors found */ export const processData = (data) => { @@ -93,7 +93,7 @@ export const processData = (data) => { for (const [ browser, supportData, - ] of /** @type {[BrowserName, SupportStatement][]} */ ( + ] of /** @type {[BrowserName, InternalSupportStatement][]} */ ( Object.entries(data.support) )) { if (typeof supportData === 'string') { @@ -130,7 +130,7 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data }) => { - const errors = processData(/** @type {CompatStatement} */ (data)); + const errors = processData(/** @type {InternalCompatStatement} */ (data)); for (const error of errors) { logger.error( diff --git a/lint/linter/test-mdn-urls.js b/lint/linter/test-mdn-urls.js index 47b09d186b545d..089cfb3aefed5e 100644 --- a/lint/linter/test-mdn-urls.js +++ b/lint/linter/test-mdn-urls.js @@ -7,7 +7,7 @@ import mdnContentInventory from '@ddbeck/mdn-content-inventory'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ /** * @typedef {object} MDNURLError @@ -69,7 +69,7 @@ const redirects = mdnContentInventory.redirects; /** * Process the data for MDN URL issues - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {string} path The path of the feature * @returns {MDNURLError[]} The issues caught in the file */ @@ -152,7 +152,10 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data, path: { full } }) => { - const issues = processData(/** @type {CompatStatement} */ (data), full); + const issues = processData( + /** @type {InternalCompatStatement} */ (data), + full, + ); for (const issue of issues) { if (issue.expected === '') { logger.warning( diff --git a/lint/linter/test-mirror.js b/lint/linter/test-mirror.js index 85745667d9cd07..43823cae2e6b3c 100644 --- a/lint/linter/test-mirror.js +++ b/lint/linter/test-mirror.js @@ -9,7 +9,7 @@ import { isMirrorEquivalent, isMirrorRequired } from '../fixer/mirror.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement} from '../../types/index.js' */ /** @import {InternalSupportBlock} from '../../types/index.js' */ /** @@ -53,7 +53,7 @@ export default { */ check: (logger, { data, path: { category } }) => { checkMirroring( - /** @type {CompatStatement} */ (data).support, + /** @type {InternalCompatStatement} */ (data).support, category, logger, ); diff --git a/lint/linter/test-multiple-statements.js b/lint/linter/test-multiple-statements.js index 1918a2f7989666..3c215ed8ffdac0 100644 --- a/lint/linter/test-multiple-statements.js +++ b/lint/linter/test-multiple-statements.js @@ -7,12 +7,12 @@ import { createStatementGroupKey } from '../utils.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatStatement, SupportStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement, InternalSupportStatement} from '../../types/index.js' */ /** * Process data and check to make sure there aren't multiple support statements without * `partial_implementation` or `prefix`/`alternative_name` - * @param {SupportStatement} data The data to test + * @param {InternalSupportStatement} data The data to test * @param {BrowserName} browser The name of the browser * @param {Logger} logger The logger to output errors to * @returns {void} @@ -56,10 +56,10 @@ export default { */ check: (logger, { data }) => { for (const [browser, support] of Object.entries( - /** @type {CompatStatement} */ (data).support, + /** @type {InternalCompatStatement} */ (data).support, )) { processData( - /** @type {SupportStatement} */ (support), + /** @type {InternalSupportStatement} */ (support), /** @type {BrowserName} */ (browser), logger, ); diff --git a/lint/linter/test-notes.js b/lint/linter/test-notes.js index 070832e914ac2b..b23b11626d1798 100644 --- a/lint/linter/test-notes.js +++ b/lint/linter/test-notes.js @@ -10,7 +10,7 @@ import { VALID_ELEMENTS } from '../utils.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatStatement, SupportStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement, InternalSupportStatement} from '../../types/index.js' */ const parser = new HTMLParser(); @@ -92,7 +92,7 @@ export const validateHTML = (string) => { * Check the notes in the data * @param {string | string[]} notes The notes to test * @param {BrowserName} browser The browser the notes belong to - * @param {string} feature The identifier of the feature + * @param {string} feature The Internalidentifier of the feature * @param {Logger} logger The logger to output errors to * @returns {void} */ @@ -116,16 +116,16 @@ const checkNotes = (notes, browser, feature, logger) => { /** * Process the data for notes errors - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {Logger} logger The logger to output errors to - * @param {string} feature The identifier of the feature + * @param {string} feature The Internalidentifier of the feature * @returns {void} */ const processData = (data, logger, feature) => { for (const [ browser, support, - ] of /** @type {[BrowserName, SupportStatement][]} */ ( + ] of /** @type {[BrowserName, InternalSupportStatement][]} */ ( Object.entries(data.support) )) { if (Array.isArray(support)) { @@ -135,7 +135,7 @@ const processData = (data, logger, feature) => { } } } else { - if (support.notes) { + if (typeof support === 'object' && support.notes) { checkNotes(support.notes, browser, feature, logger); } } @@ -153,6 +153,6 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data, path: { full } }) => { - processData(/** @type {CompatStatement} */ (data), logger, full); + processData(/** @type {InternalCompatStatement} */ (data), logger, full); }, }; diff --git a/lint/linter/test-obsolete.js b/lint/linter/test-obsolete.js index 289a1f70b7645b..5cefdc1c1c313b 100644 --- a/lint/linter/test-obsolete.js +++ b/lint/linter/test-obsolete.js @@ -2,11 +2,10 @@ * See LICENSE file for more information. */ import bcd from '../../index.js'; -const { browsers } = bcd; /** @import {Linter, LinterData, LinterMessageLevel} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement} from '../../types/index.js' */ /** @import {InternalSupportBlock, InternalSupportStatement} from '../../types/index.js' */ // Once a category has been stripped of unsupported features, remove it from this list @@ -73,7 +72,7 @@ export const implementedAndRemoved = (support) => { } const releaseDateData = - browsers[browser].releases[d.version_removed.replace('≤', '')] + bcd.browsers[browser].releases[d.version_removed.replace('≤', '')] .release_date; // No browser release date @@ -99,7 +98,7 @@ export const implementedAndRemoved = (support) => { /** * Process and test the data * @param {Logger} logger The logger to output errors to - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @returns {void} */ export const processData = (logger, data) => { @@ -134,7 +133,7 @@ export default { */ check: (logger, { data, path: { category } }) => { if (!ignoredCategories.includes(category)) { - processData(logger, /** @type {CompatStatement} */ (data)); + processData(logger, /** @type {InternalCompatStatement} */ (data)); } }, exceptions: ['html.elements.track.kind.descriptions'], diff --git a/lint/linter/test-obsolete.test.js b/lint/linter/test-obsolete.test.js index 13cec0ad51f089..50e7aa41f365d4 100644 --- a/lint/linter/test-obsolete.test.js +++ b/lint/linter/test-obsolete.test.js @@ -11,13 +11,12 @@ import { implementedAndRemoved, processData, } from './test-obsolete.js'; -const { browsers } = bcd; const errorTime = new Date(), infoTime = new Date(); errorTime.setFullYear(errorTime.getFullYear() - 2.5); infoTime.setFullYear(infoTime.getFullYear() - 2); -const release = Object.entries(browsers.chrome.releases).find((r) => { +const release = Object.entries(bcd.browsers['chrome'].releases).find((r) => { if (r[1].release_date === undefined) { return false; } @@ -124,7 +123,7 @@ describe('implementedAndRemoved', () => { implementedAndRemoved({ chrome: { version_added: '1', - version_removed: Object.keys(browsers.chrome.releases)[-1], + version_removed: Object.keys(bcd.browsers['chrome'].releases)[-1], }, }), false, @@ -134,7 +133,7 @@ describe('implementedAndRemoved', () => { chrome: [ { version_added: '2', - version_removed: Object.keys(browsers.chrome.releases)[-1], + version_removed: Object.keys(bcd.browsers['chrome'].releases)[-1], }, { version_added: '1', diff --git a/lint/linter/test-overlap.js b/lint/linter/test-overlap.js index 9704ea819a2b31..34c08ef9abda67 100644 --- a/lint/linter/test-overlap.js +++ b/lint/linter/test-overlap.js @@ -5,7 +5,7 @@ import { checkOverlap } from '../common/overlap.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatStatement, SupportStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement, InternalSupportStatement} from '../../types/index.js' */ /** @type {Linter} */ export default { @@ -19,10 +19,10 @@ export default { */ check: (logger, { data }) => { for (const [browser, support] of Object.entries( - /** @type {CompatStatement} */ (data).support, + /** @type {InternalCompatStatement} */ (data).support, )) { checkOverlap( - /** @type {SupportStatement} */ (support), + /** @type {InternalSupportStatement} */ (support), /** @type {BrowserName} */ (browser), { logger, diff --git a/lint/linter/test-overlap.test.js b/lint/linter/test-overlap.test.js index 6bba710712a147..9539405017fb4d 100644 --- a/lint/linter/test-overlap.test.js +++ b/lint/linter/test-overlap.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; @@ -20,7 +20,7 @@ describe('overlap', () => { }); it('should skip processing when data is not an array', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { chrome: { @@ -36,7 +36,7 @@ describe('overlap', () => { }); it('should log error when statements overlap', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ @@ -53,7 +53,7 @@ describe('overlap', () => { }); it('should log error when overlapping statements are not sorted', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ @@ -70,7 +70,7 @@ describe('overlap', () => { }); it('should log error when statements with same prefix overlap', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ @@ -87,7 +87,7 @@ describe('overlap', () => { }); it('should log error when statements with same alternative name overlap', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ @@ -108,7 +108,7 @@ describe('overlap', () => { }); it('should log error when there are two statements without version_added', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ @@ -129,7 +129,7 @@ describe('overlap', () => { }); it('should log error when there are two statements without version_added incl. preview', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ @@ -150,7 +150,7 @@ describe('overlap', () => { }); it('should ignore when partial support in stable and full support in preview overlap', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ @@ -171,7 +171,7 @@ describe('overlap', () => { }); it('should ignore preview version without overlap', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { support: { firefox: [ diff --git a/lint/linter/test-prefix.js b/lint/linter/test-prefix.js index cdb921a458b030..c029ecaa0c892a 100644 --- a/lint/linter/test-prefix.js +++ b/lint/linter/test-prefix.js @@ -5,11 +5,11 @@ import { styleText } from 'node:util'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ /** * Process the data for prefix errors - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {string} category The category the data belongs to * @param {string} feature The full feature path * @param {Logger} logger The logger to output errors to @@ -51,7 +51,7 @@ const processData = (data, category, feature, logger) => { const supportStatements = Array.isArray(support) ? support : [support]; for (const statement of supportStatements) { - if (!statement) { + if (typeof statement !== 'object') { continue; } if (statement.prefix && statement.alternative_name) { @@ -98,6 +98,11 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data, path: { category, full } }) => { - processData(/** @type {CompatStatement} */ (data), category, full, logger); + processData( + /** @type {InternalCompatStatement} */ (data), + category, + full, + logger, + ); }, }; diff --git a/lint/linter/test-spec-urls.js b/lint/linter/test-spec-urls.js index d416e6af75c332..f2fee7e5a386b1 100644 --- a/lint/linter/test-spec-urls.js +++ b/lint/linter/test-spec-urls.js @@ -7,7 +7,7 @@ import specData from 'web-specs' with { type: 'json' }; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ /* * Before adding an exception, open an issue with https://github.com/w3c/browser-specs to @@ -79,7 +79,7 @@ const allowedSpecURLs = [ /** * Process the data for spec URL errors - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {Logger} logger The logger to output errors to * @returns {void} */ @@ -116,6 +116,6 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data }) => { - processData(/** @type {CompatStatement} */ (data), logger); + processData(/** @type {InternalCompatStatement} */ (data), logger); }, }; diff --git a/lint/linter/test-status-inheritance.js b/lint/linter/test-status-inheritance.js index 2a428a13fb95f3..3faece162c70a1 100644 --- a/lint/linter/test-status-inheritance.js +++ b/lint/linter/test-status-inheritance.js @@ -7,11 +7,11 @@ import walk from '../../utils/walk.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatData, Identifier} from '../../types/types.js' */ +/** @import {InternalCompatData, InternalIdentifier} from '../../types/index.js' */ /** * Checks for correct inheritance of statuses. - * @param {CompatData} data The data to test + * @param {InternalCompatData} data The data to test * @param {Logger} logger The logger to output errors to * @returns {void} */ @@ -21,7 +21,7 @@ const checkStatusInheritance = (data, logger) => { if (feature.compat.status?.deprecated === true) { for (const subfeature of walk( undefined, - /** @type {Identifier} */ (feature.data), + /** @type {InternalIdentifier} */ (feature.data), )) { if (subfeature.compat.status?.deprecated === false) { logger.error( @@ -38,7 +38,7 @@ const checkStatusInheritance = (data, logger) => { if (feature.compat.status?.experimental === true) { for (const subfeature of walk( undefined, - /** @type {Identifier} */ (feature.data), + /** @type {InternalIdentifier} */ (feature.data), )) { if ( subfeature.compat.status?.experimental === false && @@ -58,7 +58,7 @@ const checkStatusInheritance = (data, logger) => { if (feature.compat.status?.standard_track === false) { for (const subfeature of walk( undefined, - /** @type {Identifier} */ (feature.data), + /** @type {InternalIdentifier} */ (feature.data), )) { if (subfeature.compat.status?.standard_track === true) { logger.error( @@ -85,6 +85,6 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data }) => { - checkStatusInheritance(/** @type {CompatData} */ (data), logger); + checkStatusInheritance(/** @type {InternalCompatData} */ (data), logger); }, }; diff --git a/lint/linter/test-status.js b/lint/linter/test-status.js index a129ca01ebf9ed..8a38d73d780557 100644 --- a/lint/linter/test-status.js +++ b/lint/linter/test-status.js @@ -4,11 +4,10 @@ import { styleText } from 'node:util'; import bcd from '../../index.js'; -const { browsers } = bcd; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatStatement} from '../../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement} from '../../types/index.js' */ // See: https://github.com/web-platform-dx/web-features/blob/main/docs/baseline.md#core-browser-set const CORE_BROWSER_SET = new Set([ @@ -23,7 +22,7 @@ const CORE_BROWSER_SET = new Set([ /** * Check if experimental should be true or false - * @param {CompatStatement} data The data to check + * @param {InternalCompatStatement} data The data to check * @returns {boolean} The expected experimental status */ export const checkExperimental = (data) => { @@ -40,7 +39,12 @@ export const checkExperimental = (data) => { // Consider only the first part of an array statement. const statement = Array.isArray(support) ? support[0] : support; // Ignore anything behind flag, prefix or alternative name - if (statement.flags || statement.prefix || statement.alternative_name) { + if ( + typeof statement !== 'object' || + statement.flags || + statement.prefix || + statement.alternative_name + ) { continue; } if (statement.version_added && !statement.version_removed) { @@ -55,7 +59,7 @@ export const checkExperimental = (data) => { const engineSupport = new Set(); for (const browser of browserSupport) { - const currentRelease = Object.values(browsers[browser].releases).find( + const currentRelease = Object.values(bcd.browsers[browser].releases).find( (r) => r.status === 'current', ); const engine = currentRelease?.engine; @@ -81,7 +85,7 @@ export const checkExperimental = (data) => { /** * Check the status blocks of the compat date - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {Logger} logger The logger to output errors to * @param {string} category The feature category * @returns {void} @@ -141,6 +145,10 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data, path: { category } }) => { - checkStatus(/** @type {CompatStatement} */ (data), logger, category); + checkStatus( + /** @type {InternalCompatStatement} */ (data), + logger, + category, + ); }, }; diff --git a/lint/linter/test-status.test.js b/lint/linter/test-status.test.js index 126afc9f56603f..515e15bef0c134 100644 --- a/lint/linter/test-status.test.js +++ b/lint/linter/test-status.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; @@ -11,7 +11,7 @@ import test, { checkExperimental } from './test-status.js'; describe('checkExperimental', () => { it('should return true when data is not experimental', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: false, @@ -25,7 +25,7 @@ describe('checkExperimental', () => { }); it('should return true when data is experimental but supported by only one engine', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: true, @@ -46,7 +46,7 @@ describe('checkExperimental', () => { }); it('should return false when data is experimental and supported by more than one engine', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: true, @@ -67,7 +67,7 @@ describe('checkExperimental', () => { }); it('should ignore non-relevant browsers when determining experimental status', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: true, @@ -101,7 +101,7 @@ describe('checkStatus', () => { }); it('should not log error when status is not defined', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: undefined, support: {}, @@ -113,7 +113,7 @@ describe('checkStatus', () => { }); it('should log error when category is webextensions and status is defined', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: false, @@ -133,7 +133,7 @@ describe('checkStatus', () => { }); it('should log error when status is both experimental and deprecated', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: true, @@ -150,7 +150,7 @@ describe('checkStatus', () => { }); it('should log error when status is non-standard but has a spec_url', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: false, @@ -168,7 +168,7 @@ describe('checkStatus', () => { }); it('should log error when status is experimental and supported by more than one engine', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { status: { experimental: true, diff --git a/lint/linter/test-tags.js b/lint/linter/test-tags.js index fa238aca066337..8331b75883e5c4 100644 --- a/lint/linter/test-tags.js +++ b/lint/linter/test-tags.js @@ -7,14 +7,14 @@ import { features } from 'web-features'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ const allowedNamespaces = ['web-features']; const validFeatureIDs = Object.keys(features); /** * Process the data for spec URL errors - * @param {CompatStatement} data The data to test + * @param {InternalCompatStatement} data The data to test * @param {Logger} logger The logger to output errors to * @returns {void} */ @@ -62,6 +62,6 @@ export default { * @param {LinterData} root The data to test */ check: (logger, { data }) => { - processData(/** @type {CompatStatement} */ (data), logger); + processData(/** @type {InternalCompatStatement} */ (data), logger); }, }; diff --git a/lint/linter/test-tags.test.js b/lint/linter/test-tags.test.js index 6c60855833b399..517bb9413d90ff 100644 --- a/lint/linter/test-tags.test.js +++ b/lint/linter/test-tags.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; @@ -18,7 +18,7 @@ describe('test.check', () => { }); it('should not log error when tags are not defined', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { tags: undefined, support: {}, @@ -30,7 +30,7 @@ describe('test.check', () => { }); it('should not log error when tags are valid', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { tags: ['web-features:javascript'], support: {}, @@ -42,7 +42,7 @@ describe('test.check', () => { }); it('should log error when tags do not have a namespace', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { tags: ['tag1'], support: {}, @@ -55,7 +55,7 @@ describe('test.check', () => { }); it('should log error when tags do not use one of the allowed namespaces', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { tags: ['namespace3:tag1'], support: {}, @@ -68,7 +68,7 @@ describe('test.check', () => { }); it('should log an error when an invalid web-feature ID is used', () => { - /** @type {CompatStatement} */ + /** @type {InternalCompatStatement} */ const data = { tags: ['web-features:foo'], support: {}, diff --git a/lint/linter/test-versions.js b/lint/linter/test-versions.js index 9d1c493de2980c..3c33d4e1a512ca 100644 --- a/lint/linter/test-versions.js +++ b/lint/linter/test-versions.js @@ -6,11 +6,10 @@ import { styleText } from 'node:util'; import { compare, validate } from 'compare-versions'; import bcd from '../../index.js'; -const { browsers } = bcd; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ -/** @import {BrowserName, CompatStatement, SimpleSupportStatement, VersionValue} from '../../types/types.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 */ @@ -36,9 +35,12 @@ const browserTips = { const isValidVersion = (browser, category, version) => { if (typeof version === 'string') { if (version === 'preview') { - return !!browsers[browser].preview_name; + return !!bcd.browsers[browser].preview_name; } - return Object.hasOwn(browsers[browser].releases, version.replace('≤', '')); + return Object.hasOwn( + bcd.browsers[browser].releases, + version.replace('≤', ''), + ); } return true; }; @@ -47,7 +49,7 @@ const isValidVersion = (browser, category, version) => { * 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 {SimpleSupportStatement} statement The statement to test + * @param {InternalSimpleSupportStatement} statement The statement to test * @returns {boolean | null} Whether the version added was earlier than the version removed */ const addedBeforeRemoved = (statement) => { @@ -91,8 +93,10 @@ const addedBeforeRemoved = (statement) => { */ const checkVersions = (supportData, category, logger) => { const browsersToCheck = /** @type {BrowserName[]} */ ( - Object.keys(browsers).filter((b) => - category === 'webextensions' ? browsers[b].accepts_webextensions : !!b, + Object.keys(bcd.browsers).filter((b) => + category === 'webextensions' + ? bcd.browsers[b].accepts_webextensions + : !!b, ) ); @@ -109,7 +113,7 @@ const checkVersions = (supportData, category, logger) => { : [supportStatement]) { if (statement === 'mirror') { // If the data is to be mirrored, make sure it is mirrorable - if (!browsers[browser].upstream) { + if (!bcd.browsers[browser].upstream) { logger.error( `${styleText('bold', browser)} is set to mirror, however ${styleText('bold', browser)} does not have an upstream browser.`, ); @@ -125,14 +129,14 @@ const checkVersions = (supportData, category, logger) => { } 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(browsers[browser].releases).join(', ')}, false`, + `${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 = - browsers[browser].releases[version.replace('≤', '')]; + bcd.browsers[browser].releases[version.replace('≤', '')]; if ( !releaseData || !releaseData.release_date || @@ -157,7 +161,7 @@ const checkVersions = (supportData, category, logger) => { } } - if ('flags' in statement && !browsers[browser].accepts_flags) { + 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.`, ); @@ -205,7 +209,7 @@ export default { */ check: (logger, { data, path: { category } }) => { checkVersions( - /** @type {CompatStatement} */ (data).support, + /** @type {InternalCompatStatement} */ (data).support, category, logger, ); diff --git a/lint/types.d.ts b/lint/types.d.ts index 968059b9adb306..efd2fc123492a5 100644 --- a/lint/types.d.ts +++ b/lint/types.d.ts @@ -1,11 +1,10 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -import { InternalDataType } from '../types/index.js'; -import { BrowserName } from '../types/types.js'; - import { Logger } from './utils.js'; +import type { BrowserName, InternalDataType } from '../types/index.js'; + export interface LintOptions { only?: string[]; } diff --git a/lint/utils.js b/lint/utils.js index 9f68684be88f17..8ed825aa230eec 100644 --- a/lint/utils.js +++ b/lint/utils.js @@ -4,7 +4,7 @@ import { platform } from 'node:os'; import { styleText } from 'node:util'; -/** @import {SimpleSupportStatement} from '../types/types.js' */ +/** @import {InternalSimpleSupportStatement} from '../types/index.js' */ /** @import {Linter, LinterData, LinterMessage, LinterScope} from './types.js' */ /** @type {Readonly>} */ @@ -264,7 +264,7 @@ export class Linters { /** * Returns the key for the group that this statement belongs to. - * @param {SimpleSupportStatement} support The support statement. + * @param {InternalSimpleSupportStatement} support The support statement. * @returns {string} The key of the support statement group. */ export const createStatementGroupKey = (support) => { diff --git a/lint/utils.test.js b/lint/utils.test.js index 3f331b9564aee2..16039575e63bce 100644 --- a/lint/utils.test.js +++ b/lint/utils.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {SimpleSupportStatement} from '../types/types.js' */ +/** @import {InternalSimpleSupportStatement} from '../types/index.js' */ import assert from 'node:assert/strict'; @@ -80,7 +80,7 @@ describe('utils', () => { }); it('createStatementGroupKey() works correctly', () => { - /** @type {Record} */ + /** @type {Record} */ const tests = { 'normal name': { version_added: '1', diff --git a/schemas/browsers-schema.md b/schemas/browsers-schema.md index 6d8c1b592f6940..d9d9600238da17 100644 --- a/schemas/browsers-schema.md +++ b/schemas/browsers-schema.md @@ -1,6 +1,6 @@ # The browser JSON schema -This document helps you to understand the structure of the browser (and JavaScript runtime) data in BCD, including the browser type, a display-friendly name, release data and more. Each browser is defined by a unique identifier (e.g. `firefox` or `chrome_android`). +This document helps you to understand the structure of the browser (and JavaScript runtime) data in BCD, including the browser type, a display-friendly name, release data and more. Each browser is defined by a unique identifier (e.g. `firefox` or `chrome_android`). Each browser JSON file describes exactly one browser. Note: while NodeJS and Deno are JavaScript runtimes and not browsers, data for them is placed in `browsers`, and are included whenever we use the term "browsers". @@ -44,15 +44,15 @@ The `type` string is a required property which indicates the platform category t ### `upstream` -The `upstream` string is an optional property which indicates the upstream browser updates are derived from. For example, Firefox Android's upstream browser is Firefox (desktop), and Edge's upstream browser is Chrome. This is used for mirroring data between browsers. Valid options are any browser defined in the data. +The `upstream` string is an optional property which indicates the upstream browser updates are derived from. For example, Firefox Android's upstream browser is Firefox (desktop), and Edge's upstream browser is Chrome. This is used for mirroring data between browsers. Valid options are `"chrome"`, `"chrome_android"`, `"firefox"`, `"safari"`, and `"safari_ios"`. ### `accepts_flags` -An optional boolean indicating whether the browser supports flags. If it is set to `false`, flag data will not be allowed for that browser. +A required boolean indicating whether the browser supports flags. If it is set to `false`, flag data will not be allowed for that browser. ### `accepts_webextensions` -An optional boolean indicating whether the browser supports web extensions. A `true` value will allow this browser to be defined in web extensions support. +A required boolean indicating whether the browser supports web extensions. A `true` value will allow this browser to be defined in web extensions support. ### `pref_url` @@ -74,13 +74,13 @@ The `releases` object contains data regarding the browsers' releases, using the - `esr`: This release is an Extended Support Release or Long Term Support release. - `planned`: This release is planned in the future. -- An optional `release_date` property with the `YYYY-MM-DD` release date of the browser's release. +- An optional `release_date` property with the `YYYY-MM-DD` date of when this version was released or will be released. - An optional `release_notes` property which points to release notes. It needs to be a valid URL. -- An optional `engine` property which is the name of the browser's engine. This property is placed on the individual release as a browser may switch to a different engine (e.g. Microsoft Edge switched to Chrome as its base engine). +- An optional `engine` property which is the name of the browser's engine. Valid values are `"Blink"`, `"EdgeHTML"`, `"Gecko"`, `"Presto"`, `"Trident"`, `"WebKit"`, and `"V8"`. This property is placed on the individual release as a browser may switch to a different engine (e.g. Microsoft Edge switched to Chrome as its base engine). If `engine` is specified, `engine_version` must also be provided. -- An optional `engine_version` property which is the version of the browser's engine. Depending on the browser, this may or may not differ from the browser version. +- An optional `engine_version` property indicating the engine version used in this release. Required when `engine` is specified. #### Initial versions @@ -104,6 +104,7 @@ The following table indicates initial versions for browsers in BCD. These are th | iOS Safari | 1 | | | Samsung Internet | 1.0 | | | WebView Android | 1 | | +| WebView iOS | 1 | | ## Exports diff --git a/schemas/browsers.schema.json b/schemas/browsers.schema.json index 91bb52e5686b8b..2e5a07f04bda68 100644 --- a/schemas/browsers.schema.json +++ b/schemas/browsers.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/schema", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "browser_type": { @@ -20,6 +20,36 @@ ] }, + "browser_name": { + "type": "string", + "description": "Browser key.", + "enum": [ + "bun", + "chrome", + "chrome_android", + "deno", + "edge", + "firefox", + "firefox_android", + "ie", + "nodejs", + "oculus", + "opera", + "opera_android", + "safari", + "safari_ios", + "samsunginternet_android", + "webview_android", + "webview_ios" + ] + }, + + "upstream_browser_name": { + "type": "string", + "description": "Upstream browser key.", + "enum": ["chrome", "chrome_android", "firefox", "safari", "safari_ios"] + }, + "browser_status": { "type": "string", "enum": ["retired", "current", "beta", "nightly", "esr", "planned"] @@ -27,12 +57,17 @@ "browsers": { "type": "object", + "propertyNames": { + "$ref": "#/definitions/browser_name" + }, "additionalProperties": { "$ref": "#/definitions/browser_statement" }, "minProperties": 1, + "maxProperties": 1, "errorMessage": { - "minProperties": "A browser must be described within the file." + "minProperties": "A browser must be described within the file.", + "maxProperties": "Each browser JSON file may only describe one browser." }, "tsType": "Record" }, @@ -42,29 +77,32 @@ "properties": { "name": { "type": "string", - "description": "The browser brand name (e.g. Firefox, Firefox Android, Chrome, etc.)." + "description": "Browser brand name.", + "examples": ["Firefox", "Firefox Android", "Chrome"] }, "type": { "$ref": "#/definitions/browser_type", - "description": "The platform the browser runs on (e.g. desktop, mobile, XR, or server engine).", + "description": "Platform the browser runs on.", "tsType": "BrowserType" }, "upstream": { "type": "string", - "description": "The upstream browser this browser derives from (e.g. Firefox Android is derived from Firefox, Edge is derived from Chrome).", + "description": "Upstream browser this browser derives from.", "tsType": "BrowserName" }, "preview_name": { "type": "string", - "description": "The name of the browser's preview channel (e.g. 'Nightly' for Firefox or 'TP' for Safari)." + "description": "Name of the browser's preview channel.", + "examples": ["Nightly", "TP"] }, "pref_url": { "type": "string", - "description": "URL of the page where feature flags can be changed (e.g. 'about:config' for Firefox or 'chrome://flags' for Chrome)." + "description": "URL of the page where feature flags can be changed.", + "examples": ["about:config", "chrome://flags"] }, "accepts_flags": { "type": "boolean", - "description": "Whether the browser supports user-toggleable flags that enable or disable features." + "description": "Whether the browser supports feature flags that enable or disable features." }, "accepts_webextensions": { "type": "boolean", @@ -73,7 +111,7 @@ "releases": { "type": "object", "additionalProperties": { "$ref": "#/definitions/release_statement" }, - "description": "The known versions of this browser.", + "description": "Releases of this browser.", "tsType": "{ [version: string]: ReleaseStatement };" } }, @@ -93,27 +131,27 @@ "release_date": { "type": "string", "format": "date", - "description": "The date on which this version was released, formatted as `YYYY-MM-DD`." + "description": "When this version was released or will be released (`YYYY-MM-DD`)." }, "release_notes": { "type": "string", "format": "uri", "pattern": "^https://", - "description": "A link to the release notes or changelog for a given release." + "description": "Link to the release notes or changelog." }, "status": { "$ref": "#/definitions/browser_status", - "description": "A property indicating where in the lifetime cycle this release is in (e.g. current, retired, beta, nightly).", + "description": "Where in the lifetime cycle this release is.", "tsType": "BrowserStatus" }, "engine": { "$ref": "#/definitions/browser_engine", - "description": "Name of the browser's underlying engine.", + "description": "Browser engine name.", "tsType": "BrowserEngine" }, "engine_version": { "type": "string", - "description": "Version of the engine corresponding to the browser version." + "description": "Engine version used in this release." } }, "required": ["status"], diff --git a/schemas/compat-data-schema.md b/schemas/compat-data-schema.md index 8f84400a1de139..d4846324f3ba64 100644 --- a/schemas/compat-data-schema.md +++ b/schemas/compat-data-schema.md @@ -72,9 +72,9 @@ A feature is described by an identifier containing the `__compat` property. In o When an identifier has a `__compat` block, it represents its basic support, indicating that a minimal implementation of a functionality is included. What it represents exactly depends on the evolution of the feature over time, both in terms of specifications and of browser support. -#### Sub-features +#### Subfeatures -To add a sub-feature, a new identifier is added below the main feature at the level of a `__compat` object (see the sub-features "start" and "end" above). The same could be done for sub-sub-features. There is no depth limit. +To add a subfeature, a new identifier is added below the main feature at the level of a `__compat` object (see the subfeatures "start" and "end" above). The same could be done for sub-subfeatures. There is no depth limit. See [Data guidelines](../docs/data-guidelines/README.md) for more information about feature naming conventions and other best practices. @@ -178,16 +178,14 @@ The `__compat` object consists of the following: It is intended to be used as a caption or title and should be kept short. This property may be formatted using Markdown, see the rules for `notes`. -- An automated `source_file` property containing the path to the source file containing the feature. This is used to create links to the repository source (in the form of `https://github.com/mdn/browser-compat-data/blob/main/`). For example, `api.History.forward` will contain a `source_file` property of `api/History.json` since the feature is defined in that file. - - An optional `mdn_url` property which **points to an MDN reference page documenting the feature**. It needs to be a valid URL, and should be the language-neutral URL (e.g. use `https://developer.mozilla.org/docs/Web/CSS/text-align` instead of `https://developer.mozilla.org/en-US/docs/Web/CSS/text-align`). - An optional `spec_url` property as a URL or an array of URLs, each of which is for a specific part of a specification in which this feature is defined. - Each URL must either contain a fragment identifier (e.g. `https://tc39.es/proposal-promise-allSettled/#sec-promise.allsettled`), or else must match the regular-expression pattern `^https://registry.khronos.org/webgl/extensions/[^/]+/` (e.g. `https://registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/`). + Each URL must either contain a fragment identifier (e.g. `https://tc39.es/proposal-promise-allSettled/#sec-promise.allsettled`), or else must match the regular-expression pattern `^https://registry.khronos.org/webgl/extensions/[^/]+/` (e.g. `https://registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/`) or `^https://github.com/WebAssembly/.+` for WebAssembly specs. Each URL must link to a specification published by a standards body or a formal proposal that may lead to such publication. -- An optional `tags` property which is an array of strings allowing to assign tags to the feature. +- An optional `tags` property to assign tags to the feature. Each tag in the array must be namespaced. The currently allowed namespaces are: - `web-features`: A namespace to tag features belonging to a web platform feature group as defined by [web-platform-dx/web-features](https://github.com/web-platform-dx/web-features/blob/main/features/README.md). @@ -215,7 +213,7 @@ The currently accepted browser identifiers should be declared in alphabetical or - `webview_android`, WebView, the embedded browser for Android applications - `webview_ios`, WebKit WebView, the embedded browser for iOS applications, based on the iOS version -Desktop browser identifiers are mandatory, with the `version_added` property set to `null` if support is unknown. +Desktop browser identifiers are mandatory, with the `version_added` property set to `false` if the feature is not supported. #### The `support_statement` object @@ -271,7 +269,7 @@ The `simple_support_statement` object is the core object containing the compatib #### `version_added` -This is the only mandatory property and it contains a string with the version number indicating when a sub-feature has been added (and is therefore supported). The Boolean values indicate that a sub-feature is supported (`true`, with the additional meaning that it is unknown in which version support was added) or not supported (`false`). A value of `null` indicates that support information is entirely unknown. Examples: +This is the only mandatory property and it contains a string with the version number indicating when a subfeature has been added (and is therefore supported), or the value `false` indicating the feature is not supported. Examples: - Support from version 3.5 (inclusive): @@ -307,7 +305,7 @@ This is the only mandatory property and it contains a string with the version nu #### `version_removed` -Contains a string with the version number the sub-feature was removed in. It may also be `true`, meaning that it is unknown in which version support was removed. If the feature has not been removed from the browser, this property is omitted, rather than being set to `false`. +Contains a string with the version number the subfeature was removed in. If the feature has not been removed from the browser, this property is omitted, rather than being set to `false`. Examples: @@ -320,21 +318,6 @@ Examples: } ``` -#### `version_last` - -> [!NOTE] -> This property is automatically generated at build time. - -If `version_removed` is present, a `version_last` is automatically generated during build time, which will be set to the version number of the last browser version that supported the feature. For example, assuming the browser version only incremented in whole numbers, if a feature was added in version 20 and supported until 29, then was no longer supported in 30, `version_removed` would be `30` and `version_last` will be `29`: - -```json -{ - "version_added": "20", - "version_removed": "30", - "version_last": "29" -} -``` - ### Ranged versions (≤) For certain browser versions, ranged versions (also called "ranged values") are allowed as it is sometimes impractical to find out in which early version of a browser a feature shipped. Ranged versions are a way to include some version data in BCD, while also stating the version number may not be accurate. These values state that the feature has been confirmed to be supported in at least a certain version of the browser, but may have been added in an earlier release. Ranged versions are indicated by the `Less Than or Equal To (U+2264)` (`≤`) symbol before the version number. @@ -351,7 +334,7 @@ Ranged versions should be used sparingly and only when it is impossible or highl #### `prefix` -A prefix to add to the sub-feature name (defaults to empty string). +A prefix to add to the subfeature name (defaults to empty string). If applicable, leading and trailing `-` must be included. Examples: @@ -392,13 +375,13 @@ Note that you can’t have both `prefix` and `alternative_name`. #### `flags` -An optional array of objects describing flags that must be configured for this browser to support this feature. Usually this array will have one item, but there are cases where two or more flags can be required to activate a feature. An object in the `flags` array consists of three properties: +An array of objects describing feature flags that must be configured for this browser to support this feature. Usually this array will have one item, but there are cases where two or more flags can be required to activate a feature. An object in the `flags` array consists of three properties: -- `type` (mandatory): an enum that indicates the flag type: +- `type` (mandatory): the flag type: - `preference` a flag the user can set (like in `about:config` in Firefox). - `runtime_flag` a flag to be set before starting the browser. -- `name` (mandatory): a string giving the value which the specified flag must be set to for this feature to work. -- `value_to_set` (optional): representing the actual value to set the flag to. It is a string, that may be converted to the right type (that is `true` or `false` for Boolean value, or `4` for an integer value). It doesn't need to be enclosed in backticks. +- `name` (mandatory): the name of the flag or preference to configure. +- `value_to_set` (optional): the value to set the flag to. It is a string, that may be converted to the right type (that is `true` or `false` for Boolean value, or `4` for an integer value). Example for one flag required: @@ -437,13 +420,11 @@ Example for two flags required: #### `impl_url` -An optional changeset/commit URL for the revision which implemented the feature in the source code, or the URL to the bug tracking the implementation, for the associated browser. The presence of an `impl_url` value indicates that the associated browser has implemented the feature or intends to implement the feature. - -For changeset/commit URLs, this is typically a https://trac.webkit.org/changeset/, https://hg.mozilla.org/mozilla-central/rev/, or https://crrev.com/ URL for a changeset with a subject line that will typically be something of the form _"Implement [feature]"_, _"Support [feature]"_, or _"Enable [feature]"_. For bug URLs, this is typically a https://webkit.org/b/, https://bugzil.la/, or https://crbug.com/ URL indicating an intent to implement and ship the feature. +A URL or array of URLs linking to the bug or issue that tracks the implementation of this feature. #### `notes` -A string or `array` of strings containing additional information. If there is only one entry, the value of `notes` must simply be a string instead of an array. +A string or `array` of strings containing additional information about the feature's support. If there is only one entry, the value of `notes` must simply be a string instead of an array. Example: @@ -463,7 +444,7 @@ Notes may be formatted in Markdown. Only links, bold, italics, codeblocks, and ` #### `partial_implementation` -A `boolean` value indicating whether or not the implementation of the sub-feature deviates from the specification in a way that may cause significant compatibility problems. It defaults to `false` (no interoperability problems expected). If set to `true`, it is [required](../docs/data-guidelines/README.md#partial_implementation-requires-a-note) that you add a note explaining how it diverges from the standard (such as that it implements an old version of the standard). +Set to `true` if the browser's support does not implement mandatory specified behavior, is inconsistent with other browsers, causes confusing feature detection results, and has a demonstrable negative impact on web developers. It defaults to `false`. If set to `true`, it is [required](../docs/data-guidelines/README.md#partial_implementation-requires-a-note) that you add a note explaining how it diverges from the standard. ```json { @@ -499,7 +480,7 @@ The mandatory status property contains information about stability of the featur - `deprecated`: a `boolean` value. - If `deprecated` is `true`, then the feature is no longer recommended. It might be removed in the future or might only be kept for compatibility purposes. Avoid using this functionality. + If `deprecated` is `true`, then the feature is no longer recommended. It might be removed in the future or kept only for compatibility purposes. ```json "__compat": { diff --git a/schemas/compat-data.schema.json b/schemas/compat-data.schema.json index 888115301b380f..9644541079b80c 100644 --- a/schemas/compat-data.schema.json +++ b/schemas/compat-data.schema.json @@ -1,12 +1,12 @@ { - "$schema": "http://json-schema.org/schema", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "simple_support_statement": { + "internal_simple_support_statement": { "type": "object", "properties": { "version_added": { - "description": "A string (indicating which browser version added this feature), or the value false (indicating the feature is not supported).", + "description": "Browser version that added this feature, or false if not supported.", "anyOf": [ { "type": "string", @@ -19,28 +19,22 @@ "tsType": "VersionValue" }, "version_removed": { - "description": "A string, indicating which browser version removed this feature.", - "type": "string", - "pattern": "^(≤?(\\d+)(\\.\\d+)*|preview)$", - "tsType": "string" - }, - "version_last": { - "description": "A string, indicating the last browser version that supported this feature. This is automatically generated.", + "description": "Browser version that removed this feature.", "type": "string", "pattern": "^(≤?(\\d+)(\\.\\d+)*|preview)$", "tsType": "string" }, "prefix": { "type": "string", - "description": "A prefix to add to the sub-feature name (defaults to empty string). If applicable, leading and trailing '-' must be included." + "description": "Prefix for the subfeature name. Leading and trailing '-' must be included if applicable." }, "alternative_name": { "type": "string", - "description": "An alternative name for the feature, for cases where a feature is implemented under an entirely different name and not just prefixed." + "description": "Alternative name for the feature, when implemented under an entirely different name." }, "flags": { "type": "array", - "description": "An optional array of objects describing flags that must be configured for this browser to support this feature.", + "description": "Feature flags that must be configured for this browser to support this feature.", "minItems": 1, "items": { "$ref": "#/definitions/flag_statement" @@ -60,7 +54,7 @@ } } ], - "description": "An optional changeset/commit URL for the revision which implemented the feature in the source code, or the URL to the bug tracking the implementation, for the associated browser.", + "description": "Link to the bug or issue that tracks the implementation of this feature.", "tsType": "string | [string, string, ...string[]]", "errorMessage": { "pattern": "impl_url must be a link to a browser commit or bug URL. URLs must be in shortened form (ex. bugs.chromium.org -> crbug.com). Note: `npm run fix` may resolve these issues." @@ -68,10 +62,10 @@ }, "partial_implementation": { "const": true, - "description": "A boolean value indicating whether or not the implementation of the sub-feature deviates from the specification in a way that may cause compatibility problems. It defaults to false (no interoperability problems expected). If set to true, it is recommended that you add a note explaining how it diverges from the standard (such as that it implements an old version of the standard, for example)." + "description": "Set if the browser's support does not implement mandatory specified behavior, is inconsistent with other browsers, causes confusing feature detection results, and has a demonstrable negative impact on web developers." }, "notes": { - "description": "A string or array of strings containing additional information.", + "description": "Additional information about the feature's support.", "anyOf": [ { "type": "string" @@ -104,59 +98,58 @@ "properties": { "type": { "type": "string", - "description": "An enum that indicates the flag type.", + "description": "Flag type.", "enum": ["preference", "runtime_flag"] }, "name": { "type": "string", - "description": "A string giving the name of the flag or preference that must be configured." + "description": "Name of the flag or preference to configure." }, "value_to_set": { "type": "string", - "description": "A string giving the value which the specified flag must be set to for this feature to work." + "description": "Value to set the flag to." } }, "additionalProperties": false, "required": ["type", "name"] }, - "support_statement": { + "internal_support_statement": { "anyOf": [ - { "$ref": "#/definitions/simple_support_statement" }, + { "$ref": "#/definitions/internal_simple_support_statement" }, { "type": "array", "minItems": 2, "items": { - "$ref": "#/definitions/simple_support_statement" + "$ref": "#/definitions/internal_simple_support_statement" } }, { "const": "mirror" } - ], - "tsType": "SimpleSupportStatement | [SimpleSupportStatement, SimpleSupportStatement, ...SimpleSupportStatement[]]" + ] }, - "status_block": { + "internal_status_block": { "type": "object", "properties": { "experimental": { "type": "boolean", "deprecated": true, - "description": "(This property is deprecated. Prefer using more well-defined stability calculations, such as Baseline, instead.) A boolean value. Usually, this value is true for single-implementer features and false for multiple-implementer features or single-implementer features that are not expected to change." + "description": "(This property is deprecated. Prefer using more well-defined stability calculations, such as Baseline, instead.) Usually, this value is true for single-implementer features and false for multiple-implementer features or single-implementer features that are not expected to change." }, "standard_track": { "type": "boolean", - "description": "A boolean value indicating whether the feature is part of an active specification or specification process." + "description": "Whether the feature is part of an active specification or specification process." }, "deprecated": { "type": "boolean", - "description": "A boolean value that indicates whether the feature is no longer recommended. It might be removed in the future or might only be kept for compatibility purposes. Avoid using this functionality." + "description": "Whether the feature is no longer recommended. It might be removed in the future or kept only for compatibility purposes." } }, "required": ["experimental", "standard_track", "deprecated"], "additionalProperties": false }, - "support_block": { + "internal_support_block": { "type": "object", "propertyNames": { "enum": [ @@ -180,9 +173,9 @@ ] }, "additionalProperties": { - "$ref": "#/definitions/support_statement" + "$ref": "#/definitions/internal_support_statement" }, - "tsType": "Partial>" + "tsType": "Partial>" }, "spec_url_value": { @@ -197,18 +190,18 @@ "pattern": "^https://(trac.webkit.org/changeset/|hg.mozilla.org/mozilla-central/rev/|crrev.com/|bugzil.la/|crbug.com/|webkit.org/b/|github.com/GoogleChromeLabs/chromium-bidi/issues/)" }, - "compat_statement": { + "internal_compat_statement": { "type": "object", "properties": { "description": { "type": "string", - "description": "A string containing a human-readable description of the feature." + "description": "Human-readable description of the feature." }, "mdn_url": { "type": "string", "format": "uri", "pattern": "^https://developer\\.mozilla\\.org/docs/", - "description": "A URL that points to an MDN reference page documenting the feature. The URL should be language-agnostic." + "description": "Link to the MDN reference page for this feature. Must be language-agnostic." }, "spec_url": { "anyOf": [ @@ -223,69 +216,67 @@ } } ], - "description": "An optional URL or array of URLs, each of which is for a specific part of a specification in which this feature is defined. Each URL must contain a fragment identifier.", + "description": "URL(s) for specific parts of a specification in which this feature is defined. Each URL must contain a fragment identifier.", "tsType": "string | [string, string, ...string[]]" }, "tags": { - "description": "An optional array of strings allowing to assign tags to the feature.", + "description": "Tags assigned to the feature.", "type": "array", "minItems": 1, "tsType": "[string, ...string[]]" }, - "source_file": { - "type": "string", - "description": "The path to the file that defines this feature in browser-compat-data, relative to the repository root. Useful for guiding potential contributors towards the correct file to edit. This is automatically generated at build time and should never manually be specified." - }, "support": { - "$ref": "#/definitions/support_block", - "description": "The data for the support of each browser, containing a `support_statement` object for each browser identifier with information about versions, prefixes, or alternate names, as well as notes." + "$ref": "#/definitions/internal_support_block", + "description": "Browser support data for this feature." }, "status": { - "$ref": "#/definitions/status_block", - "description": "An object containing information about the stability of the feature." + "$ref": "#/definitions/internal_status_block", + "description": "Information about the stability of the feature." } }, "required": ["support"], "additionalProperties": false }, - "identifier": { + "internal_identifier": { "type": "object", "properties": { "__compat": { "type": "object", - "$ref": "#/definitions/compat_statement", + "$ref": "#/definitions/internal_compat_statement", "required": ["status"], - "description": "A feature is described by an identifier containing the `__compat` property.\n\nIn other words, identifiers without `__compat` aren't necessarily features, but help to nest the features properly.\n\nWhen an identifier has a `__compat` block, it represents its basic support, indicating that a minimal implementation of a functionality is included.\n\nWhat it represents exactly depends of the evolution of the feature over time, both in terms of specifications and of browser support.", - "tsType": "CompatStatement" + "description": "The compatibility data for a feature, nested under the `__compat` property of an identifier.", + "tsType": "InternalCompatStatement" } }, "patternProperties": { - "^(?!__compat)[a-zA-Z_0-9-$@]*$": { "$ref": "#/definitions/identifier" } + "^(?!__compat)[a-zA-Z_0-9-$@]*$": { + "$ref": "#/definitions/internal_identifier" + } }, "additionalProperties": false, "errorMessage": { "additionalProperties": "Feature names can only contain alphanumerical characters or the following symbols: _-$@" }, - "tsType": "{[key: string]: Identifier} & {__compat?: CompatStatement};" + "tsType": "{__compat?: InternalCompatStatement} & Record, InternalIdentifier>" }, - "webextensions_identifier": { + "webextensions_internal_identifier": { "type": "object", "properties": { - "__compat": { "$ref": "#/definitions/compat_statement" } + "__compat": { "$ref": "#/definitions/internal_compat_statement" } }, "patternProperties": { "^(?!__compat)[a-zA-Z_0-9-$@]*$": { - "$ref": "#/definitions/webextensions_identifier", - "tsType": "Identifier" + "$ref": "#/definitions/webextensions_internal_identifier", + "tsType": "InternalIdentifier" } }, "additionalProperties": false, "errorMessage": { "additionalProperties": "Feature names can only contain alphanumerical characters or the following symbols: _-$@" }, - "tsType": "{[key: string]: Identifier} & {__compat?: CompatStatement}" + "tsType": "{[key: string]: InternalIdentifier} & {__compat?: InternalCompatStatement}" } }, @@ -293,14 +284,14 @@ "type": "object", "patternProperties": { "^(?!__compat)(?!webextensions)[a-zA-Z_0-9-$@]*$": { - "$ref": "#/definitions/identifier" + "$ref": "#/definitions/internal_identifier" }, "^webextensions*$": { - "$ref": "#/definitions/webextensions_identifier", - "tsType": "Identifier" + "$ref": "#/definitions/webextensions_internal_identifier", + "tsType": "InternalIdentifier" }, "^__compat$": { - "$ref": "#/definitions/compat_statement" + "$ref": "#/definitions/internal_compat_statement" } }, "additionalProperties": false, diff --git a/schemas/public.schema.json b/schemas/public.schema.json new file mode 100644 index 00000000000000..bcbcf39c8ae65b --- /dev/null +++ b/schemas/public.schema.json @@ -0,0 +1,472 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "definitions": { + "meta_block": { + "type": "object", + "description": "Release metadata.", + "properties": { + "version": { + "type": "string", + "description": "Package version number of this BCD release.", + "examples": ["5.6.14", "6.0.0"] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when this BCD release was built.", + "examples": ["2025-06-15T14:32:00.000Z"] + } + }, + "required": ["version", "timestamp"], + "additionalProperties": false + }, + + "browser_type": { + "type": "string", + "description": "Platform the browser runs on.", + "enum": ["desktop", "mobile", "xr", "server"] + }, + + "browser_engine": { + "type": "string", + "description": "Browser engine.", + "enum": [ + "Blink", + "EdgeHTML", + "Gecko", + "Presto", + "Trident", + "WebKit", + "V8" + ] + }, + + "browser_name": { + "type": "string", + "description": "Browser key.", + "enum": [ + "bun", + "chrome", + "chrome_android", + "deno", + "edge", + "firefox", + "firefox_android", + "ie", + "nodejs", + "oculus", + "opera", + "opera_android", + "safari", + "safari_ios", + "samsunginternet_android", + "webview_android", + "webview_ios" + ] + }, + + "upstream_browser_name": { + "type": "string", + "description": "Upstream browser key.", + "enum": ["chrome", "chrome_android", "firefox", "safari", "safari_ios"] + }, + + "browser_status": { + "type": "string", + "description": "Lifetime cycle status of a browser release.", + "enum": ["retired", "current", "beta", "nightly", "esr", "planned"] + }, + + "browsers": { + "type": "object", + "description": "Data about browsers, such as name, type and releases.", + "propertyNames": { + "$ref": "#/definitions/browser_name" + }, + "additionalProperties": { + "$ref": "#/definitions/browser_statement" + }, + "tsType": "Record" + }, + + "browser_statement": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the browser.", + "examples": ["Firefox", "Firefox Android", "Chrome"] + }, + "type": { + "$ref": "#/definitions/browser_type" + }, + "upstream": { + "$ref": "#/definitions/upstream_browser_name" + }, + "preview_name": { + "type": "string", + "description": "Name of the browser's preview channel.", + "examples": ["Nightly", "TP"] + }, + "pref_url": { + "type": "string", + "description": "URL of the page where feature flags can be changed.", + "examples": ["about:config", "chrome://flags"] + }, + "accepts_flags": { + "type": "boolean", + "description": "Whether the browser supports feature flags that enable or disable features." + }, + "accepts_webextensions": { + "type": "boolean", + "description": "Whether the browser supports extensions." + }, + "releases": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/release_statement" }, + "description": "Releases of this browser.", + "tsType": "Record" + } + }, + "required": [ + "name", + "type", + "releases", + "accepts_flags", + "accepts_webextensions" + ], + "additionalProperties": false + }, + + "release_statement": { + "type": "object", + "properties": { + "release_date": { + "type": "string", + "format": "date", + "description": "When this version was released or will be released (`YYYY-MM-DD`)." + }, + "release_notes": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "Link to the release notes or changelog." + }, + "status": { + "$ref": "#/definitions/browser_status" + }, + "engine": { + "$ref": "#/definitions/browser_engine" + }, + "engine_version": { + "type": "string", + "description": "Engine version used in this release." + } + }, + "required": ["status"], + "dependencies": { + "engine": ["engine_version"] + }, + "additionalProperties": false + }, + + "simple_support_statement": { + "type": "object", + "properties": { + "version_added": { + "$ref": "#/definitions/version_value" + }, + "version_removed": { + "description": "Browser version that removed this feature.", + "type": "string", + "pattern": "^(≤?(\\d+)(\\.\\d+)*|preview)$" + }, + "version_last": { + "description": "Last browser version that supported this feature.", + "type": "string", + "pattern": "^(≤?(\\d+)(\\.\\d+)*|preview)$" + }, + "prefix": { + "type": "string", + "description": "Prefix for the subfeature name. Leading and trailing '-' must be included if applicable." + }, + "alternative_name": { + "type": "string", + "description": "Alternative name for the feature, when implemented under an entirely different name." + }, + "flags": { + "type": "array", + "description": "Feature flags that must be configured for this browser to support this feature.", + "minItems": 1, + "items": { + "$ref": "#/definitions/flag_statement" + } + }, + "impl_url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "array", + "minItems": 2, + "items": { + "type": "string", + "format": "uri" + } + } + ], + "description": "Link to the bug or issue that tracks the implementation of this feature." + }, + "partial_implementation": { + "const": true, + "description": "Set if the browser's support does not implement mandatory specified behavior, is inconsistent with other browsers, causes confusing feature detection results, and has a demonstrable negative impact on web developers." + }, + "notes": { + "description": "Additional information about the feature's support.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 2, + "items": { + "type": "string" + } + } + ] + } + }, + "required": ["version_added"], + "dependencies": { + "partial_implementation": { + "if": { + "properties": { "partial_implementation": { "const": true } } + }, + "then": { "required": ["notes"] } + }, + "version_removed": { + "required": ["version_last"] + } + }, + "additionalProperties": false + }, + + "version_value": { + "description": "Browser version that added this feature, or false if not supported.", + "anyOf": [ + { + "type": "string", + "pattern": "^(≤?(\\d+)(\\.\\d+)*|preview)$" + }, + { + "const": false + } + ] + }, + + "flag_statement": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Flag type.", + "enum": ["preference", "runtime_flag"] + }, + "name": { + "type": "string", + "description": "Name of the flag or preference to configure." + }, + "value_to_set": { + "type": "string", + "description": "Value to set the flag to." + } + }, + "additionalProperties": false, + "required": ["type", "name"] + }, + + "support_statement": { + "anyOf": [ + { "$ref": "#/definitions/simple_support_statement" }, + { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/definitions/simple_support_statement" + } + } + ] + }, + + "status_block": { + "type": "object", + "description": "Information about the stability of the feature.", + "properties": { + "experimental": { + "type": "boolean", + "deprecated": true, + "description": "(This property is deprecated. Prefer using more well-defined stability calculations, such as Baseline, instead.) Usually, this value is true for single-implementer features and false for multiple-implementer features or single-implementer features that are not expected to change." + }, + "standard_track": { + "type": "boolean", + "description": "Whether the feature is part of an active specification or specification process." + }, + "deprecated": { + "type": "boolean", + "description": "Whether the feature is no longer recommended. It might be removed in the future or kept only for compatibility purposes." + } + }, + "required": ["experimental", "standard_track", "deprecated"], + "additionalProperties": false + }, + + "support_block": { + "type": "object", + "description": "Browser support data for this feature.", + "propertyNames": { + "$ref": "#/definitions/browser_name" + }, + "additionalProperties": { + "$ref": "#/definitions/support_statement" + }, + "tsType": "Partial>" + }, + + "compat_statement": { + "type": "object", + "description": "The compatibility data for a feature, nested under the `__compat` property of an identifier.", + "properties": { + "description": { + "type": "string", + "description": "Human-readable description of the feature." + }, + "mdn_url": { + "type": "string", + "format": "uri", + "pattern": "^https://developer\\.mozilla\\.org/docs/", + "description": "Link to the MDN reference page documenting the feature." + }, + "spec_url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "array", + "minItems": 2, + "items": { + "type": "string", + "format": "uri" + } + } + ], + "description": "URL(s) for specific parts of a specification in which this feature is defined. Each URL must contain a fragment identifier." + }, + "tags": { + "description": "Tags assigned to the feature.", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "source_file": { + "type": "string", + "description": "Path to the file defining this feature, relative to the repository root. Automatically generated at build time." + }, + "support": { + "$ref": "#/definitions/support_block" + }, + "status": { + "$ref": "#/definitions/status_block" + } + }, + "required": ["source_file", "support"], + "additionalProperties": false + }, + + "identifier": { + "type": "object", + "properties": { + "__compat": { + "$ref": "#/definitions/compat_statement" + } + }, + "patternProperties": { + "^(?!__compat)[a-zA-Z_0-9-$@]*$": { "$ref": "#/definitions/identifier" } + }, + "additionalProperties": false, + "tsType": "{__compat?: InternalCompatStatement} & Record, InternalIdentifier>" + } + }, + + "title": "CompatData", + "type": "object", + "description": "Published BCD data.", + "properties": { + "__meta": { + "$ref": "#/definitions/meta_block" + }, + "api": { + "$ref": "#/definitions/identifier" + }, + "browsers": { + "$ref": "#/definitions/browsers" + }, + "css": { + "$ref": "#/definitions/identifier" + }, + "html": { + "$ref": "#/definitions/identifier" + }, + "http": { + "$ref": "#/definitions/identifier" + }, + "javascript": { + "$ref": "#/definitions/identifier" + }, + "manifests": { + "$ref": "#/definitions/identifier" + }, + "mathml": { + "$ref": "#/definitions/identifier" + }, + "mediatypes": { + "$ref": "#/definitions/identifier" + }, + "svg": { + "$ref": "#/definitions/identifier" + }, + "webassembly": { + "$ref": "#/definitions/identifier" + }, + "webdriver": { + "$ref": "#/definitions/identifier" + }, + "webextensions": { + "$ref": "#/definitions/identifier" + } + }, + "required": [ + "__meta", + "api", + "browsers", + "css", + "html", + "http", + "javascript", + "manifests", + "mathml", + "mediatypes", + "svg", + "webassembly", + "webdriver", + "webextensions" + ], + "additionalProperties": false +} diff --git a/schemas/public.schema.md b/schemas/public.schema.md new file mode 100644 index 00000000000000..f4ea118ec70c1c --- /dev/null +++ b/schemas/public.schema.md @@ -0,0 +1,380 @@ +# The public JSON schema + +This document helps you to understand the structure of the `data.json` file included in the published `@mdn/browser-compat-data` package. The file combines all browser definitions, feature compatibility data, and metadata into one object. Several transformations are applied at build time (see [Build-time transformations](#build-time-transformations)). + +## JSON structure + +Below is a simplified example of the published data: + +```json +{ + "__meta": { + "version": "6.0.0", + "timestamp": "2024-01-15T00:00:00.000Z" + }, + "browsers": { + "firefox": { + "name": "Firefox", + "type": "desktop", + "preview_name": "Nightly", + "pref_url": "about:config", + "accepts_flags": true, + "accepts_webextensions": true, + "releases": { + "1.5": { + "release_date": "2005-11-29", + "release_notes": "https://developer.mozilla.org/Firefox/Releases/1.5", + "status": "retired", + "engine": "Gecko", + "engine_version": "1.8" + } + } + } + }, + "api": { + "AbortController": { + "__compat": { + "source_file": "api/AbortController.json", + "mdn_url": "https://developer.mozilla.org/docs/Web/API/AbortController", + "spec_url": "https://dom.spec.whatwg.org/#interface-abortcontroller", + "support": { + "chrome": { "version_added": "66" }, + "firefox": { "version_added": "57" } + }, + "status": { + "experimental": false, + "standard_track": true, + "deprecated": false + } + } + } + }, + "css": {}, + "html": {}, + "http": {}, + "javascript": {}, + "manifests": {}, + "mathml": {}, + "mediatypes": {}, + "svg": {}, + "webassembly": {}, + "webdriver": {}, + "webextensions": {} +} +``` + +## Properties + +### `__meta` + +The `__meta` object is a required property containing metadata about the published data. It has two required properties: + +- `version`: a string containing the package version (e.g. `"6.0.0"`). +- `timestamp`: a string containing the ISO 8601 date-time when the data was built (e.g. `"2024-01-15T00:00:00.000Z"`). + +### `browsers` + +The `browsers` object is a required property containing data for all browsers and JavaScript runtimes, keyed by browser identifier. See [browsers-schema.md](./browsers-schema.md) for the full browser data structure. + +### Feature categories + +The remaining top-level properties are all required and each contains a tree of feature identifiers and `__compat` objects: + +- `api` — Web API interfaces +- `css` — CSS properties, selectors, and at-rules +- `html` — HTML elements and attributes +- `http` — HTTP headers, methods, and status codes +- `javascript` — JavaScript language features +- `manifests` — Web App Manifest keys +- `mathml` — MathML elements and attributes +- `mediatypes` — Media types +- `svg` — SVG elements and attributes +- `webassembly` — WebAssembly features +- `webdriver` — WebDriver commands +- `webextensions` — WebExtension APIs and manifest keys + +### The `__compat` object + +The `__compat` object describes a feature's compatibility data. It consists of the following: + +- A mandatory `support` property for **compat information**. + A [`support_statement`](#the-support_statement-object) object for each browser identifier with information about versions, prefixes, or alternate names, as well as notes. + +- An optional `status` property for **status information**. + An object containing information about the stability of the feature: + Is it a functionality that is standard? Is it stable? Has it been deprecated and shouldn't be used anymore? ([see below](#status-information)) + +- An optional `description` property to **describe the feature**. + A string containing a human-readable description of the feature. + It is intended to be used as a caption or title and should be kept short. + In the published data, this property is formatted as HTML. + +- An optional `mdn_url` property which **points to an MDN reference page documenting the feature**. + It needs to be a valid URL, and should be the language-neutral URL (e.g. use `https://developer.mozilla.org/docs/Web/CSS/text-align` instead of `https://developer.mozilla.org/en-US/docs/Web/CSS/text-align`). + +- An optional `spec_url` property as a URL or an array of URLs, each of which is for a specific part of a specification in which this feature is defined. + Each URL must contain a fragment identifier. + +- An optional `tags` property to assign tags to the feature. + +- A mandatory `source_file` property containing the path to the source file that defines this feature, relative to the repository root (e.g. `"api/AbortController.json"`). This is automatically generated at build time. + +#### The `support_statement` object + +The `support_statement` object describes the support provided by a single browser type for the given subfeature. It is either a `simple_support_statement` object, or an array of two or more `simple_support_statement` objects. + +If there is an array, the `simple_support_statement` objects are sorted with the most relevant and general entries first. In other words, entries applying to the most recent browser releases come first and entries with prefixes or flags come after those without. + +Example of a `support` compat object (with an `array_support_statement` containing 2 entries): + +```json +"support": { + "firefox": [ + { + "version_added": "6" + }, + { + "prefix": "-moz-", + "version_added": "3.5", + "version_removed": "9", + "version_last": "8" + } + ] +} +``` + +Example of a `support` compat object (with 1 entry, array omitted): + +```json +"support": { + "ie": { "version_added": "6.0" } +} +``` + +### Compat data in support statements + +The `simple_support_statement` object is the core object containing the compatibility information for a browser. It consists of the following properties: + +#### `version_added` + +This is the only mandatory property and it contains a string with the version number indicating when a subfeature has been added (and is therefore supported), or `false` indicating the feature is not supported. Examples: + +- Support from version 3.5 (inclusive): + +```json +{ + "version_added": "3.5" +} +``` + +- Support in version 79, but possibly supported earlier: + +```json +{ + "version_added": "≤79" +} +``` + +- Support in latest beta/preview release: + +```json +{ + "version_added": "preview" +} +``` + +- No support: + +```json +{ + "version_added": false +} +``` + +#### `version_removed` + +Contains a string with the version number the subfeature was removed in. If the feature has not been removed from the browser, this property is omitted. When `version_removed` is present, `version_last` is always present as well. + +Example: + +- Removed in version 10 (added in 4 and supported up until 9): + +```json +{ + "version_added": "4", + "version_removed": "10", + "version_last": "9" +} +``` + +#### `version_last` + +A string indicating the last browser version that supported the feature. This property is always present when `version_removed` is present. For example, if a feature was removed in version 30, `version_last` will be `"29"`. + +```json +{ + "version_added": "20", + "version_removed": "30", + "version_last": "29" +} +``` + +#### `prefix` + +A prefix to add to the subfeature name (defaults to empty string). +If applicable, leading and trailing `-` must be included. + +Examples: + +- A CSS property with a standard name of `prop-name` and a vendor-prefixed name of `-moz-prop-name`: + +```json +{ + "prefix": "-moz-", + "version_added": "3.5" +} +``` + +- An API with a standard name of `FeatureName` and a vendor-prefixed name of `webkitFeatureName`: + +```json +{ + "prefix": "webkit", + "version_added": "9" +} +``` + +#### `alternative_name` + +In some cases features are named entirely differently and not just prefixed. Example: + +- Prefixed version had a different capitalization + +```json +{ + "alternative_name": "mozRequestFullScreen", + "version_added": "4", + "version_removed": "9", + "version_last": "8" +} +``` + +Note that you can't have both `prefix` and `alternative_name`. + +#### `flags` + +An array of objects describing feature flags that must be configured for this browser to support this feature. Usually this array will have one item, but there are cases where two or more flags can be required to activate a feature. An object in the `flags` array consists of three properties: + +- `type` (mandatory): the flag type: + - `preference` a flag the user can set (like in `about:config` in Firefox). + - `runtime_flag` a flag to be set before starting the browser. +- `name` (mandatory): the name of the flag or preference to configure. +- `value_to_set` (optional): the value to set the flag to. It is a string, that may be converted to the right type (that is `true` or `false` for Boolean value, or `4` for an integer value). + +Example for one flag required: + +```json +{ + "version_added": "40", + "flags": [ + { + "type": "preference", + "name": "browser.flag.name", + "value_to_set": "true" + } + ] +} +``` + +#### `impl_url` + +A URL or array of URLs linking to the bug or issue that tracks the implementation of this feature. + +#### `notes` + +A string or array of strings containing additional information about the feature's support. In the published data, notes are formatted as HTML. + +#### `partial_implementation` + +Set to `true` if the browser's support does not implement mandatory specified behavior, is inconsistent with other browsers, causes confusing feature detection results, and has a demonstrable negative impact on web developers. It defaults to `false`. If set to `true`, a `notes` field explaining the divergence is always present. + +```json +{ + "version_added": "6", + "partial_implementation": true, + "notes": "The event handler is supported, but the event never fires." +} +``` + +### Ranged versions (≤) + +Ranged versions indicate that a feature has been confirmed to be supported in at least a certain version of the browser, but may have been added in an earlier release. Ranged versions are indicated by the `Less Than or Equal To (U+2264)` (`≤`) symbol before the version number. + +For example, the statement below means, "supported in at least version 37 and possibly in earlier versions as well". + +```json +{ + "version_added": "≤37" +} +``` + +### Status information + +The status property contains information about stability of the feature. It is an object named `status` and has three mandatory properties: + +- `experimental` (DEPRECATED): a `boolean` value. + + **Warning**: The `experimental` property is deprecated. + Prefer using a more well-defined stability calculations, such as Baseline, instead. + + If `experimental` is `true`, then it usually means that the feature is implemented in only one browser engine. + + If `experimental` is `false`, then it usually means that the feature is implemented in two or more browser engines. + Sometimes a `false` value means that a single-implementer feature is not expected to change. + +- `standard_track`: a `boolean` value. + + If `standard_track` is `true`, then the feature is part of an active specification or specification process. + +- `deprecated`: a `boolean` value. + + If `deprecated` is `true`, then the feature is no longer recommended. It might be removed in the future or kept only for compatibility purposes. + +```json +"__compat": { + "status": { + "experimental": true, + "standard_track": true, + "deprecated": false + } +} +``` + +## Build-time transformations + +The published data differs from the [source data](./compat-data-schema.md) in the following ways: + +- All `"mirror"` support statements are resolved to concrete support objects with real version numbers. The string `"mirror"` never appears in published data. +- A `source_file` property is added to every `__compat` object. +- A `version_last` property is added to every support statement that has a `version_removed`. +- Markdown formatting in `description` and `notes` fields is converted to HTML. + +## Exports + +This structure is exported for consumers of `@mdn/browser-compat-data`: + +```js +import bcd from '@mdn/browser-compat-data'; +bcd.__meta.version; // "6.0.0" +bcd.browsers.firefox.releases['1.5'].status; // "retired" +bcd.api.AbortController.__compat.support.chrome.version_added; // "66" +bcd.api.AbortController.__compat.source_file; // "api/AbortController.json" +``` + +```js +const bcd = require('@mdn/browser-compat-data'); +bcd.__meta.version; +// "6.0.0" +bcd.browsers.firefox.releases['1.5'].status; +// "retired" +``` diff --git a/scripts/build/index.js b/scripts/build/index.js index f7ab95836893be..d5129948d755c2 100644 --- a/scripts/build/index.js +++ b/scripts/build/index.js @@ -11,9 +11,8 @@ import stringify from 'fast-json-stable-stringify'; import { compareVersions } from 'compare-versions'; import { marked } from 'marked'; -import compileTS from '../generate-types.js'; -import compatDataSchema from '../../schemas/compat-data.schema.json' with { type: 'json' }; -import browserDataSchema from '../../schemas/browsers.schema.json' with { type: 'json' }; +import compileTS from '../generate-public-types.js'; +import schema from '../../schemas/public.schema.json' with { type: 'json' }; import { createAjv } from '../lib/ajv.js'; import { walk } from '../../utils/index.js'; import bcd from '../../index.js'; @@ -21,8 +20,8 @@ import bcd from '../../index.js'; import mirrorSupport from './mirror.js'; /** - * @import { InternalSupportStatement } from '../../types/index.js' - * @import { BrowserName, CompatData, Identifier, VersionValue } from '../../types/types.js' + * @import { BrowserName, InternalIdentifier, InternalSupportStatement, VersionValue } from '../../types/index.js' + * @import { CompatData, MetaBlock } from '../../types/public.js' * @import { WalkOutput } from '../../utils/walk.js' */ @@ -52,11 +51,11 @@ const logWrite = (url, description = '') => { /** * Generate metadata to embed into BCD builds - * @returns {*} Metadata to embed into BCD + * @returns {MetaBlock} Metadata to embed into BCD */ export const generateMeta = () => ({ version: packageJson.version, - timestamp: new Date(), + timestamp: new Date().toISOString(), }); /** @@ -150,7 +149,7 @@ export const addVersionLast = (feature) => { * @returns {void} */ export const transformMD = (feature) => { - const featureData = /** @type {Identifier} */ (feature.data); + const featureData = /** @type {InternalIdentifier} */ (feature.data); if ( featureData.__compat && 'description' in featureData.__compat && @@ -196,7 +195,7 @@ export const transformMD = (feature) => { const addIE = (feature) => { if ( feature.path.startsWith('webextensions.') && - !bcd.browsers.ie.accepts_webextensions + !bcd.browsers['ie'].accepts_webextensions ) { return; } @@ -231,10 +230,13 @@ export const createDataBundle = async () => { applyTransforms(bcd); - return { + /** @type {*} */ + const result = { ...bcd, __meta: generateMeta(), }; + + return /** @type {CompatData}*/ (result); }; /** @@ -244,25 +246,16 @@ export const createDataBundle = async () => { const validate = (data) => { const ajv = createAjv(); - for (const [key, value] of Object.entries(data)) { - if (key === '__meta') { - // Not covered by the schema. - continue; - } - - const schema = key === 'browsers' ? browserDataSchema : compatDataSchema; - const data = { [key]: value }; - if (!ajv.validate(schema, data)) { - const errors = ajv.errors || []; - if (!errors.length) { - console.error(`${key} data failed validation with unknown errors!`); - } - // Output messages by one since better-ajv-errors wrongly joins messages - // (see https://github.com/atlassian/better-ajv-errors/pull/21) - errors.forEach((e) => { - console.error(betterAjvErrors(schema, data, [e], { indent: 2 })); - }); + if (!ajv.validate(schema, data)) { + const errors = ajv.errors || []; + if (!errors.length) { + console.error('Public data failed validation with unknown errors!'); } + // Output messages by one since better-ajv-errors wrongly joins messages + // (see https://github.com/atlassian/better-ajv-errors/pull/21) + errors.forEach((e) => { + console.error(betterAjvErrors(schema, data, [e], { indent: 2 })); + }); } }; @@ -303,9 +296,9 @@ const writeTypeScript = async () => { const content = `/* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -import { CompatData } from "./types.js"; +import { InternalCompatData } from "./types.js"; -declare var bcd: CompatData; +declare var bcd: InternalCompatData; export default bcd; export * from "./types.js";`; @@ -315,7 +308,7 @@ export * from "./types.js";`; await fs.writeFile(destImport, content); logWrite(destImport, 'ESM types'); - await compileTS(destTypes); + await compileTS('schemas/public.schema.json', destTypes); logWrite(destTypes, 'data types'); }; diff --git a/scripts/build/index.test.js b/scripts/build/index.test.js index dacd61e96a7e24..2b97add687b094 100644 --- a/scripts/build/index.test.js +++ b/scripts/build/index.test.js @@ -16,7 +16,7 @@ describe('Build functions', () => { it('generateMeta', () => { const result = generateMeta(); assert.ok(result.version); - assert.ok(result.timestamp instanceof Date); + assert.ok(result.timestamp); }); it('applyMirroring', () => { diff --git a/scripts/build/mirror.js b/scripts/build/mirror.js index c8188a893cd067..7b953fc8f0780f 100644 --- a/scripts/build/mirror.js +++ b/scripts/build/mirror.js @@ -6,7 +6,7 @@ import { compareVersions, compare } from 'compare-versions'; import bcd from '../../index.js'; /** - * @import { BrowserName, SimpleSupportStatement, SupportStatement } from '../../types/types.js' + * @import { BrowserName, InternalSimpleSupportStatement, InternalSupportStatement } from '../../types/index.js' * @import { InternalSupportBlock } from '../../types/index.js' */ @@ -14,8 +14,6 @@ import bcd from '../../index.js'; * @typedef {string | [string, string, ...string[]] | null} Notes */ -const { browsers } = bcd; - const OS_NOTES = [ 'Available on macOS and Windows only.', 'Available only on macOS.', @@ -74,7 +72,7 @@ const matchingSafariVersions = new Map([ * @throws An error when the downstream browser has no upstream */ export const getMatchingBrowserVersion = (targetBrowser, sourceVersion) => { - const browserData = browsers[targetBrowser]; + const browserData = bcd.browsers[targetBrowser]; const range = sourceVersion.includes('≤'); /* c8 ignore start */ @@ -108,7 +106,7 @@ export const getMatchingBrowserVersion = (targetBrowser, sourceVersion) => { releaseKeys.sort(compareVersions); const sourceRelease = - browsers[browserData.upstream].releases[sourceVersion.replace('≤', '')]; + bcd.browsers[browserData.upstream].releases[sourceVersion.replace('≤', '')]; if (!sourceRelease) { throw new Error( @@ -177,25 +175,25 @@ const updateNotes = (notes, regex, replace, versionMapper) => { /** * Copy a support statement - * @param {SimpleSupportStatement} data The data to copied - * @returns {SimpleSupportStatement} The new copied object + * @param {InternalSimpleSupportStatement} data The data to copied + * @returns {InternalSimpleSupportStatement} The new copied object */ const copyStatement = (data) => { - /** @type {Partial} */ + /** @type {Partial} */ const newData = {}; for (const i in data) { newData[i] = data[i]; } - return /** @type {SimpleSupportStatement} */ (newData); + return /** @type {InternalSimpleSupportStatement} */ (newData); }; /** * Perform mirroring of data - * @param {SupportStatement} sourceData The data to mirror from + * @param {InternalSupportStatement} sourceData The data to mirror from * @param {BrowserName} sourceBrowser The source browser * @param {BrowserName} destination The destination browser - * @returns {SupportStatement} The mirrored support statement + * @returns {InternalSupportStatement} The mirrored support statement */ export const bumpSupport = (sourceData, sourceBrowser, destination) => { if (Array.isArray(sourceData)) { @@ -206,7 +204,7 @@ export const bumpSupport = (sourceData, sourceBrowser, destination) => { const newData = sourceData .map( (data) => - /** @type {SimpleSupportStatement} */ ( + /** @type {InternalSimpleSupportStatement} */ ( bumpSupport(data, sourceBrowser, destination) ), ) @@ -220,18 +218,20 @@ export const bumpSupport = (sourceData, sourceBrowser, destination) => { return newData[0]; default: - return /** @type {[SimpleSupportStatement, SimpleSupportStatement, ...SimpleSupportStatement[]]} */ ( + return /** @type {[InternalSimpleSupportStatement, InternalSimpleSupportStatement, ...InternalSimpleSupportStatement[]]} */ ( newData ); } } - /** @type {SimpleSupportStatement} */ + /** @type {InternalSimpleSupportStatement} */ + // @ts-expect-error FIXME Handle "mirror" value. const newData = copyStatement(sourceData); if ( - browsers[sourceBrowser].type === 'desktop' && - browsers[destination].type === 'mobile' && + bcd.browsers[sourceBrowser].type === 'desktop' && + bcd.browsers[destination].type === 'mobile' && + typeof sourceData === 'object' && sourceData.partial_implementation ) { const notes = Array.isArray(sourceData.notes) @@ -253,24 +253,32 @@ export const bumpSupport = (sourceData, sourceBrowser, destination) => { } } - if (!browsers[destination].accepts_flags && newData.flags) { + if (!bcd.browsers[destination].accepts_flags && newData.flags) { // Remove flag data if the target browser doesn't accept flags return { version_added: false }; } - if (typeof sourceData.version_added === 'string') { + if ( + typeof sourceData === 'object' && + typeof sourceData.version_added === 'string' + ) { newData.version_added = getMatchingBrowserVersion( destination, sourceData.version_added, ); } - if (newData.version_added === false && sourceData.version_added !== false) { + if ( + newData.version_added === false && + typeof sourceData === 'object' && + sourceData.version_added !== false + ) { // If the feature is added in an upstream version newer than available in the downstream browser, don't copy notes, etc. return { version_added: false }; } if ( + typeof sourceData === 'object' && sourceData.version_removed && typeof sourceData.version_removed === 'string' ) { @@ -293,15 +301,19 @@ export const bumpSupport = (sourceData, sourceBrowser, destination) => { } // Only process notes if they weren't already removed (e.g., for OS-specific limitations) - if (sourceData.notes && newData.notes !== undefined) { + if ( + typeof sourceData === 'object' && + sourceData.notes && + newData.notes !== undefined + ) { const sourceBrowserName = sourceBrowser === 'chrome' ? '(Google )?Chrome' - : `(${browsers[sourceBrowser].name})`; + : `(${bcd.browsers[sourceBrowser].name})`; const newNotes = updateNotes( sourceData.notes, new RegExp(`\\b${sourceBrowserName}\\b`, 'g'), - browsers[destination].name, + bcd.browsers[destination].name, (v) => getMatchingBrowserVersion(destination, v), ); if (newNotes) { @@ -316,11 +328,11 @@ export const bumpSupport = (sourceData, sourceBrowser, destination) => { * Perform mirroring for the target browser * @param {BrowserName} destination The browser to mirror to * @param {InternalSupportBlock} data The data to mirror with - * @returns {SupportStatement} The mirrored data + * @returns {InternalSupportStatement} The mirrored data */ const mirrorSupport = (destination, data) => { /** @type {BrowserName | undefined} */ - const upstream = browsers[destination].upstream; + const upstream = bcd.browsers[destination].upstream; if (!upstream) { throw new Error( `Upstream is not defined for ${destination}, cannot mirror!`, diff --git a/scripts/build/mirror.test.js b/scripts/build/mirror.test.js index 30468fe4bda99e..6b793e52930f17 100644 --- a/scripts/build/mirror.test.js +++ b/scripts/build/mirror.test.js @@ -2,7 +2,7 @@ * See LICENSE file for more information. */ /** - * @import { BrowserName } from '../../types/types.js' + * @import { BrowserName } from '../../types/index.js' * @import { InternalSupportBlock } from '../../types/index.js' */ diff --git a/scripts/diff-flat.js b/scripts/diff-flat.js index 76eceac084f45d..a24c31dce074a7 100644 --- a/scripts/diff-flat.js +++ b/scripts/diff-flat.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatData, SimpleSupportStatement} from '../types/types.js' */ +/** @import {InternalCompatData, InternalSimpleSupportStatement} from '../types/index.js' */ /** * @typedef {'html' | 'plain'} Format @@ -125,13 +125,14 @@ const flattenObject = (obj, parentKey = '', result = {}) => { const { version_added, + // @ts-expect-error FIXME Handle internal-public transition. version_last, partial_implementation, alternative_name, prefix, flags, notes, - } = /** @type {SimpleSupportStatement} */ (obj[key]); + } = /** @type {InternalSimpleSupportStatement} */ (obj[key]); const parts = [ typeof version_added === 'string' @@ -281,9 +282,9 @@ const printDiffs = (base, head, options) => { /** @type {Map>} */ const groups = new Map(); - /** @type {CompatData} */ + /** @type {InternalCompatData} */ const baseContents = /** @type {*} */ ({}); - /** @type {CompatData} */ + /** @type {InternalCompatData} */ const headContents = /** @type {*} */ ({}); for (const status of getGitDiffStatuses(base, head)) { @@ -296,12 +297,12 @@ const printDiffs = (base, head, options) => { continue; } - const baseFileContents = /** @type {CompatData} */ ( + const baseFileContents = /** @type {InternalCompatData} */ ( status.value !== 'A' ? JSON.parse(getFileContent(base, status.basePath)) : {} ); - const headFileContents = /** @type {CompatData} */ ( + const headFileContents = /** @type {InternalCompatData} */ ( status.value !== 'D' ? JSON.parse(getFileContent(head, status.headPath)) : {} diff --git a/scripts/diff.js b/scripts/diff.js index 766544bf60353f..8f6b8328e34c1b 100644 --- a/scripts/diff.js +++ b/scripts/diff.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {SupportStatement, Identifier, BrowserName} from '../types/types.js' */ +/** @import {InternalSupportStatement, InternalIdentifier, BrowserName} from '../types/index.js' */ import { styleText } from 'node:util'; @@ -54,11 +54,11 @@ const stringifyChange = (lhs, rhs) => /** * Perform mirroring on specified diff statement * @param {object} diff - The diff to perform mirroring on - * @param {SupportStatement} diff.base - * @param {SupportStatement} diff.head + * @param {InternalSupportStatement} diff.base + * @param {InternalSupportStatement} diff.head * @param {object} contents - The contents to mirror from - * @param {Identifier} contents.base - * @param {Identifier} contents.head + * @param {InternalIdentifier} contents.base + * @param {InternalIdentifier} contents.head * @param {string[]} path - The feature path to mirror * @param {'base' | 'head'} direction - Whether to mirror 'base' or 'head' */ @@ -66,7 +66,7 @@ const doMirror = (diff, contents, path, direction) => { const browser = /** @type {BrowserName} */ (path[path.length - 1]); const dataPath = path.slice(0, path.length - 3).join('.'); const data = contents[direction]; - const queried = /** @type {Identifier} */ (query(dataPath, data)); + const queried = /** @type {InternalIdentifier} */ (query(dataPath, data)); if (queried.__compat?.support) { diff[direction] = mirror(browser, queried.__compat.support); diff --git a/scripts/generate-internal-types.js b/scripts/generate-internal-types.js new file mode 100644 index 00000000000000..efa8a7ef754774 --- /dev/null +++ b/scripts/generate-internal-types.js @@ -0,0 +1,178 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +/* c8 ignore start */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import esMain from 'es-main'; +import { fdir } from 'fdir'; +import { compileFromFile } from 'json-schema-to-typescript'; + +import { spawn } from '../utils/index.js'; + +import extend from './lib/extend.js'; + +const dirname = fileURLToPath(new URL('.', import.meta.url)); + +const opts = { + bannerComment: '', + unreachableDefinitions: true, +}; + +const header = + '/* This file is a part of @mdn/browser-compat-data\n * See LICENSE file for more information. */\n\n/**\n* This file was automatically generated by json-schema-to-typescript.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source schema files in\n* schemas/*, and run "npm run gentypes" to regenerate this file.\n*/'; + +const compatDataTypes = { + api: 'Contains data for each [Web API](https://developer.mozilla.org/docs/Web/API) interface.', + browsers: 'Contains data for each known and tracked browser/engine.', + css: 'Contains data for [CSS](https://developer.mozilla.org/docs/Web/CSS) properties, selectors, and at-rules.', + html: 'Contains data for [HTML](https://developer.mozilla.org/docs/Web/HTML) elements, attributes, and global attributes.', + http: 'Contains data for [HTTP](https://developer.mozilla.org/docs/Web/HTTP) headers, statuses, and methods.', + javascript: + 'Contains data for [JavaScript](https://developer.mozilla.org/docs/Web/JavaScript) built-in Objects, statement, operators, and other ECMAScript language features.', + manifests: + 'Contains data for various manifests, such as the [Web Application Manifest](https://developer.mozilla.org/docs/Web/Progressive_web_apps/manifest).', + mathml: + 'Contains data for [MathML](https://developer.mozilla.org/docs/Web/MathML) elements, attributes, and global attributes.', + mediatypes: + 'Contains data for [Media types](https://developer.mozilla.org/docs/Web/HTTP/Guides/MIME_types).', + svg: 'Contains data for [SVG](https://developer.mozilla.org/docs/Web/SVG) elements, attributes, and global attributes.', + webassembly: + 'Contains data for [WebAssembly](https://developer.mozilla.org/docs/WebAssembly) features.', + webdriver: + 'Contains data for [WebDriver](https://developer.mozilla.org/docs/Web/WebDriver) commands.', + webextensions: + 'Contains data for [WebExtensions](https://developer.mozilla.org/Add-ons/WebExtensions) JavaScript APIs and manifest keys.', +}; + +/** + * Generate the browser names TypeScript + * @returns {Promise} The stringified TypeScript typedef + */ +const generateBrowserNames = async () => { + // Load browser data independently of index.ts, since index.ts depends + // on the output of this script + const browserData = { browsers: {} }; + + const paths = /** @type {string[]} */ ( + new fdir() + .withBasePath() + .filter((fp) => fp.endsWith('.json')) + .crawl(path.join(dirname, '..', 'browsers')) + .sync() + ); + + for (const fp of paths) { + try { + const contents = await fs.readFile(fp); + extend(browserData, JSON.parse(contents.toString('utf8'))); + } catch { + // Skip invalid JSON. Tests will flag the problem separately. + continue; + } + } + + // Generate BrowserName type + const browsers = Object.keys(browserData.browsers); + return `/**\n * The names of the known browsers.\n */\nexport type BrowserName = ${browsers + .map((b) => `"${b}"`) + .join(' | ')};`; +}; + +/** + * Generate the CompatData TypeScript + * @returns {string} The stringified TypeScript typedef + */ +const generateCompatDataTypes = () => { + const props = Object.entries(compatDataTypes).map( + (t) => + ` /**\n * ${t[1]}\n */\n ${t[0]}: ${ + t[0] === 'browsers' ? 'Browsers' : 'InternalIdentifier' + };`, + ); + + return `export interface InternalCompatData {\n${props.join('\n\n')}\n}\n`; +}; + +/** + * Transform the TypeScript to remove unneeded bits of typedefs + * @param {string} browserTS Typedefs for BrowserName + * @param {string} compatTS Typedefs for CompatData + * @returns {string} Updated typedefs + */ +const transformTS = (browserTS, compatTS) => { + // XXX Temporary until the following PR is merged and released: + // https://github.com/bcherny/json-schema-to-typescript/pull/456 + let ts = browserTS + '\n\n' + compatTS; + + ts = ts + .replace( + 'export interface BrowserDataFile {\n browsers?: Browsers;\n}', + '', + ) + .replace('export interface CompatDataFile {}', '') + .replace( + /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema definition\n \* via the `patternProperty` "\^webextensions\*\$"\.\n \*\/\nexport type WebextensionsIdentifier = Identifier;\n/, + '', + ) + .replace( + /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "webextensions_identifier"\.\n \*\/\nexport type WebextensionsIdentifier1 = .*;\n/, + '', + ) + .replace( + /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "spec_url_value"\.\n \*\/\nexport type SpecUrlValue = string;\n/, + '', + ) + .replace( + /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "impl_url_value"\.\n \*\/\nexport type ImplUrlValue = string;\n/, + '', + ) + .replace( + '/**\n * This interface was referenced by `CompatDataFile`\'s JSON-Schema\n * via the `definition` "support_block".\n */\nexport type SupportBlock1 = Partial>;\n', + '', + ) + .replace( + /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "status_block"\.\n \*\/\nexport interface StatusBlock1 {(.*\n)*}\n/, + '', + ); + + return ts; +}; + +/** + * Compile the TypeScript typedefs from the schema JSON + * @param {URL | string} [destination] Output destination + */ +const compile = async ( + destination = new URL('../types/internal.d.ts', import.meta.url), +) => { + const browserTS = await compileFromFile('schemas/browsers.schema.json', opts); + const compatTS = await compileFromFile( + 'schemas/compat-data.schema.json', + opts, + ); + + const ts = [ + header, + await generateBrowserNames(), + 'export type VersionValue = string | false;', + transformTS(browserTS, compatTS), + generateCompatDataTypes(), + ].join('\n\n'); + await fs.writeFile(destination, ts); + spawn('tsc', ['--skipLibCheck', '../types/internal.d.ts'], { + cwd: dirname, + stdio: 'inherit', + }); +}; + +if (esMain(import.meta)) { + await compile(); +} + +export default compile; + +/* c8 ignore stop */ diff --git a/scripts/generate-public-types.js b/scripts/generate-public-types.js new file mode 100644 index 00000000000000..0f45827ad5ce11 --- /dev/null +++ b/scripts/generate-public-types.js @@ -0,0 +1,68 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +/* c8 ignore start */ + +import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +import esMain from 'es-main'; +import { compileFromFile } from 'json-schema-to-typescript'; + +import { spawn } from '../utils/index.js'; + +const root = new URL('..', import.meta.url); + +const opts = { + bannerComment: + '/* This file is a part of @mdn/browser-compat-data\n * See LICENSE file for more information. */\n\n/**\n* This file was automatically generated by json-schema-to-typescript.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source schema files in\n* schemas/*, and run "npm run gentypes" to regenerate this file.\n*/', + unreachableDefinitions: true, +}; + +/** + * Transform the TypeScript to remove unneeded bits of typedefs + * @param {string} ts Typedefs + * @returns {string} Updated typedefs + */ +const transformTS = (ts) => { + // Remove "interface was referenced" comments. + ts = ts + .replace( + /( \*\n)? \* This interface was referenced by [^\n]+\n \* via the [^\n]+\n/g, + '', + ) + .replace(/\/\*\*\n \*\/\n/g, ''); + + return ts; +}; + +/** + * Compile the TypeScript typedefs from the schema JSON + * @param {string} source - JSON schema source + * @param {URL | string} destination - Output destination + */ +const compile = async ( + source = 'schemas/public.schema.json', + destination = 'types/public.d.ts', +) => { + let ts = await compileFromFile(source, opts); + + ts = transformTS(ts); + + const file = + destination instanceof URL ? destination : new URL(destination, root); + + await fs.writeFile(file, ts); + spawn('tsc', ['--skipLibCheck', fileURLToPath(file)], { + cwd: fileURLToPath(root), + stdio: 'inherit', + }); +}; + +if (esMain(import.meta)) { + compile(); +} + +export default compile; + +/* c8 ignore stop */ diff --git a/scripts/generate-types.js b/scripts/generate-types.js index 5f3be0ef739cfd..3f98f7e65e6346 100644 --- a/scripts/generate-types.js +++ b/scripts/generate-types.js @@ -1,189 +1,29 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/* c8 ignore start */ - -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { rm } from 'node:fs/promises'; import esMain from 'es-main'; -import { fdir } from 'fdir'; -import { compileFromFile } from 'json-schema-to-typescript'; - -import { spawn } from '../utils/index.js'; - -import extend from './lib/extend.js'; -const dirname = fileURLToPath(new URL('.', import.meta.url)); +import compileInternal from './generate-internal-types.js'; +import compilePublic from './generate-public-types.js'; -const opts = { - bannerComment: '', - unreachableDefinitions: true, -}; - -const header = - '/* This file is a part of @mdn/browser-compat-data\n * See LICENSE file for more information. */\n\n/**\n* This file was automatically generated by json-schema-to-typescript.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source schema files in\n* schemas/*, and run "npm run gentypes" to regenerate this file.\n*/'; - -const compatDataTypes = { - __meta: - 'Contains metadata for the current BCD information, such as the BCD version.', - api: 'Contains data for each [Web API](https://developer.mozilla.org/docs/Web/API) interface.', - browsers: 'Contains data for each known and tracked browser/engine.', - css: 'Contains data for [CSS](https://developer.mozilla.org/docs/Web/CSS) properties, selectors, and at-rules.', - html: 'Contains data for [HTML](https://developer.mozilla.org/docs/Web/HTML) elements, attributes, and global attributes.', - http: 'Contains data for [HTTP](https://developer.mozilla.org/docs/Web/HTTP) headers, statuses, and methods.', - javascript: - 'Contains data for [JavaScript](https://developer.mozilla.org/docs/Web/JavaScript) built-in Objects, statement, operators, and other ECMAScript language features.', - manifests: - 'Contains data for various manifests, such as the [Web Application Manifest](https://developer.mozilla.org/docs/Web/Progressive_web_apps/manifest).', - mathml: - 'Contains data for [MathML](https://developer.mozilla.org/docs/Web/MathML) elements, attributes, and global attributes.', - mediatypes: - 'Contains data for [Media types](https://developer.mozilla.org/docs/Web/HTTP/Guides/MIME_types).', - svg: 'Contains data for [SVG](https://developer.mozilla.org/docs/Web/SVG) elements, attributes, and global attributes.', - webassembly: - 'Contains data for [WebAssembly](https://developer.mozilla.org/docs/WebAssembly) features.', - webdriver: - 'Contains data for [WebDriver](https://developer.mozilla.org/docs/Web/WebDriver) commands.', - webextensions: - 'Contains data for [WebExtensions](https://developer.mozilla.org/Add-ons/WebExtensions) JavaScript APIs and manifest keys.', -}; +/* c8 ignore start */ /** - * Generate the browser names TypeScript - * @returns {Promise} The stringified TypeScript typedef + * Cleans up old types. */ -const generateBrowserNames = async () => { - // Load browser data independently of index.ts, since index.ts depends - // on the output of this script - const browserData = { browsers: {} }; - - const paths = /** @type {string[]} */ ( - new fdir() - .withBasePath() - .filter((fp) => fp.endsWith('.json')) - .crawl(path.join(dirname, '..', 'browsers')) - .sync() - ); - - for (const fp of paths) { - try { - const contents = await fs.readFile(fp); - extend(browserData, JSON.parse(contents.toString('utf8'))); - } catch { - // Skip invalid JSON. Tests will flag the problem separately. - continue; - } +const cleanupObsolete = async () => { + const path = new URL('../types/types.d.ts', import.meta.url); + try { + await rm(path); + } catch { + // Ignore. } - - // Generate BrowserName type - const browsers = Object.keys(browserData.browsers); - return `/**\n * The names of the known browsers.\n */\nexport type BrowserName = ${browsers - .map((b) => `"${b}"`) - .join(' | ')};`; -}; - -/** - * Generate the CompatData TypeScript - * @returns {string} The stringified TypeScript typedef - */ -const generateCompatDataTypes = () => { - const props = Object.entries(compatDataTypes).map( - (t) => - ` /**\n * ${t[1]}\n */\n ${t[0]}: ${ - t[0] === '__meta' - ? 'MetaBlock' - : t[0] === 'browsers' - ? 'Browsers' - : 'Identifier' - };`, - ); - - const metaType = - 'export interface MetaBlock {\n version: string;\n timestamp: string;\n}'; - - return `${metaType}\n\nexport interface CompatData {\n${props.join( - '\n\n', - )}\n}\n`; -}; - -/** - * Transform the TypeScript to remove unneeded bits of typedefs - * @param {string} browserTS Typedefs for BrowserName - * @param {string} compatTS Typedefs for CompatData - * @returns {string} Updated typedefs - */ -const transformTS = (browserTS, compatTS) => { - // XXX Temporary until the following PR is merged and released: - // https://github.com/bcherny/json-schema-to-typescript/pull/456 - let ts = browserTS + '\n\n' + compatTS; - - ts = ts - .replace( - 'export interface BrowserDataFile {\n browsers?: Browsers;\n}', - '', - ) - .replace('export interface CompatDataFile {}', '') - .replace( - /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema definition\n \* via the `patternProperty` "\^webextensions\*\$"\.\n \*\/\nexport type WebextensionsIdentifier = Identifier;\n/, - '', - ) - .replace( - /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "webextensions_identifier"\.\n \*\/\nexport type WebextensionsIdentifier1 = .*;\n/, - '', - ) - .replace( - /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "spec_url_value"\.\n \*\/\nexport type SpecUrlValue = string;\n/, - '', - ) - .replace( - /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "impl_url_value"\.\n \*\/\nexport type ImplUrlValue = string;\n/, - '', - ) - .replace( - '/**\n * This interface was referenced by `CompatDataFile`\'s JSON-Schema\n * via the `definition` "support_block".\n */\nexport type SupportBlock1 = Partial>;\n', - '', - ) - .replace( - /\/\*\*\n \* This interface was referenced by `CompatDataFile`'s JSON-Schema\n \* via the `definition` "status_block"\.\n \*\/\nexport interface StatusBlock1 {(.*\n)*}\n/, - '', - ); - - return ts; -}; - -/** - * Compile the TypeScript typedefs from the schema JSON - * @param {URL | string} [destination] Output destination - */ -const compile = async ( - destination = new URL('../types/types.d.ts', import.meta.url), -) => { - const browserTS = await compileFromFile('schemas/browsers.schema.json', opts); - const compatTS = await compileFromFile( - 'schemas/compat-data.schema.json', - opts, - ); - - const ts = [ - header, - await generateBrowserNames(), - 'export type VersionValue = string | false;', - transformTS(browserTS, compatTS), - generateCompatDataTypes(), - ].join('\n\n'); - await fs.writeFile(destination, ts); - spawn('tsc', ['--skipLibCheck', '../types/types.d.ts'], { - cwd: dirname, - stdio: 'inherit', - }); }; if (esMain(import.meta)) { - await compile(); + await Promise.all([compileInternal(), compilePublic(), cleanupObsolete()]); } -export default compile; - /* c8 ignore stop */ diff --git a/scripts/lib/compare-statements.js b/scripts/lib/compare-statements.js index 48d07e02de1232..7e9bac00a0f4b7 100644 --- a/scripts/lib/compare-statements.js +++ b/scripts/lib/compare-statements.js @@ -3,7 +3,7 @@ import { compareVersions } from 'compare-versions'; -/** @import {SimpleSupportStatement} from '../../types/types.js' */ +/** @import {InternalSimpleSupportStatement} from '../../types/index.js' */ /** * @@ -13,8 +13,8 @@ import { compareVersions } from 'compare-versions'; * 3. Statements with partial support * 4. Statements with flags * 5. Statements with a version removed (or otherwise no support) - * @param {SimpleSupportStatement} a - The first support statement object to perform comparison with - * @param {SimpleSupportStatement} b - The second support statement object to perform comparison with + * @param {InternalSimpleSupportStatement} a - The first support statement object to perform comparison with + * @param {InternalSimpleSupportStatement} b - The second support statement object to perform comparison with * @returns {number} Direction to sort */ const compareStatements = (a, b) => { diff --git a/scripts/lib/compare-statements.test.js b/scripts/lib/compare-statements.test.js index 00bc6aafa0dc7e..25f6688afd0e86 100644 --- a/scripts/lib/compare-statements.test.js +++ b/scripts/lib/compare-statements.test.js @@ -1,13 +1,13 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {Identifier, CompatStatement} from '../../types/types.js' */ +/** @import {InternalIdentifier, InternalCompatStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; import compareStatements from './compare-statements.js'; -/** @type {{ input: Identifier; output: Identifier }[]} */ +/** @type {{ input: InternalIdentifier; output: InternalIdentifier }[]} */ const tests = /** @type {*} */ ([ { input: { @@ -130,8 +130,8 @@ const tests = /** @type {*} */ ([ /** * Update the order of the statements * @param {string} key The key of the object (make sure it's '__compat') - * @param {CompatStatement} value The compat statement to update - * @returns {CompatStatement} The updated compat statement + * @param {InternalCompatStatement} value The compat statement to update + * @returns {InternalCompatStatement} The updated compat statement */ const orderStatements = (key, value) => { if (key === '__compat') { diff --git a/scripts/lib/data-folders.js b/scripts/lib/data-folders.js index 2fc5afd880ef37..da3d82798f204d 100644 --- a/scripts/lib/data-folders.js +++ b/scripts/lib/data-folders.js @@ -1,6 +1,8 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ +/** @import { InternalCompatData } from "../../types/internal.js" */ + export const dataFoldersMinusBrowsers = [ 'api', 'css', @@ -16,4 +18,7 @@ export const dataFoldersMinusBrowsers = [ 'webextensions', ]; -export default [...dataFoldersMinusBrowsers, 'browsers']; +export default /** @type {Array} */ ([ + ...dataFoldersMinusBrowsers, + 'browsers', +]); diff --git a/scripts/lib/stringify-and-order-properties.js b/scripts/lib/stringify-and-order-properties.js index 762dc5e8e21f57..f2fc165ba72e79 100644 --- a/scripts/lib/stringify-and-order-properties.js +++ b/scripts/lib/stringify-and-order-properties.js @@ -3,7 +3,7 @@ import { compareVersions } from 'compare-versions'; -/** @import {CompatStatement, SimpleSupportStatement, StatusBlock, BrowserStatement, ReleaseStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement, InternalSimpleSupportStatement, InternalStatusBlock, BrowserStatement, ReleaseStatement} from '../../types/index.js' */ const propOrder = { browsers: { @@ -115,12 +115,12 @@ export const orderProperties = (key, value) => { // Order properties for data if ('__compat' in value) { value.__compat = doOrder( - /** @type {CompatStatement} */ (value.__compat), + /** @type {InternalCompatStatement} */ (value.__compat), propOrder.data.__compat, ); for (const browser of Object.keys(value.__compat.support)) { - /** @type {SimpleSupportStatement[]} */ + /** @type {InternalSimpleSupportStatement[]} */ const result = []; let data = value.__compat.support[browser]; if (!Array.isArray(data)) { @@ -130,7 +130,7 @@ export const orderProperties = (key, value) => { for (const statement of data) { result.push( doOrder( - /** @type {SimpleSupportStatement} */ (statement), + /** @type {InternalSimpleSupportStatement} */ (statement), propOrder.data.support, ), ); @@ -142,7 +142,7 @@ export const orderProperties = (key, value) => { if ('status' in value.__compat) { value.__compat.status = doOrder( - /** @type {StatusBlock} */ (value.__compat.status), + /** @type {InternalStatusBlock} */ (value.__compat.status), propOrder.data.status, ); } diff --git a/scripts/migrations/002-remove-webview-flags.js b/scripts/migrations/002-remove-webview-flags.js index 1cd48ed9bdb79d..f9c68816972627 100644 --- a/scripts/migrations/002-remove-webview-flags.js +++ b/scripts/migrations/002-remove-webview-flags.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement, SimpleSupportStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement, InternalSimpleSupportStatement, InternalSupportStatement} from '../../types/index.js' */ import fs from 'node:fs'; import path from 'node:path'; @@ -18,17 +18,17 @@ const dirname = fileURLToPath(new URL('.', import.meta.url)); * Check to see if the key is __compat and modify the value to remove * flags from WebView Android. * @param {string} key The key in the object - * @param {CompatStatement} value The value of the key - * @returns {CompatStatement} The new value with WebView flags removed + * @param {InternalCompatStatement} value The value of the key + * @returns {InternalCompatStatement} The new value with WebView flags removed */ export const removeWebViewFlags = (key, value) => { if (key === '__compat') { if (value.support.webview_android !== undefined) { if (Array.isArray(value.support.webview_android)) { - /** @type {SimpleSupportStatement[]} */ + /** @type {InternalSimpleSupportStatement[]} */ const result = []; for (const support of value.support.webview_android) { - if (support.flags === undefined) { + if (typeof support === 'object' && support.flags === undefined) { result.push(support); } } @@ -39,11 +39,12 @@ export const removeWebViewFlags = (key, value) => { value.support.webview_android = result[0]; } else { value.support.webview_android = - /** @type {[SimpleSupportStatement, SimpleSupportStatement, ...SimpleSupportStatement[]]} */ ( - result - ); + /** @type {InternalSupportStatement} */ (result); } - } else if (value.support.webview_android.flags !== undefined) { + } else if ( + typeof value.support.webview_android === 'object' && + value.support.webview_android.flags !== undefined + ) { value.support.webview_android = { version_added: false }; } } diff --git a/scripts/migrations/002-remove-webview-flags.test.js b/scripts/migrations/002-remove-webview-flags.test.js index 27105daace4e1b..44cd9347a03f52 100644 --- a/scripts/migrations/002-remove-webview-flags.test.js +++ b/scripts/migrations/002-remove-webview-flags.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {Identifier} from '../../types/types.js' */ +/** @import {InternalIdentifier} from '../../types/index.js' */ import assert from 'node:assert/strict'; @@ -9,7 +9,7 @@ import { removeWebViewFlags } from './002-remove-webview-flags.js'; /** * Objects of each test, with input and expected output - * @type {{ input: Identifier; output: Identifier }[]} + * @type {{ input: InternalIdentifier; output: InternalIdentifier }[]} */ const tests = /** @type {*} */ ([ { diff --git a/scripts/migrations/007-experimental-false.js b/scripts/migrations/007-experimental-false.js index 77d73daee0c3a9..a4bdf719f259f0 100644 --- a/scripts/migrations/007-experimental-false.js +++ b/scripts/migrations/007-experimental-false.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatData, BrowserName, Identifier, ReleaseStatement, SimpleSupportStatement} from '../../types/types.js' */ +/** @import {InternalCompatData, BrowserName, InternalIdentifier, InternalSimpleSupportStatement} from '../../types/index.js' */ import fs from 'node:fs'; import path from 'node:path'; @@ -18,7 +18,7 @@ const dirname = fileURLToPath(new URL('.', import.meta.url)); /** * Fix the experimental status throughout compatibility data - * @param {CompatData | Identifier} bcd Parsed BCD object to be updated in place. + * @param {InternalCompatData | InternalIdentifier} bcd Parsed BCD object to be updated in place. * @returns {void} */ export const fixExperimental = (bcd) => { @@ -33,12 +33,12 @@ export const fixExperimental = (bcd) => { const browserSupport = new Set(); for (const [browser, support] of Object.entries(compat.support)) { - if (!support) { + if (!support || support === 'mirror') { continue; } // Consider only the first part of an array statement. - /** @type {SimpleSupportStatement} */ + /** @type {InternalSimpleSupportStatement} */ const statement = Array.isArray(support) ? support[0] : support; // Ignore anything behind flag, prefix or alternative name @@ -56,7 +56,7 @@ export const fixExperimental = (bcd) => { for (const browser of browserSupport) { const currentRelease = Object.values(browsers[browser].releases).find( - (/** @type {ReleaseStatement} */ r) => r.status === 'current', + (r) => r.status === 'current', ); if (!currentRelease) { continue; diff --git a/scripts/migrations/007-experimental-false.test.js b/scripts/migrations/007-experimental-false.test.js index 52cbac5465d4f2..28c9171650ce1c 100644 --- a/scripts/migrations/007-experimental-false.test.js +++ b/scripts/migrations/007-experimental-false.test.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {CompatStatement} from '../../types/index.js' */ import assert from 'node:assert/strict'; diff --git a/scripts/migrations/010-set-oculus-to-mirror.js b/scripts/migrations/010-set-oculus-to-mirror.js index b356ab34b3f58d..3e0756b99f8c4d 100644 --- a/scripts/migrations/010-set-oculus-to-mirror.js +++ b/scripts/migrations/010-set-oculus-to-mirror.js @@ -1,8 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {InternalSupportBlock} from '../../types/index.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement, InternalSupportBlock} from '../../types/index.js' */ import fs from 'node:fs'; import path from 'node:path'; @@ -18,8 +17,8 @@ const dirname = fileURLToPath(new URL('.', import.meta.url)); /** * Check to see if the key is __compat and set 'oculus' to 'mirror' * @param {string} key The key in the object - * @param {CompatStatement} value The value of the key - * @returns {CompatStatement} The new value with 'oculus' set to 'mirror' + * @param {InternalCompatStatement} value The value of the key + * @returns {InternalCompatStatement} The new value with 'oculus' set to 'mirror' */ export const doSetOculusToMirror = (key, value) => { if (key === '__compat') { diff --git a/scripts/migrations/011-set-webview-ios-to-mirror.js b/scripts/migrations/011-set-webview-ios-to-mirror.js index 78e3b09fa160e0..fbed277127890f 100644 --- a/scripts/migrations/011-set-webview-ios-to-mirror.js +++ b/scripts/migrations/011-set-webview-ios-to-mirror.js @@ -1,8 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {InternalSupportBlock} from '../../types/index.js' */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement, InternalSupportBlock} from '../../types/index.js' */ import fs from 'node:fs'; import path from 'node:path'; @@ -18,8 +17,8 @@ const dirname = fileURLToPath(new URL('.', import.meta.url)); /** * Check to see if the key is __compat and set 'webview_ios' to 'mirror' * @param {string} key The key in the object - * @param {CompatStatement} value The value of the key - * @returns {CompatStatement} The new value with 'webview_ios' set to 'mirror' + * @param {InternalCompatStatement} value The value of the key + * @returns {InternalCompatStatement} The new value with 'webview_ios' set to 'mirror' */ export const doSetWebViewIOSToMirror = (key, value) => { if (key === '__compat') { diff --git a/scripts/migrations/012-descriptions-to-md.js b/scripts/migrations/012-descriptions-to-md.js index a4ef5ac0e6e60c..dbe8f25b259ffa 100644 --- a/scripts/migrations/012-descriptions-to-md.js +++ b/scripts/migrations/012-descriptions-to-md.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {InternalCompatStatement} from '../../types/index.js' */ import fs from 'node:fs'; import path from 'node:path'; @@ -17,8 +17,8 @@ const dirname = fileURLToPath(new URL('.', import.meta.url)); /** * Check to see if the key is __compat and convert descriptions to markdown * @param {string} key The key in the object - * @param {CompatStatement} value The value of the key - * @returns {CompatStatement} The new value with descriptions converted to markdown + * @param {InternalCompatStatement} value The value of the key + * @returns {InternalCompatStatement} The new value with descriptions converted to markdown */ export const doDescriptionsToMarkdown = (key, value) => { if (key === '__compat') { diff --git a/scripts/split.js b/scripts/split.js index a001b6996a225d..5a9358af041d1a 100644 --- a/scripts/split.js +++ b/scripts/split.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {Identifier} from '../types/types.js' */ +/** @import {InternalIdentifier} from '../types/index.js' */ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { join, relative, resolve, sep } from 'node:path'; @@ -36,7 +36,7 @@ const getBaseKeyFromPath = (filePath) => { * Creates the JSON for the subfeature. * @param {string[]} baseKeys - The parent keys in which to nest the data. * @param {string} key - The key of the subfeature. - * @param {Identifier} value - The value of the subfeature. + * @param {InternalIdentifier} value - The value of the subfeature. * @returns {object} JSON for the subfeature data nested in parent structure. */ const createSubfeatureJSON = (baseKeys, key, value) => { @@ -65,7 +65,7 @@ const hasCompatData = (data) => /** * Writes a JSON file. * @param {string} path - The path to the file. - * @param {Identifier} data - The data to write as JSON. + * @param {InternalIdentifier} data - The data to write as JSON. * @returns {Promise} Promise. */ const writeJSONFile = async (path, data) => @@ -80,7 +80,7 @@ const writeJSONFile = async (path, data) => const splitFile = async (file) => { const fullPath = resolve(file); const raw = await readFile(fullPath, 'utf-8'); - const data = /** @type {Identifier} */ (JSON.parse(raw)); + const data = /** @type {InternalIdentifier} */ (JSON.parse(raw)); const baseKeys = getBaseKeyFromPath(file); let current = data; diff --git a/scripts/statistics.js b/scripts/statistics.js index bf9ae675c6303b..fb8b0c15532f33 100644 --- a/scripts/statistics.js +++ b/scripts/statistics.js @@ -12,7 +12,7 @@ import bcdData from '../index.js'; import { getRefDate } from './release/utils.js'; -/** @import {BrowserName, CompatStatement, SupportStatement, Identifier} from '../types/types.js' */ +/** @import {BrowserName, InternalCompatStatement, InternalSupportStatement, InternalIdentifier} from '../types/index.js' */ /** * @typedef {object} VersionStatsEntry @@ -38,7 +38,7 @@ const webextensionsBrowsers = [ /** * Check whether a support statement is a specified type - * @param {SupportStatement | undefined} supportData The support statement to check + * @param {InternalSupportStatement | undefined} supportData The support statement to check * @param {string | boolean | null} type What type of support (true, null, ranged) * @returns {boolean} If the support statement has the type */ @@ -51,20 +51,23 @@ const checkSupport = (supportData, type) => { } if (type == '≤') { return ( - (typeof supportData.version_added == 'string' && + (typeof supportData === 'object' && + typeof supportData.version_added == 'string' && supportData.version_added.startsWith('≤')) || - (typeof supportData.version_removed == 'string' && + (typeof supportData === 'object' && + typeof supportData.version_removed == 'string' && supportData.version_removed.startsWith('≤')) ); } return ( - supportData.version_added === type || supportData.version_removed === type + typeof supportData === 'object' && + (supportData.version_added === type || supportData.version_removed === type) ); }; /** * Iterate through all of the browsers and count the number of exact ranged values for each browser - * @param {CompatStatement} data The data to process and count stats for + * @param {InternalCompatStatement} data The data to process and count stats for * @param {BrowserName[]} browsers The browsers to test * @param {VersionStats} stats The stats object to update * @returns {void} @@ -87,7 +90,7 @@ const processData = (data, browsers, stats) => { /** * Iterate through all of the data and process statistics - * @param {Identifier} data The compat data to iterate + * @param {InternalIdentifier} data The compat data to iterate * @param {BrowserName[]} browsers The browsers to test * @param {VersionStats} stats The stats object to update * @returns {void} @@ -95,8 +98,12 @@ const processData = (data, browsers, stats) => { const iterateData = (data, browsers, stats) => { for (const key in data) { if (key === '__compat') { - // "as CompatStatement" needed because TypeScript doesn't realize key is in data - processData(/** @type {CompatStatement} */ (data[key]), browsers, stats); + // "as InternalCompatStatement" needed because TypeScript doesn't realize key is in data + processData( + /** @type {InternalCompatStatement} */ (data[key]), + browsers, + stats, + ); } else { iterateData(data[key], browsers, stats); } diff --git a/scripts/traverse.js b/scripts/traverse.js index 231684e8a376bf..37a1284c336e9a 100644 --- a/scripts/traverse.js +++ b/scripts/traverse.js @@ -8,8 +8,7 @@ import { hideBin } from 'yargs/helpers'; import dataFolders from '../scripts/lib/data-folders.js'; import bcd from '../index.js'; -/** @import {BrowserName, Identifier, SimpleSupportStatement} from '../types/types.js' */ -/** @import {InternalSupportStatement} from '../types/index.js' */ +/** @import {BrowserName, InternalIdentifier, InternalSimpleSupportStatement, InternalSupportBlock, InternalSupportStatement} from '../types/index.js' */ /** * @typedef {object} StatusFilters @@ -20,8 +19,8 @@ import bcd from '../index.js'; /** * Traverse all of the features within a specified object and find all features that have one of the specified values - * @param {Identifier} obj The compat data to traverse through - * @param {BrowserName[]} browsers The browsers to test for + * @param {InternalIdentifier} obj The compat data to traverse through + * @param {BrowserName[]} browserNames The browsers to test for * @param {string[]} values The values to test for * @param {number} depth The depth to traverse * @param {string} tag The tag to filter results with @@ -32,7 +31,7 @@ import bcd from '../index.js'; */ export function* iterateFeatures( obj, - browsers, + browserNames, values, depth, tag, @@ -62,16 +61,20 @@ export function* iterateFeatures( } if (tag) { const tags = obj[i].__compat?.tags; - if ((tags && tags.includes(tag)) || (!tags && tag == 'false')) { + if ( + (Array.isArray(tags) && tags.includes(tag)) || + (!tags && tag == 'false') + ) { yield `${identifier}${i}`; } } else { - const comp = obj[i].__compat?.support; + const comp = /** @type {InternalSupportBlock} */ ( + obj[i].__compat?.support + ); if (!comp) { continue; } - for (const browser of browsers) { - /** @type {SimpleSupportStatement | SimpleSupportStatement[] | undefined} */ + for (const browser of browserNames) { let browserData = comp[browser]; if (!browserData) { @@ -90,9 +93,11 @@ export function* iterateFeatures( continue; } if (!Array.isArray(browserData)) { + // @ts-expect-error FIXME Handle "mirror" value. browserData = [browserData]; } + // @ts-expect-error FIXME Handle "mirror" value. for (const range in browserData) { if ( /** @type {InternalSupportStatement} */ ( @@ -136,7 +141,7 @@ export function* iterateFeatures( } yield* iterateFeatures( obj[i], - browsers, + browserNames, values, depth, tag, @@ -150,8 +155,8 @@ export function* iterateFeatures( /** * Traverse all of the features within a specified object and find all features that have one of the specified values - * @param {Identifier} obj The compat data to traverse through - * @param {BrowserName[]} browsers The browsers to traverse for + * @param {InternalIdentifier} obj The compat data to traverse through + * @param {BrowserName[]} browserNames The browsers to traverse for * @param {string[]} values The version values to traverse for * @param {number} depth The depth to traverse * @param {string} tag The tag to filter results with @@ -161,7 +166,7 @@ export function* iterateFeatures( */ const traverseFeatures = ( obj, - browsers, + browserNames, values, depth, tag, @@ -169,7 +174,7 @@ const traverseFeatures = ( status, ) => { const features = Array.from( - iterateFeatures(obj, browsers, values, depth, tag, identifier, status), + iterateFeatures(obj, browserNames, values, depth, tag, identifier, status), ); return features.filter((item, pos) => features.indexOf(item) == pos); @@ -178,7 +183,7 @@ const traverseFeatures = ( /** * Traverse the features within BCD * @param {string[]} [folders] The folders to traverse - * @param {BrowserName[]} [browsers] The browsers to traverse for + * @param {BrowserName[]} [browserNames] The browsers to traverse for * @param {string[]} [values] The version values to traverse for * @param {number} [depth] The depth to traverse * @param {string} [tag] The tag to filter results with @@ -187,8 +192,8 @@ const traverseFeatures = ( */ const main = ( folders = dataFolders.concat('webextensions'), - browsers = /** @type {BrowserName[]} */ ( - Object.keys(bcd.browsers).filter((b) => bcd.browsers[b].type !== 'server') + browserNames = Object.entries(bcd.browsers).flatMap(([name, browser]) => + browser.type !== 'server' ? [/** @type {BrowserName} */ (name)] : [], ), values = [], depth = 100, @@ -202,7 +207,7 @@ const main = ( features.push( ...traverseFeatures( bcd[folders[folder]], - browsers, + browserNames, values, depth, tag, diff --git a/scripts/update-browser-releases/bun.js b/scripts/update-browser-releases/bun.js index 8d08f1bf3cd274..fd3df7739bc946 100644 --- a/scripts/update-browser-releases/bun.js +++ b/scripts/update-browser-releases/bun.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatData} from '../../types/types.js' */ +/** @import {Browsers, InternalCompatData} from '../../types/index.js' */ /** * @typedef {object} BunVersionsResponse @@ -219,7 +219,7 @@ export const updateBunReleases = async (options) => { ); } - /** @type {CompatData} */ + /** @type {InternalCompatData} */ const data = JSON.parse(fileText); let result = ''; @@ -287,7 +287,9 @@ export const updateBunReleases = async (options) => { rel.release_notes, ); - const entry = data.browsers[browser].releases[rel.version]; + const entry = /** @type {Browsers} */ (data.browsers)[browser].releases[ + rel.version + ]; if (entry) { const { webkitRev } = await getBunInfoFromVersionData(rel); diff --git a/scripts/update-browser-releases/firefox.js b/scripts/update-browser-releases/firefox.js index 785db5c4b375fb..c78bc2c1c4024c 100644 --- a/scripts/update-browser-releases/firefox.js +++ b/scripts/update-browser-releases/firefox.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {ReleaseStatement} from '../../types/types.js' */ +/** @import {ReleaseStatement} from '../../types/index.js' */ import * as fs from 'node:fs'; diff --git a/scripts/update-browser-releases/utils.js b/scripts/update-browser-releases/utils.js index 90621a45659d2a..42fdea71476a2d 100644 --- a/scripts/update-browser-releases/utils.js +++ b/scripts/update-browser-releases/utils.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {BrowserName, BrowserStatus, CompatData} from '../../types/types.js' */ +/** @import {BrowserName, Browsers, BrowserStatus, InternalCompatData} from '../../types/index.js' */ /** * @typedef {object} RSSItem @@ -161,14 +161,16 @@ export const createOrUpdateBrowserEntry = ( /** * Updates the status of a browser release. - * @param {CompatData} json json file to update + * @param {InternalCompatData} json json file to update * @param {BrowserName} browser the entry name where to add it in the bcd file * @param {string} version the version to add * @param {BrowserStatus} status the status * @returns {string} Text describing what has been updated */ export const setBrowserReleaseStatus = (json, browser, version, status) => { - const release = json.browsers[browser].releases[version]; + const release = /** @type {Browsers} */ (json.browsers)[browser].releases[ + version + ]; if (release.status === status) { return ''; diff --git a/types/index.d.ts b/types/index.d.ts index a9bb941f9efecc..15baf46ac8c53e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,27 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -import type { - BrowserName, - BrowserStatement, - CompatData, - CompatStatement, - Identifier, - SupportStatement, -} from '../build/types.js'; - -export type InternalSupportStatement = SupportStatement | 'mirror'; - -export type InternalSupportBlock = Partial< - Record ->; - -export interface InternalCompatStatement extends Omit< - CompatStatement, - 'support' -> { - support: InternalSupportBlock; -} +export type * from './internal.js'; export type DataType = | CompatData @@ -30,7 +10,7 @@ export type DataType = | Identifier; export type InternalDataType = - | CompatData + | InternalCompatData | BrowserStatement | InternalCompatStatement - | Identifier; + | InternalIdentifier; diff --git a/utils/iter-support.js b/utils/iter-support.js index 5268d7319743f7..9b70d859ca531a 100644 --- a/utils/iter-support.js +++ b/utils/iter-support.js @@ -1,18 +1,20 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {CompatStatement, BrowserName, SimpleSupportStatement} from '../types/types.js' */ +/** @import {InternalCompatStatement, BrowserName, InternalSimpleSupportStatement, InternalSupportStatement} from '../types/index.js' */ /** * Get support for a specific browser in array form - * @param {CompatStatement} compat The compatibility data + * @param {Pick} compat The compatibility data * @param {BrowserName} browser The browser to get data for - * @returns {SimpleSupportStatement[]} The array of support statements for the browser + * @returns {InternalSimpleSupportStatement[]} The array of support statements for the browser */ export default (compat, browser) => { if (browser in compat.support) { + /** @type {InternalSupportStatement|undefined} */ const data = compat.support[browser]; if (data) { + // @ts-expect-error FIXME Handle "mirror" value. return Array.isArray(data) ? data : [data]; } } diff --git a/utils/iter-support.test.js b/utils/iter-support.test.js index ac6b6a31f135f3..521735eb26c23f 100644 --- a/utils/iter-support.test.js +++ b/utils/iter-support.test.js @@ -5,7 +5,7 @@ import assert from 'node:assert/strict'; import iterSupport from './iter-support.js'; -/** @import {CompatStatement} from '../types/types.js' */ +/** @import {InternalCompatStatement} from '../types/index.js' */ describe('iterSupport()', () => { it('returns a `"version_added": false` support statement for non-existent browsers', () => { @@ -23,7 +23,7 @@ describe('iterSupport()', () => { }); it('returns an array of support statements as an array', () => { - /** @type {CompatStatement} */ + /** @type {Pick} */ const compatObj = { support: { firefox: [ diff --git a/utils/query.js b/utils/query.js index 5fd0ed2f0fb7e9..b478e92dd99302 100644 --- a/utils/query.js +++ b/utils/query.js @@ -3,13 +3,13 @@ import bcd from '../index.js'; -/** @import {DataType} from '../types/index.js' */ +/** @import {InternalDataType} from '../types/index.js' */ /** * Get a subtree of compat data. * @param {string} path Dotted path to a given feature (e.g., `css.properties.background`) - * @param {DataType} [data] A tree to query. All of BCD, by default. - * @returns {DataType} A BCD subtree + * @param {InternalDataType} [data] A tree to query. All of BCD, by default. + * @returns {InternalDataType} A BCD subtree * @throws {ReferenceError} For invalid identifiers */ export default (path, data = bcd) => { diff --git a/utils/walk.js b/utils/walk.js index 2124bff5b43bee..72bc12056c9503 100644 --- a/utils/walk.js +++ b/utils/walk.js @@ -11,13 +11,13 @@ import { } from './walkingUtils.js'; import query from './query.js'; -/** @import {CompatData, CompatStatement, Identifier, BrowserStatement, ReleaseStatement} from '../types/types.js' */ -/** @import {DataType} from '../types/index.js' */ +/** @import {InternalCompatStatement, BrowserStatement, ReleaseStatement, InternalIdentifier} from '../types/index.js' */ +/** @import {InternalDataType} from '../types/index.js' */ /** * @typedef {object} BrowserReleaseWalkOutput * @property {string} path - * @property {DataType} data + * @property {InternalDataType} data * @property {BrowserStatement} browser * @property {ReleaseStatement} browserRelease */ @@ -25,17 +25,17 @@ import query from './query.js'; /** * @typedef {object} LowLevelWalkOutput * @property {string} path - * @property {DataType} data + * @property {InternalDataType} data * @property {BrowserStatement} [browser] - * @property {CompatStatement} [compat] + * @property {InternalCompatStatement} [compat] * @property {ReleaseStatement} [browserRelease] */ /** * @typedef {object} WalkOutput * @property {string} path - * @property {DataType} data - * @property {CompatStatement} compat + * @property {InternalDataType} data + * @property {InternalCompatStatement} compat */ /** @@ -58,7 +58,7 @@ export function* browserReleaseWalk(data, path) { /** * Walk through the compatibility statements - * @param {DataType} [data] The data to iterate + * @param {InternalDataType} [data] The data to iterate * @param {string} [path] The current path * @param {number} [depth] The maximum depth to iterate * @yields {LowLevelWalkOutput} The feature info @@ -78,7 +78,7 @@ export function* lowLevelWalk(data = bcd, path, depth = Infinity) { yield* browserReleaseWalk(data, path); } else { if (isFeature(data)) { - next.compat = data.__compat; + next.compat = /** @type {InternalCompatStatement} */ (data.__compat); } yield next; } @@ -94,7 +94,7 @@ export function* lowLevelWalk(data = bcd, path, depth = Infinity) { /** * Walk the data for compat features * @param {string | string[]} [entryPoints] Entry points to iterate - * @param {CompatData | CompatStatement | Identifier} [data] The data to iterate + * @param {InternalDataType} [data] The data to iterate * @yields {WalkOutput} The feature info * @returns {IterableIterator} */ diff --git a/utils/walkingUtils.js b/utils/walkingUtils.js index 6b9ce06f404475..98f9642cab612c 100644 --- a/utils/walkingUtils.js +++ b/utils/walkingUtils.js @@ -1,7 +1,7 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -/** @import {Identifier, BrowserStatement} from '../types/types.js' */ +/** @import {InternalIdentifier, BrowserStatement} from '../types/index.js' */ /** * Join a path array together @@ -13,7 +13,7 @@ export const joinPath = (...args) => Array.from(args).filter(Boolean).join('.'); /** * Check if an object is a BCD feature * @param {*} obj The object to check - * @returns {obj is Identifier} Whether the object is a BCD feature + * @returns {obj is InternalIdentifier} Whether the object is a BCD feature */ export const isFeature = (obj) => '__compat' in obj; diff --git a/utils/walkingUtils.test.js b/utils/walkingUtils.test.js index c3ffffc17fe620..f43929318c9234 100644 --- a/utils/walkingUtils.test.js +++ b/utils/walkingUtils.test.js @@ -26,7 +26,7 @@ describe('joinPath()', () => { describe('isBrowser()', () => { it('returns true for browser-like objects', () => { - assert.equal(isBrowser(bcd.browsers.firefox), true); + assert.equal(isBrowser(bcd.browsers['firefox']), true); }); it('returns false for feature-like objects', () => { @@ -36,7 +36,7 @@ describe('isBrowser()', () => { describe('isFeature()', () => { it('returns false for browser-like objects', () => { - assert.equal(isFeature(bcd.browsers.chrome), false); + assert.equal(isFeature(bcd.browsers['chrome']), false); }); it('returns true for feature-like objects', () => {