Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish groups and snapshots #1060

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 9 additions & 23 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}

function* yamlEntries(root: string): Generator<[string, any]> {
const filePaths = new fdir()
.withBasePath()
Expand Down Expand Up @@ -67,10 +53,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<string, any> = 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`);
}
Expand All @@ -84,14 +70,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<string, any> = 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.
Expand Down Expand Up @@ -158,12 +144,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.`);
}
}
Expand All @@ -181,7 +167,7 @@ for (const [key, data] of yamlEntries('features')) {
}
}

features[key] = scrub(data);
features[key] = data;
}

export default features;
export { features, groups, snapshots };
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions packages/web-features/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
101 changes: 99 additions & 2 deletions schemas/defs.schema.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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"
Comment on lines +216 to +220
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing you'll lose here is the patternProperties constraint from features.schema.json:

  "patternProperties": {
    "^[a-z0-9-]*$": {
      "$ref": "defs#/definitions/FeatureData"
    }
  }

That said, if I rename a file to use underscores, ajv seems to not catch the break with the schema. So maybe this doesn't matter, or never has? 🤷

See #44 (comment) for background

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to find a way to express this with TypeScript but failed. It may be possible with https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html but it wouldn't be pretty, and I can't imagine it would get turned into a pretty RegEx in JSON schema.

Maybe #1317 instead?

},
"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"
}
}
}
10 changes: 0 additions & 10 deletions schemas/features.schema.json

This file was deleted.

4 changes: 2 additions & 2 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion scripts/caniuse.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
9 changes: 4 additions & 5 deletions scripts/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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}`);
Expand Down
2 changes: 1 addition & 1 deletion scripts/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/update-drafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
yield spec.url;
Expand Down
27 changes: 26 additions & 1 deletion types.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -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 */
Expand Down Expand Up @@ -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;
}