Skip to content

Commit 0dde3fe

Browse files
authored
enhance(build): validate built data.json against schemas (#27685)
* refactor(lint): extract createAjv() to utils * enhance(build): validate data against schemas * fix(schemas/browsers): remove maxProperties constraint The built data.json contains multiple browsers. * refactor(utils): move createAjv() to scripts/lib
1 parent c7df358 commit 0dde3fe

File tree

4 files changed

+60
-17
lines changed

4 files changed

+60
-17
lines changed

lint/linter/test-schema.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,15 @@
11
/* This file is a part of @mdn/browser-compat-data
22
* See LICENSE file for more information. */
33

4-
import { Ajv } from 'ajv';
5-
import ajvErrors from 'ajv-errors';
6-
import ajvFormats from 'ajv-formats';
74
import betterAjvErrors from 'better-ajv-errors';
85

6+
import { createAjv } from '../../scripts/lib/ajv.js';
97
import { Linter, Logger, LinterData } from '../utils.js';
108

119
import compatDataSchema from './../../schemas/compat-data.schema.json' with { type: 'json' };
1210
import browserDataSchema from './../../schemas/browsers.schema.json' with { type: 'json' };
1311

14-
const ajv = new Ajv({ allErrors: true });
15-
// We use 'fast' because as a side effect that makes the "uri" format more lax.
16-
// By default the "uri" format rejects ① and similar in URLs.
17-
ajvFormats.default(ajv, { mode: 'fast' });
18-
// Allow for custom error messages to provide better directions for contributors
19-
ajvErrors.default(ajv);
20-
21-
// Define keywords for schema->TS converter
22-
ajv.addKeyword('tsEnumNames');
23-
ajv.addKeyword('tsName');
24-
ajv.addKeyword('tsType');
12+
const ajv = createAjv();
2513

2614
export default {
2715
name: 'JSON Schema',

schemas/browsers.schema.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@
3131
"$ref": "#/definitions/browser_statement"
3232
},
3333
"minProperties": 1,
34-
"maxProperties": 1,
3534
"errorMessage": {
36-
"minProperties": "A browser must be described within the file.",
37-
"maxProperties": "Each browser JSON file may only describe one browser."
35+
"minProperties": "A browser must be described within the file."
3836
},
3937
"tsType": "Record<BrowserName, BrowserStatement>"
4038
},

scripts/build/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import fs from 'node:fs/promises';
55
import { relative } from 'node:path';
66
import { fileURLToPath } from 'node:url';
77

8+
import betterAjvErrors from 'better-ajv-errors';
89
import esMain from 'es-main';
910
import stringify from 'fast-json-stable-stringify';
1011
import { compareVersions } from 'compare-versions';
@@ -13,6 +14,9 @@ import { marked } from 'marked';
1314
import { InternalSupportStatement } from '../../types/index.js';
1415
import { BrowserName, CompatData, VersionValue } from '../../types/types.js';
1516
import compileTS from '../generate-types.js';
17+
import compatDataSchema from '../../schemas/compat-data.schema.json' with { type: 'json' };
18+
import browserDataSchema from '../../schemas/browsers.schema.json' with { type: 'json' };
19+
import { createAjv } from '../lib/ajv.js';
1620
import { walk } from '../../utils/index.js';
1721
import { WalkOutput } from '../../utils/walk.js';
1822
import bcd from '../../index.js';
@@ -219,6 +223,35 @@ export const createDataBundle = async (): Promise<CompatData> => {
219223
};
220224
};
221225

226+
/**
227+
* Validates the given data against the schema.
228+
* @param data - The data to validate.
229+
*/
230+
const validate = (data: CompatData) => {
231+
const ajv = createAjv();
232+
233+
for (const [key, value] of Object.entries(data)) {
234+
if (key === '__meta') {
235+
// Not covered by the schema.
236+
continue;
237+
}
238+
239+
const schema = key === 'browsers' ? browserDataSchema : compatDataSchema;
240+
const data = { [key]: value };
241+
if (!ajv.validate(schema, data)) {
242+
const errors = ajv.errors || [];
243+
if (!errors.length) {
244+
console.error(`${key} data failed validation with unknown errors!`);
245+
}
246+
// Output messages by one since better-ajv-errors wrongly joins messages
247+
// (see https://github.com/atlassian/better-ajv-errors/pull/21)
248+
errors.forEach((e) => {
249+
console.error(betterAjvErrors(schema, data, [e], { indent: 2 }));
250+
});
251+
}
252+
}
253+
};
254+
222255
/* c8 ignore start */
223256

224257
/**
@@ -227,6 +260,7 @@ export const createDataBundle = async (): Promise<CompatData> => {
227260
const writeData = async () => {
228261
const dest = new URL('data.json', targetdir);
229262
const data = await createDataBundle();
263+
validate(data);
230264
await fs.writeFile(dest, stringify(data));
231265
logWrite(dest, 'data');
232266
};

scripts/lib/ajv.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Ajv } from 'ajv';
2+
import ajvErrors from 'ajv-errors';
3+
import ajvFormats from 'ajv-formats';
4+
5+
/**
6+
* Returns a new pre-configured Ajv instance.
7+
* @returns the Ajv instance.
8+
*/
9+
export const createAjv = (): Ajv => {
10+
const ajv = new Ajv({ allErrors: true });
11+
// We use 'fast' because as a side effect that makes the "uri" format more lax.
12+
// By default the "uri" format rejects ① and similar in URLs.
13+
ajvFormats.default(ajv, { mode: 'fast' });
14+
// Allow for custom error messages to provide better directions for contributors
15+
ajvErrors.default(ajv);
16+
17+
// Define keywords for schema->TS converter
18+
ajv.addKeyword('tsEnumNames');
19+
ajv.addKeyword('tsName');
20+
ajv.addKeyword('tsType');
21+
22+
return ajv;
23+
};

0 commit comments

Comments
 (0)