@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
584 lines • 23.3 kB
JavaScript
// 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