From a7a96f3b004929fd8e31233bf655b9e89d51addb Mon Sep 17 00:00:00 2001 From: Bashamega Date: Tue, 2 Dec 2025 13:51:42 +0200 Subject: [PATCH 1/2] Migrate to using classes --- src/build/patches.ts | 737 +++++++++++++++++++++++-------------------- 1 file changed, 402 insertions(+), 335 deletions(-) diff --git a/src/build/patches.ts b/src/build/patches.ts index f8a07b343..532e0baf4 100644 --- a/src/build/patches.ts +++ b/src/build/patches.ts @@ -22,353 +22,446 @@ type DeepPartial = T extends object interface OverridableMethod extends Omit { signature: DeepPartial[] | Record>; } - -function optionalMember(prop: string, type: T, value?: Value) { - if (value === undefined) { - return {}; - } - if (typeof value !== type) { - throw new Error(`Expected type ${value} for ${prop}`); - } - return { - [prop]: value as T extends "string" - ? string - : T extends "number" - ? number - : T extends "boolean" - ? boolean - : never, +interface OverridableDictionary extends Omit, "members"> { + members: { + member: Record | null>; }; } -function string(arg: unknown): string { - if (typeof arg !== "string") { - throw new Error(`Expected a string but found ${typeof arg}`); - } - return arg; -} +/** + * Class that converts parsed KDL Document nodes to match the [types](types.d.ts). -function handleTyped(type: Node): Typed { - const isTyped = type.name == "type"; - if (!isTyped) { - throw new Error("Expected a type node"); + */ +class KDLConverter { + node: Node; + childNode: Node; + type: "mixin" | "interface" | undefined; + isRemoval: boolean; + result: DeepPartial = {}; + + constructor(nodes: Node[], isRemoval = false) { + const node = { + name: "", + values: [], + properties: {}, + children: [], + tags: { name: undefined, values: [], properties: {} }, + }; + this.node = node; + this.childNode = node; + this.isRemoval = isRemoval; + this.convertKDLNodes(nodes); } - const name = string(type.values[0]); - const subType = - type.children.length > 0 ? handleTyped(type.children[0]) : undefined; - return { - type: name, - subtype: subType, - ...optionalMember("nullable", "boolean", type.properties?.nullable), - }; -} -function handleTypeParameters(value: Value | Node) { - if (!value) { - return {}; - } - if (typeof value === "string") { - return { typeParameters: [{ name: value }] }; + private optionalMember( + prop: string, + type: T, + value?: Value, + ) { + if (value === undefined) { + return {}; + } + if (typeof value !== type) { + throw new Error( + `Expected type ${type} for ${prop}, but got ${typeof value}`, + ); + } + return { + [prop]: value, + }; } - const node = value as Node; - return { - typeParameters: [ - { - name: string(node.values[0]), - ...optionalMember("default", "string", node.properties?.default), - }, - ], - }; -} -function undefinedIfEmpty(object: object, output: object) { - return Object.entries(object).length ? output : undefined; -} - -/** - * Converts parsed KDL Document nodes to match the [types](types.d.ts). - */ -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] = merge( - mixin[name], - handleMixinAndInterfaces(node, "mixin"), - ); - break; - case "interface": - interfaces[name] = handleMixinAndInterfaces(node, "interface"); - break; - case "dictionary": - dictionary[name] = handleDictionary(node); - break; - default: - throw new Error(`Unknown node name: ${node.name}`); + private string(arg: unknown): string { + if (typeof arg !== "string") { + throw new Error(`Expected a string but found ${typeof arg}`); } + return arg; } - return { - enums: undefinedIfEmpty(enums, { enum: enums }), - mixins: undefinedIfEmpty(mixin, { mixin }), - interfaces: undefinedIfEmpty(interfaces, { interface: interfaces }), - dictionaries: undefinedIfEmpty(dictionary, { dictionary }), - }; -} + private handleTyped(type: Node): Typed { + const isTyped = type.name == "type"; + if (!isTyped) { + throw new Error("Expected a type node"); + } + const name = this.string(type.values[0]); + const subType = + type.children.length > 0 ? this.handleTyped(type.children[0]) : undefined; + return { + type: name, + subtype: subType, + ...this.optionalMember("nullable", "boolean", type.properties.nullable), + }; + } -/** - * Handles an enum node by extracting its name and values. - * Throws an error if the enum name is missing or if the values are not in the correct format. - * @param node The enum node to handle. - * @param enums The record of enums to update. - */ -function handleEnum(node: Node): Enum { - const name = string(node.properties?.name || node.values[0]); - const values: string[] = []; + private handleTypeParameters(value: Value | Node) { + if (!value || typeof value === "boolean" || typeof value === "number") { + return {}; + } + if (typeof value === "string") { + return { typeParameters: [{ name: value }] }; + } + return { + typeParameters: [ + { + name: this.string(value.values[0]), + ...this.optionalMember( + "default", + "string", + this.childNode.properties.default, + ), + }, + ], + }; + } - for (const child of node.children) { - values.push(child.name); + private undefinedIfEmpty(object: object, output: object) { + return Object.entries(object).length ? output : undefined; } - return { - name, - value: values, - ...optionalMember( - "legacyNamespace", - "string", - node.properties.legacyNamespace, - ), - }; -} + // This method returns the original object if this KDLConverter is not in "removal" mode. + // If it is in removal mode, it returns undefined (which is ignored on merge), rather than an empty object. + private additionOnly(obj: T) { + if (!this.isRemoval) { + return obj; + } + return undefined; + } -/** - * Handles a mixin node by extracting its name and associated members. - * Throws an error if the mixin name is missing. - * Adds them to the mixins record under the mixin's name. - * @param node The mixin node to handle. - * @param mixins The record of mixins to update. - */ -function handleMixinAndInterfaces( - node: Node, - type: "mixin" | "interface", -): DeepPartial { - const name = string(node.properties?.name || node.values[0]); - - const event: Event[] = []; - const property: Record> = {}; - let method: Record> = {}; - - for (const child of node.children) { - switch (child.name) { - case "event": - event.push(handleEvent(child)); - break; - case "property": { - const propName = string(child.values[0]); - property[propName] = handleProperty(child); - break; - } - case "method": { - const methodName = string(child.values[0]); - const m = handleMethod(child); - method = merge(method, { - [methodName]: m, - }); - break; + private convertKDLNodes(nodes: Node[]) { + const enums: Record> = {}; + const mixin: Record> = {}; + const interfaces: Record> = {}; + const dictionary: Record = {}; + + for (const node of nodes) { + this.node = node; + // Note: no "removals" handling here; caller is responsible for splitting + const name = this.string(node.values[0]); + switch (node.name) { + case "enum": { + const enumObj = this.handleEnum(); + enums[name] = enumObj; + break; + } + case "interface-mixin": { + this.type = "mixin"; + const mixinObj = this.handleMixinAndInterfaces(); + mixin[name] = merge(mixin[name], mixinObj); + break; + } + case "interface": { + this.type = "interface"; + const ifaceObj = this.handleMixinAndInterfaces(); + interfaces[name] = ifaceObj; + break; + } + case "dictionary": { + const dictObj = this.handleDictionary(); + dictionary[name] = dictObj; + break; + } + default: + throw new Error(`Unknown node name: ${node.name}`); } - default: - throw new Error(`Unknown node name: ${child.name}`); } + + this.result = { + enums: this.undefinedIfEmpty(enums, { enum: enums }), + mixins: this.undefinedIfEmpty(mixin, { mixin }), + interfaces: this.undefinedIfEmpty(interfaces, { interface: interfaces }), + dictionaries: this.undefinedIfEmpty(dictionary, { dictionary }), + }; } - const interfaceObject = type === "interface" && { - ...optionalMember("exposed", "string", node.properties?.exposed), - ...optionalMember("deprecated", "string", node.properties?.deprecated), - ...optionalMember( - "noInterfaceObject", - "boolean", - node.properties?.noInterfaceObject, - ), - }; - return { - name, - events: { event }, - properties: { property }, - methods: { method }, - ...optionalMember("extends", "string", node.properties?.extends), - ...optionalMember("overrideThis", "string", node.properties?.overrideThis), - ...optionalMember("forward", "string", node.properties?.forward), - ...optionalMember( - "forwardExtends", - "string", - node.properties?.forwardExtends, - ), - ...optionalMember( - "replaceReference", - "string", - node.properties?.replaceReference, - ), - ...handleTypeParameters(node.properties?.typeParameters), - ...interfaceObject, - } as DeepPartial; -} + /** + * Handles an enum node (uses this.node). + */ + private handleEnum(): Partial { + const name = this.string(this.node.properties.name || this.node.values[0]); + const values: string[] = []; -/** - * Handles a child node of type "event" and adds it to the event array. - * @param child The child node to handle. - */ -function handleEvent(child: Node): Event { - return { - name: string(child.values[0]), - type: string(child.properties.type), - }; -} - -/** - * Handles a child node of type "property" and adds it to the property object. - * @param child The child node to handle. - */ -function handleProperty(child: Node): Partial { - let typeNode: Node | undefined; - for (const c of child.children) { - if (c.name === "type") { - typeNode = c; - break; + for (const child of this.node.children) { + values.push(child.name); } + + return { + value: values, + ...this.optionalMember( + "legacyNamespace", + "string", + this.node.properties.legacyNamespace, + ), + ...this.additionOnly({ name }), + }; } - return { - name: string(child.values[0]), - ...optionalMember("exposed", "string", child.properties?.exposed), - ...optionalMember("optional", "boolean", child.properties?.optional), - ...optionalMember("overrideType", "string", child.properties?.overrideType), - ...(typeNode - ? handleTyped(typeNode) - : optionalMember("type", "string", child.properties?.type)), - ...optionalMember("readonly", "boolean", child.properties?.readonly), - ...optionalMember("deprecated", "boolean", child.properties?.deprecated), - }; -} + /** + * Handles a mixin or interface node by extracting its name and associated members (uses this.node). + */ + private handleMixinAndInterfaces(): DeepPartial { + const name = this.string(this.node.properties.name || this.node.values[0]); + + const event: Event[] = []; + const property: Record> = {}; + let method: Record> = {}; + + for (const child of this.node.children) { + this.childNode = child; + switch (child.name) { + case "event": { + const eventObj = this.handleEvent(); + event.push(eventObj); + break; + } + case "property": { + const propName = this.string(child.values[0]); + const propertyObj = this.handleProperty(); + property[propName] = propertyObj; + break; + } + case "method": { + const methodName = this.string(child.values[0]); + const methodObj = this.handleMethod(); + method = merge(method, { + [methodName]: methodObj, + }); + break; + } + default: + throw new Error(`Unknown node name: ${child.name}`); + } + } -/** - * Handles a child node of type "method" and adds it to the method object. - * @param child The child node to handle. - */ -function handleMethod(child: Node): DeepPartial { - const name = string(child.values[0]); + const interfaceObject = this.type === "interface" && { + ...this.optionalMember("exposed", "string", this.node.properties.exposed), + ...this.optionalMember( + "deprecated", + "string", + this.node.properties.deprecated, + ), + ...this.optionalMember( + "noInterfaceObject", + "boolean", + this.node.properties.noInterfaceObject, + ), + }; + return { + events: { event }, + properties: { property }, + methods: { method }, + ...this.optionalMember("extends", "string", this.node.properties.extends), + ...this.optionalMember( + "overrideThis", + "string", + this.node.properties.overrideThis, + ), + ...this.optionalMember("forward", "string", this.node.properties.forward), + ...this.optionalMember( + "forwardExtends", + "string", + this.node.properties.forwardExtends, + ), + ...this.optionalMember( + "replaceReference", + "string", + this.node.properties.replaceReference, + ), + ...this.handleTypeParameters(this.node.properties.typeParameters), + ...interfaceObject, + ...this.additionOnly({ name }), + } as DeepPartial; + } - let typeNode: Node | undefined; - const params: Partial[] = []; + /** + * Handles a child node of type "event" and adds it to the event array (uses this.node). + */ + private handleEvent(): Event { + const child = this.childNode; + return { + name: this.string(child.values[0]), + type: this.string(child.properties.type), + }; + } - for (const c of child.children) { - switch (c.name) { - case "type": - if (typeNode) { - throw new Error(`Method "${name}" has multiple type nodes (invalid)`); - } + /** + * Handles a child node of type "property" and adds it to the property object (uses this.node). + */ + private handleProperty(): Partial { + let typeNode: Node | undefined; + for (const c of this.childNode.children) { + if (c.name === "type") { typeNode = c; break; + } + } - case "param": - params.push({ - name: string(c.values[0]), - ...optionalMember("type", "string", c.properties?.type), - ...optionalMember( - "overrideType", + return { + name: this.string(this.childNode.values[0]), + ...this.optionalMember( + "exposed", + "string", + this.childNode.properties.exposed, + ), + ...this.optionalMember( + "optional", + "boolean", + this.childNode.properties.optional, + ), + ...this.optionalMember( + "overrideType", + "string", + this.childNode.properties.overrideType, + ), + ...(typeNode + ? this.handleTyped(typeNode) + : this.optionalMember( + "type", "string", - c.properties?.overrideType, - ), - }); - break; - - default: - throw new Error(`Unexpected child "${c.name}" in method "${name}"`); - } + this.childNode.properties.type, + )), + ...this.optionalMember( + "readonly", + "boolean", + this.childNode.properties.readonly, + ), + ...this.optionalMember( + "deprecated", + "boolean", + this.childNode.properties.deprecated, + ), + }; } - // Determine the actual signature object - const signatureObj: DeepPartial = { - param: params, - ...(typeNode - ? handleTyped(typeNode) - : { - type: string(child.properties?.returns), - subtype: undefined, - }), - }; + /** + * Handles a child node of type "method" and adds it to the method object (uses this.node). + */ + private handleMethod(): DeepPartial { + const name = this.string(this.childNode.values[0]); + + let typeNode: Node | undefined; + const params: Partial[] = []; + + for (const c of this.childNode.children) { + switch (c.name) { + case "type": + if (typeNode) { + throw new Error( + `Method "${name}" has multiple type nodes (invalid)`, + ); + } + typeNode = c; + break; + + case "param": + params.push({ + name: this.string(c.values[0]), + ...this.optionalMember("type", "string", c.properties.type), + ...this.optionalMember( + "overrideType", + "string", + c.properties.overrideType, + ), + }); + break; + + default: + throw new Error(`Unexpected child "${c.name}" in method "${name}"`); + } + } - let signature: OverridableMethod["signature"]; - const signatureIndex = child.properties?.signatureIndex; - if (typeof signatureIndex == "number") { - signature = { [signatureIndex]: signatureObj }; - } else { - signature = [signatureObj]; + // Determine the actual signature object + const signatureObj: DeepPartial = { + param: params, + ...(typeNode + ? this.handleTyped(typeNode) + : { + type: this.string(this.childNode.properties.returns), + subtype: undefined, + }), + }; + + let signature: OverridableMethod["signature"]; + const signatureIndex = this.childNode.properties.signatureIndex; + if (typeof signatureIndex == "number") { + signature = { [signatureIndex]: signatureObj }; + } else { + signature = [signatureObj]; + } + return { signature, ...this.additionOnly({ name }) }; } - return { name, signature }; -} -/** - * Handles dictionary nodes - * @param child The dictionary node to handle. - */ -function handleDictionary(child: Node): DeepPartial { - const name = string(child.values[0]); - const member: Record> = {}; - let typeParameters = {}; - - for (const c of child.children) { - switch (c.name) { - case "member": { - const memberName = string(c.values[0]); - member[memberName] = handleMember(c); - break; - } - case "typeParameters": { - typeParameters = handleTypeParameters(c); - break; + /** + * Handles dictionary nodes (uses this.node). + */ + private handleDictionary(): OverridableDictionary { + const name = this.string(this.node.values[0]); + const member: Record | null> = {}; + let typeParameters = {}; + + for (const c of this.node.children) { + this.childNode = c; + switch (c.name) { + case "member": { + const memberName = this.string(c.values[0]); + const memberObj = this.handleMember(); + member[memberName] = memberObj; + break; + } + case "typeParameters": { + typeParameters = this.handleTypeParameters(c); + break; + } + default: + throw new Error(`Unknown node name: ${c.name}`); } - default: - throw new Error(`Unknown node name: ${c.name}`); } - } - return { - name, - members: { member }, - ...typeParameters, - ...handleTypeParameters(child.properties?.typeParameters), - ...optionalMember( - "legacyNamespace", - "string", - child.properties?.legacyNamespace, - ), - ...optionalMember("overrideType", "string", child.properties?.overrideType), - }; -} + return { + members: { member }, + ...typeParameters, + ...this.handleTypeParameters(this.node.properties.typeParameters), + ...this.optionalMember( + "legacyNamespace", + "string", + this.node.properties.legacyNamespace, + ), + ...this.optionalMember( + "overrideType", + "string", + this.node.properties.overrideType, + ), + ...this.additionOnly({ name }), + }; + } -/** - * Handles dictionary member nodes - * @param c The member node to handle. - */ -function handleMember(c: Node): Partial { - const name = string(c.values[0]); - return { - name, - ...optionalMember("type", "string", c.properties?.type), - ...optionalMember("required", "boolean", c.properties?.required), - ...optionalMember("deprecated", "boolean", c.properties?.deprecated), - ...optionalMember("overrideType", "string", c.properties?.overrideType), - }; + /** + * Handles dictionary member nodes (uses this.node). + */ + private handleMember(): Partial | null { + if (this.isRemoval) { + return null; + } + const name = this.string(this.childNode.values[0]); + return { + ...this.additionOnly({ name }), + ...this.optionalMember("type", "string", this.childNode.properties.type), + ...this.optionalMember( + "required", + "boolean", + this.childNode.properties.required, + ), + ...this.optionalMember( + "deprecated", + "boolean", + this.childNode.properties.deprecated, + ), + ...this.optionalMember( + "overrideType", + "string", + this.childNode.properties.overrideType, + ), + }; + } } /** @@ -392,30 +485,6 @@ async function readPatchDocument(fileUrl: URL): Promise { } 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. @@ -446,10 +515,8 @@ export default async function readPatches(): Promise<{ .flat(); // Stage 3: Convert the nodes for patches and removals respectively - const patches = convertKDLNodes(patchNodes); - const removalPatches = convertForRemovals( - convertKDLNodes(removalNodes), - ) as DeepPartial; + const patches = new KDLConverter(patchNodes).result; + const removalPatches = new KDLConverter(removalNodes, true).result; return { patches, removalPatches }; } From 4567c6a4b6ac52be3e514cb0fbdb824c9b288e90 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Tue, 2 Dec 2025 13:54:13 +0200 Subject: [PATCH 2/2] - --- src/build/patches.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/build/patches.ts b/src/build/patches.ts index 532e0baf4..85d63800f 100644 --- a/src/build/patches.ts +++ b/src/build/patches.ts @@ -22,6 +22,7 @@ type DeepPartial = T extends object interface OverridableMethod extends Omit { signature: DeepPartial[] | Record>; } + interface OverridableDictionary extends Omit, "members"> { members: { member: Record | null>;