UNPKG

@ts-for-gir/lib

Version:

Typescript .d.ts generator from GIR for gjs

584 lines 23.3 kB
// TODO move this class into a web-worker? https://www.npmjs.com/package/web-worker import { transformGirDocTagText } from './utils/index.js'; import { Logger } from './logger.js'; import { DependencyManager } from './dependency-manager.js'; import { find, isIntrospectable } from './utils/index.js'; import { ClosureType, TypeIdentifier, PromiseType, VoidType, BooleanType, TupleType, BinaryType, NullableType, ObjectType, } from './gir.js'; import { IntrospectedAlias } from './gir/alias.js'; import { IntrospectedBase } from './gir/base.js'; import { IntrospectedBaseClass, IntrospectedClass, IntrospectedRecord, IntrospectedInterface } from './gir/class.js'; import { IntrospectedConstant } from './gir/const.js'; import { IntrospectedEnum, IntrospectedError } from './gir/enum.js'; import { IntrospectedFunction, IntrospectedCallback, } from './gir/function.js'; import { isPrimitiveType } from './gir/util.js'; export class GirModule { dependency; /** * E.g. 'Gtk' */ get namespace() { return this.dependency.namespace; } /** * E.g. '4.0' */ get version() { return this.dependency.version; } /** * E.g. 'Gtk-4.0' */ get packageName() { return this.dependency.packageName; } /** * E.g. 'Gtk40' * Is used in the generated index.d.ts, for example: `import * as Gtk40 from "./Gtk-4.0.js";` */ get importNamespace() { return this.dependency.importNamespace; } /** * The NPM package name E.g. 'gtk-4.0' */ get importName() { return this.dependency.importName; } /** * Import path for the package E.g. './Gtk-4.0.js' or '@girs/Gtk-4.0' */ get importPath() { return this.dependency.importPath; } prefixes = []; /** * The version of the library as an object. * E.g. `{ major: 4, minor: 0, patch: 0 }` or as string `4.0.0`' */ get libraryVersion() { // GObject and Gio are following the version of GLib if (this.namespace === 'GObject' || this.namespace === 'Gio') { const dep = this.allDependencies.find((girModule) => girModule.namespace === 'GLib'); if (dep) { return dep.libraryVersion; } } return this.dependency.libraryVersion; } _dependencies = null; _transitiveDependencies = null; get dependencies() { if (!this._dependencies) { throw new Error('dependencies is not initialized, run initDependencies() first'); } return this._dependencies; } get transitiveDependencies() { if (!this._transitiveDependencies) { throw new Error('transitiveDependencies is not initialized, run initTransitiveDependencies() first'); } return this._transitiveDependencies; } get allDependencies() { if (!this.dependencies) { throw new Error('dependencies is not initialized, run init() first'); } return [...new Set([...this.dependencies, ...this.transitiveDependencies])]; } dependencyManager; log; extends; /** * To prevent constants from being exported twice, the names already exported are saved here for comparison. * Please note: Such a case is only known for Zeitgeist-2.0 with the constant "ATTACHMENT" */ constNames = {}; c_prefixes; _members; _enum_constants; _resolve_names = new Map(); __dts__references; package_version; parent; config; constructor(dependency, prefixes, config) { this.dependency = dependency; this.c_prefixes = [...prefixes]; this.package_version = ['0', '0']; this.config = config; // TODO: Make this a singleton this.dependencyManager = DependencyManager.getInstance(this.config); } async initDependencies() { this._dependencies = await this.dependencyManager.fromGirIncludes(this.dependency.girXML?.repository[0]?.include || []); } async initTransitiveDependencies(transitiveDependencies) { this._transitiveDependencies = await this.checkTransitiveDependencies(transitiveDependencies); } get ns() { return this; } async checkTransitiveDependencies(transitiveDependencies) { // Always pull in GObject-2.0, as we may need it for e.g. GObject-2.0.type if (this.packageName !== 'GObject-2.0') { if (!find(transitiveDependencies, (x) => x.packageName === 'GObject-2.0')) { transitiveDependencies.push(await this.dependencyManager.get('GObject', '2.0')); } } // Add missing dependencies if (this.packageName === 'UnityExtras-7.0') { if (!find(transitiveDependencies, (x) => x.packageName === 'Unity-7.0')) { transitiveDependencies.push(await this.dependencyManager.get('Unity', '7.0')); } } if (this.packageName === 'UnityExtras-6.0') { if (!find(transitiveDependencies, (x) => x.packageName === 'Unity-6.0')) { transitiveDependencies.push(await this.dependencyManager.get('Unity', '6.0')); } } if (this.packageName === 'GTop-2.0') { if (!find(transitiveDependencies, (x) => x.packageName === 'GLib-2.0')) { transitiveDependencies.push(await this.dependencyManager.get('GLib', '2.0')); } } // Gio if (this.packageName === 'GioUnix-2.0') { if (!find(transitiveDependencies, (x) => x.packageName === 'Gio-2.0')) { transitiveDependencies.push(await this.dependencyManager.get('Gio', '2.0')); } if (!find(transitiveDependencies, (x) => x.packageName === 'GLib-2.0')) { transitiveDependencies.push(await this.dependencyManager.get('GLib', '2.0')); } } // Filter out the dependency with the same namespace among each other transitiveDependencies = transitiveDependencies.filter((dep, index, self) => { const samePackage = self.findIndex((t) => t.namespace === dep.namespace); this.log.debug(`Filtering out dependency with same namespace: ${dep.namespace} ${index} ${samePackage}`); return index === samePackage; }); return transitiveDependencies; } getTsDocReturnTags(girElement) { const girReturnValue = girElement?.returnTypeDoc; if (!girReturnValue) { return []; } const returnTag = { tagName: 'returns', paramName: '', text: transformGirDocTagText(girReturnValue), }; return [returnTag]; } getTsDocInParamTags(inParams) { const tags = []; if (!inParams?.length) { return tags; } for (const inParam of inParams) { if (inParam.name) { tags.push({ paramName: inParam.name, tagName: 'param', text: typeof inParam.doc === 'string' ? transformGirDocTagText(inParam.doc) : '', }); } } return tags; } registerResolveName(resolveName, namespace, name) { this._resolve_names.set(resolveName, new TypeIdentifier(name, namespace)); } get members() { if (!this._members) { this._members = new Map(); } return this._members; } get enum_constants() { if (!this._enum_constants) { this._enum_constants = new Map(); } return this._enum_constants; } accept(visitor) { for (const key of [...this.members.keys()]) { const member = this.members.get(key); if (!member) continue; if (Array.isArray(member)) { this.members.set(key, member.map((m) => { return m.accept(visitor); })); } else { this.members.set(key, member.accept(visitor)); } } return this; } getImportsForCPrefix(c_prefix) { return this.parent.namespacesForPrefix(c_prefix); } // TODO: Move this into the generator hasImport(name) { return this.dependencies.some((dep) => dep.importName === name); } _getImport(namespace) { if (namespace === this.namespace) { return this; } const dep = this.dependencies?.find((dep) => dep.namespace === namespace) ?? this.transitiveDependencies.find((dep) => dep.namespace === namespace); // Handle finding imports via their other prefixes if (!dep) { this.log.info(`Failed to find namespace ${namespace} in dependencies, resolving via c:prefixes`); // TODO: It might make more sense to move this conversion _before_ // the _getImport call. const resolvedNamespaces = this.dependencyManager.namespacesForPrefix(namespace); if (resolvedNamespaces.length > 0) { this.log.info(`Found namespaces for prefix ${namespace}: ${resolvedNamespaces.map((r) => `${r.namespace} (${r.version})`).join(', ')}`); } for (const resolvedNamespace of resolvedNamespaces) { if (resolvedNamespace.namespace === this.namespace && resolvedNamespace.version === this.version) { return this; } const dep = this.dependencies?.find((dep) => dep.namespace === resolvedNamespace.namespace && dep.version === resolvedNamespace.version) ?? this.transitiveDependencies.find((dep) => dep.namespace === resolvedNamespace.namespace && dep.version === resolvedNamespace.version); if (dep) { return this.parent.namespace(resolvedNamespace.namespace, dep.version); } } } let version = dep?.version; if (!version) { version = this.parent.assertDefaultVersionOf(namespace); } return this.parent.namespace(namespace, version); } getInstalledImport(_namespace) { if (_namespace === this.namespace) { return this; } const dep = this.dependencies?.find((dep) => dep.namespace === _namespace) ?? this.transitiveDependencies.find((dep) => dep.namespace === _namespace); let version = dep?.version; if (!version) { version = this.parent.defaultVersionOf(_namespace) ?? undefined; } if (!version) { return null; } const namespace = this.parent.namespace(_namespace, version); return namespace; } assertInstalledImport(_namespace) { const namespace = this._getImport(_namespace); if (!namespace) { throw new Error(`Failed to import ${_namespace} in ${this.namespace}, not installed or accessible.`); } return namespace; } getMembers(name) { const members = this.members.get(name); if (Array.isArray(members)) { return [...members]; } return members ? [members] : []; } getMemberWithoutOverrides(name) { if (this.members.has(name)) { const member = this.members.get(name); if (!Array.isArray(member)) { return member; } return null; } const resolvedName = this._resolve_names.get(name); if (resolvedName) { const member = this.members.get(resolvedName.name); if (!Array.isArray(member)) { return member; } } return null; } assertClass(name) { const clazz = this.getClass(name); if (!clazz) { throw new Error(`[${this.packageName}] Class ${name} does not exist in namespace ${this.namespace}.`); } return clazz; } getClass(name) { const member = this.getMemberWithoutOverrides(name); if (member instanceof IntrospectedBaseClass) { return member; } return null; } getEnum(name) { const member = this.getMemberWithoutOverrides(name); if (member instanceof IntrospectedEnum) { return member; } return null; } getAlias(name) { const member = this.getMemberWithoutOverrides(name); if (member instanceof IntrospectedAlias) { return member; } return null; } hasSymbol(name) { return this.members.has(name); } resolveSymbolFromTypeName(name) { const resolvedName = this._resolve_names.get(name); if (!resolvedName) { return null; } const member = this.members.get(resolvedName.name); if (member instanceof IntrospectedBase) { return member.name; } return null; } findClassCallback(name) { const clazzes = Array.from(this.members.values()).filter((m) => m instanceof IntrospectedBaseClass); const res = clazzes .map((m) => [m, m.callbacks.find((c) => c.name === name || c.resolve_names.includes(name))]) .find((r) => r[1] != null); if (res) { return [res[0].name, res[1].name]; } else { return [null, name]; } } /** * This is an internal method to add TypeScript <reference> * comments when overrides use parts of the TypeScript standard * libraries that are newer than default. */ ___dts___addReference(reference) { this.__dts__references ??= []; this.__dts__references.push(reference); } static async load(dependency, config, registry) { const girXML = dependency.girXML; const ns = girXML?.repository[0]?.namespace?.[0]; if (!girXML) { throw new Error(`Failed to load gir xml of ${dependency.packageName}`); } if (!ns) { throw new Error(`Missing namespace in ${girXML.repository[0].package[0].$.name}`); } const modName = ns.$['name']; const version = ns.$['version']; if (!modName) { throw new Error('Invalid GIR file: no namespace name specified.'); } if (!version) { throw new Error('Invalid GIR file: no version name specified.'); } const c_prefix = ns.$?.['c:identifier-prefixes']?.split(',') ?? []; const building = new GirModule(dependency, c_prefix, config); await building.initDependencies(); building.parent = registry; // Set the namespace object here to prevent re-parsing the namespace if // another namespace imports it. registry.register(building); const prefixes = girXML.repository[0]?.$?.['c:identifier-prefixes']?.split(','); const unknownPrefixes = prefixes?.filter((pre) => pre !== modName); if (unknownPrefixes && unknownPrefixes.length > 0) { Logger.log(`Found additional prefixes for ${modName}: ${unknownPrefixes.join(', ')}`); building.prefixes.push(...unknownPrefixes); } building.log = new Logger(config.verbose, `GirModule(${building.packageName})`); return building; } /** Start to parse all the data from the XML we need for the typescript generation */ parse() { this.log.debug(`Parsing ${this.dependency.packageName}...`); const girXML = this.dependency.girXML; const ns = girXML?.repository[0]?.namespace?.[0]; const options = { loadDocs: !this.config.noComments, propertyCase: 'both', verbose: this.config.verbose, }; if (!girXML) { throw new Error(`Failed to load gir xml of ${this.dependency.packageName}`); } if (!ns) { throw new Error(`Missing namespace in ${girXML.repository[0].package[0].$.name}`); } const importConflicts = (el) => { return !this.hasImport(el.name); }; if (ns.enumeration) { // Get the requested enums ns.enumeration ?.map((enumeration) => { if (enumeration.$['glib:error-domain']) { return IntrospectedError.fromXML(enumeration, this, options); } else { return IntrospectedEnum.fromXML(enumeration, this, options); } }) .forEach((c) => this.members.set(c.name, c)); } // Constants if (ns.constant) { ns.constant ?.filter(isIntrospectable) .map((constant) => IntrospectedConstant.fromXML(constant, this, options)) .filter(importConflicts) .forEach((c) => this.members.set(c.name, c)); } // Get the requested functions if (ns.function) { ns.function ?.filter(isIntrospectable) .map((func) => IntrospectedFunction.fromXML(func, this, options)) .filter(importConflicts) .forEach((c) => this.members.set(c.name, c)); } if (ns.callback) { ns.callback ?.filter(isIntrospectable) .map((callback) => IntrospectedCallback.fromXML(callback, this, options)) .filter(importConflicts) .forEach((c) => this.members.set(c.name, c)); } if (ns['glib:boxed']) { ns['glib:boxed'] ?.filter(isIntrospectable) .map((boxed) => new IntrospectedAlias({ name: boxed.$['glib:name'], namespace: this, type: new NullableType(ObjectType), })) .forEach((c) => this.members.set(c.name, c)); } // Bitfield is a type of enum if (ns.bitfield) { ns.bitfield ?.filter(isIntrospectable) .map((field) => IntrospectedEnum.fromXML(field, this, options, true)) .forEach((c) => this.members.set(c.name, c)); } // The `enum_constants` map maps the C identifiers (GTK_BUTTON_TYPE_Y) // to the name of the enum (Button) to resolve references (Gtk.Button.Y) Array.from(this.members.values()) .filter((m) => m instanceof IntrospectedEnum) .forEach((m) => { m.members.forEach((member) => { this.enum_constants.set(member.c_identifier, [m.name, member.name]); }); }); // Get the requested classes if (ns.class) { ns.class ?.filter(isIntrospectable) .map((klass) => IntrospectedClass.fromXML(klass, this, options)) .filter(importConflicts) .forEach((c) => this.members.set(c.name, c)); } if (ns.record) { ns.record ?.filter(isIntrospectable) .map((record) => IntrospectedRecord.fromXML(record, this, options)) .filter(importConflicts) .forEach((c) => this.members.set(c.name, c)); } if (ns.union) { ns.union ?.filter(isIntrospectable) .map((union) => IntrospectedRecord.fromXML(union, this, options)) .filter(importConflicts) .forEach((c) => this.members.set(c.name, c)); } if (ns.interface) { ns.interface ?.map((inter) => IntrospectedInterface.fromXML(inter, this, options)) .filter(importConflicts) .forEach((c) => this.members.set(c.name, c)); } if (ns.alias) { ns.alias ?.filter(isIntrospectable) // Avoid attempting to alias non-introspectable symbols. .map((b) => { b.type = b.type ?.filter((t) => !!(t && t.$.name)) .map((t) => { if (t.$.name && !this.hasSymbol(t.$.name) && !isPrimitiveType(t.$.name) && !t.$.name.includes('.')) { return { $: { name: 'unknown', 'c:type': 'unknown' } }; } return t; }); return b; }) .map((alias) => IntrospectedAlias.fromXML(alias, this, options)) .filter((alias) => alias != null) .forEach((c) => this.members.set(c.name, c)); } } } export function promisifyNamespaceFunctions(namespace) { return namespace.members.forEach((node) => { if (!(node instanceof IntrospectedFunction)) return; if (node.parameters.length < 1) return; const last_param = node.parameters[node.parameters.length - 1]; if (!last_param) return; const last_param_unwrapped = last_param.type.unwrap(); if (!(last_param_unwrapped instanceof ClosureType)) return; const internal = last_param_unwrapped.type; if (internal instanceof TypeIdentifier && internal.is('Gio', 'AsyncReadyCallback')) { const async_res = [ ...Array.from(namespace.members.values()).filter((m) => m instanceof IntrospectedFunction), ].find((m) => m.name === `${node.name.replace(/_async$/, '')}_finish` || m.name === `${node.name}_finish`); if (async_res) { const async_parameters = node.parameters.slice(0, -1).map((p) => p.copy()); const sync_parameters = node.parameters.map((p) => p.copy({ isOptional: false })); const output_parameters = async_res.output_parameters; let async_return = new PromiseType(async_res.return()); if (output_parameters.length > 0) { const raw_return = async_res.return(); if (raw_return.equals(VoidType) || raw_return.equals(BooleanType)) { const [output_type, ...output_types] = output_parameters.map((op) => op.type); async_return = new PromiseType(new TupleType(output_type, ...output_types)); } else { const [...output_types] = output_parameters.map((op) => op.type); async_return = new PromiseType(new TupleType(raw_return, ...output_types)); } } namespace.members.set(node.name, [ node.copy({ parameters: async_parameters, return_type: async_return, }), node.copy({ parameters: sync_parameters, }), node.copy({ return_type: new BinaryType(async_return, node.return()), }), ]); } } }); } //# sourceMappingURL=gir-module.js.map