node-gtk 1.0.0 → 2.0.0

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.
@@ -0,0 +1,1045 @@
1
+ /*
2
+ * generate-types.js — PROTOTYPE
3
+ *
4
+ * Generates TypeScript declaration files (.d.ts) for GObject-Introspection
5
+ * namespaces, using node-gtk's *own* runtime introspection (the libgirepository
6
+ * API exposed at `require('node-gtk')._GIRepository`).
7
+ *
8
+ * Because we read the same typelib data and apply the same name/shape rules that
9
+ * lib/bootstrap.js applies at runtime, the emitted types match what node-gtk
10
+ * actually produces (camelCase methods, instance dropped from signal callbacks,
11
+ * etc.) — no GJS-vs-node-gtk guesswork.
12
+ *
13
+ * Usage:
14
+ * node-gtk generate-types Gtk-4.0 [More-X.Y ...] [--outdir DIR]
15
+ *
16
+ * Each requested namespace plus its full transitive dependency closure is
17
+ * emitted as `<Namespace>-<version>.d.ts`.
18
+ *
19
+ * STATUS: proof-of-concept. Known simplifications are marked `// LIMITATION`.
20
+ */
21
+
22
+ const fs = require('fs')
23
+ const path = require('path')
24
+ const camelCase = require('lodash.camelcase')
25
+
26
+ const gi = require('../lib/index.js')
27
+ const GI = gi._GIRepository
28
+ const T = GI.InfoType
29
+ const Tag = GI.TypeTag
30
+ const repo = GI.Repository_get_default()
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // thin wrappers over the GI API (mirrors the calling conventions in bootstrap.js)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const baseName = (i) => GI.BaseInfo_get_name.call(i)
37
+ const baseNamespace = (i) => GI.BaseInfo_get_namespace.call(i)
38
+ const baseType = (i) => GI.BaseInfo_get_type.call(i)
39
+ const isDeprecated = (i) => GI.BaseInfo_is_deprecated.call(i)
40
+
41
+ const Flags = GI.FunctionInfoFlags
42
+ const FieldFlags = GI.FieldInfoFlags
43
+
44
+ function each(info, nFn, getFn) {
45
+ const out = []
46
+ const n = nFn(info)
47
+ for (let i = 0; i < n; i++) out.push(getFn(info, i))
48
+ return out
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // name helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const RESERVED = new Set(['function', 'arguments', 'default', 'in', 'new', 'delete',
56
+ 'class', 'this', 'var', 'const', 'let', 'enum', 'export', 'import', 'void',
57
+ 'with', 'yield', 'case', 'do', 'switch', 'break', 'continue', 'return', 'for',
58
+ 'while', 'if', 'else', 'try', 'catch', 'finally', 'throw', 'typeof',
59
+ 'instanceof', 'extends', 'super', 'debugger', 'null', 'true', 'false'])
60
+
61
+ // For declaration names (classes, enums, functions, type aliases) and for
62
+ // parameter identifiers: reserved words are illegal, so suffix them.
63
+ function safeIdent(name) {
64
+ if (!name) return '_'
65
+ let n = name.replace(/[^A-Za-z0-9_$]/g, '_')
66
+ if (/^[0-9]/.test(n)) n = '_' + n
67
+ if (RESERVED.has(n)) n = n + '_'
68
+ return n
69
+ }
70
+
71
+ // For class/interface MEMBER names (methods, properties, fields, constants):
72
+ // reserved words ARE legal as members (`obj.default`), so don't mangle them;
73
+ // only quote names that aren't valid identifiers.
74
+ function memberName(name) {
75
+ if (!name) return '"_"'
76
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name)
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // documentation (from .gir XML — the compiled typelib does not carry docs)
81
+ // ---------------------------------------------------------------------------
82
+
83
+ // Doc-map keys. Names match GIR `name` attributes, i.e. node-gtk's baseName()
84
+ // (snake_case methods, dash-case properties), so the generator and parser agree.
85
+ const DocKey = {
86
+ type: (name) => `T\0${name}`,
87
+ fn: (container, name) => `M\0${container}\0${name}`, // method/ctor/static; '' container = top-level
88
+ prop: (container, name) => `P\0${container}\0${name}`,
89
+ signal: (container, name) => `S\0${container}\0${name}`,
90
+ field: (container, name) => `F\0${container}\0${name}`,
91
+ enumVal: (container, name) => `V\0${container}\0${name}`,
92
+ constant: (container, name) => `C\0${container}\0${name}`,
93
+ }
94
+
95
+ function girSearchDirs() {
96
+ const dirs = []
97
+ const xdg = process.env.XDG_DATA_DIRS || '/usr/local/share:/usr/share'
98
+ for (const d of xdg.split(':')) if (d) dirs.push(path.join(d, 'gir-1.0'))
99
+ dirs.push('/usr/share/gir-1.0', '/usr/local/share/gir-1.0')
100
+ return [...new Set(dirs)]
101
+ }
102
+
103
+ function findGir(ns, version) {
104
+ for (const d of girSearchDirs()) {
105
+ const p = path.join(d, `${ns}-${version}.gir`)
106
+ try { if (fs.statSync(p).isFile()) return p } catch (e) {}
107
+ }
108
+ return null
109
+ }
110
+
111
+ function unescapeXml(s) {
112
+ return s
113
+ .replace(/&lt;/g, '<').replace(/&gt;/g, '>')
114
+ .replace(/&quot;/g, '"').replace(/&apos;/g, "'")
115
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
116
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(+d))
117
+ .replace(/&amp;/g, '&') // last, to avoid double-unescaping
118
+ }
119
+
120
+ // Turn GTK-doc markup into something readable inside a JSDoc comment.
121
+ function cleanDoc(raw) {
122
+ let t = unescapeXml(raw)
123
+ t = t.replace(/\[(?:func|method|ctor|class|iface|struct|enum|flags|error|const|signal|property|callback|alias|vfunc|id)@([^\]]+)\]/g, '`$1`')
124
+ t = t.replace(/%(TRUE|FALSE|NULL)\b/g, (_, w) => '`' + w.toLowerCase() + '`')
125
+ t = t.replace(/%([A-Za-z_]\w*)/g, '`$1`')
126
+ t = t.replace(/#([A-Za-z_]\w*)/g, '`$1`')
127
+ t = t.replace(/\B@([A-Za-z_]\w*)/g, '`$1`')
128
+ t = t.replace(/\*\//g, '*\\/') // never terminate the enclosing comment
129
+ return t.trim()
130
+ }
131
+
132
+ // Minimal GIR scanner: walks the XML, attributing each <doc>/<doc-deprecated> to
133
+ // its enclosing element (always the most-recently-opened element), and parameter
134
+ // / return docs to their enclosing callable. Returns null if the .gir is absent
135
+ // (docs are best-effort; types still generate without them).
136
+ const TYPE_TAGS = new Set(['class', 'interface', 'record', 'union', 'enumeration', 'bitfield'])
137
+ const CALLABLE_TAGS = new Set(['method', 'constructor', 'function', 'glib:signal', 'callback', 'virtual-method'])
138
+
139
+ function loadGirDocs(ns, version) {
140
+ const file = findGir(ns, version)
141
+ if (!file) return null
142
+ let data
143
+ try { data = fs.readFileSync(file, 'utf8') } catch (e) { return null }
144
+
145
+ const docs = new Map(), deprecated = new Map()
146
+ const paramDocs = new Map(), returnDocs = new Map()
147
+ const stack = []
148
+ const nearestTypeName = (below) => { for (let j = below; j >= 0; j--) if (TYPE_TAGS.has(stack[j].tag)) return stack[j].name; return '' }
149
+ const nearestCallableKey = () => {
150
+ for (let j = stack.length - 1; j >= 0; j--) {
151
+ const f = stack[j]
152
+ if (CALLABLE_TAGS.has(f.tag)) {
153
+ const c = nearestTypeName(j - 1)
154
+ return f.tag === 'glib:signal' ? DocKey.signal(c, f.name) : DocKey.fn(c, f.name)
155
+ }
156
+ }
157
+ return null
158
+ }
159
+
160
+ const handleDoc = (tag, text) => {
161
+ const parent = stack[stack.length - 1]
162
+ if (!parent) return
163
+ if (tag === 'doc-deprecated') {
164
+ const k = symbolKey(parent, stack.length - 1)
165
+ if (k) deprecated.set(k, cleanDoc(text))
166
+ return
167
+ }
168
+ if (parent.tag === 'parameter') { // not instance-parameter (that's `this`)
169
+ const ck = nearestCallableKey()
170
+ if (ck && parent.name) { (paramDocs.get(ck) || paramDocs.set(ck, new Map()).get(ck)).set(parent.name, cleanDoc(text)) }
171
+ return
172
+ }
173
+ if (parent.tag === 'instance-parameter') return
174
+ if (parent.tag === 'return-value') {
175
+ const ck = nearestCallableKey()
176
+ if (ck) returnDocs.set(ck, cleanDoc(text))
177
+ return
178
+ }
179
+ const k = symbolKey(parent, stack.length - 1)
180
+ if (k) docs.set(k, cleanDoc(text))
181
+ }
182
+ const symbolKey = (frame, idx) => {
183
+ const c = nearestTypeName(idx - 1)
184
+ switch (frame.tag) {
185
+ case 'class': case 'interface': case 'record': case 'union':
186
+ case 'enumeration': case 'bitfield': case 'callback': return DocKey.type(frame.name)
187
+ case 'method': case 'constructor': case 'function': return DocKey.fn(c, frame.name)
188
+ case 'glib:signal': return DocKey.signal(c, frame.name)
189
+ case 'property': return DocKey.prop(c, frame.name)
190
+ case 'field': return DocKey.field(c, frame.name)
191
+ case 'member': return DocKey.enumVal(c, frame.name)
192
+ case 'constant': return DocKey.constant(c, frame.name)
193
+ default: return null
194
+ }
195
+ }
196
+
197
+ let i = 0, n = data.length
198
+ while (i < n) {
199
+ const lt = data.indexOf('<', i)
200
+ if (lt < 0) break
201
+ i = lt
202
+ if (data.startsWith('<!--', i)) { i = data.indexOf('-->', i) + 3; continue }
203
+ if (data.startsWith('<![CDATA[', i)) { i = data.indexOf(']]>', i) + 3; continue }
204
+ if (data.startsWith('<?', i)) { i = data.indexOf('?>', i) + 2; continue }
205
+ const gt = data.indexOf('>', i)
206
+ if (gt < 0) break
207
+ const raw = data.slice(i + 1, gt)
208
+ i = gt + 1
209
+ if (raw[0] === '/') { stack.pop(); continue }
210
+ const selfClose = raw.endsWith('/')
211
+ const body = selfClose ? raw.slice(0, -1) : raw
212
+ const sp = body.search(/\s/)
213
+ const tag = sp < 0 ? body : body.slice(0, sp)
214
+ if (tag === 'doc' || tag === 'doc-deprecated') {
215
+ const close = '</' + tag + '>'
216
+ const end = data.indexOf(close, i)
217
+ if (end < 0) break
218
+ handleDoc(tag, data.slice(i, end))
219
+ i = end + close.length
220
+ continue
221
+ }
222
+ if (selfClose) continue
223
+ const nm = /\bname="([^"]*)"/.exec(body)
224
+ stack.push({ tag, name: nm ? nm[1] : null })
225
+ }
226
+
227
+ return { docs, deprecated, paramDocs, returnDocs }
228
+ }
229
+
230
+ const oneLine = (s) => s.replace(/\s*\n\s*/g, ' ').trim()
231
+
232
+ // Render a JSDoc block (with trailing newline) for `key`, or '' if no doc.
233
+ // `opts.callable` pulls @param/@returns; `opts.deprecated` adds @deprecated.
234
+ function docBlock(ctx, key, indent, opts = {}) {
235
+ if (!ctx.doc) return opts.deprecated ? `${indent}/** @deprecated */\n` : ''
236
+ const d = ctx.doc
237
+ const summary = d.docs.get(key)
238
+ const depReason = d.deprecated.get(key)
239
+ const params = opts.callable ? d.paramDocs.get(key) : null
240
+ const ret = opts.callable ? d.returnDocs.get(key) : null
241
+ if (!summary && !depReason && !params && !ret && !opts.deprecated) return ''
242
+
243
+ const lines = summary ? summary.split('\n') : []
244
+ const tags = []
245
+ if (params) for (const [pn, pd] of params) if (pd) tags.push(`@param ${camelCase(pn)} ${oneLine(pd)}`)
246
+ if (ret) tags.push(`@returns ${oneLine(ret)}`)
247
+ if (opts.deprecated || depReason) tags.push(`@deprecated${depReason ? ' ' + oneLine(depReason) : ''}`)
248
+ if (lines.length && tags.length) lines.push('')
249
+ lines.push(...tags)
250
+
251
+ const out = [`${indent}/**`]
252
+ for (const l of lines) out.push(`${indent} *${l ? ' ' + l : ''}`)
253
+ out.push(`${indent} */`)
254
+ return out.join('\n') + '\n'
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // type resolution: GITypeInfo -> TypeScript type string
259
+ // ---------------------------------------------------------------------------
260
+
261
+ // `ctx` carries the namespace currently being generated + a set collecting the
262
+ // foreign namespaces we reference (so we can emit imports).
263
+ function resolveType(typeInfo, ctx) {
264
+ const tag = GI.type_info_get_tag(typeInfo)
265
+
266
+ switch (tag) {
267
+ case Tag.VOID:
268
+ return GI.type_info_is_pointer(typeInfo) ? 'any' : 'void'
269
+ case Tag.BOOLEAN:
270
+ return 'boolean'
271
+ case Tag.INT8: case Tag.UINT8: case Tag.INT16: case Tag.UINT16:
272
+ case Tag.INT32: case Tag.UINT32:
273
+ case Tag.FLOAT: case Tag.DOUBLE: case Tag.UNICHAR:
274
+ return 'number'
275
+ case Tag.INT64: case Tag.UINT64:
276
+ // node-gtk marshals 64-bit integers as BigInt for full precision
277
+ // (#323/#149). Params additionally accept number — handled at the param
278
+ // site in signature().
279
+ return 'bigint'
280
+ case Tag.GTYPE:
281
+ return 'bigint' // node-gtk represents GType as BigInt (see getGType)
282
+ case Tag.UTF8: case Tag.FILENAME:
283
+ return 'string'
284
+ case Tag.ARRAY: {
285
+ const elem = GI.type_info_get_param_type(typeInfo, 0)
286
+ const inner = elem ? resolveType(elem, ctx) : 'any'
287
+ return arrayWrap(inner)
288
+ }
289
+ case Tag.GLIST: case Tag.GSLIST: {
290
+ const elem = GI.type_info_get_param_type(typeInfo, 0)
291
+ return arrayWrap(elem ? resolveType(elem, ctx) : 'any')
292
+ }
293
+ case Tag.GHASH: {
294
+ const v = GI.type_info_get_param_type(typeInfo, 1)
295
+ return `Record<string, ${v ? resolveType(v, ctx) : 'any'}>`
296
+ }
297
+ case Tag.ERROR:
298
+ return qualify('GLib', 'Error', ctx)
299
+ case Tag.INTERFACE:
300
+ return resolveInterfaceType(typeInfo, ctx)
301
+ default:
302
+ return 'any'
303
+ }
304
+ }
305
+
306
+ function arrayWrap(inner) {
307
+ return /[^A-Za-z0-9_.$\[\]<>, ]/.test(inner) ? `Array<${inner}>` : `${inner}[]`
308
+ }
309
+
310
+ // A TYPE_TAG_INTERFACE references another registered info (object, struct,
311
+ // enum, callback, ...). Resolve to its qualified TS name.
312
+ function resolveInterfaceType(typeInfo, ctx) {
313
+ const iface = GI.type_info_get_interface(typeInfo)
314
+ if (!iface) return 'any'
315
+ const itype = baseType(iface)
316
+ const ns = baseNamespace(iface)
317
+ const name = baseName(iface)
318
+
319
+ switch (itype) {
320
+ case T.CALLBACK:
321
+ return callbackType(iface, ctx)
322
+ case T.STRUCT:
323
+ // gtype "class struct" (e.g. GObject.ObjectClass) is intentionally not
324
+ // emitted (bootstrap.js skips it); references resolve to `any`.
325
+ if (GI.struct_info_is_gtype_struct(iface)) return 'any'
326
+ return qualify(ns, safeIdent(name), ctx)
327
+ case T.OBJECT: case T.INTERFACE: case T.BOXED:
328
+ case T.UNION: case T.ENUM: case T.FLAGS:
329
+ return qualify(ns, safeIdent(name), ctx)
330
+ default:
331
+ return 'any'
332
+ }
333
+ }
334
+
335
+ // Expand a callback type to a TS function type. node-gtk invokes the JS callback
336
+ // with the native args positionally (callback.cc); a TS type with the same/typed
337
+ // params is assignable. Guard against deep self-referential callback nesting.
338
+ function callbackType(iface, ctx) {
339
+ if ((ctx.cbDepth || 0) > 3) return '(...args: any[]) => any'
340
+ ctx.cbDepth = (ctx.cbDepth || 0) + 1
341
+ try {
342
+ const sig = signature(iface, ctx, {})
343
+ return `(${sig.params}) => ${sig.ret}`
344
+ } catch (e) {
345
+ return '(...args: any[]) => any'
346
+ } finally {
347
+ ctx.cbDepth--
348
+ }
349
+ }
350
+
351
+ // Produce `Name` (same namespace) or `Ns.Name` (foreign), recording the import.
352
+ function qualify(ns, name, ctx) {
353
+ if (!ns) return name
354
+ if (ns === ctx.ns) return name
355
+ ctx.imports.add(ns)
356
+ return `${ns}.${name}`
357
+ }
358
+
359
+ // ---------------------------------------------------------------------------
360
+ // callables (functions / methods / constructors / signals / vfuncs)
361
+ // ---------------------------------------------------------------------------
362
+
363
+ const DIR = GI.Direction
364
+
365
+ // Mirrors src/function.cc: classify each arg, hide the ones node-gtk manages
366
+ // automatically (array-length args; a callback's user_data/GDestroyNotify), and
367
+ // model the return as node-gtk does — a tuple of
368
+ // [ C return (unless void/skip), ...each OUT/INOUT param ]
369
+ // where 0 values -> void, 1 -> the bare value, >1 -> a [tuple].
370
+ // Returns { params: string, ret: string }.
371
+ function signature(callable, ctx, { isConstructor = false, ownerName = null } = {}) {
372
+ const args = each(callable, GI.callable_info_get_n_args, GI.callable_info_get_arg)
373
+ const n = args.length
374
+ const dir = args.map(a => GI.arg_info_get_direction(a))
375
+ const types = args.map(a => GI.arg_info_get_type(a))
376
+ const kind = new Array(n).fill('NORMAL') // NORMAL | ARRAY | CALLBACK | SKIP
377
+
378
+ // classification pass (function.cc:157-238)
379
+ for (let i = 0; i < n; i++) {
380
+ if (kind[i] === 'SKIP') continue
381
+ const tag = GI.type_info_get_tag(types[i])
382
+
383
+ if (tag === Tag.ARRAY && GI.type_info_get_array_length(types[i]) >= 0) {
384
+ kind[i] = 'ARRAY'
385
+ kind[GI.type_info_get_array_length(types[i])] = 'SKIP' // length arg is hidden
386
+ } else if (tag === Tag.INTERFACE) {
387
+ const iface = GI.type_info_get_interface(types[i])
388
+ if (iface && baseType(iface) === T.CALLBACK) {
389
+ kind[i] = 'CALLBACK'
390
+ const destroyI = GI.arg_info_get_destroy(args[i])
391
+ const closureI = GI.arg_info_get_closure(args[i])
392
+ if (destroyI >= 0 && destroyI < n) kind[destroyI] = 'SKIP' // GDestroyNotify
393
+ if (closureI >= 0 && closureI < n) kind[closureI] = 'SKIP' // user_data
394
+ }
395
+ }
396
+ }
397
+
398
+ // JS input params: IN/INOUT, not hidden
399
+ const params = []
400
+ for (let i = 0; i < n; i++) {
401
+ if (kind[i] === 'SKIP' || dir[i] === DIR.OUT) continue
402
+ let t = resolveType(types[i], ctx)
403
+ // 64-bit integers come back as bigint, but the IN side also accepts number.
404
+ const tag = GI.type_info_get_tag(types[i])
405
+ if (tag === Tag.INT64 || tag === Tag.UINT64) t = 'number | bigint'
406
+ if (GI.arg_info_may_be_null(args[i])) t += ' | null'
407
+ params.push(`${safeIdent(camelCase(baseName(args[i]))) || `arg${i}`}: ${t}`)
408
+ }
409
+
410
+ if (isConstructor && ownerName)
411
+ return { params: params.join(', '), ret: ownerName }
412
+
413
+ // out-values, in node-gtk's order
414
+ const retType = GI.callable_info_get_return_type(callable)
415
+ const retLengthI = GI.type_info_get_array_length(retType)
416
+ const skipReturn =
417
+ GI.type_info_get_tag(retType) === Tag.VOID || GI.callable_info_skip_return(callable)
418
+
419
+ const outs = []
420
+ if (!skipReturn) {
421
+ let rt = resolveType(retType, ctx)
422
+ if (GI.callable_info_may_return_null(callable) && rt !== 'any') rt += ' | null'
423
+ outs.push(rt)
424
+ }
425
+ for (let i = 0; i < n; i++) {
426
+ if (i === retLengthI || kind[i] === 'SKIP' || kind[i] === 'CALLBACK') continue
427
+ if (dir[i] === DIR.OUT || dir[i] === DIR.INOUT) {
428
+ let t = resolveType(types[i], ctx)
429
+ if (GI.arg_info_may_be_null(args[i])) t += ' | null'
430
+ outs.push(t)
431
+ }
432
+ }
433
+
434
+ const ret = outs.length === 0 ? 'void'
435
+ : outs.length === 1 ? outs[0]
436
+ : `[${outs.join(', ')}]`
437
+ return { params: params.join(', '), ret }
438
+ }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // member emitters
442
+ // ---------------------------------------------------------------------------
443
+
444
+ // Returns a filter that dedups members within one class/interface body while
445
+ // PRESERVING method overloads: non-method members (properties/fields/constants)
446
+ // dedup by name (first wins); methods dedup by full signature so distinct
447
+ // overloads survive; a method name colliding with a non-method is dropped.
448
+ function makeMemberDedup() {
449
+ const nonMethod = new Set()
450
+ const methodNames = new Set()
451
+ const methodLines = new Set()
452
+ return (l) => {
453
+ const m = l.match(/^\s*(?:\/\*\*[\s\S]*?\*\/\s*)?(?:static\s+|readonly\s+)*("[^"]*"|[A-Za-z_$][\w$]*)(\s*\()?/)
454
+ if (!m) return true
455
+ const name = m[1], isMethod = !!m[2]
456
+ if (isMethod) {
457
+ if (nonMethod.has(name) || methodLines.has(l)) return false
458
+ methodLines.add(l); methodNames.add(name); return true
459
+ }
460
+ if (nonMethod.has(name) || methodNames.has(name)) return false
461
+ nonMethod.add(name); return true
462
+ }
463
+ }
464
+
465
+ // The signal/event API every GObject inherits from BaseClass (lib/bootstrap.js).
466
+ // Kept as [name, signature] so a subclass method that shadows one of these (e.g.
467
+ // Gio.SocketConnection.connect) can carry it as an overload and stay assignable.
468
+ const SIGNAL_API_INSTANCE = [
469
+ ['connect', '(signal: string, callback: (...args: any[]) => any, after?: boolean): number'],
470
+ ['disconnect', '(handlerId: number): void'],
471
+ ['on', '(signal: string, callback: (...args: any[]) => any, after?: boolean): this'],
472
+ ['once', '(signal: string, callback: (...args: any[]) => any, after?: boolean): this'],
473
+ ['off', '(signal: string, callback: (...args: any[]) => any): this'],
474
+ ['emit', '(signal: string, ...args: any[]): any'],
475
+ ]
476
+
477
+ // Walk ancestors + implemented interfaces, mapping method name -> set of emitted
478
+ // signatures `(params): ret`. Used to reconcile overrides: TS requires a
479
+ // subclass member to be assignable to the inherited one, so when a class's own
480
+ // method shadows an inherited method with a different signature we re-emit the
481
+ // inherited signature(s) as extra overloads. Mirrors what works at runtime,
482
+ // where the JS method simply shadows the inherited one.
483
+ function collectInheritedMethods(info, ctx) {
484
+ const instance = new Map(), statics = new Map()
485
+ const add = (map, k, s) => { (map.get(k) || map.set(k, new Set()).get(k)).add(s) }
486
+
487
+ if (GI.object_info_get_parent(info))
488
+ for (const [n, s] of SIGNAL_API_INSTANCE) add(instance, n, s)
489
+
490
+ const addMethods = (klass, ownerName) => {
491
+ for (const m of each(klass, GI.object_info_get_n_methods, GI.object_info_get_method)) {
492
+ try {
493
+ const flags = GI.function_info_get_flags(m)
494
+ const isMethod = (flags & Flags.IS_METHOD) !== 0 && (flags & Flags.IS_CONSTRUCTOR) === 0
495
+ const isCtor = (flags & Flags.IS_CONSTRUCTOR) !== 0
496
+ const sig = signature(m, ctx, { isConstructor: isCtor, ownerName })
497
+ add(isMethod ? instance : statics, memberName(camelCase(baseName(m))), `(${sig.params}): ${sig.ret}`)
498
+ } catch (e) {}
499
+ }
500
+ }
501
+ const addIfaceMethods = (klass) => {
502
+ for (const iface of each(klass, GI.object_info_get_n_interfaces, GI.object_info_get_interface))
503
+ for (const m of each(iface, GI.interface_info_get_n_methods, GI.interface_info_get_method)) {
504
+ try {
505
+ const sig = signature(m, ctx, {})
506
+ add(instance, memberName(camelCase(baseName(m))), `(${sig.params}): ${sig.ret}`)
507
+ } catch (e) {}
508
+ }
509
+ }
510
+
511
+ addIfaceMethods(info) // own interfaces (merged into the class type)
512
+ let p = GI.object_info_get_parent(info)
513
+ while (p && baseType(p) === T.OBJECT) {
514
+ // qualify so an inherited constructor's return type (ownerName) is valid
515
+ // across namespaces (e.g. Gtk.NumerableIcon extends Gio.EmblemedIcon).
516
+ addMethods(p, qualify(baseNamespace(p), safeIdent(baseName(p)), ctx))
517
+ addIfaceMethods(p)
518
+ p = GI.object_info_get_parent(p)
519
+ }
520
+ return { instance, statics }
521
+ }
522
+
523
+ // Like collectInheritedMethods but for an interface: gather methods from its
524
+ // prerequisites (objects walked as classes, interfaces walked recursively) so
525
+ // emitInterface can reconcile members it shadows (e.g. ToolShell.getStyle vs
526
+ // Widget.getStyle).
527
+ function collectInterfaceInheritedMethods(info, ctx) {
528
+ const instance = new Map()
529
+ const add = (k, s) => { (instance.get(k) || instance.set(k, new Set()).get(k)).add(s) }
530
+ const visited = new Set()
531
+
532
+ const addObj = (klass) => {
533
+ let p = klass
534
+ while (p && baseType(p) === T.OBJECT) {
535
+ for (const m of each(p, GI.object_info_get_n_methods, GI.object_info_get_method)) {
536
+ try {
537
+ const flags = GI.function_info_get_flags(m)
538
+ if ((flags & Flags.IS_METHOD) === 0 || (flags & Flags.IS_CONSTRUCTOR) !== 0) continue
539
+ const sig = signature(m, ctx, {})
540
+ add(memberName(camelCase(baseName(m))), `(${sig.params}): ${sig.ret}`)
541
+ } catch (e) {}
542
+ }
543
+ for (const iface of each(p, GI.object_info_get_n_interfaces, GI.object_info_get_interface)) visitIface(iface)
544
+ p = GI.object_info_get_parent(p)
545
+ }
546
+ }
547
+ function visitIface(iface) {
548
+ const key = baseNamespace(iface) + '.' + baseName(iface)
549
+ if (visited.has(key)) return
550
+ visited.add(key)
551
+ for (const m of each(iface, GI.interface_info_get_n_methods, GI.interface_info_get_method)) {
552
+ try { const sig = signature(m, ctx, {}); add(memberName(camelCase(baseName(m))), `(${sig.params}): ${sig.ret}`) } catch (e) {}
553
+ }
554
+ for (const pr of each(iface, GI.interface_info_get_n_prerequisites, GI.interface_info_get_prerequisite)) {
555
+ if (baseType(pr) === T.OBJECT) addObj(pr); else if (baseType(pr) === T.INTERFACE) visitIface(pr)
556
+ }
557
+ }
558
+
559
+ for (const pr of each(info, GI.interface_info_get_n_prerequisites, GI.interface_info_get_prerequisite)) {
560
+ if (baseType(pr) === T.OBJECT) addObj(pr); else if (baseType(pr) === T.INTERFACE) visitIface(pr)
561
+ }
562
+ for (const [n, s] of SIGNAL_API_INSTANCE) add(n, s)
563
+ return { instance, statics: new Map() }
564
+ }
565
+
566
+ function emitMethods(info, nFn, getFn, ctx, ownerName, inherited) {
567
+ const lines = []
568
+ for (const m of each(info, nFn, getFn)) {
569
+ try {
570
+ const flags = GI.function_info_get_flags(m)
571
+ const isMethod = (flags & Flags.IS_METHOD) !== 0 && (flags & Flags.IS_CONSTRUCTOR) === 0
572
+ const isCtor = (flags & Flags.IS_CONSTRUCTOR) !== 0
573
+ const isStatic = !isMethod // ctor or static
574
+ const rawName = baseName(m)
575
+ const name = memberName(camelCase(rawName))
576
+ const sig = signature(m, ctx, { isConstructor: isCtor, ownerName })
577
+ const decl = `(${sig.params}): ${sig.ret}`
578
+ const kw = isStatic ? 'static ' : ''
579
+
580
+ // override reconciliation: carry differing inherited signatures as overloads
581
+ if (inherited) {
582
+ const inh = (isStatic ? inherited.statics : inherited.instance).get(name)
583
+ if (inh) for (const s of inh) if (s !== decl) lines.push(` ${kw}${name}${s}`)
584
+ }
585
+ const doc = docBlock(ctx, DocKey.fn(ownerName || '', rawName), ' ', { callable: true, deprecated: isDeprecated(m) })
586
+ lines.push(`${doc} ${kw}${name}${decl}`)
587
+ } catch (e) { /* skip unrepresentable member */ }
588
+ }
589
+ return lines
590
+ }
591
+
592
+ function emitProperties(info, nFn, getFn, ctx, inherited, containerName) {
593
+ const lines = []
594
+ const writable = []
595
+ for (const p of each(info, nFn, getFn)) {
596
+ try {
597
+ const rawName = baseName(p)
598
+ const name = memberName(camelCase(rawName))
599
+ let t = resolveType(GI.property_info_get_type(p), ctx)
600
+ const flags = GI.property_info_get_flags(p)
601
+ // GParamFlags: READABLE=1, WRITABLE=2, CONSTRUCT=4, CONSTRUCT_ONLY=8
602
+ const isWritable = (flags & 2) !== 0
603
+ // A property whose name shadows an inherited METHOD (e.g. GTK3
604
+ // AppChooserWidget.show-all vs Widget.show_all()) is irreconcilable as a
605
+ // plain field. node-gtk's accessor wins at runtime; intersect with a
606
+ // callable so the declaration stays assignable to the inherited method.
607
+ if (inherited && inherited.instance.has(name)) t = `${t} & ((...args: any[]) => any)`
608
+ const doc = docBlock(ctx, DocKey.prop(containerName || '', rawName), ' ')
609
+ lines.push(`${doc} ${isWritable ? '' : 'readonly '}${name}: ${t}`)
610
+ if (isWritable) writable.push({ name, t })
611
+ } catch (e) {}
612
+ }
613
+ return { lines, writable }
614
+ }
615
+
616
+ function emitFields(info, nFn, getFn, ctx, containerName) {
617
+ const lines = []
618
+ for (const f of each(info, nFn, getFn)) {
619
+ try {
620
+ const rawName = baseName(f)
621
+ const name = memberName(camelCase(rawName))
622
+ const t = resolveType(GI.field_info_get_type(f), ctx)
623
+ const flags = GI.field_info_get_flags(f)
624
+ const writable = (flags & FieldFlags.WRITABLE) !== 0
625
+ const doc = docBlock(ctx, DocKey.field(containerName || '', rawName), ' ')
626
+ lines.push(`${doc} ${writable ? '' : 'readonly '}${name}: ${t}`)
627
+ } catch (e) {}
628
+ }
629
+ return lines
630
+ }
631
+
632
+ function collectSignalsFrom(info, nFn, getFn, ctx, seen, out) {
633
+ for (const s of each(info, nFn, getFn)) {
634
+ try {
635
+ const rawName = baseName(s)
636
+ if (seen.has(rawName)) continue
637
+ seen.add(rawName)
638
+ const sig = signature(s, ctx, {})
639
+ out.push({ rawName, params: sig.params, ret: sig.ret, container: baseName(info) })
640
+ } catch (e) {}
641
+ }
642
+ }
643
+
644
+ // own signals only
645
+ function collectSignals(info, ctx) {
646
+ const out = []
647
+ collectSignalsFrom(info, GI.object_info_get_n_signals, GI.object_info_get_signal, ctx, new Set(), out)
648
+ return out
649
+ }
650
+
651
+ // all signals reachable: self + ancestors + every implemented interface. Used so
652
+ // a class that merges interfaces can declare a single `on` that is a superset of
653
+ // (and therefore assignable to) each base's `on`, resolving multiple-inheritance
654
+ // conflicts (TS2320).
655
+ function collectAllSignals(info, ctx) {
656
+ const seen = new Set(), out = []
657
+ let k = info
658
+ while (k && baseType(k) === T.OBJECT) {
659
+ collectSignalsFrom(k, GI.object_info_get_n_signals, GI.object_info_get_signal, ctx, seen, out)
660
+ for (const iface of each(k, GI.object_info_get_n_interfaces, GI.object_info_get_interface))
661
+ collectSignalsFrom(iface, GI.interface_info_get_n_signals, GI.interface_info_get_signal, ctx, seen, out)
662
+ k = GI.object_info_get_parent(k)
663
+ }
664
+ return out
665
+ }
666
+
667
+ // typed on()/once()/off()/emit() overloads (node-gtk EventEmitter style)
668
+ function renderSignals(sigs, ctx) {
669
+ if (sigs.length === 0) return []
670
+ const lines = []
671
+ for (const verb of ['on', 'once']) {
672
+ for (const s of sigs) {
673
+ // node-gtk drops the emitting instance from the callback args (issue #21).
674
+ // Attach the signal's doc to the `on` overload (skip `once` to avoid dupes).
675
+ const doc = verb === 'on' && s.container ? docBlock(ctx, DocKey.signal(s.container, s.rawName), ' ') : ''
676
+ lines.push(`${doc} ${verb}(signal: ${JSON.stringify(s.rawName)}, callback: (${s.params}) => ${s.ret}, after?: boolean): this`)
677
+ }
678
+ lines.push(` ${verb}(signal: string, callback: (...args: any[]) => any, after?: boolean): this`)
679
+ }
680
+ lines.push(` off(signal: string, callback: (...args: any[]) => any): this`)
681
+ lines.push(` emit(signal: string, ...args: any[]): any`)
682
+ return lines
683
+ }
684
+
685
+ // ---------------------------------------------------------------------------
686
+ // top-level declaration emitters
687
+ // ---------------------------------------------------------------------------
688
+
689
+ // Walk the class + ancestors + implemented interfaces, collecting writable
690
+ // (settable at construction) properties. Child declarations win over inherited.
691
+ function collectConstructProps(info, ctx) {
692
+ const props = new Map()
693
+ const addFrom = (list) => {
694
+ for (const p of list) {
695
+ try {
696
+ if ((GI.property_info_get_flags(p) & 2) === 0) continue // writable only
697
+ const name = memberName(camelCase(baseName(p)))
698
+ if (props.has(name)) continue
699
+ props.set(name, resolveType(GI.property_info_get_type(p), ctx))
700
+ } catch (e) {}
701
+ }
702
+ }
703
+ let cur = info
704
+ while (cur && baseType(cur) === T.OBJECT) {
705
+ addFrom(each(cur, GI.object_info_get_n_properties, GI.object_info_get_property))
706
+ for (const iface of each(cur, GI.object_info_get_n_interfaces, GI.object_info_get_interface))
707
+ addFrom(each(iface, GI.interface_info_get_n_properties, GI.interface_info_get_property))
708
+ cur = GI.object_info_get_parent(cur)
709
+ }
710
+ return props
711
+ }
712
+
713
+ function emitObject(info, ctx) {
714
+ const name = safeIdent(baseName(info))
715
+ const parent = GI.object_info_get_parent(info)
716
+ const parentRef = parent ? qualify(baseNamespace(parent), safeIdent(baseName(parent)), ctx) : null
717
+
718
+ // interfaces implemented -> declaration-merged into the class type
719
+ const ifaces = each(info, GI.object_info_get_n_interfaces, GI.object_info_get_interface)
720
+ .map(i => qualify(baseNamespace(i), safeIdent(baseName(i)), ctx))
721
+
722
+ const hasIfaces = ifaces.length > 0
723
+ const inherited = collectInheritedMethods(info, ctx)
724
+ const props = emitProperties(info, GI.object_info_get_n_properties, GI.object_info_get_property, ctx, inherited, name)
725
+ const methods = emitMethods(info, GI.object_info_get_n_methods, GI.object_info_get_method, ctx, name, inherited)
726
+ // When the class merges interfaces, declare the signal API in the companion
727
+ // interface (unified across the whole hierarchy) instead of the class body.
728
+ const signals = hasIfaces ? [] : renderSignals(collectSignals(info, ctx), ctx)
729
+ const constants = each(info, GI.object_info_get_n_constants, GI.object_info_get_constant)
730
+ .map(c => { try { return `${docBlock(ctx, DocKey.constant(name, baseName(c)), ' ')} static readonly ${memberName(baseName(c))}: ${resolveType(GI.constant_info_get_type(c), ctx)}` } catch { return null } })
731
+ .filter(Boolean)
732
+
733
+ // constructor property bag: writable props from this class, its ancestors, and
734
+ // its implemented interfaces (e.g. Orientable.orientation), camelCase (#320).
735
+ const ctor = collectConstructProps(info, ctx)
736
+ const ctorProps = ctor.size
737
+ ? `{ ${[...ctor].map(([n, t]) => `${n}?: ${t}`).join(', ')} }`
738
+ : '{}'
739
+
740
+ // The synthetic signal API is declared once on root classes; subclasses
741
+ // inherit it (and reconcile via collectInheritedMethods when they shadow it).
742
+ const signalApi = []
743
+ if (!parentRef) {
744
+ for (const [n, s] of SIGNAL_API_INSTANCE) signalApi.push(` ${n}${s}`)
745
+ signalApi.push(` readonly __gtype__: bigint`)
746
+ }
747
+
748
+ const dedup = makeMemberDedup()
749
+ const body = [...constants, ...props.lines, ...methods, ...signals, ...signalApi].filter(dedup)
750
+
751
+ const out = []
752
+ const ext = parentRef ? ` extends ${parentRef}` : ''
753
+ out.push(`${docBlock(ctx, DocKey.type(name), '', { deprecated: isDeprecated(info) })}export class ${name}${ext} {`)
754
+ out.push(` constructor(properties?: ${ctorProps})`)
755
+ out.push(...body)
756
+ out.push(`}`)
757
+
758
+ // Companion interface: declaration-merges the implemented interfaces, and
759
+ // resolves multiple-inheritance conflicts (TS2320) by declaring a unified,
760
+ // assignable-to-all version of any member two bases disagree on:
761
+ // - `on`/`once`/... unified across the whole signal hierarchy
762
+ // - real methods present on >1 base with differing signatures, as overloads
763
+ if (hasIfaces) {
764
+ const ownMethodNames = new Set(each(info, GI.object_info_get_n_methods, GI.object_info_get_method)
765
+ .map(m => { try { return memberName(camelCase(baseName(m))) } catch { return null } }))
766
+ const reserved = new Set(SIGNAL_API_INSTANCE.map(([n]) => n))
767
+
768
+ const cdedup = makeMemberDedup()
769
+ const cbody = renderSignals(collectAllSignals(info, ctx), ctx).filter(cdedup)
770
+ for (const [mname, sigSet] of inherited.instance) {
771
+ if (sigSet.size < 2 || ownMethodNames.has(mname) || reserved.has(mname)) continue
772
+ for (const s of sigSet) { const line = ` ${mname}${s}`; if (cdedup(line)) cbody.push(line) }
773
+ }
774
+
775
+ out.push(`export interface ${name} extends ${ifaces.join(', ')} {${cbody.length ? '\n' + cbody.join('\n') + '\n' : ''}}`)
776
+ }
777
+
778
+ return out.join('\n')
779
+ }
780
+
781
+ function emitInterface(info, ctx) {
782
+ const name = safeIdent(baseName(info))
783
+ const prereqs = each(info, GI.interface_info_get_n_prerequisites, GI.interface_info_get_prerequisite)
784
+ .map(p => qualify(baseNamespace(p), safeIdent(baseName(p)), ctx))
785
+ .filter(r => !r.endsWith('.Object') && r !== 'Object') // avoid trivial cycles in prototype
786
+
787
+ const inherited = collectInterfaceInheritedMethods(info, ctx)
788
+ const props = emitProperties(info, GI.interface_info_get_n_properties, GI.interface_info_get_property, ctx, inherited, name)
789
+ const methods = emitMethods(info, GI.interface_info_get_n_methods, GI.interface_info_get_method, ctx, name, inherited)
790
+
791
+ const ext = prereqs.length ? ` extends ${prereqs.join(', ')}` : ''
792
+ const dedup = makeMemberDedup()
793
+ const instanceLines = methods.filter(l => !l.includes('static '))
794
+ const out = []
795
+ out.push(`${docBlock(ctx, DocKey.type(name), '', { deprecated: isDeprecated(info) })}export interface ${name}${ext} {`)
796
+ out.push(...[...props.lines, ...instanceLines].filter(dedup))
797
+ out.push(`}`)
798
+
799
+ // node-gtk exposes an interface as a runtime value carrying its constructor
800
+ // functions (e.g. Gio.File.newForPath) and constants. Emit a same-named const
801
+ // (coexists with the interface type) so the name is usable as a value too. An
802
+ // object-type member may be named `new`/`default` etc., unlike a namespace fn.
803
+ const ndedup = makeMemberDedup()
804
+ const statics = methods
805
+ .filter(l => l.includes('static '))
806
+ .map(l => l.replace(/(^|\n)(\s*)static /, '$1$2')) // drop `static ` keyword (doc block preserved)
807
+ const constants = each(info, GI.interface_info_get_n_constants, GI.interface_info_get_constant)
808
+ .map(c => { try {
809
+ return ` ${memberName(baseName(c))}: ${resolveType(GI.constant_info_get_type(c), ctx)}`
810
+ } catch (e) { return null } })
811
+ .filter(Boolean)
812
+ const valueLines = [...statics, ...constants].filter(ndedup)
813
+ if (valueLines.length)
814
+ out.push(`export const ${name}: {\n${valueLines.join('\n')}\n}`)
815
+
816
+ return out.join('\n')
817
+ }
818
+
819
+ function emitStruct(info, ctx, kind) {
820
+ const name = safeIdent(baseName(info))
821
+ const nFieldsFn = kind === 'union' ? GI.union_info_get_n_fields : GI.struct_info_get_n_fields
822
+ const getFieldFn = kind === 'union' ? GI.union_info_get_field : GI.struct_info_get_field
823
+ const nMethFn = kind === 'union' ? GI.union_info_get_n_methods : GI.struct_info_get_n_methods
824
+ const getMethFn = kind === 'union' ? GI.union_info_get_method : GI.struct_info_get_method
825
+
826
+ const fields = emitFields(info, nFieldsFn, getFieldFn, ctx, name)
827
+ const methods = emitMethods(info, nMethFn, getMethFn, ctx, name)
828
+
829
+ const dedup = makeMemberDedup()
830
+ const out = []
831
+ out.push(`${docBlock(ctx, DocKey.type(name), '', { deprecated: isDeprecated(info) })}export class ${name} {`)
832
+ out.push(` constructor(fields?: { [key: string]: any })`)
833
+ out.push(...[...fields, ...methods].filter(dedup))
834
+ out.push(`}`)
835
+ return out.join('\n')
836
+ }
837
+
838
+ function emitEnum(info, ctx) {
839
+ const name = safeIdent(baseName(info))
840
+ const out = []
841
+ out.push(`${docBlock(ctx, DocKey.type(name), '', { deprecated: isDeprecated(info) })}export enum ${name} {`)
842
+ for (const v of each(info, GI.enum_info_get_n_values, GI.enum_info_get_value)) {
843
+ const rawV = baseName(v)
844
+ const vname = safeIdent(rawV.toUpperCase())
845
+ const value = GI.value_info_get_value(v)
846
+ out.push(`${docBlock(ctx, DocKey.enumVal(name, rawV), ' ')} ${vname} = ${value},`)
847
+ }
848
+ out.push(`}`)
849
+
850
+ // node-gtk attaches enum methods to the enum object (bootstrap.js makeEnum);
851
+ // surface them via a declaration-merged namespace.
852
+ const methods = each(info, GI.enum_info_get_n_methods, GI.enum_info_get_method)
853
+ .map(m => { try {
854
+ const sig = signature(m, ctx, {})
855
+ return ` export function ${memberName(camelCase(baseName(m)))}(${sig.params}): ${sig.ret}`
856
+ } catch (e) { return null } })
857
+ .filter(Boolean)
858
+ if (methods.length)
859
+ out.push(`export namespace ${name} {\n${methods.join('\n')}\n}`)
860
+
861
+ return out.join('\n')
862
+ }
863
+
864
+ function emitFunction(info, ctx) {
865
+ const rawName = baseName(info)
866
+ const name = safeIdent(camelCase(rawName))
867
+ const sig = signature(info, ctx, {})
868
+ const doc = docBlock(ctx, DocKey.fn('', rawName), '', { callable: true, deprecated: isDeprecated(info) })
869
+ return `${doc}export function ${name}(${sig.params}): ${sig.ret}`
870
+ }
871
+
872
+ function emitConstant(info, ctx) {
873
+ const rawName = baseName(info)
874
+ const name = safeIdent(rawName)
875
+ const t = resolveType(GI.constant_info_get_type(info), ctx)
876
+ const doc = docBlock(ctx, DocKey.constant('', rawName), '', { deprecated: isDeprecated(info) })
877
+ return `${doc}export const ${name}: ${t}`
878
+ }
879
+
880
+ function emitCallback(info, ctx) {
881
+ const name = safeIdent(baseName(info))
882
+ const sig = signature(info, ctx, {})
883
+ const doc = docBlock(ctx, DocKey.type(name), '', { deprecated: isDeprecated(info) })
884
+ return `${doc}export type ${name} = (${sig.params}) => ${sig.ret}`
885
+ }
886
+
887
+ // ---------------------------------------------------------------------------
888
+ // namespace driver
889
+ // ---------------------------------------------------------------------------
890
+
891
+ function generateNamespace(ns, version) {
892
+ GI.Repository_require.call(repo, ns, version || null, 0)
893
+ version = version || GI.Repository_get_version.call(repo, ns)
894
+
895
+ const ctx = { ns, imports: new Set(), doc: DOCS_ENABLED ? loadGirDocs(ns, version) : null }
896
+ const decls = []
897
+ const n = GI.Repository_get_n_infos.call(repo, ns)
898
+
899
+ for (let i = 0; i < n; i++) {
900
+ const info = GI.Repository_get_info.call(repo, ns, i)
901
+ try {
902
+ switch (baseType(info)) {
903
+ case T.OBJECT: decls.push(emitObject(info, ctx)); break
904
+ case T.INTERFACE: decls.push(emitInterface(info, ctx)); break
905
+ case T.STRUCT:
906
+ if (GI.struct_info_is_gtype_struct(info)) break
907
+ decls.push(emitStruct(info, ctx, 'struct')); break
908
+ case T.BOXED: decls.push(emitStruct(info, ctx, 'struct')); break
909
+ case T.UNION: decls.push(emitStruct(info, ctx, 'union')); break
910
+ case T.ENUM: case T.FLAGS: decls.push(emitEnum(info, ctx)); break
911
+ case T.FUNCTION: decls.push(emitFunction(info, ctx)); break
912
+ case T.CONSTANT: decls.push(emitConstant(info, ctx)); break
913
+ case T.CALLBACK: decls.push(emitCallback(info, ctx)); break
914
+ }
915
+ } catch (e) {
916
+ decls.push(`// SKIPPED ${baseName(info)}: ${e.message}`)
917
+ }
918
+ }
919
+
920
+ const deps = GI.Repository_get_dependencies.call(repo, ns, version) || []
921
+ const depVersion = {}
922
+ for (const d of deps) {
923
+ const [dn, dv] = d.split('-')
924
+ depVersion[dn] = dv
925
+ }
926
+
927
+ const header = [
928
+ `// AUTO-GENERATED by tools/generate-types.js — node-gtk TypeScript prototype`,
929
+ `// Namespace: ${ns}-${version}`,
930
+ ``,
931
+ ]
932
+ const imports = [...ctx.imports]
933
+ .filter(dep => dep !== ns)
934
+ // `.js` extension: required under moduleResolution node16/nodenext (and fine
935
+ // for bundler); resolves to the sibling `.d.ts`.
936
+ .map(dep => `import type * as ${dep} from './${dep}-${depVersion[dep] || '*'}.js'`)
937
+ if (imports.length) { header.push(...imports, '') }
938
+
939
+ return { version, deps, body: header.join('\n') + decls.join('\n\n') + '\n' }
940
+ }
941
+
942
+ // The hand-written part of the module shim: node-gtk's own static API. The
943
+ // `require()` overloads are generated per-namespace and prepended to this.
944
+ const SHIM_STATIC_API = ` export function isLoaded(ns: string, version?: string): boolean
945
+ export function prependSearchPath(path: string): void
946
+ export function prependLibraryPath(path: string): void
947
+ export function listAvailableModules(): Promise<{ name: string, version: string }[]>
948
+ export function registerClass(klass: Function): Function
949
+ export function startLoop(): void
950
+ export function getGType(value: Function | object | bigint): bigint
951
+ export const System: any`
952
+
953
+ // Emit `node-gtk.d.ts` next to the generated namespaces. It overloads
954
+ // gi.require() so `gi.require('Gtk','4.0')` resolves to the matching namespace.
955
+ function writeShim(outdir, nsVersions) {
956
+ const relDir = path.relative(process.cwd(), outdir).split(path.sep).join('/') || '.'
957
+ const lines = []
958
+ lines.push(`// AUTO-GENERATED by \`node-gtk generate-types\` — module shim for node-gtk.`)
959
+ lines.push(`// Point your tsconfig at this file:`)
960
+ lines.push(`// "paths": { "node-gtk": ["./${relDir}/node-gtk.d.ts"] }`)
961
+ lines.push(``)
962
+ lines.push(`declare module 'node-gtk' {`)
963
+ for (const [ns, version] of nsVersions) {
964
+ lines.push(` export function require(ns: ${JSON.stringify(ns)}, version: ${JSON.stringify(version)}): typeof import('./${ns}-${version}.js')`)
965
+ }
966
+ lines.push(` export function require(ns: string, version?: string): any`)
967
+ lines.push(SHIM_STATIC_API)
968
+ lines.push(`}`)
969
+ fs.writeFileSync(path.join(outdir, 'node-gtk.d.ts'), lines.join('\n') + '\n')
970
+ }
971
+
972
+ // Generate the requested namespaces plus their full dependency closure, then the
973
+ // module shim. Returns the map of generated namespace -> version.
974
+ function generate(roots, outdir) {
975
+ fs.mkdirSync(outdir, { recursive: true })
976
+ const queue = roots.map(r => r.split('-'))
977
+ const nsVersions = new Map() // ns -> resolved version
978
+
979
+ while (queue.length) {
980
+ const [ns, version] = queue.shift()
981
+ if (nsVersions.has(ns)) continue
982
+
983
+ process.stderr.write(`generating ${ns}-${version || '(latest)'} ...\n`)
984
+ const { version: v, deps, body } = generateNamespace(ns, version)
985
+ nsVersions.set(ns, v)
986
+ fs.writeFileSync(path.join(outdir, `${ns}-${v}.d.ts`), body)
987
+
988
+ for (const d of deps) {
989
+ const [dn, dv] = d.split('-')
990
+ if (!nsVersions.has(dn)) queue.push([dn, dv])
991
+ }
992
+ }
993
+
994
+ writeShim(outdir, nsVersions)
995
+ process.stderr.write(`\nwrote ${nsVersions.size} namespace(s) + node-gtk.d.ts to ${outdir}\n`)
996
+ return nsVersions
997
+ }
998
+
999
+ // ---------------------------------------------------------------------------
1000
+ // cli — `node-gtk generate-types <Namespace-Version> [...] [--outdir DIR]`
1001
+ // ---------------------------------------------------------------------------
1002
+
1003
+ // Default output is hidden inside node_modules: it's a generated cache (per
1004
+ // machine / per installed library versions), so it doesn't belong in the repo.
1005
+ const DEFAULT_OUTDIR = ['node_modules', '.node-gtk-types']
1006
+
1007
+ // Doc comments are pulled from .gir XML; toggled off with --no-docs.
1008
+ let DOCS_ENABLED = true
1009
+
1010
+ const USAGE = `Usage: node-gtk generate-types <Namespace-Version> [...] [options]
1011
+
1012
+ Generates TypeScript declarations for the given GObject-Introspection
1013
+ namespaces (plus their dependency closure) from the typelibs installed on
1014
+ THIS machine, and a node-gtk.d.ts module shim.
1015
+
1016
+ Options:
1017
+ --outdir DIR output directory (default: ./node_modules/.node-gtk-types)
1018
+ --no-docs omit JSDoc comments (smaller output; docs come from .gir XML)
1019
+
1020
+ Examples:
1021
+ node-gtk generate-types Gtk-4.0
1022
+ node-gtk generate-types Gtk-3.0 Gio-2.0 --outdir ./some/dir
1023
+
1024
+ Then in tsconfig.json:
1025
+ { "compilerOptions": {
1026
+ "skipLibCheck": true,
1027
+ "paths": { "node-gtk": ["./node_modules/.node-gtk-types/node-gtk.d.ts"] } } }`
1028
+
1029
+ function run(argv) {
1030
+ let outdir = path.join(process.cwd(), ...DEFAULT_OUTDIR)
1031
+ const roots = []
1032
+ for (let i = 0; i < argv.length; i++) {
1033
+ if (argv[i] === '--outdir') { outdir = path.resolve(argv[++i]); continue }
1034
+ if (argv[i] === '--no-docs') { DOCS_ENABLED = false; continue }
1035
+ if (argv[i] === '-h' || argv[i] === '--help') { console.log(USAGE); return }
1036
+ roots.push(argv[i])
1037
+ }
1038
+ if (roots.length === 0) { console.error(USAGE); process.exit(1) }
1039
+ generate(roots, outdir)
1040
+ }
1041
+
1042
+ module.exports = { generate, run }
1043
+
1044
+ if (require.main === module)
1045
+ run(process.argv.slice(2))