@ts-for-gir/lib
Version:
Typescript .d.ts generator from GIR for gjs
188 lines (147 loc) • 6 kB
text/typescript
import { DefaultFormatter } from "../formatters/default.ts";
import type { Formatter } from "../formatters/formatter.ts";
import { JSONFormatter } from "../formatters/json.ts";
import type { FormatGenerator } from "../generators/generator.ts";
import { generify } from "../generics/generify.ts";
import type { GirModule } from "../index.ts";
import { inject } from "../injections/inject.ts";
import type { GeneratorConstructor, OptionsGeneration, OptionsTransform } from "../types/index.ts";
import { TwoKeyMap } from "../util.ts";
import { ClassVisitor } from "../validators/class.ts";
import { InterfaceVisitor } from "../validators/interface.ts";
import type { GirVisitor } from "../visitor.ts";
import type { IntrospectedNamespace } from "./namespace.ts";
export class NSRegistry {
mapping: TwoKeyMap<string, string, IntrospectedNamespace> = new TwoKeyMap();
private formatters: Map<string, Formatter> = new Map();
private generators: Map<string, GeneratorConstructor<unknown>> = new Map();
c_mapping: Map<string, { version: string; name: string }[]> = new Map();
transformations: GirVisitor[] = [];
subtypes = new TwoKeyMap<string, string, TwoKeyMap<string, string, boolean>>();
constructor() {
this.formatters.set("json", new JSONFormatter());
}
registerTransformation(visitor: GirVisitor) {
this.transformations.push(visitor);
// Apply transformations to already built namespaces.
this.mapping.forEach((n) => {
n.accept(visitor);
});
}
registerFormatter(output: string, formatter: Formatter) {
this.formatters.set(output, formatter);
}
getFormatter(output: string) {
return this.formatters.get(output) ?? new DefaultFormatter();
}
registerGenerator<T>(
output: string,
generator: { new (namespace: IntrospectedNamespace, options: OptionsGeneration): FormatGenerator<T> },
) {
this.generators.set(output, generator);
}
async getGenerator<T>(output: string): Promise<GeneratorConstructor<T> | undefined> {
// Handle loading external generators...
if (!this.generators.has(output)) {
let Generator: { default: GeneratorConstructor<unknown> };
try {
Generator = (await import(`@gi.ts/generator-${output}`)) as { default: GeneratorConstructor<unknown> };
if (Generator) {
console.log(`Loading generator "@gi.ts/generator-${output}"...`);
this.generators.set(output, Generator.default);
return Generator.default as GeneratorConstructor<T>;
}
} catch {
try {
Generator = (await import(`gi-ts-generator-${output}`)) as {
default: GeneratorConstructor<unknown>;
};
console.log(`Loading generator "gi-ts-generator-${output}"...`);
this.generators.set(output, Generator.default);
return Generator.default as GeneratorConstructor<T>;
} catch {
try {
Generator = (await import(`${output}`)) as { default: GeneratorConstructor<unknown> };
console.log(`Loading generator "${output}"...`);
this.generators.set(output, Generator.default);
return Generator.default as GeneratorConstructor<T>;
} catch {}
}
}
}
return this.generators.get(output) as GeneratorConstructor<T> | undefined;
}
private _transformNamespace(namespace: IntrospectedNamespace) {
this.transformations.forEach((t) => {
namespace.accept(t);
});
}
namespace(name: string, version: string): IntrospectedNamespace | null {
const namespace = this.mapping.get(name, version);
return namespace ?? null;
}
namespacesForPrefix(c_prefix: string): IntrospectedNamespace[] {
return (this.c_mapping.get(c_prefix) ?? []).map((c_mapping) =>
this.assertNamespace(c_mapping.name, c_mapping.version),
);
}
transform(options: OptionsTransform) {
// In tolerant external-deps mode (with --allow-missing-deps) the core namespaces
// may not be loaded. Sync their package_version only when actually present;
// generify/inject still run on whatever IS loaded (other modules' transformations
// don't depend on GLib being in the registry).
const GLib = this.namespace("GLib", "2.0");
const Gio = this.namespace("Gio", "2.0");
const GObject = this.namespace("GObject", "2.0");
// These follow the GLib version.
if (GLib && Gio) Gio.package_version = [...GLib.package_version];
if (GLib && GObject) GObject.package_version = [...GLib.package_version];
const interfaceVisitor = new InterfaceVisitor();
this.registerTransformation(interfaceVisitor);
const classVisitor = new ClassVisitor();
this.registerTransformation(classVisitor);
if (GLib && Gio) {
console.log("Adding generics...");
generify(this, options.inferGenerics);
console.log("Injecting types...");
inject(this);
}
}
defaultVersionOf(name: string): string | null {
// GJS has a hard dependency on these versions.
if (name === "GLib" || name === "Gio" || name === "GObject") {
return "2.0";
}
const meta = this.mapping.getIfOnly(name);
if (meta) {
return meta[0];
}
return null;
}
assertDefaultVersionOf(name: string): string {
const version = this.defaultVersionOf(name);
if (version) {
return version;
}
// This mirrors GJS' and GI's default behavior.
// If we can't find a single version of an unspecified dependency, we throw an error.
throw new Error(`No single version found for unspecified dependency: ${JSON.stringify(name)}.`);
}
assertNamespace(name: string, version: string): IntrospectedNamespace {
const namespace = this.mapping.get(name, version) ?? null;
if (!namespace) {
throw new Error(`Namespace '${name}' not found.`);
}
return namespace;
}
register(namespace: GirModule): IntrospectedNamespace {
this.mapping.set(namespace.namespace, namespace.version, namespace);
namespace.c_prefixes.forEach((c_prefix) => {
const c_map = this.c_mapping.get(c_prefix) || [];
c_map.push({ name: namespace.namespace, version: namespace.version });
this.c_mapping.set(c_prefix, c_map);
});
this._transformNamespace(namespace);
return namespace;
}
}