Skip to content

Commit

Permalink
Publish groups and snapshots
Browse files Browse the repository at this point in the history
  • Loading branch information
foolip committed Jul 2, 2024
1 parent 416eb15 commit 3e84661
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 46 deletions.
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"
},
"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"
}
}
}
8 changes: 1 addition & 7 deletions schemas/features.schema.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"patternProperties": {
"^[a-z0-9-]*$": {
"$ref": "defs#/definitions/FeatureData"
}
},
"additionalProperties": false
"$ref": "defs#/definitions/WebFeaturesData"
}
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
4 changes: 2 additions & 2 deletions scripts/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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' };
Expand All @@ -32,7 +32,7 @@ function validate() {

const validate = ajv.compile(schema);

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;
}

0 comments on commit 3e84661

Please sign in to comment.