diff --git a/lint/linter/test-schema.ts b/lint/linter/test-schema.ts index c8847ec88ca6d4..6507d069c3d160 100644 --- a/lint/linter/test-schema.ts +++ b/lint/linter/test-schema.ts @@ -1,27 +1,15 @@ /* This file is a part of @mdn/browser-compat-data * See LICENSE file for more information. */ -import { Ajv } from 'ajv'; -import ajvErrors from 'ajv-errors'; -import ajvFormats from 'ajv-formats'; import betterAjvErrors from 'better-ajv-errors'; +import { createAjv } from '../../scripts/lib/ajv.js'; import { Linter, Logger, LinterData } from '../utils.js'; import compatDataSchema from './../../schemas/compat-data.schema.json' with { type: 'json' }; import browserDataSchema from './../../schemas/browsers.schema.json' with { type: 'json' }; -const ajv = new Ajv({ allErrors: true }); -// We use 'fast' because as a side effect that makes the "uri" format more lax. -// By default the "uri" format rejects ① and similar in URLs. -ajvFormats.default(ajv, { mode: 'fast' }); -// Allow for custom error messages to provide better directions for contributors -ajvErrors.default(ajv); - -// Define keywords for schema->TS converter -ajv.addKeyword('tsEnumNames'); -ajv.addKeyword('tsName'); -ajv.addKeyword('tsType'); +const ajv = createAjv(); export default { name: 'JSON Schema', diff --git a/schemas/browsers.schema.json b/schemas/browsers.schema.json index baf77891608baa..91bb52e5686b8b 100644 --- a/schemas/browsers.schema.json +++ b/schemas/browsers.schema.json @@ -31,10 +31,8 @@ "$ref": "#/definitions/browser_statement" }, "minProperties": 1, - "maxProperties": 1, "errorMessage": { - "minProperties": "A browser must be described within the file.", - "maxProperties": "Each browser JSON file may only describe one browser." + "minProperties": "A browser must be described within the file." }, "tsType": "Record" }, diff --git a/scripts/build/index.ts b/scripts/build/index.ts index 45b8576e4a0542..f8cd7aba3f9738 100644 --- a/scripts/build/index.ts +++ b/scripts/build/index.ts @@ -5,6 +5,7 @@ import fs from 'node:fs/promises'; import { relative } from 'node:path'; import { fileURLToPath } from 'node:url'; +import betterAjvErrors from 'better-ajv-errors'; import esMain from 'es-main'; import stringify from 'fast-json-stable-stringify'; import { compareVersions } from 'compare-versions'; @@ -13,6 +14,9 @@ import { marked } from 'marked'; import { InternalSupportStatement } from '../../types/index.js'; import { BrowserName, CompatData, VersionValue } from '../../types/types.js'; 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 { createAjv } from '../lib/ajv.js'; import { walk } from '../../utils/index.js'; import { WalkOutput } from '../../utils/walk.js'; import bcd from '../../index.js'; @@ -219,6 +223,35 @@ export const createDataBundle = async (): Promise => { }; }; +/** + * Validates the given data against the schema. + * @param data - The data to validate. + */ +const validate = (data: CompatData) => { + 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 })); + }); + } + } +}; + /* c8 ignore start */ /** @@ -227,6 +260,7 @@ export const createDataBundle = async (): Promise => { const writeData = async () => { const dest = new URL('data.json', targetdir); const data = await createDataBundle(); + validate(data); await fs.writeFile(dest, stringify(data)); logWrite(dest, 'data'); }; diff --git a/scripts/lib/ajv.ts b/scripts/lib/ajv.ts new file mode 100644 index 00000000000000..0660b25bd7a701 --- /dev/null +++ b/scripts/lib/ajv.ts @@ -0,0 +1,23 @@ +import { Ajv } from 'ajv'; +import ajvErrors from 'ajv-errors'; +import ajvFormats from 'ajv-formats'; + +/** + * Returns a new pre-configured Ajv instance. + * @returns the Ajv instance. + */ +export const createAjv = (): Ajv => { + const ajv = new Ajv({ allErrors: true }); + // We use 'fast' because as a side effect that makes the "uri" format more lax. + // By default the "uri" format rejects ① and similar in URLs. + ajvFormats.default(ajv, { mode: 'fast' }); + // Allow for custom error messages to provide better directions for contributors + ajvErrors.default(ajv); + + // Define keywords for schema->TS converter + ajv.addKeyword('tsEnumNames'); + ajv.addKeyword('tsName'); + ajv.addKeyword('tsType'); + + return ajv; +};