diff --git a/inputfiles/patches/webauthn.kdl b/inputfiles/patches/webauthn.kdl new file mode 100644 index 000000000..81e5fcf47 --- /dev/null +++ b/inputfiles/patches/webauthn.kdl @@ -0,0 +1,24 @@ +removals { + enum AuthenticatorTransport { + smart-card // WebKit only as of 2023-05 + } + dictionary AuthenticationExtensionsClientInputs { + // https://searchfox.org/mozilla-central/source/dom/webidl/WebAuthentication.webidl + // https://searchfox.org/wubkat/source/Source/WebCore/Modules/webauthn/AuthenticationExtensionsClientInputs.idl + // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/credentialmanagement/authentication_extensions_client_inputs.idl + member appidExclude + member credBlob + member getCredBlob + member hmacGetSecret // No implementation as of 2025-05 + member payment + } + dictionary AuthenticationExtensionsClientInputsJSON { + member appidExclude + } + dictionary AuthenticationExtensionsClientOutputs { + // (same as *Inputs) + member appidExclude // No implementation as of 2025-05 + member hmacGetSecret // No implementation as of 2025-05 + member payment // Blink only as of 2025-06 + } +} diff --git a/inputfiles/removedTypes.jsonc b/inputfiles/removedTypes.jsonc index f0a655543..347abb03e 100644 --- a/inputfiles/removedTypes.jsonc +++ b/inputfiles/removedTypes.jsonc @@ -13,9 +13,6 @@ }, "enums": { "enum": { - "AuthenticatorTransport": { - "value": ["smart-card"] // WebKit only as of 2023-05 - }, "ConnectionType": { "value": ["wimax"] }, @@ -264,37 +261,6 @@ } } }, - "AuthenticationExtensionsClientInputs": { - "members": { - "member": { - // https://searchfox.org/mozilla-central/source/dom/webidl/WebAuthentication.webidl - // https://searchfox.org/wubkat/source/Source/WebCore/Modules/webauthn/AuthenticationExtensionsClientInputs.idl - // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/credentialmanagement/authentication_extensions_client_inputs.idl - "appidExclude": null, - "credBlob": null, - "getCredBlob": null, - "hmacGetSecret": null, // No implementation as of 2025-05 - "payment": null - } - } - }, - "AuthenticationExtensionsClientInputsJSON": { - "members": { - "member": { - "appidExclude": null - } - } - }, - "AuthenticationExtensionsClientOutputs": { - "members": { - "member": { - // (same as *Inputs) - "appidExclude": null, // No implementation as of 2025-05 - "hmacGetSecret": null, // No implementation as of 2025-05 - "payment": null // Blink only as of 2025-06 - } - } - }, "CanvasRenderingContext2DSettings": { "members": { "member": { diff --git a/src/build.ts b/src/build.ts index 489fcbc85..1b1be02ff 100644 --- a/src/build.ts +++ b/src/build.ts @@ -89,7 +89,7 @@ async function emitDom() { const overriddenItems = await readInputJSON("overridingTypes.jsonc"); const addedItems = await readInputJSON("addedTypes.jsonc"); - const patches = await readPatches(); + const { patches, removalPatches } = await readPatches(); const comments = await readInputJSON("comments.json"); const documentationFromMDN = await generateDescriptions(); const removedItems = await readInputJSON("removedTypes.jsonc"); @@ -204,6 +204,7 @@ async function emitDom() { webidl = merge(webidl, getRemovalData(webidl)); webidl = merge(webidl, getDocsData(webidl)); webidl = prune(webidl, removedItems); + webidl = prune(webidl, removalPatches); webidl = merge(webidl, addedItems); webidl = merge(webidl, overriddenItems); webidl = merge(webidl, patches); diff --git a/src/build/patches.ts b/src/build/patches.ts index 7c949ebce..3be5f962e 100644 --- a/src/build/patches.ts +++ b/src/build/patches.ts @@ -1,4 +1,4 @@ -import { parse, type Value, type Node } from "kdljs"; +import { parse, type Value, type Node, Document } from "kdljs"; import type { Enum, Event, @@ -76,33 +76,34 @@ function handleTypeParameters(value: Value) { }; } +function undefinedIfEmpty(object: object, output: object) { + return Object.entries(object).length ? output : undefined; +} + /** - * Converts patch files in KDL to match the [types](types.d.ts). + * Converts parsed KDL Document nodes to match the [types](types.d.ts). */ -function parseKDL(kdlText: string): DeepPartial { - const { output, errors } = parse(kdlText); - - if (errors.length) { - throw new Error("KDL parse errors", { cause: errors }); - } - - const nodes = output!; +function convertKDLNodes(nodes: Node[]): DeepPartial { const enums: Record = {}; const mixin: Record> = {}; const interfaces: Record> = {}; const dictionary: Record> = {}; for (const node of nodes) { + // Note: no "removals" handling here; caller is responsible for splitting const name = string(node.values[0]); switch (node.name) { case "enum": enums[name] = handleEnum(node); break; case "interface-mixin": - mixin[name] = handleMixinandInterfaces(node, "mixin"); + mixin[name] = merge( + mixin[name], + handleMixinAndInterfaces(node, "mixin"), + ); break; case "interface": - interfaces[name] = handleMixinandInterfaces(node, "interface"); + interfaces[name] = handleMixinAndInterfaces(node, "interface"); break; case "dictionary": dictionary[name] = handleDictionary(node); @@ -113,10 +114,10 @@ function parseKDL(kdlText: string): DeepPartial { } return { - enums: { enum: enums }, - mixins: { mixin }, - interfaces: { interface: interfaces }, - dictionaries: { dictionary }, + enums: undefinedIfEmpty(enums, { enum: enums }), + mixins: undefinedIfEmpty(mixin, { mixin }), + interfaces: undefinedIfEmpty(interfaces, { interface: interfaces }), + dictionaries: undefinedIfEmpty(dictionary, { dictionary }), }; } @@ -152,7 +153,7 @@ function handleEnum(node: Node): Enum { * @param node The mixin node to handle. * @param mixins The record of mixins to update. */ -function handleMixinandInterfaces( +function handleMixinAndInterfaces( node: Node, type: "mixin" | "interface", ): DeepPartial { @@ -368,21 +369,76 @@ async function getAllFileURLs(folder: URL): Promise { } /** - * Read and parse a single KDL file. + * Read and parse a single KDL file into its KDL Document structure. */ -export async function readPatch(fileUrl: URL): Promise { +async function readPatchDocument(fileUrl: URL): Promise { const text = await readFile(fileUrl, "utf8"); - return parseKDL(text); + const { output, errors } = parse(text); + if (errors.length) { + throw new Error(`KDL parse errors in ${fileUrl.toString()}`, { + cause: errors, + }); + } + return output!; +} +/** + * Recursively remove all 'name' fields from the object and its children, and + * replace any empty objects ({} or []) with null. + */ +function convertForRemovals(obj: unknown): unknown { + if (Array.isArray(obj)) { + const result = obj.map(convertForRemovals).filter((v) => v !== undefined); + return result.length === 0 ? null : result; + } + if (obj && typeof obj === "object") { + const newObj: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (key !== "name") { + const cleaned = convertForRemovals(value); + if (cleaned !== undefined) { + newObj[key] = cleaned; + } + } + } + // Replace empty objects with null + return Object.keys(newObj).length === 0 ? null : newObj; + } + return obj; } /** * Read, parse, and merge all KDL files under the input folder. + * Splits the main patch content and the removals from each file for combined processing. + * + * Returns: + * { + * patches: merged patch contents (excluding removals), + * removalPatches: merged removals, with names stripped + * } */ -export default async function readPatches(): Promise { +export default async function readPatches(): Promise<{ + patches: any; + removalPatches: any; +}> { const patchDirectory = new URL("../../inputfiles/patches/", import.meta.url); const fileUrls = await getAllFileURLs(patchDirectory); - const parsedContents = await Promise.all(fileUrls.map(readPatch)); + // Stage 1: Parse all file KDLs into Documents + const documents = await Promise.all(fileUrls.map(readPatchDocument)); + + // Stage 2: Group by patches or removals + const merged = documents.flat(); + const patchNodes = merged.filter((node) => node.name !== "removals"); + const removalNodes = merged + .filter((node) => node.name === "removals") + .map((node) => node.children) + .flat(); + + // Stage 3: Convert the nodes for patches and removals respectively + const patches = convertKDLNodes(patchNodes); + const removalPatches = convertForRemovals( + convertKDLNodes(removalNodes), + ) as DeepPartial; - return parsedContents.reduce((acc, current) => merge(acc, current), {}); + return { patches, removalPatches }; }