@ts-for-gir/generator-typescript 4.0.0-beta.40 → 4.0.0-beta.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-for-gir/generator-typescript",
3
- "version": "4.0.0-beta.40",
3
+ "version": "4.0.0-beta.41",
4
4
  "description": "TypeScript type definition generator for ts-for-gir",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -31,17 +31,18 @@
31
31
  "generator"
32
32
  ],
33
33
  "devDependencies": {
34
+ "@ts-for-gir/tsconfig": "^4.0.0-beta.41",
34
35
  "@types/ejs": "^3.1.5",
35
- "@types/node": "^24.11.0",
36
+ "@types/node": "^24.12.0",
36
37
  "@types/xml2js": "^0.4.14",
37
38
  "typescript": "^5.9.3"
38
39
  },
39
40
  "dependencies": {
40
- "@gi.ts/parser": "^4.0.0-beta.40",
41
- "@ts-for-gir/generator-base": "^4.0.0-beta.40",
42
- "@ts-for-gir/lib": "^4.0.0-beta.40",
43
- "@ts-for-gir/templates": "^4.0.0-beta.40",
44
- "ejs": "^4.0.1",
41
+ "@gi.ts/parser": "^4.0.0-beta.41",
42
+ "@ts-for-gir/generator-base": "^4.0.0-beta.41",
43
+ "@ts-for-gir/lib": "^4.0.0-beta.41",
44
+ "@ts-for-gir/templates": "^4.0.0-beta.41",
45
+ "ejs": "^5.0.1",
45
46
  "xml2js": "^0.6.2"
46
47
  }
47
48
  }
