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
114 changes: 113 additions & 1 deletion assertions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import assert from "node:assert/strict";
import { assertValidFeatureReference } from "./assertions";
import {
assertFreshRegressionNotes,
assertValidFeatureReference,
} from "./assertions";
import { FeatureData } from "./types";

describe("assertValidReference()", function () {
it("throws if target ID is a move", function () {
Expand Down Expand Up @@ -34,3 +38,111 @@ describe("assertValidReference()", function () {
});
});
});

describe("assertFreshRegressionNotes", function () {
it("throws when the current status is the same as the previous status", function () {
const f = {
kind: "feature",
status: { baseline: "low" },
notes: [
{
category: "baseline-regression",
previous_baseline_value: "low",
},
],
} as Partial<FeatureData> as FeatureData;
assert.throws(() => {
assertFreshRegressionNotes("a", f);
});
});

it("throws when the current status is better than the previous status", function () {
const highLow = {
kind: "feature",
status: { baseline: "high" },
notes: [
{
category: "baseline-regression",
previous_baseline_value: "low",
},
],
} as Partial<FeatureData> as FeatureData;
assert.throws(() => {
assertFreshRegressionNotes("a", highLow);
});

const lowFalse = {
kind: "feature",
status: { baseline: "low" },
notes: [
{
category: "baseline-regression",
previous_baseline_value: false,
},
],
} as Partial<FeatureData> as FeatureData;
assert.throws(() => {
assertFreshRegressionNotes("a", lowFalse);
});

const highFalse = {
kind: "feature",
status: { baseline: "high" },
notes: [
{
category: "baseline-regression",
previous_baseline_value: false,
},
],
} as Partial<FeatureData> as FeatureData;
assert.throws(() => {
assertFreshRegressionNotes("a", highFalse);
});
});

it("does not throw when the current status is lower than the previous status", function () {
const lowHigh = {
kind: "feature",
status: { baseline: "low" },
notes: [
{
category: "baseline-regression",
previous_baseline_value: "high",
},
],
} as Partial<FeatureData> as FeatureData;
assertFreshRegressionNotes("a", lowHigh);

const falseLow = {
kind: "feature",
status: { baseline: false },
notes: [
{
category: "baseline-regression",
previous_baseline_value: "low",
},
],
} as Partial<FeatureData> as FeatureData;
assertFreshRegressionNotes("a", falseLow);

const falseHigh = {
kind: "feature",
status: { baseline: false },
notes: [
{
category: "baseline-regression",
previous_baseline_value: "high",
},
],
} as Partial<FeatureData> as FeatureData;
assertFreshRegressionNotes("a", falseHigh);
});

it("does not throw without a regression note", function () {
const noRegressionNotes = {
kind: "feature",
status: { baseline: "high" },
} as Partial<FeatureData> as FeatureData;
assertFreshRegressionNotes("a", noRegressionNotes);
});
});
45 changes: 44 additions & 1 deletion assertions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isOrdinaryFeatureData } from "./type-guards";
import { FeatureData } from "./types";
import { BaselineValue, FeatureData } from "./types";
import { WebFeaturesData } from "./types.quicktype";

/**
Expand Down Expand Up @@ -27,6 +27,49 @@ export function assertValidFeatureReference(
}
}

/**
* Assert that a regression note is still relevant.
*
* A fresh regression note must represent a status change where the
* `previous_baseline_value` value is better than the current `status.baseline`
* value. A regression note must represent a change in status from high to low,
* high to not Baseline, or low to not Baseline. A regression note must not
* represent a status change that has aged, such that the current
* `status.baseline` value has progressed back to `previous_baseline_value`.
*
* @export
* @param {string} id The ID of the feature to be checked
* @param {FeatureData} data The ordinary feature data to be checked
*/
export function assertFreshRegressionNotes(
id: string,
data: FeatureData,
): void {
if (!isOrdinaryFeatureData(data)) {
return;
}

const { baseline } = data.status;
const notes = data.notes ? data.notes : [];

for (const [index, note] of notes.entries()) {
if (compareBaselineValue(note.previous_baseline_value, baseline) <= 0) {
throw new Error(
`regression note ${index} on ${id}.yml no longer applies (status is ${baseline}, was ${note.previous_baseline_value}). Delete this note.`,
);
}
}
}

function compareBaselineValue(a: BaselineValue, b: BaselineValue): number {
const statusToNumber = new Map<BaselineValue, number>([
["high", 2],
["low", 1],
[false, 0],
]);
return statusToNumber.get(a) - statusToNumber.get(b);
}

