Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/publish_next_web-features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,5 @@ jobs:
${{ env.package_dir }}/data.json
data.extended.json
schemas/data.schema.json
data.proposed.json
schemas/data.proposed.schema.json
2 changes: 1 addition & 1 deletion .github/workflows/publish_web-features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build
- run: gh release upload ${{ github.ref_name }} packages/web-features/data.json data.extended.json schemas/data.schema.json
- run: gh release upload ${{ github.ref_name }} packages/web-features/data.json data.extended.json schemas/data.schema.json data.proposed.json schemas/data.proposed.schema.json
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A nice to have thing here would be to extend this to .github/workflows/publish_next_web-features.yml.

env:
GH_TOKEN: ${{ github.token }}
publish:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ packages/web-features/data.schema.json
packages/web-features/types*
packages/web-features/index.js
data.extended.json
data.proposed.json

# Ignore files created & used by pages & 11ty
/_site/**
44 changes: 44 additions & 0 deletions features/draft/proposed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Proposed features

This directory is for proposed features, to support the early use of
web-features before there is a published feature, with a way to transition from
a proposed ID to the eventual published feature's ID.

Proposed features can be referred to using the `proposed/a-feature` notation in
external projects, and the `proposed/` prefix is reserved for this use in
web-features.

A proposed feature goes through these stages:

- A bot creates a `.yml` file in this directory, with enough information for a
maintainer of web-features to follow up and act on. An `explainer` or `spec`
URL and a proposed-specific URL like `chromestatus` is recommended.
- A web-features maintainer creates a real feature entry and replaces the
proposed feature with a pointer to that feature.
- References to the proposed feature in tools or source code are updated to
point to the final feature.

Write access for bots can be granted by web-features maintainers, and should
only be used to create files in this directory.

## Example

The [processing instructions in HTML](https://chromestatus.com/feature/6534495085920256)
feature is created as `proposed/html-pis.yml` file with this contents:

```yml
# features/draft/proposed/html-pis.yml
explainer: https://github.com/WICG/declarative-partial-updates/blob/main/patching-explainer.md
chromestatus: https://chromestatus.com/feature/6534495085920256
```

When the real web-features entry is created, it is decided to consider this part
of `<template for>`, so the proposed feature is updated like this:

```yml
# features/draft/proposed/html-pis.yml
kind: moved
redirect_target: template-for
```

Any references to `proposed/html-pis` can be updated to refer to `template-for`.
2 changes: 2 additions & 0 deletions features/draft/proposed/html-pis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
explainer: https://github.com/WICG/declarative-partial-updates/blob/main/patching-explainer.md
chromestatus: https://chromestatus.com/feature/6534495085920256
11 changes: 9 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,17 @@ function* yamlEntries(root: string): Generator<[string, any]> {
// The feature identifier/key is the filename without extension.
const { name: key } = path.parse(fp);
const pathParts = fp.split(path.sep);
const isDraft = pathParts.includes('draft');
const isSpec = isDraft && pathParts.includes('spec');
const isProposed = isDraft && pathParts.includes('proposed');

if (isProposed) {
continue;
}

// Assert ID uniqueness
for (const [pool, map] of Object.entries(uniqueIdMaps)) {
if (!pathParts.includes("spec") && pathParts.includes(pool)) {
if (!isSpec && pathParts.includes(pool)) {
const otherFile: string | undefined = map.get(key);
if (otherFile) {
throw new Error(`ID collision between ${fp} and ${otherFile}`);
Expand All @@ -68,7 +75,7 @@ function* yamlEntries(root: string): Generator<[string, any]> {
Object.assign(data, dist);
}

if (pathParts.includes('draft')) {
if (isDraft) {
data[draft] = true;
}

Expand Down
22 changes: 22 additions & 0 deletions schemas/data.proposed.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"description": "Proposed web-features data",
"type": "object",
"properties": {
"features": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"kind": {
"enum": ["proposed", "moved", "split"]
}
},
"required": ["kind"],
"additionalProperties": true
}
}
},
"required": ["features"],
"additionalProperties": false
}
64 changes: 59 additions & 5 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { DefinedError } from "ajv";
import stringify from "fast-json-stable-stringify";
import { execSync } from "node:child_process";
import fs from "node:fs";
import { basename } from "node:path";
import path from "node:path";
import winston from "winston";
import yargs from "yargs";
import * as data from "../index.js";
import { validate } from "./validate.js";
import { validate, validateProposed } from "./validate.js";
import { fdir } from "fdir";
import YAML from "yaml";

const logger = winston.createLogger({
format: winston.format.combine(
Expand Down Expand Up @@ -42,17 +44,25 @@ function buildPackage() {
}

const json = stringify(data);
const path = new URL("data.json", packageDir);
fs.writeFileSync(path, json);
const dataPath = new URL("data.json", packageDir);
fs.writeFileSync(dataPath, json);

// TODO: Remove the extended data artifact in the next major release.
const extendedPath = new URL("data.extended.json", rootDir);
fs.writeFileSync(extendedPath, json);

const proposedPath = new URL("data.proposed.json", rootDir);
const proposedData = buildProposed();
if (!validProposed(proposedData)) {
logger.error("Proposed data failed schema validation. No package built.");
process.exit(1);
}
fs.writeFileSync(proposedPath, stringify(proposedData));

for (const file of filesToCopy) {
fs.copyFileSync(
new URL(file, rootDir),
new URL(basename(file), packageDir),
new URL(path.basename(file), packageDir),
);
}
execSync("npm install", {
Expand All @@ -78,3 +88,47 @@ function valid(data: any): boolean {
}
return true;
}

function buildProposed() {
const features: any = {};
const filePaths = new fdir()
.withBasePath()
.filter((fp) => fp.endsWith(".yml"))
.crawl("features/draft/proposed")
.sync() as string[];
for (const fp of filePaths) {
const { name: key } = path.parse(fp);
let data;
try {
data = YAML.parse(fs.readFileSync(fp, { encoding: "utf-8" }));
} catch {
console.warn(`${fp} is not a valid YAML file. Skipping.`);
continue;
}
if (data.kind === undefined) {
data.kind = "proposed";
} else if (!["proposed", "moved", "split"].includes(data.kind)) {
console.log(
`${fp} uses an unexpected kind ${JSON.stringify(data.kind)}. Skipping.`,
);
continue;
}
features[key] = data;
}
const proposed = { features };
return proposed;
}

function validProposed(data: any): boolean {
const valid = validateProposed(data);
if (!valid) {
// TODO: turn on strictNullChecks, fix all the errors, and replace this with:
// const errors = validate.errors;
const errors = validate.errors as DefinedError[];
for (const error of errors) {
logger.error(`${error.instancePath}: ${error.message}`);
}
return false;
}
return true;
}
3 changes: 3 additions & 0 deletions scripts/dist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ const tagsToFeatures: Map<string, Feature[]> = (() => {
* mistakes, such as `.yaml` files.
*/
function isDistOrDistable(path: string): boolean {
if (path.startsWith("features/draft/proposed/")) {
return false;
}
if (path.endsWith(".yaml.dist") || path.endsWith(".yaml")) {
throw new Error(
`YAML files must use .yml extension; ${path} has invalid extension`,
Expand Down
17 changes: 16 additions & 1 deletion scripts/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Ajv from "ajv";
import assert from "node:assert/strict";

import * as schema from "../schemas/data.schema.json" with { type: "json" };
import * as proposedSchema from "../schemas/data.proposed.schema.json" with { type: "json" };

const ajv = new Ajv({ allErrors: true, allowUnionTypes: true });
// TODO: turn on strictNullChecks, fix all the errors, and replace this with:
Expand All @@ -14,4 +15,18 @@ assert.equal(
"Failed confidence check: schema validates empty object",
);

export { validator as validate };
const proposedValidator = ajv.compile(proposedSchema);

assert.equal(
proposedValidator({}),
false,
"Failed confidence check: schema validates empty object",
);

assert.equal(
proposedValidator({ features: {} }),
true,
"Failed confidence check: schema rejects empty features object",
);

export { validator as validate, proposedValidator as validateProposed };