@@ -0,0 +1,2 @@
1
+ export { ModuleExporter } from "./module-exporter.ts";
2
+ export { SignalGenerator } from "./signal-generator.ts";
@@ -0,0 +1,88 @@
1
+ import type { DependencyManager, GirModule, NSRegistry, OptionsGeneration, Reporter } from "@ts-for-gir/lib";
2
+ import type { ModuleGenerator } from "../module-generator.ts";
3
+ import { NpmPackage } from "../npm-package.ts";
4
+ import type { TemplateProcessor } from "../template-processor.ts";
5
+
6
+ /** Handles exporting generated modules to files. */
7
+ export class ModuleExporter {
8
+ constructor(private readonly core: ModuleGenerator) {}
9
+
10
+ private get config(): OptionsGeneration {
11
+ return this.core.config;
12
+ }
13
+
14
+ private get log(): Reporter {
15
+ return this.core.log;
16
+ }
17
+
18
+ private get moduleTemplateProcessor(): TemplateProcessor {
19
+ return this.core.moduleTemplateProcessor;
20
+ }
21
+
22
+ private get dependencyManager(): DependencyManager {
23
+ return this.core.dependencyManager;
24
+ }
25
+
26
+ /** Export a template file to outdir or log its content. */
27
+ private async exportTemplate(template: string, target: string): Promise<void> {
28
+ if (this.config.outdir) {
29
+ await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
30
+ } else {
31
+ const { append, prepend } = await this.moduleTemplateProcessor.load(template);
32
+ this.log.log(append + prepend);
33
+ }
34
+ }
35
+
36
+ async exportModuleTS(): Promise<void> {
37
+ const girModule = this.core.girNamespace;
38
+ const template = "module.d.ts";
39
+ const explicitTemplate = `${girModule.importName}.d.ts`;
40
+ const output = await this.core.generateModule(girModule);
41
+
42
+ if (!output) {
43
+ this.log.error("Failed to generate gir module");
44
+ return;
45
+ }
46
+
47
+ if (await this.moduleTemplateProcessor.exists(explicitTemplate)) {
48
+ const { append: appendExplicit, prepend: prependExplicit } =
49
+ await this.moduleTemplateProcessor.load(explicitTemplate);
50
+ output.unshift(prependExplicit);
51
+ output.push(appendExplicit);
52
+ }
53
+
54
+ const { append, prepend } = await this.moduleTemplateProcessor.load(template);
55
+ output.unshift(prepend);
56
+ output.push(append);
57
+
58
+ if (this.config.outdir) {
59
+ await this.moduleTemplateProcessor.write(output.join("\n"), this.config.outdir, explicitTemplate);
60
+ } else {
61
+ this.log.log(output.join("\n"));
62
+ }
63
+ }
64
+
65
+ async exportModule(registry: NSRegistry, girModule: GirModule): Promise<void> {
66
+ await this.exportModuleTS();
67
+
68
+ if (this.config.package) {
69
+ const name = girModule.importName;
70
+ await this.exportTemplate("module.js", `${name}.js`);
71
+ await this.exportTemplate("index.d.ts", "index.d.ts");
72
+ await this.exportTemplate("index.js", "index.js");
73
+ await this.exportTemplate("module-ambient.d.ts", `${name}-ambient.d.ts`);
74
+ await this.exportTemplate("module-ambient.js", `${name}-ambient.js`);
75
+ await this.exportTemplate("module-import.d.ts", `${name}-import.d.ts`);
76
+ await this.exportTemplate("module-import.js", `${name}-import.js`);
77
+
78
+ const pkg = new NpmPackage(
79
+ this.config,
80
+ this.dependencyManager,
81
+ registry,
82
+ girModule,
83
+ girModule.transitiveDependencies,
84
+ );
85
+ await pkg.exportNPMPackage();
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,254 @@
1
+ import {
2
+ BinaryType,
3
+ BooleanType,
4
+ FilterBehavior,
5
+ filterConflicts,
6
+ type GirModule,
7
+ generateIndent,
8
+ type IntrospectedBaseClass,
9
+ IntrospectedClass,
10
+ IntrospectedClassFunction,
11
+ IntrospectedInterface,
12
+ type IntrospectedRecord,
13
+ mergeDescs,
14
+ NumberType,
15
+ type OptionsGeneration,
16
+ VoidType,
17
+ } from "@ts-for-gir/lib";
18
+ import type { ModuleGenerator } from "../module-generator.ts";
19
+
20
+ const SIGNAL_JSDOC = "/** @signal */";
21
+
22
+ /** Handles generation of GObject signal-related TypeScript definitions. */
23
+ export class SignalGenerator {
24
+ constructor(private readonly core: ModuleGenerator) {}
25
+
26
+ private get namespace(): GirModule {
27
+ return this.core.girNamespace;
28
+ }
29
+
30
+ private get config(): OptionsGeneration {
31
+ return this.core.config;
32
+ }
33
+
34
+ /**
35
+ * Generate SignalSignatures interface for type-safe signal handling.
36
+ *
37
+ * Creates a comprehensive mapping of signal names to their callback types,
38
+ * enabling TypeScript to provide proper type checking and IntelliSense for
39
+ * GObject signals using the centralized getAllSignals() method from the model.
40
+ */
41
+ generateClassSignalInterfaces(girClass: IntrospectedClass, indentCount = 0): string[] {
42
+ const def: string[] = [];
43
+ const indent = generateIndent(indentCount);
44
+
45
+ def.push(`${indent}// Signal signatures`);
46
+ def.push(`${indent}interface SignalSignatures`);
47
+
48
+ const parentSignatures: string[] = [];
49
+
50
+ // Inherit signal signatures from parent class
51
+ const parentResolution = girClass.resolveParents().extends();
52
+ if (parentResolution && parentResolution.node instanceof IntrospectedClass) {
53
+ const parentClass = parentResolution.node as IntrospectedClass;
54
+ const parentTypeIdentifier = parentResolution.identifier
55
+ .resolveIdentifier(this.namespace, this.config)
56
+ ?.print(this.namespace, this.config);
57
+
58
+ const hasSignalMethods = parentClass.signals?.length > 0;
59
+ const isNotTemplateWorkaround = !(
60
+ this.namespace.namespace === "Gimp" && ["ParamObject", "ParamItem", "ParamArray"].includes(parentClass.name)
61
+ );
62
+
63
+ if (parentTypeIdentifier && (hasSignalMethods || isNotTemplateWorkaround)) {
64
+ parentSignatures.push(`${parentTypeIdentifier}.SignalSignatures`);
65
+ }
66
+ }
67
+
68
+ // Inherit signal signatures from implemented interfaces
69
+ const interfaceSignatures = girClass
70
+ .resolveParents()
71
+ .implements()
72
+ .filter((iface) => iface.node instanceof IntrospectedInterface)
73
+ .filter((iface) => {
74
+ const node = iface.node as unknown as { signals?: unknown[] };
75
+ return node.signals && node.signals.length > 0;
76
+ })
77
+ .map((iface) => {
78
+ const interfaceTypeIdentifier = iface.identifier
79
+ .resolveIdentifier(this.namespace, this.config)
80
+ ?.print(this.namespace, this.config);
81
+ return interfaceTypeIdentifier ? `${interfaceTypeIdentifier}.SignalSignatures` : null;
82
+ })
83
+ .filter((sig): sig is string => !!sig);
84
+
85
+ parentSignatures.push(...interfaceSignatures);
86
+
87
+ if (parentSignatures.length > 0) {
88
+ def.push(` extends ${parentSignatures.join(", ")} {`);
89
+ } else {
90
+ const isGObjectObject = girClass.name === "Object" && girClass.namespace.namespace === "GObject";
91
+
92
+ if (isGObjectObject) {
93
+ def.push(" {");
94
+ } else {
95
+ const gobjectNamespace = this.namespace.assertInstalledImport("GObject");
96
+ const gobjectObjectClass = gobjectNamespace.assertClass("Object");
97
+ const gobjectRef = gobjectObjectClass
98
+ .getType()
99
+ .resolveIdentifier(this.namespace, this.config)
100
+ ?.print(this.namespace, this.config);
101
+
102
+ const fallbackRef = gobjectRef ? `${gobjectRef}.SignalSignatures` : "GObject.Object.SignalSignatures";
103
+ def.push(` extends ${fallbackRef} {`);
104
+ }
105
+ }
106
+
107
+ const allSignals = girClass.getAllSignals();
108
+
109
+ allSignals.forEach((signalInfo) => {
110
+ const signalKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(signalInfo.name) ? signalInfo.name : `"${signalInfo.name}"`;
111
+
112
+ let cbType: string;
113
+
114
+ if (signalInfo.isNotifySignal) {
115
+ const gobjectRef = this.namespace.namespace === "GObject" ? "" : "GObject.";
116
+ cbType = `(pspec: ${gobjectRef}ParamSpec) => void`;
117
+ } else if (signalInfo.signal) {
118
+ const paramTypes = signalInfo.signal.parameters
119
+ .map((p, idx) => `arg${idx}: ${this.core.generateType(p.type)}`)
120
+ .join(", ");
121
+
122
+ let returnType = signalInfo.signal.return_type;
123
+ if (signalInfo.signal.return_type.equals(BooleanType)) {
124
+ returnType = new BinaryType(BooleanType, VoidType);
125
+ }
126
+ const returnTypeStr = this.core.generateType(returnType);
127
+
128
+ cbType = `(${paramTypes}) => ${returnTypeStr}`;
129
+ } else {
130
+ const paramTypes = signalInfo.parameterTypes?.map((type, idx) => `arg${idx}: ${type}`) || [];
131
+ const returnTypeStr = signalInfo.returnType || "void";
132
+ cbType = `(${paramTypes.join(", ")}) => ${returnTypeStr}`;
133
+ }
134
+
135
+ // Add signal doc comment with @signal tag and signal-specific modifier tags
136
+ if (!signalInfo.isNotifySignal && signalInfo.signal) {
137
+ const signalTags = [
138
+ { tagName: "signal", paramName: "", text: "" },
139
+ ...this.namespace.getTsDocMetadataTags(signalInfo.signal.metadata),
140
+ ];
141
+ if (signalInfo.signal.detailed) signalTags.push({ tagName: "detailed", paramName: "", text: "" });
142
+ if (signalInfo.signal.action) signalTags.push({ tagName: "action", paramName: "", text: "" });
143
+ if (signalInfo.signal.when)
144
+ signalTags.push({ tagName: `run-${signalInfo.signal.when}`, paramName: "", text: "" });
145
+ const comment = this.core.addGirDocComment(signalInfo.signal.doc, signalTags, indentCount + 1);
146
+ if (comment.length) {
147
+ def.push(...comment);
148
+ } else {
149
+ def.push(`${indent} /** @signal */`);
150
+ }
151
+ } else if (!signalInfo.isNotifySignal) {
152
+ def.push(`${indent} /** @signal */`);
153
+ }
154
+ def.push(`${indent} ${signalKey}: ${cbType};`);
155
+ });
156
+
157
+ def.push(`${indent}}`);
158
+ def.push("");
159
+
160
+ return def;
161
+ }
162
+
163
+ /**
164
+ * Generate signal methods section with header comment
165
+ */
166
+ generateClassSignals(girClass: IntrospectedClass): string[] {
167
+ return mergeDescs(this.generateSignalMethods(girClass), "Signals", 1);
168
+ }
169
+
170
+ /**
171
+ * Generate the $signals property for type-safe signal access
172
+ */
173
+ generateClassSignalsProperty(girClass: IntrospectedClass | IntrospectedRecord, indentCount = 1): string[] {
174
+ const isGObjectObject = girClass.name === "Object" && girClass.namespace.namespace === "GObject";
175
+ const hasGObjectParent =
176
+ isGObjectObject ||
177
+ girClass.someParent((p: IntrospectedBaseClass) => p.namespace.namespace === "GObject" && p.name === "Object");
178
+
179
+ if (!hasGObjectParent) return [];
180
+
181
+ const indent = generateIndent(indentCount);
182
+ return [
183
+ "",
184
+ `${indent}/**`,
185
+ `${indent} * Compile-time signal type information.`,
186
+ `${indent} *`,
187
+ `${indent} * This instance property is generated only for TypeScript type checking.`,
188
+ `${indent} * It is not defined at runtime and should not be accessed in JS code.`,
189
+ `${indent} * @internal`,
190
+ `${indent} */`,
191
+ `${indent}$signals: ${girClass.name}.SignalSignatures;`,
192
+ "",
193
+ ];
194
+ }
195
+
196
+ /**
197
+ * Generate type-safe connect/connect_after/emit signal methods
198
+ */
199
+ private generateSignalMethods(girClass: IntrospectedClass): string[] {
200
+ const signalFunctions = [
201
+ new IntrospectedClassFunction({
202
+ name: "connect",
203
+ parent: girClass,
204
+ parameters: [],
205
+ return_type: NumberType,
206
+ }),
207
+ new IntrospectedClassFunction({
208
+ name: "connect_after",
209
+ parent: girClass,
210
+ parameters: [],
211
+ return_type: NumberType,
212
+ }),
213
+ new IntrospectedClassFunction({
214
+ name: "emit",
215
+ parent: girClass,
216
+ parameters: [],
217
+ return_type: VoidType,
218
+ }),
219
+ ];
220
+
221
+ const filteredFunctions = filterConflicts(girClass.namespace, girClass, signalFunctions, FilterBehavior.DELETE);
222
+ const allowedNames = new Set(filteredFunctions.map((f) => f.name));
223
+
224
+ const gobjectRef = this.namespace.namespace === "GObject" ? "" : "GObject.";
225
+
226
+ const methods: string[] = [];
227
+
228
+ if (allowedNames.has("connect")) {
229
+ methods.push(
230
+ SIGNAL_JSDOC,
231
+ `connect<K extends keyof ${girClass.name}.SignalSignatures>(signal: K, callback: ${gobjectRef}SignalCallback<this, ${girClass.name}.SignalSignatures[K]>): number;`,
232
+ "connect(signal: string, callback: (...args: any[]) => any): number;",
233
+ );
234
+ }
235
+
236
+ if (allowedNames.has("connect_after")) {
237
+ methods.push(
238
+ SIGNAL_JSDOC,
239
+ `connect_after<K extends keyof ${girClass.name}.SignalSignatures>(signal: K, callback: ${gobjectRef}SignalCallback<this, ${girClass.name}.SignalSignatures[K]>): number;`,
240
+ "connect_after(signal: string, callback: (...args: any[]) => any): number;",
241
+ );
242
+ }
243
+
244
+ if (allowedNames.has("emit")) {
245
+ methods.push(
246
+ SIGNAL_JSDOC,
247
+ `emit<K extends keyof ${girClass.name}.SignalSignatures>(signal: K, ...args: ${gobjectRef}GjsParameters<${girClass.name}.SignalSignatures[K]> extends [any, ...infer Q] ? Q : never): void;`,
248
+ "emit(signal: string, ...args: any[]): void;",
249
+ );
250
+ }
251
+
252
+ return methods;
253
+ }
254
+ }
@@ -4,7 +4,6 @@ import {
4
4
  addInfoComment,
5
5
  addTSDocCommentLines,
6
6
  BinaryType,
7
- BooleanType,
8
7
  ClassStructTypeIdentifier,
9
8
  ConflictType,
10
9
  DependencyManager,
@@ -13,6 +12,7 @@ import {
13
12
  filterConflicts,
14
13
  filterFunctionConflict,
15
14
  type Generic,
15
+ type GirDocContext,
16
16
  type GirEnumMember,
17
17
  type GirModule,
18
18
  generateIndent,
@@ -41,10 +41,8 @@ import {
41
41
  IntrospectedStaticClassFunction,
42
42
  IntrospectedVirtualClassFunction,
43
43
  isInvalid,
44
- mergeDescs,
45
44
  NativeType,
46
45
  type NSRegistry,
47
- NumberType,
48
46
  type OptionsGeneration,
49
47
  printGirDocComment,
50
48
  promisifyFunctions,
@@ -57,11 +55,11 @@ import {
57
55
  type TsDocTag,
58
56
  TypeConflict,
59
57
  type TypeExpression,
58
+ transformGirDocTagTextWithContext,
60
59
  transformGirDocText,
61
- VoidType,
62
60
  } from "@ts-for-gir/lib";
61
+ import { ModuleExporter, SignalGenerator } from "./generators/index.ts";
63
62
  // import { PackageDataParser } from './package-data-parser.ts'
64
- import { NpmPackage } from "./npm-package.ts";
65
63
  import { override as overrideGLib } from "./overrides/glib.ts";
66
64
  import { override as overrideGObject } from "./overrides/gobject.ts";
67
65
  import { TemplateProcessor } from "./template-processor.ts";
@@ -80,6 +78,33 @@ export enum ModuleGeneratorFormat {
80
78
  Inline = "inline",
81
79
  }
82
80
 
81
+ /**
82
+ * Base URLs for gi-docgen content pages per namespace.
83
+ * Version-specific keys (e.g. "Gtk-4.0") take priority over plain namespace keys.
84
+ */
85
+ const DOC_BASE_URLS = new Map<string, string>([
86
+ // GNOME core (docs.gtk.org)
87
+ ["GLib", "https://docs.gtk.org/glib/"],
88
+ ["GObject", "https://docs.gtk.org/gobject/"],
89
+ ["Gio", "https://docs.gtk.org/gio/"],
90
+ ["GdkPixbuf", "https://docs.gtk.org/gdk-pixbuf/"],
91
+ ["Pango", "https://docs.gtk.org/Pango/"],
92
+ ["PangoCairo", "https://docs.gtk.org/PangoCairo/"],
93
+ // GTK 4
94
+ ["Gtk-4.0", "https://docs.gtk.org/gtk4/"],
95
+ ["Gdk-4.0", "https://docs.gtk.org/gdk4/"],
96
+ ["Gsk-4.0", "https://docs.gtk.org/gsk4/"],
97
+ ["GdkWayland-4.0", "https://docs.gtk.org/gdk4-wayland/"],
98
+ ["GdkX11-4.0", "https://docs.gtk.org/gdk4-x11/"],
99
+ // GTK 3
100
+ ["Gtk-3.0", "https://docs.gtk.org/gtk3/"],
101
+ ["Gdk-3.0", "https://docs.gtk.org/gdk3/"],
102
+ // Libadwaita
103
+ ["Adw-1", "https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/"],
104
+ // GtkSourceView
105
+ ["GtkSource-5", "https://gnome.pages.gitlab.gnome.org/gtksourceview/gtksourceview5/"],
106
+ ]);
107
+
83
108
  export class ModuleGenerator extends FormatGenerator<string[]> {
84
109
  log: Reporter;
85
110
  dependencyManager: DependencyManager;
@@ -88,6 +113,14 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
88
113
  config: OptionsGeneration;
89
114
  moduleTemplateProcessor: TemplateProcessor;
90
115
 
116
+ readonly signalGenerator: SignalGenerator;
117
+ readonly moduleExporter: ModuleExporter;
118
+
119
+ /** Public accessor for the protected namespace from FormatGenerator */
120
+ get girNamespace() {
121
+ return this.namespace;
122
+ }
123
+
91
124
  /**
92
125
  * @param _config The config to use without the override config
93
126
  */
@@ -124,6 +157,82 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
124
157
  girModule.transitiveDependencies,
125
158
  this.config,
126
159
  );
160
+
161
+ this.signalGenerator = new SignalGenerator(this);
162
+ this.moduleExporter = new ModuleExporter(this);
163
+ this._docContext = this.buildDocContext();
164
+ }
165
+
166
+ private readonly _docContext: GirDocContext;
167
+
168
+ /**
169
+ * Build a GirDocContext that resolves C identifiers to TypeScript paths
170
+ * by searching the current namespace and all its dependencies.
171
+ */
172
+ private buildDocContext(): GirDocContext {
173
+ const ns = this.namespace;
174
+
175
+ // Collect all available namespaces (current + dependencies)
176
+ const allNamespaces: GirModule[] = [ns];
177
+ for (const dep of ns.allDependencies) {
178
+ const imported = ns.getInstalledImport(dep.namespace);
179
+ if (imported) allNamespaces.push(imported);
180
+ }
181
+
182
+ // Resolve base URL for gi-docgen content pages
183
+ const docBaseUrl = DOC_BASE_URLS.get(`${ns.namespace}-${ns.version}`) ?? DOC_BASE_URLS.get(ns.namespace);
184
+
185
+ // Pre-merge enum constants for O(1) lookup
186
+ const constantMap = new Map<string, string>();
187
+ for (const mod of allNamespaces) {
188
+ for (const [cId, result] of mod.enum_constants) {
189
+ if (!constantMap.has(cId)) {
190
+ constantMap.set(cId, `${mod.namespace}.${result[0]}.${result[1]}`);
191
+ }
192
+ }
193
+ }
194
+
195
+ // Type resolution cache — populated lazily on first miss
196
+ const typeCache = new Map<string, string | null>();
197
+
198
+ return {
199
+ docBaseUrl,
200
+ resolveType(cTypeName: string): string | null {
201
+ const cached = typeCache.get(cTypeName);
202
+ if (cached !== undefined) return cached;
203
+
204
+ let result: string | null = null;
205
+ // Strategy 1: _resolve_names lookup
206
+ for (const mod of allNamespaces) {
207
+ const member = mod.getMemberWithoutOverrides(cTypeName);
208
+ if (member && "name" in member) {
209
+ result = `${mod.namespace}.${member.name}`;
210
+ break;
211
+ }
212
+ }
213
+ // Strategy 2: C prefix stripping
214
+ if (!result) {
215
+ for (const mod of allNamespaces) {
216
+ for (const prefix of mod.c_prefixes) {
217
+ if (cTypeName.startsWith(prefix) && cTypeName.length > prefix.length) {
218
+ const girName = cTypeName.slice(prefix.length);
219
+ if (mod.hasSymbol(girName)) {
220
+ result = `${mod.namespace}.${girName}`;
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ if (result) break;
226
+ }
227
+ }
228
+
229
+ typeCache.set(cTypeName, result);
230
+ return result;
231
+ },
232
+ resolveConstant(cIdentifier: string): string | null {
233
+ return constantMap.get(cIdentifier) ?? null;
234
+ },
235
+ };
127
236
  }
128
237
 
129
238
  /**
@@ -311,14 +420,22 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
311
420
  }
312
421
 
313
422
  generateSignal(node: IntrospectedSignal, type: IntrospectedSignalType = IntrospectedSignalType.CONNECT): string[] {
423
+ let fn: IntrospectedClassFunction;
314
424
  switch (type) {
315
425
  case IntrospectedSignalType.CONNECT:
316
- return node.asConnect(false).asString(this);
426
+ fn = node.asConnect(false);
427
+ break;
317
428
  case IntrospectedSignalType.CONNECT_AFTER:
318
- return node.asConnect(true).asString(this);
429
+ fn = node.asConnect(true);
430
+ break;
319
431
  case IntrospectedSignalType.EMIT:
320
- return node.asEmit().asString(this);
432
+ fn = node.asEmit();
433
+ break;
321
434
  }
435
+ fn.doc = node.doc;
436
+ fn.metadata = node.metadata;
437
+ fn.signalOrigin = node.name;
438
+ return fn.asString(this);
322
439
  }
323
440
 
324
441
  generateStaticClassFunction(node: IntrospectedStaticClassFunction): string[] {
@@ -340,7 +457,11 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
340
457
  generateProperty(tsProp: IntrospectedProperty, construct?: boolean, indentCount = 0) {
341
458
  const desc: string[] = [];
342
459
 
343
- desc.push(...this.addGirDocComment(tsProp.doc, [], indentCount));
460
+ const propTags = [...this.namespace.getTsDocMetadataTags(tsProp.metadata)];
461
+ if (tsProp.constructOnly) propTags.push({ tagName: "construct-only", paramName: "", text: "" });
462
+ else if (tsProp.readable && !tsProp.writable) propTags.push({ tagName: "read-only", paramName: "", text: "" });
463
+ else if (tsProp.writable && !tsProp.readable) propTags.push({ tagName: "write-only", paramName: "", text: "" });
464
+ desc.push(...this.addGirDocComment(tsProp.doc, propTags, indentCount));
344
465
 
345
466
  const indent = generateIndent(indentCount);
346
467
  const name = generateMemberName(tsProp);
@@ -421,7 +542,7 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
421
542
  const desc: string[] = [];
422
543
  const isStatic = tsProp.isStatic;
423
544
 
424
- desc.push(...this.addGirDocComment(tsProp.doc, [], indentCount));
545
+ desc.push(...this.addGirDocComment(tsProp.doc, this.namespace.getTsDocMetadataTags(tsProp.metadata), indentCount));
425
546
 
426
547
  const indent = generateIndent(indentCount);
427
548
  const name = generateMemberName(tsProp);
@@ -532,9 +653,9 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
532
653
  return desc;
533
654
  }
534
655
 
535
- const text = tsDoc ? transformGirDocText(tsDoc) : null;
656
+ const text = tsDoc ? transformGirDocText(tsDoc, this._docContext) : null;
536
657
 
537
- if (text) {
658
+ if (text || tags.length) {
538
659
  desc.push(`${indent}/**`);
539
660
 
540
661
  if (text) {
@@ -547,10 +668,13 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
547
668
  }
548
669
 
549
670
  for (const tag of tags) {
671
+ const tagText = tag.text ? transformGirDocTagTextWithContext(tag.text, this._docContext) : "";
550
672
  if (tag.paramName) {
551
- desc.push(`${indent} * @${tag.tagName} ${tag.paramName} ${tag.text}`);
673
+ desc.push(`${indent} * @${tag.tagName} ${tag.paramName} ${tagText}`);
674
+ } else if (tagText) {
675
+ desc.push(`${indent} * @${tag.tagName} ${tagText}`);
552
676
  } else {
553
- desc.push(`${indent} * @${tag.tagName} ${tag.text}`);
677
+ desc.push(`${indent} * @${tag.tagName}`);
554
678
  }
555
679
  }
556
680
  desc.push(`${indent} */`);
@@ -558,6 +682,29 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
558
682
  return desc;
559
683
  }
560
684
 
685
+ private getGirTypeTags(
686
+ obj: IntrospectedClass | IntrospectedRecord | IntrospectedInterface | IntrospectedCallback | IntrospectedAlias,
687
+ ): TsDocTag[] {
688
+ let girType: string;
689
+
690
+ if (obj instanceof IntrospectedRecord) {
691
+ if (obj.structFor) girType = "Class Struct";
692
+ else if (obj.isForeign()) girType = "Foreign Struct";
693
+ else girType = "Struct";
694
+ } else if (obj instanceof IntrospectedInterface) {
695
+ girType = "Interface";
696
+ } else if (obj instanceof IntrospectedClass) {
697
+ girType = "Class";
698
+ } else if (obj instanceof IntrospectedCallback) {
699
+ girType = "Callback";
700
+ } else {
701
+ // IntrospectedAlias
702
+ girType = "Alias";
703
+ }
704
+
705
+ return [{ tagName: "gir-type", paramName: "", text: girType }];
706
+ }
707
+
561
708
  /**
562
709
  * Adds an info comment, is used for debugging the generated types
563
710
  * @param comment
@@ -693,17 +840,23 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
693
840
 
694
841
  const { parameters: inParams } = tsFunction;
695
842
 
696
- if (tsFunction.doc)
697
- def.push(
698
- ...this.addGirDocComment(
699
- tsFunction.doc,
700
- [
701
- ...this.namespace.getTsDocInParamTags(tsFunction.parameters),
702
- ...this.namespace.getTsDocReturnTags(tsFunction),
703
- ],
704
- indentCount,
705
- ),
706
- );
843
+ def.push(
844
+ ...this.addGirDocComment(
845
+ tsFunction.doc,
846
+ [
847
+ ...this.namespace.getTsDocInParamTags(tsFunction.parameters),
848
+ ...this.namespace.getTsDocReturnTags(tsFunction),
849
+ ...this.namespace.getTsDocMetadataTags(tsFunction.metadata),
850
+ ...(tsFunction instanceof IntrospectedVirtualClassFunction
851
+ ? [{ tagName: "virtual", paramName: "", text: "" } as const]
852
+ : []),
853
+ ...("signalOrigin" in tsFunction && tsFunction.signalOrigin
854
+ ? [{ tagName: "signal", paramName: "", text: "" } as const]
855
+ : []),
856
+ ],
857
+ indentCount,
858
+ ),
859
+ );
707
860
 
708
861
  const warning = tsFunction.getWarning();
709
862
  if (warning) def.push(warning);
@@ -775,7 +928,11 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
775
928
  ) {
776
929
  const def: string[] = [];
777
930
 
778
- def.push(...this.addGirDocComment(tsCallback.doc, [], indentCount));
931
+ const callbackTags =
932
+ tsCallback instanceof IntrospectedCallback && !(tsCallback instanceof IntrospectedClassCallback)
933
+ ? [...this.getGirTypeTags(tsCallback), ...this.namespace.getTsDocMetadataTags(tsCallback.metadata)]
934
+ : this.namespace.getTsDocMetadataTags(tsCallback.metadata);
935
+ def.push(...this.addGirDocComment(tsCallback.doc, callbackTags, indentCount));
779
936
 
780
937
  const indent = generateIndent(indentCount);
781
938
  const indentBody = generateIndent(indentCount + 1);
@@ -835,6 +992,8 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
835
992
  }
836
993
 
837
994
  if (girEnum.isRegistered) {
995
+ const nsTags = [{ tagName: "gir-type", paramName: "", text: girEnum.flags ? "Flags" : "Enum" } as const];
996
+ desc.push(...this.addGirDocComment(null, nsTags, indentCount));
838
997
  desc.push(`export namespace ${name} {`);
839
998
  const gtypeNamespace = namespace.namespace === "GObject" ? "" : "GObject.";
840
999
  desc.push(` export const $gtype: ${gtypeNamespace}GType<${name}>;`);
@@ -842,7 +1001,11 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
842
1001
  desc.push("");
843
1002
  }
844
1003
 
845
- desc.push(...this.addGirDocComment(girEnum.doc, [], indentCount));
1004
+ const enumTags = [
1005
+ { tagName: "gir-type", paramName: "", text: girEnum.flags ? "Flags" : "Enum" } as const,
1006
+ ...this.namespace.getTsDocMetadataTags(girEnum.metadata),
1007
+ ];
1008
+ desc.push(...this.addGirDocComment(girEnum.doc, enumTags, indentCount));
846
1009
  desc.push(this.generateExport("enum", name, "{", indentCount));
847
1010
  if (girEnum.members) {
848
1011
  for (const girEnumMember of girEnum.members.values()) {
@@ -875,7 +1038,9 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
875
1038
  generateConst(tsConst: IntrospectedConstant, indentCount = 0) {
876
1039
  const desc: string[] = [];
877
1040
 
878
- desc.push(...this.addGirDocComment(tsConst.doc, [], indentCount));
1041
+ desc.push(
1042
+ ...this.addGirDocComment(tsConst.doc, this.namespace.getTsDocMetadataTags(tsConst.metadata), indentCount),
1043
+ );
879
1044
 
880
1045
  const indent = generateIndent(indentCount);
881
1046
  const exp = !this.config.noNamespace ? "" : "export ";
@@ -892,6 +1057,14 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
892
1057
 
893
1058
  const desc: string[] = [];
894
1059
 
1060
+ desc.push(
1061
+ ...this.addGirDocComment(
1062
+ girAlias.doc,
1063
+ [...this.getGirTypeTags(girAlias), ...this.namespace.getTsDocMetadataTags(girAlias.metadata)],
1064
+ indentCount,
1065
+ ),
1066
+ );
1067
+
895
1068
  const indent = generateIndent(indentCount);
896
1069
 
897
1070
  const genericList = girAlias.generics
@@ -1000,33 +1173,6 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1000
1173
  return def;
1001
1174
  }
1002
1175
 
1003
- generateClassSignalsProperty(girClass: IntrospectedClass | IntrospectedRecord, indentCount = 1) {
1004
- const def: string[] = [];
1005
-
1006
- // Add instance $signals property for type-safe signal access (compile-time only)
1007
- const isGObjectObject = girClass.name === "Object" && girClass.namespace.namespace === "GObject";
1008
- const hasGObjectParent =
1009
- isGObjectObject ||
1010
- girClass.someParent((p: IntrospectedBaseClass) => p.namespace.namespace === "GObject" && p.name === "Object");
1011
-
1012
- if (hasGObjectParent) {
1013
- def.push(
1014
- "",
1015
- `${generateIndent(indentCount)}/**`,
1016
- `${generateIndent(indentCount)} * Compile-time signal type information.`,
1017
- `${generateIndent(indentCount)} *`,
1018
- `${generateIndent(indentCount)} * This instance property is generated only for TypeScript type checking.`,
1019
- `${generateIndent(indentCount)} * It is not defined at runtime and should not be accessed in JS code.`,
1020
- `${generateIndent(indentCount)} * @internal`,
1021
- `${generateIndent(indentCount)} */`,
1022
- `${generateIndent(indentCount)}$signals: ${girClass.name}.SignalSignatures;`,
1023
- "",
1024
- );
1025
- }
1026
-
1027
- return def;
1028
- }
1029
-
1030
1176
  generateClassMemberFields(girClass: IntrospectedClass | IntrospectedRecord | IntrospectedInterface, indentCount = 1) {
1031
1177
  const def: string[] = [];
1032
1178
 
@@ -1272,219 +1418,6 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1272
1418
  return `${method.name}(${params}):${returnType}`;
1273
1419
  }
1274
1420
 
1275
- generateClassSignalInterfaces(girClass: IntrospectedClass, indentCount = 0) {
1276
- const def: string[] = [];
1277
- const _tsSignals = girClass.signals;
1278
-
1279
- // No separate callback interfaces generated. All callback types are inlined directly in SignalSignatures.
1280
-
1281
- // Always generate SignalSignatures interface for proper inheritance
1282
- def.push(...this.generateSignalSignatures(girClass, indentCount));
1283
-
1284
- return def;
1285
- }
1286
-
1287
- /**
1288
- * Generate SignalSignatures interface for type-safe signal handling
1289
- *
1290
- * This creates a comprehensive mapping of signal names to their callback types,
1291
- * enabling TypeScript to provide proper type checking and IntelliSense for
1292
- * GObject signals using the centralized getAllSignals() method from the model.
1293
- */
1294
- generateSignalSignatures(girClass: IntrospectedClass, indentCount = 0): string[] {
1295
- const def: string[] = [];
1296
- const indent = generateIndent(indentCount);
1297
-
1298
- // Generate SignalSignatures interface to maintain type inheritance chain
1299
- def.push(`${indent}// Signal signatures`);
1300
- def.push(`${indent}interface SignalSignatures`);
1301
-
1302
- // Build inheritance chain for signal signatures
1303
- const parentSignatures: string[] = [];
1304
-
1305
- // Inherit signal signatures from parent class
1306
- const parentResolution = girClass.resolveParents().extends();
1307
- if (parentResolution && parentResolution.node instanceof IntrospectedClass) {
1308
- const parentClass = parentResolution.node as IntrospectedClass;
1309
- const parentTypeIdentifier = parentResolution.identifier
1310
- .resolveIdentifier(this.namespace, this.config)
1311
- ?.print(this.namespace, this.config);
1312
-
1313
- // Include parent signals unless it's a template workaround class
1314
- const hasSignalMethods = parentClass.signals?.length > 0;
1315
- const isNotTemplateWorkaround = !(
1316
- this.namespace.namespace === "Gimp" && ["ParamObject", "ParamItem", "ParamArray"].includes(parentClass.name)
1317
- );
1318
-
1319
- if (parentTypeIdentifier && (hasSignalMethods || isNotTemplateWorkaround)) {
1320
- parentSignatures.push(`${parentTypeIdentifier}.SignalSignatures`);
1321
- }
1322
- }
1323
-
1324
- // Inherit signal signatures from implemented interfaces
1325
- const interfaceSignatures = girClass
1326
- .resolveParents()
1327
- .implements()
1328
- .filter((iface) => iface.node instanceof IntrospectedInterface)
1329
- .filter((iface) => {
1330
- // Only include interfaces that actually define signals
1331
- const node = iface.node as unknown as { signals?: unknown[] };
1332
- return node.signals && node.signals.length > 0;
1333
- })
1334
- .map((iface) => {
1335
- const interfaceTypeIdentifier = iface.identifier
1336
- .resolveIdentifier(this.namespace, this.config)
1337
- ?.print(this.namespace, this.config);
1338
- return interfaceTypeIdentifier ? `${interfaceTypeIdentifier}.SignalSignatures` : null;
1339
- })
1340
- .filter((sig): sig is string => !!sig);
1341
-
1342
- parentSignatures.push(...interfaceSignatures);
1343
-
1344
- // Apply inheritance or fallback to base GObject signals
1345
- if (parentSignatures.length > 0) {
1346
- def.push(` extends ${parentSignatures.join(", ")} {`);
1347
- } else {
1348
- // Handle root GObject.Object class to avoid circular references
1349
- const isGObjectObject = girClass.name === "Object" && girClass.namespace.namespace === "GObject";
1350
-
1351
- if (isGObjectObject) {
1352
- def.push(" {");
1353
- } else {
1354
- // All other classes inherit from GObject.Object's signal signatures as fallback
1355
- const gobjectNamespace = this.namespace.assertInstalledImport("GObject");
1356
- const gobjectObjectClass = gobjectNamespace.assertClass("Object");
1357
- const gobjectRef = gobjectObjectClass
1358
- .getType()
1359
- .resolveIdentifier(this.namespace, this.config)
1360
- ?.print(this.namespace, this.config);
1361
-
1362
- const fallbackRef = gobjectRef ? `${gobjectRef}.SignalSignatures` : "GObject.Object.SignalSignatures";
1363
- def.push(` extends ${fallbackRef} {`);
1364
- }
1365
- }
1366
-
1367
- // Use the centralized getAllSignals method from the model
1368
- const allSignals = girClass.getAllSignals();
1369
-
1370
- allSignals.forEach((signalInfo) => {
1371
- // Ensure valid TypeScript property names by quoting invalid identifiers
1372
- const signalKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(signalInfo.name) ? signalInfo.name : `"${signalInfo.name}"`;
1373
-
1374
- let cbType: string;
1375
-
1376
- if (signalInfo.isNotifySignal) {
1377
- // Property notification signals have a standard signature
1378
- const gobjectRef = this.namespace.namespace === "GObject" ? "" : "GObject.";
1379
- cbType = `(pspec: ${gobjectRef}ParamSpec) => void`;
1380
- } else if (signalInfo.signal) {
1381
- // Regular signals - use the signal's parameters and return type
1382
- const paramTypes = signalInfo.signal.parameters
1383
- .map((p, idx) => `arg${idx}: ${this.generateType(p.type)}`)
1384
- .join(", ");
1385
-
1386
- // For boolean return types, allow boolean | void for flexibility
1387
- let returnType = signalInfo.signal.return_type;
1388
- if (signalInfo.signal.return_type.equals(BooleanType)) {
1389
- returnType = new BinaryType(BooleanType, VoidType);
1390
- }
1391
- const returnTypeStr = this.generateType(returnType);
1392
-
1393
- cbType = `(${paramTypes}) => ${returnTypeStr}`;
1394
- } else {
1395
- // Fallback for custom signal types
1396
- const paramTypes = signalInfo.parameterTypes?.map((type, idx) => `arg${idx}: ${type}`) || [];
1397
- const returnTypeStr = signalInfo.returnType || "void";
1398
- cbType = `(${paramTypes.join(", ")}) => ${returnTypeStr}`;
1399
- }
1400
-
1401
- def.push(`${indent} ${signalKey}: ${cbType};`);
1402
- });
1403
-
1404
- def.push(`${indent}}`);
1405
- def.push("");
1406
-
1407
- return def;
1408
- }
1409
-
1410
- generateSignals(girClass: IntrospectedClass) {
1411
- // Create IntrospectedClassFunction instances for the signal methods
1412
- // These represent the GObject signal methods that we want to generate
1413
- const signalFunctions = [
1414
- new IntrospectedClassFunction({
1415
- name: "connect",
1416
- parent: girClass,
1417
- parameters: [],
1418
- return_type: NumberType,
1419
- }),
1420
- new IntrospectedClassFunction({
1421
- name: "connect_after",
1422
- parent: girClass,
1423
- parameters: [],
1424
- return_type: NumberType,
1425
- }),
1426
- new IntrospectedClassFunction({
1427
- name: "emit",
1428
- parent: girClass,
1429
- parameters: [],
1430
- return_type: VoidType,
1431
- }),
1432
- ];
1433
-
1434
- // Filter out signal methods that conflict with existing methods in the class or parent classes
1435
- // For example, if a class already has a connect() method (like Camel.Service), we don't generate
1436
- // the signal connect() method to avoid conflicts
1437
- const filteredFunctions = filterConflicts(girClass.namespace, girClass, signalFunctions, FilterBehavior.DELETE);
1438
-
1439
- // Get the names of methods that should be kept (non-conflicting)
1440
- const allowedNames = new Set(filteredFunctions.map((f) => f.name));
1441
-
1442
- const gobjectRef = this.namespace.namespace === "GObject" ? "" : "GObject.";
1443
-
1444
- // Generate only the non-conflicting type-safe signal methods
1445
- const methods: string[] = [];
1446
-
1447
- if (allowedNames.has("connect")) {
1448
- methods.push(
1449
- // Type-safe overload for known signals
1450
- `connect<K extends keyof ${girClass.name}.SignalSignatures>(signal: K, callback: ${gobjectRef}SignalCallback<this, ${girClass.name}.SignalSignatures[K]>): number;`,
1451
- // Fallback overload for dynamic signals
1452
- "connect(signal: string, callback: (...args: any[]) => any): number;",
1453
- );
1454
- }
1455
-
1456
- if (allowedNames.has("connect_after")) {
1457
- methods.push(
1458
- // Type-safe overload for known signals
1459
- `connect_after<K extends keyof ${girClass.name}.SignalSignatures>(signal: K, callback: ${gobjectRef}SignalCallback<this, ${girClass.name}.SignalSignatures[K]>): number;`,
1460
- // Fallback overload for dynamic signals
1461
- "connect_after(signal: string, callback: (...args: any[]) => any): number;",
1462
- );
1463
- }
1464
-
1465
- if (allowedNames.has("emit")) {
1466
- // Fix: Use a conditional type to extract parameters from the signal signature
1467
- methods.push(
1468
- // Type-safe overload for known signals
1469
- `emit<K extends keyof ${girClass.name}.SignalSignatures>(signal: K, ...args: ${gobjectRef}GjsParameters<${girClass.name}.SignalSignatures[K]> extends [any, ...infer Q] ? Q : never): void;`,
1470
- // Fallback overload for dynamic signals
1471
- "emit(signal: string, ...args: any[]): void;",
1472
- );
1473
- }
1474
-
1475
- return methods;
1476
- }
1477
-
1478
- generateClassSignals(girClass: IntrospectedClass) {
1479
- const def: string[] = [];
1480
-
1481
- const signalDescs = this.generateSignals(girClass);
1482
-
1483
- def.push(...mergeDescs(signalDescs, "Signals", 1));
1484
-
1485
- return def;
1486
- }
1487
-
1488
1421
  generateClassNamespaces(girClass: IntrospectedClass | IntrospectedRecord | IntrospectedInterface, indentCount = 0) {
1489
1422
  const def: string[] = [];
1490
1423
  const bodyDef: string[] = [];
@@ -1495,7 +1428,7 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1495
1428
 
1496
1429
  if (girClass instanceof IntrospectedClass) {
1497
1430
  // Signal interfaces
1498
- bodyDef.push(...this.generateClassSignalInterfaces(girClass, indentCount + 1));
1431
+ bodyDef.push(...this.signalGenerator.generateClassSignalInterfaces(girClass, indentCount + 1));
1499
1432
  }
1500
1433
 
1501
1434
  if (girClass instanceof IntrospectedInterface) {
@@ -1580,6 +1513,18 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1580
1513
 
1581
1514
  const ext = implementationNames.length ? ` extends ${implementationNames.join(", ")}` : "";
1582
1515
  const interfaceHead = `${girClass.name}${genericParameters}${ext}`;
1516
+
1517
+ // Add @gir-type doc comment for interfaces (classes/records handle this in generateClass)
1518
+ if (girClass instanceof IntrospectedInterface) {
1519
+ def.push(
1520
+ ...this.addGirDocComment(
1521
+ girClass.doc,
1522
+ [...this.getGirTypeTags(girClass), ...this.namespace.getTsDocMetadataTags(girClass.metadata)],
1523
+ 0,
1524
+ ),
1525
+ );
1526
+ }
1527
+
1583
1528
  def.push(this.generateExport("interface", interfaceHead, "{"));
1584
1529
 
1585
1530
  if (girClass.__ts__indexSignature) {
@@ -1745,7 +1690,13 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1745
1690
 
1746
1691
  def.push(...this.generateClassNamespaces(girClass));
1747
1692
 
1748
- def.push(...this.addGirDocComment(girClass.doc, [], 0));
1693
+ def.push(
1694
+ ...this.addGirDocComment(
1695
+ girClass.doc,
1696
+ [...this.getGirTypeTags(girClass), ...this.namespace.getTsDocMetadataTags(girClass.metadata)],
1697
+ 0,
1698
+ ),
1699
+ );
1749
1700
 
1750
1701
  const genericParameters = this.generateGenericParameters(girClass.generics);
1751
1702
  const ext = this.extends(girClass);
@@ -1774,7 +1725,7 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1774
1725
  def.push(...this.generateClassProperties(girClass));
1775
1726
 
1776
1727
  // $signals property (instance property for type-safe signal access)
1777
- def.push(...this.generateClassSignalsProperty(girClass));
1728
+ def.push(...this.signalGenerator.generateClassSignalsProperty(girClass));
1778
1729
 
1779
1730
  // Static and member Fields
1780
1731
  def.push(...this.generateClassFields(girClass));
@@ -1784,7 +1735,7 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1784
1735
 
1785
1736
  if (girClass instanceof IntrospectedClass) {
1786
1737
  // Signals
1787
- def.push(...this.generateClassSignals(girClass));
1738
+ def.push(...this.signalGenerator.generateClassSignals(girClass));
1788
1739
  }
1789
1740
 
1790
1741
  // Static Methods
@@ -1797,27 +1748,45 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1797
1748
  def.push(...this.generateClassMethods(girClass));
1798
1749
 
1799
1750
  if (girClass instanceof IntrospectedClass) {
1800
- const implementedProperties = girClass.implementedProperties().map((prop) => prop.copy({ parent: girClass }));
1801
- const implementedMethods = girClass
1802
- .implementedMethods(implementedProperties)
1803
- .map((method) => method.copy({ parent: girClass }));
1804
-
1805
- const generatedImplementedProperties = filterConflicts(
1806
- girClass.namespace,
1807
- girClass,
1808
- implementedProperties,
1809
- ).flatMap((m) => m.asString(this));
1810
-
1811
- if (generatedImplementedProperties.length > 0)
1812
- def.push("\n// Inherited properties", ...generatedImplementedProperties);
1813
-
1814
- const filteredImplMethods = promisifyIfEnabled(
1815
- this.options,
1816
- filterFunctionConflict(girClass.namespace, girClass, implementedMethods, []),
1817
- );
1818
- const generatedImplementedMethods = filteredImplMethods.flatMap((m) => m.asString(this));
1751
+ const rawProperties = girClass.implementedProperties();
1752
+ const rawMethods = girClass.implementedMethods(rawProperties);
1753
+ const selfName = `${girClass.namespace.namespace}.${girClass.name}`;
1754
+
1755
+ // Group inherited properties by source interface
1756
+ const propsBySource = groupBySource(rawProperties);
1757
+ for (const [source, props] of propsBySource) {
1758
+ const copied = props.map((p) => p.copy({ parent: girClass }));
1759
+ for (const m of filterConflicts(girClass.namespace, girClass, copied)) {
1760
+ const memberLines = m.asString(this);
1761
+ if (memberLines.length > 0) {
1762
+ // Only tag as inherited if source is a different class
1763
+ if (source !== selfName) {
1764
+ injectInheritedTags(memberLines, source);
1765
+ }
1766
+ def.push(...memberLines);
1767
+ }
1768
+ }
1769
+ }
1819
1770
 
1820
- if (generatedImplementedMethods.length > 0) def.push("\n// Inherited methods", ...generatedImplementedMethods);
1771
+ // Group inherited methods by source interface
1772
+ const methodsBySource = groupBySource(rawMethods);
1773
+ for (const [source, methods] of methodsBySource) {
1774
+ const copied = methods.map((m) => m.copy({ parent: girClass }));
1775
+ const filtered = promisifyIfEnabled(
1776
+ this.options,
1777
+ filterFunctionConflict(girClass.namespace, girClass, copied, []),
1778
+ );
1779
+ for (const m of filtered) {
1780
+ const memberLines = m.asString(this);
1781
+ if (memberLines.length > 0) {
1782
+ // Only tag as inherited if source is a different class
1783
+ if (source !== selfName) {
1784
+ injectInheritedTags(memberLines, source);
1785
+ }
1786
+ def.push(...memberLines);
1787
+ }
1788
+ }
1789
+ }
1821
1790
  }
1822
1791
  // END BODY
1823
1792
 
@@ -1901,126 +1870,6 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
1901
1870
  }
1902
1871
  }
1903
1872
 
1904
- async exportModuleIndexJS(): Promise<void> {
1905
- const template = "index.js";
1906
- const target = "index.js";
1907
-
1908
- if (this.config.outdir) {
1909
- await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
1910
- } else {
1911
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
1912
- this.log.log(append + prepend);
1913
- }
1914
- }
1915
-
1916
- async exportModuleIndexTS(): Promise<void> {
1917
- const template = "index.d.ts";
1918
- const target = "index.d.ts";
1919
-
1920
- if (this.config.outdir) {
1921
- await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
1922
- } else {
1923
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
1924
- this.log.log(append + prepend);
1925
- }
1926
- }
1927
-
1928
- async exportModuleJS(girModule: GirModule): Promise<void> {
1929
- const template = "module.js";
1930
- const target = `${girModule.importName}.js`;
1931
-
1932
- if (this.config.outdir) {
1933
- await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
1934
- } else {
1935
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
1936
- this.log.log(append + prepend);
1937
- }
1938
- }
1939
-
1940
- async exportModuleAmbientTS(girModule: GirModule): Promise<void> {
1941
- const template = "module-ambient.d.ts";
1942
- const target = `${girModule.importName}-ambient.d.ts`;
1943
-
1944
- if (this.config.outdir) {
1945
- await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
1946
- } else {
1947
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
1948
- this.log.log(append + prepend);
1949
- }
1950
- }
1951
-
1952
- protected async exportModuleAmbientJS(girModule: GirModule): Promise<void> {
1953
- const template = "module-ambient.js";
1954
- const target = `${girModule.importName}-ambient.js`;
1955
-
1956
- if (this.config.outdir) {
1957
- await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
1958
- } else {
1959
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
1960
- this.log.log(append + prepend);
1961
- }
1962
- }
1963
-
1964
- protected async exportModuleImportTS(girModule: GirModule): Promise<void> {
1965
- const template = "module-import.d.ts";
1966
- const target = `${girModule.importName}-import.d.ts`;
1967
-
1968
- if (this.config.outdir) {
1969
- await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
1970
- } else {
1971
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
1972
- this.log.log(append + prepend);
1973
- }
1974
- }
1975
-
1976
- protected async exportModuleImportJS(girModule: GirModule): Promise<void> {
1977
- const template = "module-import.js";
1978
- const target = `${girModule.importName}-import.js`;
1979
-
1980
- if (this.config.outdir) {
1981
- await this.moduleTemplateProcessor.create(template, this.config.outdir, target);
1982
- } else {
1983
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
1984
- this.log.log(append + prepend);
1985
- }
1986
- }
1987
-
1988
- async exportModuleTS(): Promise<void> {
1989
- const { namespace: girModule } = this;
1990
- const template = "module.d.ts";
1991
- const explicitTemplate = `${girModule.importName}.d.ts`;
1992
- const target = explicitTemplate;
1993
- const output = await this.generateModule(girModule);
1994
-
1995
- if (!output) {
1996
- this.log.error("Failed to generate gir module");
1997
- return;
1998
- }
1999
-
2000
- // Output is always an array now
2001
- const outputArray = output;
2002
-
2003
- // Extra interfaces if a template with the module name (e.g. '../templates/gobject-2-0.d.ts') is found
2004
- // E.g. used for GObject-2.0 to help define GObject classes in js;
2005
- // these aren't part of gi.
2006
- if (await this.moduleTemplateProcessor.exists(explicitTemplate)) {
2007
- const { append: appendExplicit, prepend: prependExplicit } =
2008
- await this.moduleTemplateProcessor.load(explicitTemplate);
2009
- outputArray.unshift(prependExplicit);
2010
- outputArray.push(appendExplicit);
2011
- }
2012
-
2013
- const { append, prepend } = await this.moduleTemplateProcessor.load(template);
2014
- outputArray.unshift(prepend);
2015
- outputArray.push(append);
2016
-
2017
- if (this.config.outdir) {
2018
- await this.moduleTemplateProcessor.write(outputArray.join("\n"), this.config.outdir, target);
2019
- } else {
2020
- this.log.log(outputArray.join("\n"));
2021
- }
2022
- }
2023
-
2024
1873
  async generateModule(girModule: GirModule): Promise<string[]> {
2025
1874
  const out: string[] = [];
2026
1875
 
@@ -2153,32 +2002,45 @@ export class ModuleGenerator extends FormatGenerator<string[]> {
2153
2002
  const result = await this.generateNamespace(girModule);
2154
2003
  return result.join("\n");
2155
2004
  }
2005
+ }
2156
2006
 
2157
- async exportModule(_registry: NSRegistry, girModule: GirModule) {
2158
- // Used for package.json and local ambient mode
2159
- await this.exportModuleTS();
2160
-
2161
- if (this.config.package) {
2162
- await this.exportModuleJS(girModule);
2163
-
2164
- await this.exportModuleIndexTS();
2165
- await this.exportModuleIndexJS();
2166
-
2167
- await this.exportModuleAmbientTS(girModule);
2168
- await this.exportModuleAmbientJS(girModule);
2169
-
2170
- await this.exportModuleImportTS(girModule);
2171
- await this.exportModuleImportJS(girModule);
2007
+ /**
2008
+ * Groups items by their source interface/class name (e.g. "Gtk.Accessible").
2009
+ * Must be called BEFORE copy({ parent: ... }) so the original parent is preserved.
2010
+ */
2011
+ function groupBySource<T extends { parent: { namespace: { namespace: string }; name: string } }>(
2012
+ items: T[],
2013
+ ): Map<string, T[]> {
2014
+ const groups = new Map<string, T[]>();
2015
+ for (const item of items) {
2016
+ const source = `${item.parent.namespace.namespace}.${item.parent.name}`;
2017
+ const list = groups.get(source);
2018
+ if (list) list.push(item);
2019
+ else groups.set(source, [item]);
2020
+ }
2021
+ return groups;
2022
+ }
2172
2023
 
2173
- const pkg = new NpmPackage(
2174
- this.config,
2175
- this.dependencyManager,
2176
- _registry,
2177
- girModule,
2178
- girModule.transitiveDependencies,
2179
- );
2180
- await pkg.exportNPMPackage();
2181
- }
2024
+ /**
2025
+ * Injects a `@category` TSDoc tag into generated member strings.
2026
+ * Places the member in a subcategory "Inherited from X" within its kind group,
2027
+ * so inherited members appear grouped after own members.
2028
+ */
2029
+ function injectInheritedTags(lines: string[], source: string): void {
2030
+ const category = `Inherited from ${source}`;
2031
+ // Search backwards — `*/` is typically on the last or second-to-last line
2032
+ let closingIdx = -1;
2033
+ for (let i = lines.length - 1; i >= 0; i--) {
2034
+ if (lines[i].trimEnd().endsWith("*/")) {
2035
+ closingIdx = i;
2036
+ break;
2037
+ }
2038
+ }
2039
+ if (closingIdx >= 0) {
2040
+ const indent = lines[closingIdx].match(/^(\s*)/)?.[1] ?? "";
2041
+ lines.splice(closingIdx, 0, `${indent} * @category ${category}`);
2042
+ } else {
2043
+ lines.unshift(`/** @category ${category} */`);
2182
2044
  }
2183
2045
  }
2184
2046
 
@@ -84,6 +84,9 @@ export class TypeDefinitionGenerator implements Generator {
84
84
  await templateProcessor.create("gjs/console.d.ts", config.outdir, "console.d.ts");
85
85
  await templateProcessor.create("gjs/console.js", config.outdir, "console.js");
86
86
 
87
+ await templateProcessor.create("gjs/gi.d.ts", config.outdir, "gi.d.ts");
88
+ await templateProcessor.create("gjs/gi.js", config.outdir, "gi.js");
89
+
87
90
  // Import ambient types
88
91
  await templateProcessor.create("gjs/gjs-ambient.d.ts", config.outdir, "gjs-ambient.d.ts");
89
92
  await templateProcessor.create("gjs/gjs-ambient.js", config.outdir, "gjs-ambient.js");
@@ -114,6 +117,9 @@ export class TypeDefinitionGenerator implements Generator {
114
117
  "console.d.ts",
115
118
  );
116
119
 
120
+ const giContent = await templateProcessor.load("gjs/gi.d.ts");
121
+ await templateProcessor.write(`${giContent.prepend}\n${giContent.append}`, config.outdir, "gi.d.ts");
122
+
117
123
  // Additional DOM types supported by GJS
118
124
  const domContent = await templateProcessor.load("gjs/dom.d.ts");
119
125
  await templateProcessor.write(`${domContent.prepend}\n${domContent.append}`, config.outdir, "dom.d.ts");
@@ -144,7 +150,7 @@ export class TypeDefinitionGenerator implements Generator {
144
150
 
145
151
  public async generate(module: GirModule) {
146
152
  const moduleGenerator = new ModuleGenerator(module, this.config, this.registry);
147
- await moduleGenerator.exportModule(this.registry, module);
153
+ await moduleGenerator.moduleExporter.exportModule(this.registry, module);
148
154
  }
149
155
 
150
156
  public async start() {