/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /** * Build: /dist/@types/lib.gecko.dom.d.ts, * * from: /dom/webidl/*.webidl and * /dom/chrome-webidl/*.webidl sources, * using @typescript/dom-lib-generator. */ const fs = require("fs"); const { RESERVED_WORDS } = require("peggy"); const TAGLIST = require.resolve("../../parser/htmlparser/nsHTMLTagList.inc"); const BINDINGS = require.resolve("../../dom/bindings/Bindings.conf"); // TODO Bug 2022802: Ideally we should get details about the generated files from // the build system. // This list should match the list of `GeneratedFile` in dom/bindings/moz.build. const GENERATED_WEDIDL_FILES = [ "CSSPageDescriptors.webidl", "CSSPositionTryDescriptors.webidl", "CSSStyleProperties.webidl", "CSSFontFaceDescriptors.webidl", "CSSCounterStyleRule.webidl", ]; // Support overrides using the syntax from @typescript/dom-lib-generator which // is parsing our webidl files and generating types. // https://github.com/microsoft/TypeScript-DOM-lib-generator/blob/main/inputfiles/overridingTypes.jsonc const OVERRIDE_TYPES = { callbackFunctions: { callbackFunction: { CustomElementConstructor: { overrideSignatures: ["new (...params: any[]): HTMLElement"], }, }, }, }; const HEADER = `/** * NOTE: Do not modify this file by hand. * Content was generated from source .webidl files. * If you're updating some of the sources, see README for instructions. */ /// type HTMLCollectionOf = any; type IsInstance = (obj: any) => obj is T; type NodeListOf = any; interface WindowProxy extends Window {} `; // Convert Mozilla-flavor webidl into idl parsable by @w3c/webidl2.js. function preprocess(webidl) { return webidl .replaceAll(/^#.+/gm, "") .replaceAll(/\binterface \w+;/gm, "") .replaceAll(/\bUTF8String\b/gm, "DOMString") .replaceAll(/^\s*legacycaller /gm, "getter ") .replaceAll(/^callback constructor /gm, "callback ") .replaceAll(/(ElementCreationOptions) or (DOMString)/gm, "$2 or $1") .replaceAll(/(attribute boolean aecDebug;)/gm, "readonly $1"); } function customize(all, baseTypes) { // Parse HTML element interfaces from nsHTMLTagList.inc. const RE = /^HTML_(HTMLELEMENT_)?TAG\((\w+)(, \w+, (\w*))?\)/gm; for (let match of fs.readFileSync(TAGLIST, "utf8").matchAll(RE)) { let iface = all.interfaces.interface[`HTML${match[4] ?? ""}Element`]; if (iface) { iface.element ??= []; iface.element.push({ name: match[2] ?? match[3] }); } } // Add static isInstance methods to interfaces with constructors. for (let i of Object.values(all.interfaces.interface)) { if (i.name && !i.noInterfaceObject) { i.properties.property.isInstance = { name: "isInstance", type: "IsInstance", subtype: { type: i.name }, static: true, }; } } // Parse DOM<->XPCOM type aliases from Bindings.conf. let conf = fs.readFileSync(BINDINGS, "utf8"); let aliases = []; for (let [desc, id] of conf.matchAll(/^\s*'(\w+)'\s*:\s*\{[^}]*/gm)) { let type = desc.match(/'nativeType'\s*:\s*'(nsI\w+)'/); aliases.push([id, type ? type[1] : id]); } for (let [desc, id] of conf.matchAll(/addExternalIface\('(\w+)[^)]*/gm)) { let type = desc.match(/nativeType\s*=\s*'(\w+)'/); aliases.push([id, type ? type[1] : `nsIDOM${id}`]); } for (let [name, type] of aliases) { if (name != type && !all.interfaces.interface[name]) { all.typedefs.typedef.push({ name, type }); baseTypes.set(type, type); baseTypes.delete(name); } } // Some namespaces have methods that use reserved words. Prefix them with // `_` and keep a list, so that we can export them properly later. let additionalExports = new Map(); for (let namespace of all.namespaces) { if (!namespace.methods) { continue; } for (let reserved of RESERVED_WORDS) { if (reserved in namespace.methods.method) { namespace.methods.method["_" + reserved] = namespace.methods.method[reserved]; namespace.methods.method["_" + reserved].name = "_" + reserved; delete namespace.methods.method[reserved]; if (additionalExports.has(namespace.name)) { additionalExports.get(namespace.name).push(reserved); } else { additionalExports.set(namespace.name, [reserved]); } } } } return additionalExports; } // Preprocess, convert, merge and customize webidl, emit and postprocess dts. async function emitDom(webidls, builtin = "builtin.webidl") { const { merge, baseTypeConversionMap } = await import("@typescript/dom-lib-generator/lib/build/helpers.js"); const { emitWebIdl } = await import("@typescript/dom-lib-generator/lib/build/emitter.js"); const { convert } = await import("@typescript/dom-lib-generator/lib/build/widlprocess.js"); const { getExposedTypes } = await import("@typescript/dom-lib-generator/lib/build/expose.js"); function mergePartial(partials, bases) { for (let p of partials) { let base = Object.values(bases).find(b => b.name === p.name); merge(base.members, p.members, true); merge(base.constants, p.constants, true); merge(base.properties, p.properties, true); merge(base.methods, p.methods, true); } } webidls.push(require.resolve(`./config/${builtin}`)); let idls = webidls.map(f => fs.readFileSync(f, "utf-8")); let all = {}; // Add external forward type declarations to base types. for (let [, id] of idls.join().matchAll(/\binterface (\w+);/gm)) { baseTypeConversionMap.set(id, id); } for (let w of idls.map(idl => convert(preprocess(idl), {}))) { merge(all, w.browser, true); mergePartial(w.partialDictionaries, all.dictionaries.dictionary); mergePartial(w.partialInterfaces, all.interfaces.interface); mergePartial(w.partialMixins, all.mixins.mixin); mergePartial(w.partialNamespaces, all.namespaces); for (let inc of w.includes) { let target = all.interfaces.interface[inc.target]; target.implements ??= []; target.implements.push(inc.includes); } } merge(all, OVERRIDE_TYPES); let additionalExports = customize(all, baseTypeConversionMap); let exposed = getExposedTypes(all, ["Window"], new Set()); let dts = await Promise.all([ emitWebIdl(exposed, "Window", "", {}), emitWebIdl(exposed, "Window", "sync", {}), emitWebIdl(exposed, "Window", "async", {}), ]); return postprocess(additionalExports, dts.join("\n")); } exports.emitDom = emitDom; // Post-process dom.generated.d.ts into lib.gecko.dom.d.ts. function postprocess(additionalExports, generated) { let text = `${HEADER}\n${generated}`; // Re-export any reserved word functions. for (let [namespace, functions] of additionalExports) { text += `\ndeclare namespace ${namespace} {\n export {`; for (let functionName of functions) { text += `\n _${functionName} as ${functionName},\n`; } text += " };\n}\n"; } return text .replaceAll(/declare var isInstance: /g, "// @ts-ignore\n$&") .replace(/interface BeforeUnloadEvent /, "// @ts-ignore\n$&") .replace(/interface HTMLScriptElement /, "// @ts-ignore\n$&") .replace(/interface SVGElement /, "// @ts-ignore\n$&") .replace("interface IsInstance {\n}\n", "") .replace("type JSON = any;\n", ""); } // Build and save the dom lib. async function main(lib_dts, webidl_dir, objdir_webidl, ...webidl_files) { let files = [ ...GENERATED_WEDIDL_FILES.map(f => `${objdir_webidl}/${f}`), ...webidl_files.map(f => `${webidl_dir}/${f}`), ]; let dts = await emitDom(files); console.log(`[INFO] ${lib_dts} (${dts.length.toLocaleString()} bytes)`); fs.writeFileSync(lib_dts, dts); } if (require.main === module) { main(...process.argv.slice(2)); }