node-gtk 1.0.0 → 2.1.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.
- package/README.md +123 -190
- package/bin/node-gtk.js +31 -0
- package/lib/bootstrap.js +22 -4
- package/lib/index.js +25 -0
- package/lib/loop.js +34 -0
- package/lib/overrides/GLib-2.0.js +12 -9
- package/lib/overrides/Gio-2.0.js +26 -0
- package/lib/overrides/Gtk-3.0.js +17 -12
- package/lib/overrides/Gtk-4.0.js +4 -2
- package/lib/register-class.js +1 -1
- package/package.json +9 -2
- package/scripts/build-test-fixtures.js +237 -0
- package/scripts/ci.sh +5 -3
- package/src/boxed.cc +33 -3
- package/src/boxed.h +13 -0
- package/src/callback.cc +12 -0
- package/src/callback.h +1 -0
- package/src/closure.cc +110 -2
- package/src/function.cc +68 -7
- package/src/function.h +1 -0
- package/src/gi.cc +72 -0
- package/src/gobject.cc +148 -27
- package/src/loop.cc +39 -3
- package/src/loop.h +2 -0
- package/src/type.cc +3 -2
- package/src/value.cc +369 -31
- package/src/value.h +22 -0
- package/tools/README.md +91 -0
- package/tools/generate-types.js +1045 -0
- package/lib/binding/node-v102-linux-x64/node_gtk.node +0 -0
- package/lib/binding/node-v108-linux-x64/node_gtk.node +0 -0
- package/lib/binding/node-v115-linux-x64/node_gtk.node +0 -0
- package/lib/binding/node-v127-linux-x64/node_gtk.node +0 -0
- package/lib/binding/node-v93-linux-x64/node_gtk.node +0 -0
|
@@ -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(/</g, '<').replace(/>/g, '>')
|
|
114
|
+
.replace(/"/g, '"').replace(/'/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(/&/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))
|