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/package.json b/package.json index c78a26c5861..bf476070a20 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ "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", "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 8d07d51fcd8..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": { @@ -111,6 +112,68 @@ "description": "Date the feature achieved Baseline low status", "type": "string" }, + "by_compat_key": { + "additionalProperties": { + "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" + }, "support": { "additionalProperties": false, "description": "Browser versions that most-recently introduced the feature", diff --git a/scripts/build.ts b/scripts/build.ts index 239affce9f3..3204cd6bd57 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,9 +1,21 @@ -import { basename } from "node:path"; +import { DefinedError } from "ajv"; +import { getStatus } from "compute-baseline"; +import stringify from "fast-json-stable-stringify"; import { execSync } from "node:child_process"; import fs from "node:fs"; -import stringify from "fast-json-stable-stringify"; +import { basename } from "node:path"; +import winston from "winston"; import yargs from "yargs"; import * as data from "../index.js"; +import { validate } from "./validate.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); @@ -14,14 +26,23 @@ 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() { 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) { @@ -39,3 +60,42 @@ function buildPackage() { encoding: "utf-8", }); } + +function buildExtendedJSON() { + for (const [id, featureData] of Object.entries(data.features)) { + if ( + Array.isArray(featureData.compat_features) && + featureData.compat_features.length && + featureData.status + ) { + featureData.status.by_compat_key = {}; + for (const key of featureData.compat_features) { + featureData.status.by_compat_key[key] = getStatus(id, key); + } + } + } + + 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 valid = validate(data); + if (!valid) { + // 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; + } + return true; +} diff --git a/scripts/schema.ts b/scripts/schema.ts index 81a110a6a14..357bcf85a21 100644 --- a/scripts/schema.ts +++ b/scripts/schema.ts @@ -3,12 +3,9 @@ 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; @@ -25,15 +22,13 @@ function checkSchemaConsistency(): void { } } -function validate() { - const ajv = new Ajv({allErrors: true}); - addFormats(ajv); - - const validate = ajv.compile(schema); - +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; @@ -41,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; +} diff --git a/types.ts b/types.ts index 524999a9504..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 */ @@ -45,6 +45,11 @@ interface SupportStatus { }; } +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 +} + /** Specification URL * @format uri */