From 33f243de25108b0993dbfcce17429f36f5dd95e7 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 16 Jul 2024 17:25:17 +0200 Subject: [PATCH 1/7] Add extended JSON file build output --- package.json | 1 + schemas/defs.schema.json | 72 ++++++++++++++++++++++++++++++++++++++++ scripts/build.ts | 28 ++++++++++++++++ types.ts | 2 ++ 4 files changed, 103 insertions(+) diff --git a/package.json b/package.json index e0c92abba37..d84faee3ec3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "node": ">=18.19.0" }, "scripts": { + "build:extended": "tsx scripts/build.ts extended-json", "build": "tsx scripts/build.ts package", "dist": "tsx scripts/dist.ts", "feature-init": "tsx scripts/feature-init.ts", diff --git a/schemas/defs.schema.json b/schemas/defs.schema.json index f183bdee26a..8439b1a2837 100644 --- a/schemas/defs.schema.json +++ b/schemas/defs.schema.json @@ -128,6 +128,13 @@ "description": "Date the feature achieved Baseline low status", "type": "string" }, + "by_compat_key": { + "additionalProperties": { + "$ref": "#/definitions/interface-types.ts-1387-2059-types.ts-0-2394" + }, + "description": "Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features.", + "type": "object" + }, "support": { "additionalProperties": false, "description": "Browser versions that most-recently introduced the feature", @@ -240,6 +247,71 @@ "snapshots" ], "type": "object" + }, + "interface-types.ts-1387-2059-types.ts-0-2394": { + "additionalProperties": false, + "properties": { + "baseline": { + "description": "Whether the feature is Baseline (low substatus), Baseline (high substatus), or not (false)", + "enum": [ + "high", + "low", + false + ], + "type": [ + "string", + "boolean" + ] + }, + "baseline_high_date": { + "description": "Date the feature achieved Baseline high status", + "type": "string" + }, + "baseline_low_date": { + "description": "Date the feature achieved Baseline low status", + "type": "string" + }, + "by_compat_key": { + "additionalProperties": { + "$ref": "#/definitions/interface-types.ts-1387-2059-types.ts-0-2394" + }, + "description": "Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features.", + "type": "object" + }, + "support": { + "additionalProperties": false, + "description": "Browser versions that most-recently introduced the feature", + "properties": { + "chrome": { + "type": "string" + }, + "chrome_android": { + "type": "string" + }, + "edge": { + "type": "string" + }, + "firefox": { + "type": "string" + }, + "firefox_android": { + "type": "string" + }, + "safari": { + "type": "string" + }, + "safari_ios": { + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "baseline", + "support" + ], + "type": "object" } } } \ No newline at end of file diff --git a/scripts/build.ts b/scripts/build.ts index fae23a6d8e0..cb7ebcedfe1 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,4 +1,5 @@ import { execSync } from "child_process"; +import { getStatus } from "compute-baseline"; import stringify from "fast-json-stable-stringify"; import fs from "fs"; import yargs from "yargs"; @@ -13,6 +14,11 @@ yargs(process.argv.slice(2)) describe: "Generate the web-features npm package", handler: buildPackage, }) + .command({ + command: "extended-json", + describe: "Generate a web-features JSON file with BCD per-key statuses", + handler: buildExtendedJSON, + }) .parseSync(); function buildPackage() { @@ -35,3 +41,25 @@ function buildPackage() { encoding: "utf-8", }); } + +function buildExtendedJSON() { + // TODO: Validate the resulting JSON against a schema. + for (const [id, featureData] of Object.entries(data.features)) { + if (Array.isArray(featureData.compat_features) && featureData.status) { + const by_compat_key = {}; + + for (const key of featureData.compat_features) { + by_compat_key[key] = { status: getStatus(id, key) }; + } + + if (Object.keys(by_compat_key).length) { + featureData.status.by_compat_key = by_compat_key; + } + } + } + + fs.writeFileSync( + new URL("./web-features.extended.json", rootDir), + stringify(data), + ); +} diff --git a/types.ts b/types.ts index 3277ad89e2e..02c097c3ba9 100644 --- a/types.ts +++ b/types.ts @@ -45,6 +45,8 @@ interface SupportStatus { support: { [K in browserIdentifier]?: string; }; + /** Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features. */ + by_compat_key?: Record } /** Specification URL From 2bb0da797dd3263b2923df3d030d280355c9d359 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Fri, 26 Jul 2024 11:07:05 +0200 Subject: [PATCH 2/7] Refresh schema --- schemas/data.schema.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schemas/data.schema.json b/schemas/data.schema.json index 386964b3e01..62b9efdf999 100644 --- a/schemas/data.schema.json +++ b/schemas/data.schema.json @@ -113,7 +113,7 @@ }, "by_compat_key": { "additionalProperties": { - "$ref": "#/definitions/interface-types.ts-1387-2059-types.ts-0-2394" + "$ref": "#/definitions/interface-types.ts-1307-1979-types.ts-0-2314" }, "description": "Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features.", "type": "object" @@ -231,7 +231,7 @@ ], "type": "object" }, - "interface-types.ts-1387-2059-types.ts-0-2394": { + "interface-types.ts-1307-1979-types.ts-0-2314": { "additionalProperties": false, "properties": { "baseline": { @@ -256,7 +256,7 @@ }, "by_compat_key": { "additionalProperties": { - "$ref": "#/definitions/interface-types.ts-1387-2059-types.ts-0-2394" + "$ref": "#/definitions/interface-types.ts-1307-1979-types.ts-0-2314" }, "description": "Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features.", "type": "object" From bfaf30be0837882cb2d44719785da209b66fdd86 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Fri, 26 Jul 2024 11:21:58 +0200 Subject: [PATCH 3/7] Make the generated JSON Schema a little more pretty --- schemas/data.schema.json | 122 ++++++++++++++++++--------------------- types.ts | 7 ++- 2 files changed, 61 insertions(+), 68 deletions(-) diff --git a/schemas/data.schema.json b/schemas/data.schema.json index 62b9efdf999..8b743def87c 100644 --- a/schemas/data.schema.json +++ b/schemas/data.schema.json @@ -113,7 +113,62 @@ }, "by_compat_key": { "additionalProperties": { - "$ref": "#/definitions/interface-types.ts-1307-1979-types.ts-0-2314" + "additionalProperties": false, + "properties": { + "baseline": { + "description": "Whether the feature is Baseline (low substatus), Baseline (high substatus), or not (false)", + "enum": [ + "high", + "low", + false + ], + "type": [ + "string", + "boolean" + ] + }, + "baseline_high_date": { + "description": "Date the feature achieved Baseline high status", + "type": "string" + }, + "baseline_low_date": { + "description": "Date the feature achieved Baseline low status", + "type": "string" + }, + "support": { + "additionalProperties": false, + "description": "Browser versions that most-recently introduced the feature", + "properties": { + "chrome": { + "type": "string" + }, + "chrome_android": { + "type": "string" + }, + "edge": { + "type": "string" + }, + "firefox": { + "type": "string" + }, + "firefox_android": { + "type": "string" + }, + "safari": { + "type": "string" + }, + "safari_ios": { + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "baseline", + "support" + ], + "type": "object" }, "description": "Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features.", "type": "object" @@ -230,71 +285,6 @@ "snapshots" ], "type": "object" - }, - "interface-types.ts-1307-1979-types.ts-0-2314": { - "additionalProperties": false, - "properties": { - "baseline": { - "description": "Whether the feature is Baseline (low substatus), Baseline (high substatus), or not (false)", - "enum": [ - "high", - "low", - false - ], - "type": [ - "string", - "boolean" - ] - }, - "baseline_high_date": { - "description": "Date the feature achieved Baseline high status", - "type": "string" - }, - "baseline_low_date": { - "description": "Date the feature achieved Baseline low status", - "type": "string" - }, - "by_compat_key": { - "additionalProperties": { - "$ref": "#/definitions/interface-types.ts-1307-1979-types.ts-0-2314" - }, - "description": "Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features.", - "type": "object" - }, - "support": { - "additionalProperties": false, - "description": "Browser versions that most-recently introduced the feature", - "properties": { - "chrome": { - "type": "string" - }, - "chrome_android": { - "type": "string" - }, - "edge": { - "type": "string" - }, - "firefox": { - "type": "string" - }, - "firefox_android": { - "type": "string" - }, - "safari": { - "type": "string" - }, - "safari_ios": { - "type": "string" - } - }, - "type": "object" - } - }, - "required": [ - "baseline", - "support" - ], - "type": "object" } } } \ No newline at end of file diff --git a/types.ts b/types.ts index 86f8c4e1847..c5770950ff1 100644 --- a/types.ts +++ b/types.ts @@ -32,7 +32,7 @@ type browserIdentifier = "chrome" | "chrome_android" | "edge" | "firefox" | "fir type BaselineHighLow = "high" | "low"; -interface SupportStatus { +interface Status { /** Whether the feature is Baseline (low substatus), Baseline (high substatus), or not (false) */ baseline: BaselineHighLow | false; /** Date the feature achieved Baseline low status */ @@ -43,8 +43,11 @@ interface SupportStatus { support: { [K in browserIdentifier]?: string; }; +} + +interface SupportStatus extends Status { /** Statuses for each key in the feature's compat_features list, if applicable. Not available to the npm release of web-features. */ - by_compat_key?: Record + by_compat_key?: Record } /** Specification URL From 563381c134833568a0c557429d6660e242fac2f5 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 30 Jul 2024 16:49:06 +0200 Subject: [PATCH 4/7] Fix never-failing schema configuration --- package.json | 2 +- schemas/data.schema.json | 1 + scripts/schema.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a4cee9878bc..bf476070a20 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "feature-init": "tsx scripts/feature-init.ts", "format": "npx prettier --write .", "schema:write": "npm run schema -- --out ./schemas/data.schema.json", - "schema": "ts-json-schema-generator --tsconfig ./tsconfig.json --path ./types.ts", + "schema": "ts-json-schema-generator --tsconfig ./tsconfig.json --path ./types.ts --type=WebFeaturesData", "test:caniuse": "tsx scripts/caniuse.ts", "test:coverage": "npm run --workspaces test:coverage", "test:dist": "tsx scripts/dist.ts --check", diff --git a/schemas/data.schema.json b/schemas/data.schema.json index 8b743def87c..f4ff58970ae 100644 --- a/schemas/data.schema.json +++ b/schemas/data.schema.json @@ -1,4 +1,5 @@ { + "$ref": "#/definitions/WebFeaturesData", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "FeatureData": { diff --git a/scripts/schema.ts b/scripts/schema.ts index 81a110a6a14..638c6fa2149 100644 --- a/scripts/schema.ts +++ b/scripts/schema.ts @@ -1,3 +1,4 @@ +import assert from "node:assert/strict"; import child_process from "node:child_process"; import fs from "node:fs"; import path from "node:path"; @@ -31,6 +32,9 @@ function validate() { const validate = ajv.compile(schema); + // confidence check that the schema finds any errors at all + assert.equal(validate({}), false) + const valid = validate(data); if (!valid) { for (const error of validate.errors) { From b3be4b122c51cb7c2db0ec4f6102f98ec3ca7656 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 30 Jul 2024 17:05:08 +0200 Subject: [PATCH 5/7] Add schema validation to builds --- scripts/build.ts | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index 9a60cf399d3..c4f18c40d9d 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,10 +1,23 @@ +import Ajv from "ajv"; +import addFormats from "ajv-formats"; import { getStatus } from "compute-baseline"; import stringify from "fast-json-stable-stringify"; import { execSync } from "node:child_process"; import fs from "node:fs"; import { basename } from "node:path"; +import winston from "winston"; import yargs from "yargs"; import * as data from "../index.js"; +import schema from "../schemas/data.schema.json" assert { type: "json" }; +import { FeatureData } from "../types.js"; + +const logger = winston.createLogger({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple(), + ), + transports: new winston.transports.Console(), +}); const rootDir = new URL("..", import.meta.url); @@ -26,8 +39,12 @@ function buildPackage() { const packageDir = new URL("./packages/web-features/", rootDir); const filesToCopy = ["LICENSE.txt", "types.ts", "schemas/data.schema.json"]; + if (!valid(data)) { + logger.error("Data failed schema validation. No package built."); + process.exit(1); + } + const json = stringify(data); - // TODO: Validate the resulting JSON against a schema. const path = new URL("data.json", packageDir); fs.writeFileSync(path, json); for (const file of filesToCopy) { @@ -47,13 +64,12 @@ function buildPackage() { } function buildExtendedJSON() { - // TODO: Validate the resulting JSON against a schema. for (const [id, featureData] of Object.entries(data.features)) { if (Array.isArray(featureData.compat_features) && featureData.status) { - const by_compat_key = {}; + const by_compat_key: FeatureData["status"]["by_compat_key"] = {}; for (const key of featureData.compat_features) { - by_compat_key[key] = { status: getStatus(id, key) }; + by_compat_key[key] = getStatus(id, key); } if (Object.keys(by_compat_key).length) { @@ -62,8 +78,27 @@ function buildExtendedJSON() { } } + if (!valid(data)) { + logger.error("Data failed schema validation. No JSON file written."); + process.exit(1); + } + fs.writeFileSync( new URL("./web-features.extended.json", rootDir), stringify(data), ); } + +function valid(data: any): boolean { + const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); + addFormats(ajv); + const validate = ajv.compile(schema); + const valid = validate(data); + if (!valid) { + for (const error of validate.errors) { + console.error(`${error.instancePath}: ${error.message}`); + } + return false; + } + return true; +} From 054b21938e082739eebe483feb0717a503c94d6f Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 30 Jul 2024 17:47:22 +0200 Subject: [PATCH 6/7] Consolidate schema checks --- .prettierignore | 1 + scripts/build.ts | 15 +++++++-------- scripts/schema.ts | 25 ++++++++----------------- scripts/validate.ts | 21 +++++++++++++++++++++ 4 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 scripts/validate.ts diff --git a/.prettierignore b/.prettierignore index 8cca56c93fb..86750c6ed78 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,3 +21,4 @@ !/scripts/find-troublesome-ancestors.ts !/scripts/release.ts !/scripts/update-drafts.ts +!/scripts/validate.ts diff --git a/scripts/build.ts b/scripts/build.ts index c4f18c40d9d..9890d4f145d 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,5 +1,4 @@ -import Ajv from "ajv"; -import addFormats from "ajv-formats"; +import { DefinedError } from "ajv"; import { getStatus } from "compute-baseline"; import stringify from "fast-json-stable-stringify"; import { execSync } from "node:child_process"; @@ -8,8 +7,8 @@ import { basename } from "node:path"; import winston from "winston"; import yargs from "yargs"; import * as data from "../index.js"; -import schema from "../schemas/data.schema.json" assert { type: "json" }; import { FeatureData } from "../types.js"; +import { validate } from "./validate.js"; const logger = winston.createLogger({ format: winston.format.combine( @@ -90,13 +89,13 @@ function buildExtendedJSON() { } function valid(data: any): boolean { - const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); - addFormats(ajv); - const validate = ajv.compile(schema); const valid = validate(data); if (!valid) { - for (const error of validate.errors) { - console.error(`${error.instancePath}: ${error.message}`); + // TODO: turn on strictNullChecks, fix all the errors, and replace this with: + // const errors = validate.errors; + const errors = (valid as any).errors as DefinedError[]; + for (const error of errors) { + logger.error(`${error.instancePath}: ${error.message}`); } return false; } diff --git a/scripts/schema.ts b/scripts/schema.ts index 638c6fa2149..357bcf85a21 100644 --- a/scripts/schema.ts +++ b/scripts/schema.ts @@ -1,15 +1,11 @@ -import assert from "node:assert/strict"; import child_process from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import url from 'url'; -import Ajv from 'ajv'; -import addFormats from 'ajv-formats'; - +import { DefinedError } from "ajv"; import * as data from '../index.js'; - -import schema from '../schemas/data.schema.json' assert { type: 'json' }; +import { validate } from "./validate.js"; let status: 0 | 1 = 0; @@ -26,18 +22,13 @@ function checkSchemaConsistency(): void { } } -function validate() { - const ajv = new Ajv({allErrors: true}); - addFormats(ajv); - - const validate = ajv.compile(schema); - - // confidence check that the schema finds any errors at all - assert.equal(validate({}), false) - +function valid() { const valid = validate(data); if (!valid) { - for (const error of validate.errors) { + // TODO: turn on strictNullChecks, fix all the errors, and replace this with: + // const errors = validate.errors; + const errors = (validate as any).errors as DefinedError[]; + for (const error of errors) { console.error(`${error.instancePath}: ${error.message}`); } status = 1; @@ -45,5 +36,5 @@ function validate() { } checkSchemaConsistency(); -validate(); +valid(); process.exit(status); diff --git a/scripts/validate.ts b/scripts/validate.ts new file mode 100644 index 00000000000..9a21ca7fdf3 --- /dev/null +++ b/scripts/validate.ts @@ -0,0 +1,21 @@ +import Ajv from "ajv"; +import addFormats from "ajv-formats"; +import assert from "node:assert/strict"; + +import * as schema from "../schemas/data.schema.json" with { type: "json" }; + +export function validate(data: any) { + const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); + addFormats(ajv); + // TODO: turn on strictNullChecks, fix all the errors, and replace this with: + // const validator = ajv.compile(schema); + const validator = ajv.compile(schema); + + assert.equal( + validator({}), + false, + "Failed confidence check: schema validates empty object", + ); + + return validator; +} From c95aefd74bf5ea6226215b85bdff985dd6aab09f Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 30 Jul 2024 17:56:21 +0200 Subject: [PATCH 7/7] Check size of compat_features before setting per-key statuses --- scripts/build.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index 9890d4f145d..3204cd6bd57 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -7,7 +7,6 @@ import { basename } from "node:path"; import winston from "winston"; import yargs from "yargs"; import * as data from "../index.js"; -import { FeatureData } from "../types.js"; import { validate } from "./validate.js"; const logger = winston.createLogger({ @@ -64,15 +63,14 @@ function buildPackage() { function buildExtendedJSON() { for (const [id, featureData] of Object.entries(data.features)) { - if (Array.isArray(featureData.compat_features) && featureData.status) { - const by_compat_key: FeatureData["status"]["by_compat_key"] = {}; - + if ( + Array.isArray(featureData.compat_features) && + featureData.compat_features.length && + featureData.status + ) { + featureData.status.by_compat_key = {}; for (const key of featureData.compat_features) { - by_compat_key[key] = getStatus(id, key); - } - - if (Object.keys(by_compat_key).length) { - featureData.status.by_compat_key = by_compat_key; + featureData.status.by_compat_key[key] = getStatus(id, key); } } }