diff --git a/index.ts b/index.ts index ca9ea484e69..819593b21ee 100644 --- a/index.ts +++ b/index.ts @@ -3,7 +3,7 @@ import path from 'path'; import { fdir } from 'fdir'; import YAML from 'yaml'; -import { FeatureData } from './types'; +import { FeatureData, GroupData, SnapshotData } from './types'; import { Temporal } from '@js-temporal/polyfill'; import { toString as hastTreeToString } from 'hast-util-to-string'; @@ -24,20 +24,6 @@ const descriptionMaxLength = 300; // of a draft directory doesn't work. const draft = Symbol('draft'); -// Some FeatureData keys aren't (and may never) be ready for publishing. -// They're not part of the public schema (yet). -const omittables = [ - "snapshot", - "group" -] - -function scrub(data: any) { - for (const key of omittables) { - delete data[key]; - } - return data as FeatureData; -} - const identifierPattern = /^[a-z0-9-]*$/; function* yamlEntries(root: string): Generator<[string, any]> { @@ -73,10 +59,10 @@ function* yamlEntries(root: string): Generator<[string, any]> { // Load groups and snapshots first so that those identifiers can be validated // while loading features. -const groups: Map = new Map(yamlEntries('groups')); +const groups: { [key: string]: GroupData } = Object.fromEntries(yamlEntries('groups')); // Validate group name and parent fields. -for (const [key, data] of groups) { +for (const [key, data] of Object.entries(groups)) { if (typeof data.name !== 'string') { throw new Error(`group ${key} does not have a name`); } @@ -90,14 +76,14 @@ for (const [key, data] of groups) { if (chain.at(0) === chain.at(-1)) { throw new Error(`cycle in group parent chain: ${chain.join(' < ')}`); } - iter = groups.get(iter.parent); + iter = groups[iter.parent]; if (!iter) { throw new Error(`group ${chain.at(-2)} refers to parent ${chain.at(-1)} which does not exist.`); } } } -const snapshots: Map = new Map(yamlEntries('snapshots')); +const snapshots: { [key: string]: SnapshotData } = Object.fromEntries(yamlEntries('snapshots')); // TODO: validate the snapshot data. // Helper to iterate an optional string-or-array-of-strings value. @@ -164,12 +150,12 @@ for (const [key, data] of yamlEntries('features')) { // Ensure that only known group and snapshot identifiers are used. for (const group of identifiers(data.group)) { - if (!groups.has(group)) { + if (!Object.hasOwn(groups, group)) { throw new Error(`group ${group} used in ${key}.yml is not a valid group. Add it to groups/ if needed.`); } } for (const snapshot of identifiers(data.snapshot)) { - if (!snapshots.has(snapshot)) { + if (!Object.hasOwn(snapshots, snapshot)) { throw new Error(`snapshot ${snapshot} used in ${key}.yml is not a valid snapshot. Add it to snapshots/ if needed.`); } } @@ -187,7 +173,7 @@ for (const [key, data] of yamlEntries('features')) { } } - features[key] = scrub(data); + features[key] = data; } -export default features; +export { features, groups, snapshots }; diff --git a/package.json b/package.json index bdf16f932b2..3924a857d5f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "scripts": { "build": "tsx scripts/build.ts package", "dist": "tsx scripts/dist.ts", - "schema-defs": "ts-json-schema-generator --tsconfig ./tsconfig.json --type FeatureData --path ./types.ts --id defs", + "schema-defs": "ts-json-schema-generator --tsconfig ./tsconfig.json --type WebFeaturesData --path ./types.ts --id defs", "schema-defs:write": "npm run schema-defs -- --out ./schemas/defs.schema.json", "test": "npm run test:caniuse -- --quiet && npm run test:schema && npm run test:specs && npm run test:format && npm run test:dist && npm run test --workspaces", "test:caniuse": "tsx scripts/caniuse.ts", diff --git a/packages/web-features/index.ts b/packages/web-features/index.ts index 6b980d9724a..ae95fc9dbed 100644 --- a/packages/web-features/index.ts +++ b/packages/web-features/index.ts @@ -1,11 +1,11 @@ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { FeatureData } from "./types"; +import { WebFeaturesData } from "./types"; const jsonPath = fileURLToPath(new URL("./index.json", import.meta.url)); -const features = JSON.parse(readFileSync(jsonPath, { encoding: "utf-8" })) as { - [id: string]: FeatureData; -}; +const { features, groups, snapshots } = JSON.parse( + readFileSync(jsonPath, { encoding: "utf-8" }), +) as WebFeaturesData; -export default features; +export { features, groups, snapshots }; diff --git a/schemas/defs.schema.json b/schemas/defs.schema.json index bf146a7a2c3..f183bdee26a 100644 --- a/schemas/defs.schema.json +++ b/schemas/defs.schema.json @@ -1,11 +1,10 @@ { "$id": "defs", - "$ref": "#/definitions/FeatureData", + "$ref": "#/definitions/WebFeaturesData", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "FeatureData": { "additionalProperties": false, - "description": "Web platform feature", "properties": { "alias": { "anyOf": [ @@ -52,10 +51,40 @@ "description": "Short description of the feature, as an HTML string", "type": "string" }, + "group": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "minItems": 2, + "type": "array" + } + ], + "description": "Group identifier" + }, "name": { "description": "Short name", "type": "string" }, + "snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "minItems": 2, + "type": "array" + } + ], + "description": "Snapshot identifier" + }, "spec": { "anyOf": [ { @@ -143,6 +172,74 @@ "status" ], "type": "object" + }, + "GroupData": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Short name", + "type": "string" + }, + "parent": { + "description": "Identifier of parent group", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SnapshotData": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Short name", + "type": "string" + }, + "spec": { + "description": "Specification", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "spec" + ], + "type": "object" + }, + "WebFeaturesData": { + "additionalProperties": false, + "properties": { + "features": { + "additionalProperties": { + "$ref": "#/definitions/FeatureData" + }, + "description": "Feature identifiers and data", + "type": "object" + }, + "groups": { + "additionalProperties": { + "$ref": "#/definitions/GroupData" + }, + "description": "Group identifiers and data", + "type": "object" + }, + "snapshots": { + "additionalProperties": { + "$ref": "#/definitions/SnapshotData" + }, + "description": "Snapshot identifiers and data", + "type": "object" + } + }, + "required": [ + "features", + "groups", + "snapshots" + ], + "type": "object" } } } \ No newline at end of file diff --git a/schemas/features.schema.json b/schemas/features.schema.json deleted file mode 100644 index b3ddbd92f9a..00000000000 --- a/schemas/features.schema.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema#", - "type": "object", - "patternProperties": { - "^[a-z0-9-]*$": { - "$ref": "defs#/definitions/FeatureData" - } - }, - "additionalProperties": false -} diff --git a/scripts/build.ts b/scripts/build.ts index 70994b3d56a..fae23a6d8e0 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -2,7 +2,7 @@ import { execSync } from "child_process"; import stringify from "fast-json-stable-stringify"; import fs from "fs"; import yargs from "yargs"; -import features from "../index.js"; +import * as data from "../index.js"; const rootDir = new URL("..", import.meta.url); @@ -19,7 +19,7 @@ function buildPackage() { const packageDir = new URL("./packages/web-features/", rootDir); const filesToCopy = ["LICENSE.txt", "types.ts"]; - const json = stringify(features); + const json = stringify(data); // TODO: Validate the resulting JSON against a schema. const path = new URL("index.json", packageDir); fs.writeFileSync(path, json); diff --git a/scripts/caniuse.ts b/scripts/caniuse.ts index d090003fdab..ef4906fde3c 100644 --- a/scripts/caniuse.ts +++ b/scripts/caniuse.ts @@ -1,7 +1,7 @@ import lite from 'caniuse-lite'; import winston from "winston"; -import features from '../index.js'; +import { features } from '../index.js'; const logger = winston.createLogger({ level: 'info', diff --git a/scripts/schema.ts b/scripts/schema.ts index 43aeed9c720..6eabf047312 100644 --- a/scripts/schema.ts +++ b/scripts/schema.ts @@ -6,10 +6,9 @@ import url from 'url'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; -import features from '../index.js'; +import * as data from '../index.js'; import defs from '../schemas/defs.schema.json' assert { type: 'json' }; -import schema from '../schemas/features.schema.json' assert { type: 'json' }; let status: 0 | 1 = 0; @@ -27,12 +26,12 @@ function checkDefsConsistency(): void { } function validate() { - const ajv = new Ajv({allErrors: true, schemas: [defs]}); + const ajv = new Ajv({allErrors: true}); addFormats(ajv); - const validate = ajv.compile(schema); + const validate = ajv.compile(defs); - const valid = validate(features); + const valid = validate(data); if (!valid) { for (const error of validate.errors) { console.error(`${error.instancePath}: ${error.message}`); diff --git a/scripts/specs.ts b/scripts/specs.ts index d4ebbde9dad..f4b6317fa4c 100644 --- a/scripts/specs.ts +++ b/scripts/specs.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import webSpecs from 'web-specs' assert { type: 'json' }; -import features from '../index.js'; +import { features } from '../index.js'; // Specs needs to be in "good standing". Nightly URLs are used if available, // otherwise the snapshot/versioned URL is used. See browser-specs/web-specs diff --git a/scripts/update-drafts.ts b/scripts/update-drafts.ts index 7415a2620aa..fd6e17c22f4 100644 --- a/scripts/update-drafts.ts +++ b/scripts/update-drafts.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import { fileURLToPath } from "node:url"; import webSpecs from 'web-specs' assert { type: 'json' }; import YAML from "yaml"; -import features from '../index.js'; +import { features } from '../index.js'; function* getPages(spec): Generator { yield spec.url; diff --git a/types.ts b/types.ts index 05f3d9b54e9..3277ad89e2e 100644 --- a/types.ts +++ b/types.ts @@ -1,4 +1,11 @@ -/** Web platform feature */ +export interface WebFeaturesData { + /** Feature identifiers and data */ + features: { [key: string]: FeatureData }; + /** Group identifiers and data */ + groups: { [key: string]: GroupData }; + /** Snapshot identifiers and data */ + snapshots: { [key: string]: SnapshotData }; +} export interface FeatureData { /** Short name */ @@ -11,6 +18,10 @@ export interface FeatureData { alias?: string | [string, string, ...string[]]; /** Specification */ spec: specification_url | [specification_url, specification_url, ...specification_url[]]; + /** Group identifier */ + group?: string | [string, string, ...string[]]; + /** Snapshot identifier */ + snapshot?: string | [string, string, ...string[]]; /** caniuse.com identifier */ caniuse?: string | [string, string, ...string[]]; /** Whether a feature is considered a "baseline" web platform feature and when it achieved that status */ @@ -40,3 +51,17 @@ interface SupportStatus { * @format uri */ type specification_url = string; + +export interface GroupData { + /** Short name */ + name: string; + /** Identifier of parent group */ + parent?: string; +} + +export interface SnapshotData { + /** Short name */ + name: string; + /** Specification */ + spec: specification_url; +}