@ts-for-gir/lib 4.0.0-rc.5 → 4.0.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-for-gir/lib",
3
- "version": "4.0.0-rc.5",
3
+ "version": "4.0.0-rc.7",
4
4
  "description": "Typescript .d.ts generator from GIR for gjs",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -41,7 +41,7 @@
41
41
  "type definitions"
42
42
  ],
43
43
  "devDependencies": {
44
- "@ts-for-gir/tsconfig": "^4.0.0-rc.5",
44
+ "@ts-for-gir/tsconfig": "^4.0.0-rc.7",
45
45
  "@types/ejs": "^3.1.5",
46
46
  "@types/lodash": "^4.17.24",
47
47
  "@types/node": "^25.6.0",
@@ -49,9 +49,9 @@
49
49
  "typescript": "^6.0.3"
50
50
  },
51
51
  "dependencies": {
52
- "@gi.ts/parser": "^4.0.0-rc.5",
53
- "@ts-for-gir/reporter": "^4.0.0-rc.5",
54
- "@ts-for-gir/templates": "^4.0.0-rc.5",
52
+ "@gi.ts/parser": "^4.0.0-rc.7",
53
+ "@ts-for-gir/reporter": "^4.0.0-rc.7",
54
+ "@ts-for-gir/templates": "^4.0.0-rc.7",
55
55
  "colorette": "^2.0.20",
56
56
  "ejs": "^5.0.2",
57
57
  "glob": "^13.0.6",
@@ -132,6 +132,15 @@ export class DependencyManager {
132
132
  }
133
133
 
134
134
  createImportPath(packageName: string, namespace: string, version: string): string {
135
+ // In external-deps mode every dep import is resolved against an installed npm package
136
+ // (e.g. `@girs/glib-2.0`), regardless of `package` mode. User-supplied overrides win
137
+ // for namespaces with non-default scopes/versions (e.g. `Soup → @girs/soup-3.0`).
138
+ if (this.config.externalDeps) {
139
+ const override = this.config.externalPackages?.[namespace];
140
+ if (override) return override;
141
+ const importName = transformImportName(packageName);
142
+ return `${this.config.npmScope}/${importName}`;
143
+ }
135
144
  if (!this.config.package) {
136
145
  return `gi://${namespace}?version=${version}`;
137
146
  }
@@ -248,9 +248,7 @@ export class IntrospectedFunction extends IntrospectedNamespaceMember {
248
248
  const { type, isOptional } = p;
249
249
 
250
250
  if (allowOptions) {
251
- if (type instanceof NullableType) {
252
- params.push(p.copy({ isOptional: true }));
253
- } else if (!isOptional) {
251
+ if (!isOptional) {
254
252
  params.push(p);
255
253
  return { allowOptions: false, params };
256
254
  } else {
@@ -5,6 +5,7 @@ import {
5
5
  Generic,
6
6
  GenericType,
7
7
  GenerifiedTypeIdentifier,
8
+ NullableType,
8
9
  type TypeExpression,
9
10
  TypeIdentifier,
10
11
  UnknownType,
@@ -47,6 +48,40 @@ import { IntrospectedSignal } from "./signal.ts";
47
48
 
48
49
  const log = new ConsoleReporter(true, "gir/introspected-classes", true);
49
50
 
51
+ /**
52
+ * Walks the implementing class's `extends` chain (skipping the prerequisite class
53
+ * itself) and reports whether any ancestor *shadows* a member by declaring its
54
+ * own member of the given name. A shadow indicates the parent chain has
55
+ * overridden the prerequisite's member with a possibly incompatible type, which
56
+ * would otherwise leave the implementing class unable to satisfy the
57
+ * interface's contract.
58
+ */
59
+ function hasExtendsShadowOf(cls: IntrospectedBaseClass, prerequisite: IntrospectedBaseClass, name: string): boolean {
60
+ let current = cls.resolveParents().extends();
61
+ while (current) {
62
+ const node = current.node;
63
+ if (node !== prerequisite) {
64
+ const hasOwn = [...node.props, ...node.fields, ...node.members].some((m) => m.name === name);
65
+ if (hasOwn) return true;
66
+ }
67
+ current = current.extends();
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function resolveNullableProperties(cls: IntrospectedBaseClass): void {
73
+ for (const prop of cls.props) {
74
+ if (prop.type instanceof NullableType) continue;
75
+
76
+ const getterName = prop.getter ?? `get_${prop.name}`;
77
+ const getter = cls.members.find((m) => m.name === getterName && !(m instanceof IntrospectedStaticClassFunction));
78
+
79
+ if (getter instanceof IntrospectedClassFunction && getter.return() instanceof NullableType) {
80
+ prop.type = new NullableType(prop.type);
81
+ }
82
+ }
83
+ }
84
+
50
85
  /**
51
86
  * Represents a signal with metadata
52
87
  */
@@ -75,6 +110,8 @@ export class IntrospectedClassFunction<
75
110
  returnTypeDoc?: string | null;
76
111
  /** If this function was generated from a signal, stores the signal name. */
77
112
  signalOrigin?: string;
113
+ /** GIR glib:finish-func attribute: name of the function that finishes this async operation. */
114
+ finishFuncName?: string;
78
115
 
79
116
  generics: Generic[] = [];
80
117
 
@@ -143,6 +180,7 @@ export class IntrospectedClassFunction<
143
180
 
144
181
  fn.generics = [...this.generics];
145
182
  fn.returnTypeDoc = this.returnTypeDoc;
183
+ fn.finishFuncName = this.finishFuncName;
146
184
 
147
185
  if (interfaceParent) {
148
186
  fn.interfaceParent = interfaceParent;
@@ -177,6 +215,10 @@ export class IntrospectedClassFunction<
177
215
  // Convert the function to a class function
178
216
  const { raw_name: name, output_parameters, parameters, return_type, doc, isIntrospectable } = fn;
179
217
 
218
+ // A function with shadowed-by is superseded by the shadowing function (which uses `shadows`)
219
+ // and takes the original name. Do not emit the shadowed function to avoid duplicate declarations.
220
+ const isShadowedBy = element.$["shadowed-by"] != null;
221
+
180
222
  const classFn = new IntrospectedClassFunction({
181
223
  parent,
182
224
  name,
@@ -184,11 +226,12 @@ export class IntrospectedClassFunction<
184
226
  parameters,
185
227
  return_type,
186
228
  doc,
187
- isIntrospectable,
229
+ isIntrospectable: isIntrospectable && !isShadowedBy,
188
230
  });
189
231
 
190
232
  classFn.returnTypeDoc = fn.returnTypeDoc;
191
233
  classFn.generics = [...fn.generics];
234
+ classFn.finishFuncName = element.$["glib:finish-func"];
192
235
 
193
236
  return classFn;
194
237
  }
@@ -396,6 +439,8 @@ export class IntrospectedStaticClassFunction extends IntrospectedClassFunction {
396
439
  // Convert the function to a static class function
397
440
  const { raw_name: name, output_parameters, parameters, return_type, doc, isIntrospectable } = fn;
398
441
 
442
+ const isShadowedBy = m.$["shadowed-by"] != null;
443
+
399
444
  return new IntrospectedStaticClassFunction({
400
445
  parent,
401
446
  name,
@@ -403,7 +448,7 @@ export class IntrospectedStaticClassFunction extends IntrospectedClassFunction {
403
448
  parameters,
404
449
  return_type,
405
450
  doc,
406
- isIntrospectable,
451
+ isIntrospectable: isIntrospectable && !isShadowedBy,
407
452
  });
408
453
  }
409
454
  }
@@ -853,13 +898,19 @@ export class IntrospectedClass extends IntrospectedBaseClass {
853
898
  });
854
899
  }
855
900
 
856
- // If an interface inherits from a class (such as Gtk.Widget)
857
- // we need to pull in every item from that class...
901
+ // An interface's <prerequisite> of class type is always satisfied by the
902
+ // implementing class's actual parent chain those members are already
903
+ // inherited via TS class inheritance, so we don't re-emit them. The one
904
+ // case we still need to handle: when the parent chain has a same-named
905
+ // member with an incompatible signature/type, we re-emit the interface's
906
+ // version so `filterConflicts` / `filterFunctionConflict` can broaden or
907
+ // override it to satisfy both `extends` and `implements` simultaneously.
858
908
  for (const implemented of resolution.implements()) {
859
909
  const extended = implemented.extends();
860
910
  if (extended?.node instanceof IntrospectedClass) {
861
911
  for (const item of getItems(extended.node)) {
862
912
  if (items.has(item.name) || !validate(item)) continue;
913
+ if (!hasExtendsShadowOf(this, extended.node, item.name)) continue;
863
914
  items.set(item.name, item);
864
915
  }
865
916
  }
@@ -1036,6 +1087,7 @@ export class IntrospectedClass extends IntrospectedBaseClass {
1036
1087
  IntrospectedClass.parseBasicProperties(element, clazz, ns, options);
1037
1088
  IntrospectedClass.parseResolveNames(element, clazz, ns, name);
1038
1089
  IntrospectedClass.parseInheritanceAndMembers(element, clazz, ns, options);
1090
+ resolveNullableProperties(clazz);
1039
1091
 
1040
1092
  return clazz;
1041
1093
  }
@@ -1342,6 +1394,7 @@ export class IntrospectedInterface extends IntrospectedBaseClass {
1342
1394
  IntrospectedInterface.parseInterfaceBasicProperties(element, iface, namespace, options);
1343
1395
  IntrospectedInterface.parseInterfaceResolveNames(element, iface, namespace, name);
1344
1396
  IntrospectedInterface.parseInterfaceMembers(element, iface, namespace, options);
1397
+ resolveNullableProperties(iface);
1345
1398
 
1346
1399
  return iface;
1347
1400
  }
@@ -62,6 +62,11 @@ function findFinishMethodInClass(cls: IntrospectedBaseClass, node: IntrospectedC
62
62
  ? [...cls.constructors, ...cls.members.filter((m) => m instanceof IntrospectedStaticClassFunction)]
63
63
  : [...cls.members.filter((m) => !(m instanceof IntrospectedStaticClassFunction))];
64
64
 
65
+ // Prefer the GIR-specified finish function name over name heuristics
66
+ if (node.finishFuncName) {
67
+ return members.find((m) => m.name === node.finishFuncName);
68
+ }
69
+
65
70
  return members.find(
66
71
  (m) => m.name === `${node.name.replace(/_async$/, "")}_finish` || m.name === `${node.name}_finish`,
67
72
  );
@@ -1,5 +1,5 @@
1
1
  import type { FormatGenerator } from "../generators/generator.ts";
2
- import type { TypeExpression } from "../gir.ts";
2
+ import { NullableType, type TypeExpression } from "../gir.ts";
3
3
  import type { GirFieldElement, GirPropertyElement } from "../index.ts";
4
4
  import type { OptionsLoad } from "../types/index.ts";
5
5
  import type { Options } from "../types/introspected.ts";
@@ -98,6 +98,10 @@ export class IntrospectedProperty extends IntrospectedBase<IntrospectedEnum | In
98
98
  readonly writable: boolean = false;
99
99
  readonly readable: boolean = true;
100
100
  readonly constructOnly: boolean;
101
+ /** GIR default-value attribute: the default value of the property as a string. */
102
+ defaultValue?: string;
103
+ /** GIR getter attribute: name of the getter method for this property (used for nullable inference). */
104
+ getter?: string;
101
105
 
102
106
  get namespace() {
103
107
  return this.parent.namespace;
@@ -110,7 +114,7 @@ export class IntrospectedProperty extends IntrospectedBase<IntrospectedEnum | In
110
114
  }): IntrospectedProperty {
111
115
  const { name, writable, readable, type, constructOnly, parent } = this;
112
116
 
113
- return new IntrospectedProperty({
117
+ const prop = new IntrospectedProperty({
114
118
  name: options?.name ?? name,
115
119
  writable,
116
120
  readable,
@@ -118,6 +122,9 @@ export class IntrospectedProperty extends IntrospectedBase<IntrospectedEnum | In
118
122
  constructOnly,
119
123
  parent: options?.parent ?? parent,
120
124
  })._copyBaseProperties(this);
125
+ prop.defaultValue = this.defaultValue;
126
+ prop.getter = this.getter;
127
+ return prop;
121
128
  }
122
129
 
123
130
  accept(visitor: GirVisitor): IntrospectedProperty {
@@ -198,6 +205,14 @@ export class IntrospectedProperty extends IntrospectedBase<IntrospectedEnum | In
198
205
  property.metadata = parseMetadata(element);
199
206
  }
200
207
 
208
+ property.defaultValue = element.$["default-value"];
209
+
210
+ property.getter = element.$.getter;
211
+
212
+ if (element.$.nullable === "1" || element.$["allow-none"] === "1") {
213
+ property.type = new NullableType(property.type);
214
+ }
215
+
201
216
  return property;
202
217
  }
203
218
  }
@@ -103,13 +103,17 @@ export class NSRegistry {
103
103
  }
104
104
 
105
105
  transform(options: OptionsTransform) {
106
- const GLib = this.assertNamespace("GLib", "2.0");
107
- const Gio = this.assertNamespace("Gio", "2.0");
108
- const GObject = this.assertNamespace("GObject", "2.0");
106
+ // In tolerant external-deps mode (with --allow-missing-deps) the core namespaces
107
+ // may not be loaded. Sync their package_version only when actually present;
108
+ // generify/inject still run on whatever IS loaded (other modules' transformations
109
+ // don't depend on GLib being in the registry).
110
+ const GLib = this.namespace("GLib", "2.0");
111
+ const Gio = this.namespace("Gio", "2.0");
112
+ const GObject = this.namespace("GObject", "2.0");
109
113
 
110
114
  // These follow the GLib version.
111
- Gio.package_version = [...GLib.package_version];
112
- GObject.package_version = [...GLib.package_version];
115
+ if (GLib && Gio) Gio.package_version = [...GLib.package_version];
116
+ if (GLib && GObject) GObject.package_version = [...GLib.package_version];
113
117
 
114
118
  const interfaceVisitor = new InterfaceVisitor();
115
119
 
@@ -119,11 +123,13 @@ export class NSRegistry {
119
123
 
120
124
  this.registerTransformation(classVisitor);
121
125
 
122
- console.log("Adding generics...");
123
- generify(this, options.inferGenerics);
126
+ if (GLib && Gio) {
127
+ console.log("Adding generics...");
128
+ generify(this, options.inferGenerics);
124
129
 
125
- console.log("Injecting types...");
126
- inject(this);
130
+ console.log("Injecting types...");
131
+ inject(this);
132
+ }
127
133
  }
128
134
 
129
135
  defaultVersionOf(name: string): string | null {
@@ -64,4 +64,25 @@ export interface OptionsGeneration extends OptionsBase {
64
64
  merge?: boolean;
65
65
  /** Directory containing pre-generated TypeDoc JSON files for merge mode (from 'ts-for-gir json') */
66
66
  jsonDir?: string;
67
+ /**
68
+ * External-deps mode: emit `import` statements that reference dependency types from
69
+ * already-installed npm packages (e.g. `@girs/glib-2.0`) instead of regenerating them.
70
+ * Designed for project-local GIRs (Vala bridges etc.) where the surrounding `@girs/*`
71
+ * ecosystem is already in node_modules. No GJS supporting files, no index aggregator.
72
+ */
73
+ externalDeps: boolean;
74
+ /**
75
+ * In `externalDeps` mode, allow generation to proceed even when some transitive dep GIRs
76
+ * cannot be found. Default is strict: missing deps abort the run, since divergent dep
77
+ * availability between environments (dev with -devel packages vs CI without) would
78
+ * silently produce inconsistent generated `.d.ts` output.
79
+ */
80
+ allowMissingDeps: boolean;
81
+ /**
82
+ * Override the default `<npmScope>/<importName>` mapping for individual namespaces when
83
+ * resolving external dependency imports in `externalDeps` mode.
84
+ *
85
+ * Example: `{ Soup: '@girs/soup-3.0', GLib: '@girs/glib-2.0' }`
86
+ */
87
+ externalPackages?: Record<string, string>;
67
88
  }
@@ -77,4 +77,24 @@ export interface UserConfig {
77
77
  merge?: boolean;
78
78
  /** Directory containing pre-generated TypeDoc JSON files for merge mode (from 'ts-for-gir json') */
79
79
  jsonDir?: string;
80
+ /**
81
+ * External-deps mode: emit `import` statements that reference dependency types from
82
+ * already-installed npm packages (e.g. `@girs/glib-2.0`) instead of regenerating them.
83
+ * Designed for project-local GIRs (Vala bridges etc.) where the surrounding `@girs/*`
84
+ * ecosystem is already in node_modules.
85
+ */
86
+ externalDeps: boolean;
87
+ /**
88
+ * Allow `externalDeps` generation to proceed when some transitive dep GIRs are missing.
89
+ * Default is strict: missing deps cause an error to prevent silent type-quality drift
90
+ * between environments with and without system GIR -devel packages installed.
91
+ */
92
+ allowMissingDeps: boolean;
93
+ /**
94
+ * Override the default `<npmScope>/<importName>` mapping for individual namespaces when
95
+ * resolving external dependency imports in `externalDeps` mode.
96
+ *
97
+ * Example: `{ Soup: '@girs/soup-3.0', GLib: '@girs/glib-2.0' }`
98
+ */
99
+ externalPackages?: Record<string, string>;
80
100
  }
@@ -261,8 +261,13 @@ function checkFunctionConflicts<T extends IntrospectedClassFunction | Introspect
261
261
  });
262
262
  });
263
263
 
264
- // Check field/property conflicts
265
- const hasFieldConflicts = checkFieldPropertyConflicts(base, functionElement.name);
264
+ // Static methods can coexist with instance fields/properties of the same name
265
+ // in TypeScript (e.g. `static map(...)` alongside `map: T[]`), so skip the check
266
+ // for them. The conflict only applies to instance methods.
267
+ const hasFieldConflicts =
268
+ functionElement instanceof IntrospectedStaticClassFunction
269
+ ? false
270
+ : checkFieldPropertyConflicts(base, functionElement.name);
266
271
 
267
272
  // Check GObject reserved methods
268
273
  const hasGObjectConflicts = checkGObjectConflicts(base, functionElement.name);
@@ -0,0 +1,66 @@
1
+ import type { GirModule } from "../gir-module.ts";
2
+
3
+ /**
4
+ * Resolves a single C enum/bitfield constant to its GJS-qualified path by searching
5
+ * the module's own enum_constants map and then all direct and transitive dependencies.
6
+ * Returns [namespaceName, enumTypeName, memberName] or null if not found.
7
+ *
8
+ * Called during the generation phase (after all modules have been parsed), so all
9
+ * dependency enum_constants maps are fully populated. Each GirModule.enum_constants
10
+ * is an O(1) Map; the total cost per call is O(deps) — negligible given the small
11
+ * fraction of properties that carry a default-value attribute.
12
+ */
13
+ function resolveCEnumConstant(cIdentifier: string, ns: GirModule): readonly [string, string, string] | null {
14
+ // Check own namespace first
15
+ const own = ns.enum_constants.get(cIdentifier);
16
+ if (own) return [ns.namespace, own[0], own[1]] as const;
17
+
18
+ // Check all direct and transitive dependencies
19
+ for (const dep of ns.allDependencies) {
20
+ const depModule = ns.getInstalledImport(dep.namespace);
21
+ if (!depModule) continue;
22
+ const entry = depModule.enum_constants.get(cIdentifier);
23
+ if (entry) return [depModule.namespace, entry[0], entry[1]] as const;
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ function convertSingleCValue(value: string, ns: GirModule): string {
30
+ const trimmed = value.trim();
31
+
32
+ if (trimmed === "NULL") return "null";
33
+ if (trimmed === "TRUE") return "true";
34
+ if (trimmed === "FALSE") return "false";
35
+
36
+ // Normalize C floats: "0.000000" → "0", "1.500000" → "1.5"
37
+ if (/^-?\d+\.\d+$/.test(trimmed)) {
38
+ const n = parseFloat(trimmed);
39
+ if (!Number.isNaN(n)) return String(n);
40
+ }
41
+
42
+ // Resolve C enum/bitfield constant (own namespace + all dependencies)
43
+ // e.g. "GTK_ALIGN_FILL" → "Gtk.Align.FILL"
44
+ // e.g. "GDK_NO_MODIFIER_MASK" → "Gdk.ModifierType.NO_MODIFIER_MASK"
45
+ // e.g. "PANGO_ALIGN_LEFT" → "Pango.Alignment.LEFT"
46
+ const entry = resolveCEnumConstant(trimmed, ns);
47
+ if (entry) return `${entry[0]}.${entry[1]}.${entry[2]}`;
48
+
49
+ return trimmed;
50
+ }
51
+
52
+ /**
53
+ * Converts a raw C default-value string from GIR XML to its JavaScript equivalent.
54
+ * Handles NULL/TRUE/FALSE, C float literals, C enum/bitfield constants (including
55
+ * cross-namespace lookups), and bitmask combinations (A | B | C).
56
+ */
57
+ export function convertCDefaultValue(rawValue: string, ns: GirModule): string {
58
+ // Handle bitmask combinations: "A | B | C"
59
+ if (rawValue.includes("|")) {
60
+ return rawValue
61
+ .split("|")
62
+ .map((part) => convertSingleCValue(part, ns))
63
+ .join(" | ");
64
+ }
65
+ return convertSingleCValue(rawValue, ns);
66
+ }
@@ -2,6 +2,7 @@ export * from "./conflicts.ts";
2
2
  export * from "./documentation.ts";
3
3
  export * from "./files.ts";
4
4
  export * from "./generation.ts";
5
+ export * from "./gir-defaults.ts";
5
6
  export * from "./gir-parsing.ts";
6
7
  export * from "./girs.ts";
7
8
  export * from "./naming.ts";
@@ -3,7 +3,7 @@ import { IntrospectedError } from "../gir/error.ts";
3
3
  import type { IntrospectedBaseClass, IntrospectedClass, IntrospectedInterface } from "../gir/introspected-classes.ts";
4
4
  import { IntrospectedClassFunction, IntrospectedStaticClassFunction } from "../gir/introspected-classes.ts";
5
5
  import { IntrospectedRecord } from "../gir/record.ts";
6
- import { AnyType, NativeType, TypeIdentifier } from "../gir.ts";
6
+ import { AnyType, ArrayType, NativeType, TypeIdentifier } from "../gir.ts";
7
7
  import { resolveTypeIdentifier } from "../utils/type-resolution.ts";
8
8
  import { GirVisitor } from "../visitor.ts";
9
9
 
@@ -125,9 +125,10 @@ const fixMissingParent = <T extends IntrospectedBaseClass>(node: T): T => {
125
125
  };
126
126
 
127
127
  /**
128
- * Fields cannot be array types, error types,
129
- * or class-like types in GJS. This strips
130
- * fields which have these "complex" types.
128
+ * Removes fields with types that GJS cannot directly expose on a struct instance.
129
+ * Error types and non-simple non-pointer struct fields are removed.
130
+ * Array fields (zero-terminated pointer arrays, T**) are always safe in GJS and are only
131
+ * rejected if the element type is private or disguised.
131
132
  *
132
133
  * @param node
133
134
  */
@@ -135,6 +136,17 @@ const removeComplexFields = <T extends IntrospectedBaseClass>(node: T): T => {
135
136
  const { namespace } = node;
136
137
 
137
138
  node.fields = node.fields.filter((f) => {
139
+ // Array fields (T**) are marshalled by GJS for any GBoxed element type.
140
+ // Only reject arrays of private/disguised element types.
141
+ if (f.type instanceof ArrayType) {
142
+ const elementType = f.type.deepUnwrap();
143
+ if (elementType instanceof TypeIdentifier) {
144
+ const classNode = resolveTypeIdentifier(namespace, elementType);
145
+ return !classNode?.isPrivate;
146
+ }
147
+ return true;
148
+ }
149
+
138
150
  const type = f.type.deepUnwrap();
139
151
 
140
152
  if (type instanceof NativeType) {