/**
* Assert that a discouraged feature with no supporting browsers has a
* `removal_date`.
Expand Down
18 changes: 18 additions & 0 deletions docs/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,21 @@ When you set a `discouraged` block in a feature file, do:
- Set one or more (optional) `alternatives` feature IDs that are whole or partial substitutes for the discouraged feature.
An alternative doesn't have to be a narrow drop-in replacement for the discouraged feature but it must handle some use case of the discouraged feature.
Guide developers to the most relevant features that would help them stop using the discouraged feature.

## Notes

Features may have notes.
Presently, there is one type of note, a Baseline regression note.

### Baseline regression note

Use a note with a `category: baseline-regression` whenever the Baseline status goes backwards (such as from `"high"` to `"low"`).
This note type applies to any regression, whether it was caused by changes in upstream data or an editorial override.

In the `message` field, explain the cause of the regression.
Write the message to developers, to help them understand whether the regression applies to their use case.
If the cause is a newly-discovered or reported bug then briefly describe the nature of the bug.
If the cause is a correction then briefly describe the nature and origin of the correction (usually, upstream data).

In the `citations` field, include the URLs that are most important to understanding the nature and origin of the change.
For example, if BCD marked a feature as a `partial_implementation` due to a browser bug, include the URL for the browser bug and the BCD pull request or issue where the `partial_implementation` status was agreed.
6 changes: 0 additions & 6 deletions features/content-visibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ description: The `content-visibility` CSS property delays rendering an element,
spec: https://drafts.csswg.org/css-contain-2/#content-visibility
group: css
caniuse: css-content-visibility
# TODO: https://github.com/web-platform-dx/web-features/issues/1971
# Status changed: https://github.com/web-platform-dx/web-features/pull/2591
# 2025-01-30 — low → false — Safari hides text behind `content-visibility: auto` from "Find…" in the page.
# References:
# - https://github.com/mdn/browser-compat-data/pull/25781
# - https://bugs.webkit.org/show_bug.cgi?id=283846
compat_features:
- api.ContentVisibilityAutoStateChangeEvent
- api.ContentVisibilityAutoStateChangeEvent.ContentVisibilityAutoStateChangeEvent
Expand Down
8 changes: 8 additions & 0 deletions features/createimagebitmap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ name: createImageBitmap
description: The `createImageBitmap()` global method creates an `ImageBitmap` object from a source such as an image, SVG, blob, or canvas. An `ImageBitmap` object represents pixel data that can be drawn to a canvas with lower latency than other types, such as `ImageData`.
spec: https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#imagebitmap
caniuse: createimagebitmap
notes:
- date: 2025-08-11
category: baseline-regression
previous_baseline_value: high
message: >
This feature's status was recalculated to be more consistent with caniuse's criteria for full support, requiring `SVGImageElement` as a supported image source.
citations:
- https://github.com/web-platform-dx/web-features/pull/3173
status:
compute_from:
- api.createImageBitmap
Expand Down
15 changes: 9 additions & 6 deletions features/font-variant-position.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ name: font-variant-position
description: The `font-variant-position` CSS property sets whether to use alternate glyphs for subscript and superscript text.
spec: https://drafts.csswg.org/css-fonts-4/#font-variant-position-prop
group: font-features
# TODO: https://github.com/web-platform-dx/web-features/issues/1971
# Status changed: https://github.com/web-platform-dx/web-features/pull/1958
# 2024-10-15 — low → false — Chrome, Edge, and Safari do not implement font synthesis for missing superscript or subscript glyphs.
# References:
# - https://issues.chromium.org/issues/352218916
# - https://bugs.webkit.org/show_bug.cgi?id=151471
notes:
- date: 2024-10-15
category: baseline-regression
previous_baseline_value: low
message: >
Chrome, Edge, and Safari do not implement font synthesis for missing superscript or subscript glyphs.
citations:
- https://issues.chromium.org/issues/352218916
- https://bugs.webkit.org/show_bug.cgi?id=151471
6 changes: 0 additions & 6 deletions features/link-rel-dns-prefetch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,3 @@ caniuse: link-rel-dns-prefetch
group: resource-hints
compat_features:
- html.elements.link.rel.dns-prefetch
# TODO: https://github.com/web-platform-dx/web-features/issues/1971
# Status changed: https://github.com/web-platform-dx/web-features/pull/3074/
# 2025-06-23 — low → false — On iOS, it was erroneously reported that this feature was supported.
# References:
# - https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes#Networking
# - https://github.com/mdn/browser-compat-data/pull/27057
15 changes: 9 additions & 6 deletions features/popover.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ name: Popover
description: The `popover` HTML attribute creates an overlay to display content on top of other page content. Popovers can be shown declaratively using HTML, or using the `showPopover()` method.
spec: https://html.spec.whatwg.org/multipage/popover.html
group: html
# TODO: https://github.com/web-platform-dx/web-features/issues/1971
# Status changed: https://github.com/web-platform-dx/web-features/pull/1797
# 2024-09-18 — low → false — Safari on iOS has a bug that prevents dismissing popovers by touch.
# References:
# - https://github.com/mdn/browser-compat-data/issues/22927
# - https://bugs.webkit.org/show_bug.cgi?id=267688
# notes:
# - date: 2024-09-18
# category: baseline-regression
# previous_baseline_value: low
# message: >
# Safari on iOS has a bug that prevents light dismiss (tapping outside the element to close it).
# citations:
# - https://bugs.webkit.org/show_bug.cgi?id=267688
# - https://github.com/mdn/browser-compat-data/issues/22927
Comment on lines 5 to 13
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When I opened this PR, I had written this note. However, the feature has since progressed. It occurred to me that we should not show irrelevant notes, so I commented it out. This is a rather new idea, so I'd like to delete this entirely, if we decide this is the right way to go.

Other options considered: some sort of "historic" flag on notes that are no longer relevant, or filtering historic notes from the published package. I figured both of those added complexity that wouldn't be very useful to anyone, least of all developers.

Copy link
Contributor

Choose a reason for hiding this comment

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

I almost feel like we should have a linting step in place which checks that notes.new_baseline_value is equal to status.baseline, and if not, deletes that particular note. I think keeping old notes about status regressions that no longer apply would be confusing. I don't think that web-features necessarily needs to keep track of the history of a feature. Its role is to document the platform as it is now.

status:
compute_from:
- api.HTMLElement.popover
Expand Down
6 changes: 0 additions & 6 deletions features/streams.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ name: Streams
description: The streams API creates, composes, and consumes continuously generated data.
spec: https://streams.spec.whatwg.org/
group: streams
# TODO: https://github.com/web-platform-dx/web-features/issues/1971
# Status changed: https://github.com/web-platform-dx/web-features/pull/2358, https://github.com/web-platform-dx/web-features/pull/2491
# 2024-12-19 — low → false — Regressed status to match Caniuse, which considers support beginning at BYOB shipping.
# 2025-01-30 — false → high — Split BYOB into a separate "readable-byte-streams" feature. Linked that one to Caniuse.
# References:
# - https://caniuse.com/streams
Comment on lines -5 to -10
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 fussed over this for a while and ended up deciding to omit the note entirely. The first note would say that the feature regressed, the second note would be some new category (advance? unregression?) showing that we more or less reverted the second note. It seemed to me that the correct course of action in that scenario would be to withdraw the note, so that's what I've done.

Copy link
Contributor

Choose a reason for hiding this comment

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

I tend to agree. If our consumers really only care about baseline regressions, then we don't need a note here. Unless we have reasons to believe that the history of a feature would be useful. Which I don't think we do.

status:
compute_from:
- api.ReadableStream
Expand Down
17 changes: 13 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import path from 'path';
import { Temporal } from '@js-temporal/polyfill';
import { fdir } from 'fdir';
import YAML from 'yaml';
import { convertMarkdown } from "./text";
import { GroupData, SnapshotData, WebFeaturesData } from './types';

import { BASELINE_LOW_TO_HIGH_DURATION, coreBrowserSet, getStatus, parseRangedDateString } from 'compute-baseline';
import { Compat } from 'compute-baseline/browser-compat-data';
import { assertRequiredRemovalDateSet, assertValidFeatureReference } from './assertions';
import { assertFreshRegressionNotes, assertRequiredRemovalDateSet, assertValidFeatureReference } from './assertions';
import { convertMarkdown } from "./text";
import { isMoved, isOrdinaryFeatureData, isSplit } from './type-guards';
import { FeatureData, GroupData, SnapshotData, WebFeaturesData } from './types';

// The longest name allowed, to allow for compact display.
const nameMaxLength = 80;
Expand Down Expand Up @@ -178,6 +178,14 @@ for (const [key, data] of yamlEntries('features')) {
data.discouraged.reason = text;
data.discouraged.reason_html = html;
}

if (Array.isArray(data.notes)) {
for (const note of data.notes as FeatureData["notes"]) {
const { text, html } = convertMarkdown(note.message);
note.message = text;
note.message_html = html;
}
}
}

// Compute Baseline high date from low date.
Expand Down Expand Up @@ -233,7 +241,7 @@ for (const [key, data] of yamlEntries('features')) {
}
}

assertRequiredRemovalDateSet(key, data);
assertRequiredRemovalDateSet(key, data);

features[key] = data;
}
Expand All @@ -242,6 +250,7 @@ for (const [id, feature] of Object.entries(features)) {
const { kind } = feature;
switch (kind) {
case "feature":
assertFreshRegressionNotes(id, feature);
for (const alternative of feature.discouraged?.alternatives ?? []) {
assertValidFeatureReference(id, alternative, features)
}
Expand Down
Loading
Loading