@ts-for-gir/lib 4.0.0-rc.1 → 4.0.0-rc.12

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.1",
3
+ "version": "4.0.0-rc.12",
4
4
  "description": "Typescript .d.ts generator from GIR for gjs",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -41,19 +41,19 @@
41
41
  "type definitions"
42
42
  ],
43
43
  "devDependencies": {
44
- "@ts-for-gir/tsconfig": "^4.0.0-rc.1",
44
+ "@ts-for-gir/tsconfig": "^4.0.0-rc.12",
45
45
  "@types/ejs": "^3.1.5",
46
46
  "@types/lodash": "^4.17.24",
47
- "@types/node": "^24.12.2",
47
+ "@types/node": "^25.6.0",
48
48
  "rimraf": "^6.1.3",
49
- "typescript": "^6.0.2"
49
+ "typescript": "^6.0.3"
50
50
  },
51
51
  "dependencies": {
52
- "@gi.ts/parser": "^4.0.0-rc.1",
53
- "@ts-for-gir/reporter": "^4.0.0-rc.1",
54
- "@ts-for-gir/templates": "^4.0.0-rc.1",
52
+ "@gi.ts/parser": "^4.0.0-rc.12",
53
+ "@ts-for-gir/reporter": "^4.0.0-rc.12",
54
+ "@ts-for-gir/templates": "^4.0.0-rc.12",
55
55
  "colorette": "^2.0.20",
56
- "ejs": "^5.0.1",
56
+ "ejs": "^5.0.2",
57
57
  "glob": "^13.0.6",
58
58
  "lodash": "4.18.1"
59
59
  }
package/src/constants.ts CHANGED
@@ -18,11 +18,18 @@ function getPackageVersion(): string {
18
18
  if (typeof __TS_FOR_GIR_VERSION__ !== "undefined") {
19
19
  return __TS_FOR_GIR_VERSION__;
20
20
  }
21
- const currentModulePath = fileURLToPath(import.meta.url);
22
- const currentDir = dirname(currentModulePath);
23
- const packageJsonPath = join(currentDir, "..", "package.json");
24
- const content = readFileSync(packageJsonPath, "utf-8");
25
- return (JSON.parse(content) as { version: string }).version;
21
+ // Dev mode reads the sibling package.json relative to this source file.
22
+ // Wrapped in try/catch so a bundle missing the __TS_FOR_GIR_VERSION__ define
23
+ // degrades to a fallback string instead of throwing at module load.
24
+ try {
25
+ const currentModulePath = fileURLToPath(import.meta.url);
26
+ const currentDir = dirname(currentModulePath);
27
+ const packageJsonPath = join(currentDir, "..", "package.json");
28
+ const content = readFileSync(packageJsonPath, "utf-8");
29
+ return (JSON.parse(content) as { version: string }).version;
30
+ } catch {
31
+ return "0.0.0-unknown";
32
+ }
26
33
  }
27
34
 
28
35
  export const APP_NAME = "ts-for-gir";
@@ -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
  }
@@ -147,13 +156,22 @@ export class DependencyManager {
147
156
  }
148
157
 
149
158
  createPackageJsonImport(importPath: string, libraryVersion?: LibraryVersion): string {
159
+ const pinnedVersion = libraryVersion ? `${libraryVersion.toString()}-${APP_VERSION}` : APP_VERSION;
160
+ const format = this.config.depVersionFormat ?? (this.config.workspace ? "workspace" : "exact");
150
161
  let depVersion: string;
151
- if (this.config.workspace) {
152
- depVersion = "workspace:^";
153
- } else if (libraryVersion) {
154
- depVersion = `${libraryVersion.toString()}-${APP_VERSION}`;
155
- } else {
156
- depVersion = APP_VERSION;
162
+ switch (format) {
163
+ case "workspace":
164
+ depVersion = "workspace:^";
165
+ break;
166
+ case "caret":
167
+ depVersion = `^${pinnedVersion}`;
168
+ break;
169
+ case "any":
170
+ depVersion = "*";
171
+ break;
172
+ default:
173
+ depVersion = pinnedVersion;
174
+ break;
157
175
  }
158
176
  return `"${importPath}": "${depVersion}"`;
159
177
  }
@@ -4,7 +4,7 @@ import {
4
4
  ArrayType,
5
5
  ClosureType,
6
6
  type Generic,
7
- NullableType,
7
+ makeNullable,
8
8
  type TypeExpression,
9
9
  TypeIdentifier,
10
10
  UnknownType,
@@ -235,7 +235,10 @@ export class IntrospectedFunction extends IntrospectedNamespaceMember {
235
235
  ): IntrospectedFunctionParameter[] {
236
236
  return parameters
237
237
  .filter((_, i) => !length_params.includes(i) && !user_data_params.includes(i))
238
- .filter((v) => !(v.type instanceof TypeIdentifier && v.type.is("GLib", "DestroyNotify")));
238
+ .filter((v) => {
239
+ const unwrapped = v.type.unwrap();
240
+ return !(unwrapped instanceof TypeIdentifier && unwrapped.is("GLib", "DestroyNotify"));
241
+ });
239
242
  }
240
243
 
241
244
  private static processOptionalParameters(
@@ -248,9 +251,7 @@ export class IntrospectedFunction extends IntrospectedNamespaceMember {
248
251
  const { type, isOptional } = p;
249
252
 
250
253
  if (allowOptions) {
251
- if (type instanceof NullableType) {
252
- params.push(p.copy({ isOptional: true }));
253
- } else if (!isOptional) {
254
+ if (!isOptional) {
254
255
  params.push(p);
255
256
  return { allowOptions: false, params };
256
257
  } else {
@@ -258,7 +259,7 @@ export class IntrospectedFunction extends IntrospectedNamespaceMember {
258
259
  }
259
260
  } else {
260
261
  if (isOptional) {
261
- params.push(p.copy({ type: new NullableType(type), isOptional: false }));
262
+ params.push(p.copy({ type: makeNullable(type), isOptional: false }));
262
263
  } else {
263
264
  params.push(p);
264
265
  }
@@ -5,6 +5,8 @@ import {
5
5
  Generic,
6
6
  GenericType,
7
7
  GenerifiedTypeIdentifier,
8
+ makeNullable,
9
+ NullableType,
8
10
  type TypeExpression,
9
11
  TypeIdentifier,
10
12
  UnknownType,
@@ -47,6 +49,40 @@ import { IntrospectedSignal } from "./signal.ts";
47
49
 
48
50
  const log = new ConsoleReporter(true, "gir/introspected-classes", true);
49
51
 
52
+ /**
53
+ * Walks the implementing class's `extends` chain (skipping the prerequisite class
54
+ * itself) and reports whether any ancestor *shadows* a member by declaring its
55
+ * own member of the given name. A shadow indicates the parent chain has
56
+ * overridden the prerequisite's member with a possibly incompatible type, which
57
+ * would otherwise leave the implementing class unable to satisfy the
58
+ * interface's contract.
59
+ */
60
+ function hasExtendsShadowOf(cls: IntrospectedBaseClass, prerequisite: IntrospectedBaseClass, name: string): boolean {
61
+ let current = cls.resolveParents().extends();
62
+ while (current) {
63
+ const node = current.node;
64
+ if (node !== prerequisite) {
65
+ const hasOwn = [...node.props, ...node.fields, ...node.members].some((m) => m.name === name);
66
+ if (hasOwn) return true;
67
+ }
68
+ current = current.extends();
69
+ }
70
+ return false;
71
+ }
72
+
73
+ function resolveNullableProperties(cls: IntrospectedBaseClass): void {
74
+ for (const prop of cls.props) {
75
+ if (prop.type instanceof NullableType) continue;
76
+
77
+ const getterName = prop.getter ?? `get_${prop.name}`;
78
+ const getter = cls.members.find((m) => m.name === getterName && !(m instanceof IntrospectedStaticClassFunction));
79
+
80
+ if (getter instanceof IntrospectedClassFunction && getter.return() instanceof NullableType) {
81
+ prop.type = makeNullable(prop.type);
82
+ }
83
+ }
84
+ }
85
+
50
86
  /**
51
87
  * Represents a signal with metadata
52
88
  */
@@ -75,6 +111,8 @@ export class IntrospectedClassFunction<
75
111
  returnTypeDoc?: string | null;
76
112
  /** If this function was generated from a signal, stores the signal name. */
77
113
  signalOrigin?: string;
114
+ /** GIR glib:finish-func attribute: name of the function that finishes this async operation. */
115
+ finishFuncName?: string;
78
116
 
79
117
  generics: Generic[] = [];
80
118
 
@@ -143,6 +181,7 @@ export class IntrospectedClassFunction<
143
181
 
144
182
  fn.generics = [...this.generics];
145
183
  fn.returnTypeDoc = this.returnTypeDoc;
184
+ fn.finishFuncName = this.finishFuncName;
146
185
 
147
186
  if (interfaceParent) {
148
187
  fn.interfaceParent = interfaceParent;
@@ -177,6 +216,10 @@ export class IntrospectedClassFunction<
177
216
  // Convert the function to a class function
178
217
  const { raw_name: name, output_parameters, parameters, return_type, doc, isIntrospectable } = fn;
179
218
 
219
+ // A function with shadowed-by is superseded by the shadowing function (which uses `shadows`)
220
+ // and takes the original name. Do not emit the shadowed function to avoid duplicate declarations.
221
+ const isShadowedBy = element.$["shadowed-by"] != null;
222
+
180
223
  const classFn = new IntrospectedClassFunction({
181
224
  parent,
182
225
  name,
@@ -184,11 +227,12 @@ export class IntrospectedClassFunction<
184
227
  parameters,
185
228
  return_type,
186
229
  doc,
187
- isIntrospectable,
230
+ isIntrospectable: isIntrospectable && !isShadowedBy,
188
231
  });
189
232
 
190
233
  classFn.returnTypeDoc = fn.returnTypeDoc;
191
234
  classFn.generics = [...fn.generics];
235
+ classFn.finishFuncName = element.$["glib:finish-func"];
192
236
 
193
237
  return classFn;
194
238
  }
@@ -396,6 +440,8 @@ export class IntrospectedStaticClassFunction extends IntrospectedClassFunction {
396
440
  // Convert the function to a static class function
397
441
  const { raw_name: name, output_parameters, parameters, return_type, doc, isIntrospectable } = fn;
398
442
 
443
+ const isShadowedBy = m.$["shadowed-by"] != null;
444
+
399
445
  return new IntrospectedStaticClassFunction({
400
446
  parent,
401
447
  name,
@@ -403,7 +449,7 @@ export class IntrospectedStaticClassFunction extends IntrospectedClassFunction {
403
449
  parameters,
404
450
  return_type,
405
451
  doc,
406
- isIntrospectable,
452
+ isIntrospectable: isIntrospectable && !isShadowedBy,
407
453
  });
408
454
  }
409
455
  }
@@ -853,13 +899,19 @@ export class IntrospectedClass extends IntrospectedBaseClass {
853
899
  });
854
900
  }
855
901
 
856
- // If an interface inherits from a class (such as Gtk.Widget)
857
- // we need to pull in every item from that class...
902
+ // An interface's <prerequisite> of class type is always satisfied by the
903
+ // implementing class's actual parent chain those members are already
904
+ // inherited via TS class inheritance, so we don't re-emit them. The one
905
+ // case we still need to handle: when the parent chain has a same-named
906
+ // member with an incompatible signature/type, we re-emit the interface's
907
+ // version so `filterConflicts` / `filterFunctionConflict` can broaden or
908
+ // override it to satisfy both `extends` and `implements` simultaneously.
858
909
  for (const implemented of resolution.implements()) {
859
910
  const extended = implemented.extends();
860
911
  if (extended?.node instanceof IntrospectedClass) {
861
912
  for (const item of getItems(extended.node)) {
862
913
  if (items.has(item.name) || !validate(item)) continue;
914
+ if (!hasExtendsShadowOf(this, extended.node, item.name)) continue;
863
915
  items.set(item.name, item);
864
916
  }
865
917
  }
@@ -1036,6 +1088,7 @@ export class IntrospectedClass extends IntrospectedBaseClass {
1036
1088
  IntrospectedClass.parseBasicProperties(element, clazz, ns, options);
1037
1089
  IntrospectedClass.parseResolveNames(element, clazz, ns, name);
1038
1090
  IntrospectedClass.parseInheritanceAndMembers(element, clazz, ns, options);
1091
+ resolveNullableProperties(clazz);
1039
1092
 
1040
1093
  return clazz;
1041
1094
  }
@@ -1342,6 +1395,7 @@ export class IntrospectedInterface extends IntrospectedBaseClass {
1342
1395
  IntrospectedInterface.parseInterfaceBasicProperties(element, iface, namespace, options);
1343
1396
  IntrospectedInterface.parseInterfaceResolveNames(element, iface, namespace, name);
1344
1397
  IntrospectedInterface.parseInterfaceMembers(element, iface, namespace, options);
1398
+ resolveNullableProperties(iface);
1345
1399
 
1346
1400
  return iface;
1347
1401
  }
@@ -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 { makeNullable, 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 = makeNullable(property.type);
214
+ }
215
+
201
216
  return property;
202
217
  }
203
218
  }
@@ -8,7 +8,6 @@ import { inject } from "../injections/inject.ts";
8
8
  import type { GeneratorConstructor, OptionsGeneration, OptionsTransform } from "../types/index.ts";
9
9
  import { TwoKeyMap } from "../util.ts";
10
10
  import { ClassVisitor } from "../validators/class.ts";
11
- import { FunctionParametersVisitor } from "../validators/function-parameters.ts";
12
11
  import { InterfaceVisitor } from "../validators/interface.ts";
13
12
  import type { GirVisitor } from "../visitor.ts";
14
13
  import type { IntrospectedNamespace } from "./namespace.ts";
@@ -104,28 +103,33 @@ export class NSRegistry {
104
103
  }
105
104
 
106
105
  transform(options: OptionsTransform) {
107
- const GLib = this.assertNamespace("GLib", "2.0");
108
- const Gio = this.assertNamespace("Gio", "2.0");
109
- 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");
110
113
 
111
114
  // These follow the GLib version.
112
- Gio.package_version = [...GLib.package_version];
113
- 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];
114
117
 
115
118
  const interfaceVisitor = new InterfaceVisitor();
119
+
116
120
  this.registerTransformation(interfaceVisitor);
117
121
 
118
122
  const classVisitor = new ClassVisitor();
119
- this.registerTransformation(classVisitor);
120
123
 
121
- const enumParamsVisitor = new FunctionParametersVisitor();
122
- this.registerTransformation(enumParamsVisitor);
124
+ this.registerTransformation(classVisitor);
123
125
 
124
- console.log("Adding generics...");
125
- generify(this, options.inferGenerics);
126
+ if (GLib && Gio) {
127
+ console.log("Adding generics...");
128
+ generify(this, options.inferGenerics);
126
129
 
127
- console.log("Injecting types...");
128
- inject(this);
130
+ console.log("Injecting types...");
131
+ inject(this);
132
+ }
129
133
  }
130
134
 
131
135
  defaultVersionOf(name: string): string | null {
package/src/gir/signal.ts CHANGED
@@ -3,6 +3,7 @@ import { GirDirection } from "@gi.ts/parser";
3
3
  import type { FormatGenerator } from "../generators/generator.ts";
4
4
  import {
5
5
  ArrayType,
6
+ makeNullable,
6
7
  NativeType,
7
8
  NullableType,
8
9
  NumberType,
@@ -171,7 +172,7 @@ export class IntrospectedSignal extends IntrospectedClassMember<IntrospectedClas
171
172
  }
172
173
  } else {
173
174
  if (isOptional) {
174
- params.push(p.copy({ type: new NullableType(type), isOptional: false }));
175
+ params.push(p.copy({ type: makeNullable(type), isOptional: false }));
175
176
  } else {
176
177
  params.push(p);
177
178
  }
package/src/gir.ts CHANGED
@@ -23,7 +23,7 @@ export enum ConflictType {
23
23
 
24
24
  import { type ConsoleReporter, LazyReporter } from "@ts-for-gir/reporter";
25
25
  import type { IntrospectedField, IntrospectedProperty } from "./gir/property.ts";
26
- import type { OptionsBase } from "./types/index.ts";
26
+ import type { OptionsGeneration } from "./types/index.ts";
27
27
  import { isInvalid, sanitizeIdentifierName, sanitizeNamespace } from "./utils/naming.ts";
28
28
 
29
29
  export abstract class TypeExpression {
@@ -37,10 +37,10 @@ export abstract class TypeExpression {
37
37
  }
38
38
 
39
39
  abstract rewrap(type: TypeExpression): TypeExpression;
40
- abstract resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression;
40
+ abstract resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression;
41
41
 
42
- abstract print(namespace: IntrospectedNamespace, options: OptionsBase): string;
43
- rootPrint(namespace: IntrospectedNamespace, options: OptionsBase): string {
42
+ abstract print(namespace: IntrospectedNamespace, options: OptionsGeneration): string;
43
+ rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
44
44
  return this.print(namespace, options);
45
45
  }
46
46
  }
@@ -89,7 +89,7 @@ export class TypeIdentifier extends TypeExpression {
89
89
  return new TypeIdentifier(sanitizeIdentifierName(this.namespace, this.name), sanitizeNamespace(this.namespace));
90
90
  }
91
91
 
92
- protected _resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeIdentifier | null {
92
+ protected _resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeIdentifier | null {
93
93
  const name: string = sanitizeIdentifierName(null, this.name);
94
94
  const unresolvedNamespaceName = this.namespace;
95
95
 
@@ -183,11 +183,11 @@ export class TypeIdentifier extends TypeExpression {
183
183
  return null;
184
184
  }
185
185
 
186
- resolveIdentifier(namespace: IntrospectedNamespace, options: OptionsBase): TypeIdentifier | null {
186
+ resolveIdentifier(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeIdentifier | null {
187
187
  return this._resolve(namespace, options);
188
188
  }
189
189
 
190
- resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression {
190
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
191
191
  const resolved = this._resolve(namespace, options);
192
192
 
193
193
  // Generally if we can't resolve a type it is not introspectable,
@@ -199,7 +199,7 @@ export class TypeIdentifier extends TypeExpression {
199
199
  return new TypeIdentifier(name, namespace);
200
200
  }
201
201
 
202
- print(namespace: IntrospectedNamespace, _options: OptionsBase): string {
202
+ print(namespace: IntrospectedNamespace, _options: OptionsGeneration): string {
203
203
  if (namespace.hasSymbol(this.namespace) && this.namespace !== namespace.namespace) {
204
204
  // TODO: Move to TypeScript generator...
205
205
  // Libraries like zbar have classes named things like "Gtk"
@@ -248,11 +248,11 @@ export class ModuleTypeIdentifier extends TypeIdentifier {
248
248
  );
249
249
  }
250
250
 
251
- protected _resolve(_namespace: IntrospectedNamespace, _options: OptionsBase): ModuleTypeIdentifier | null {
251
+ protected _resolve(_namespace: IntrospectedNamespace, _options: OptionsGeneration): ModuleTypeIdentifier | null {
252
252
  return this;
253
253
  }
254
254
 
255
- print(namespace: IntrospectedNamespace, _options: OptionsBase): string {
255
+ print(namespace: IntrospectedNamespace, _options: OptionsGeneration): string {
256
256
  if (namespace.namespace === this.namespace) {
257
257
  return `${this.moduleName}.${this.name}`;
258
258
  } else {
@@ -269,7 +269,7 @@ export class ClassStructTypeIdentifier extends TypeIdentifier {
269
269
  return type instanceof ClassStructTypeIdentifier && super.equals(type);
270
270
  }
271
271
 
272
- print(namespace: IntrospectedNamespace, _options: OptionsBase): string {
272
+ print(namespace: IntrospectedNamespace, _options: OptionsGeneration): string {
273
273
  if (namespace.namespace === this.namespace) {
274
274
  // TODO: Mapping to invalid names should happen at the generator level...
275
275
  return `typeof ${isInvalid(this.name) ? `__${this.name}` : this.name}`;
@@ -287,7 +287,7 @@ export class GenerifiedTypeIdentifier extends TypeIdentifier {
287
287
  this.generics = generics;
288
288
  }
289
289
 
290
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
290
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
291
291
  const Generics = this.generics.map((generic) => generic.print(namespace, options)).join(", ");
292
292
 
293
293
  if (namespace.namespace === this.namespace) {
@@ -297,7 +297,7 @@ export class GenerifiedTypeIdentifier extends TypeIdentifier {
297
297
  }
298
298
  }
299
299
 
300
- _resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeIdentifier | null {
300
+ _resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeIdentifier | null {
301
301
  const iden = super._resolve(namespace, options);
302
302
 
303
303
  if (iden) {
@@ -309,9 +309,9 @@ export class GenerifiedTypeIdentifier extends TypeIdentifier {
309
309
  }
310
310
 
311
311
  export class NativeType extends TypeExpression {
312
- readonly expression: (options?: OptionsBase) => string;
312
+ readonly expression: (options?: OptionsGeneration) => string;
313
313
 
314
- constructor(expression: ((options?: OptionsBase) => string) | string) {
314
+ constructor(expression: ((options?: OptionsGeneration) => string) | string) {
315
315
  super();
316
316
  this.expression = typeof expression === "string" ? () => expression : expression;
317
317
  }
@@ -324,11 +324,11 @@ export class NativeType extends TypeExpression {
324
324
  return this;
325
325
  }
326
326
 
327
- print(_namespace: IntrospectedNamespace, options: OptionsBase): string {
327
+ print(_namespace: IntrospectedNamespace, options: OptionsGeneration): string {
328
328
  return this.expression(options);
329
329
  }
330
330
 
331
- equals(type: TypeExpression, options?: OptionsBase): boolean {
331
+ equals(type: TypeExpression, options?: OptionsGeneration): boolean {
332
332
  return type instanceof NativeType && this.expression(options) === type.expression(options);
333
333
  }
334
334
 
@@ -336,7 +336,7 @@ export class NativeType extends TypeExpression {
336
336
  return this;
337
337
  }
338
338
 
339
- static withGenerator(generator: (options?: OptionsBase) => string): TypeExpression {
339
+ static withGenerator(generator: (options?: OptionsGeneration) => string): TypeExpression {
340
340
  return new NativeType(generator);
341
341
  }
342
342
 
@@ -361,17 +361,17 @@ export class OrType extends TypeExpression {
361
361
  return this;
362
362
  }
363
363
 
364
- resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression {
364
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
365
365
  const [type, ...types] = this.types;
366
366
 
367
367
  return new OrType(type.resolve(namespace, options), ...types.map((t) => t.resolve(namespace, options)));
368
368
  }
369
369
 
370
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
370
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
371
371
  return `(${this.types.map((t) => t.print(namespace, options)).join(" | ")})`;
372
372
  }
373
373
 
374
- rootPrint(namespace: IntrospectedNamespace, options: OptionsBase): string {
374
+ rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
375
375
  return `${this.types.map((t) => t.print(namespace, options)).join(" | ")}`;
376
376
  }
377
377
 
@@ -385,15 +385,15 @@ export class OrType extends TypeExpression {
385
385
  }
386
386
 
387
387
  export class TupleType extends OrType {
388
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
388
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
389
389
  return `[${this.types.map((t) => t.print(namespace, options)).join(", ")}]`;
390
390
  }
391
391
 
392
- rootPrint(namespace: IntrospectedNamespace, options: OptionsBase): string {
392
+ rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
393
393
  return this.print(namespace, options);
394
394
  }
395
395
 
396
- resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression {
396
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
397
397
  const [type, ...types] = this.types;
398
398
 
399
399
  return new TupleType(type.resolve(namespace, options), ...types.map((t) => t.resolve(namespace, options)));
@@ -413,7 +413,7 @@ export class BinaryType extends OrType {
413
413
  return this;
414
414
  }
415
415
 
416
- resolve(namespace: IntrospectedNamespace, options: OptionsBase) {
416
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration) {
417
417
  return new BinaryType(this.a.resolve(namespace, options), this.b.resolve(namespace, options));
418
418
  }
419
419
 
@@ -465,7 +465,7 @@ export class FunctionType extends TypeExpression {
465
465
  return this;
466
466
  }
467
467
 
468
- resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression {
468
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
469
469
  return new FunctionType(
470
470
  Object.fromEntries(
471
471
  Object.entries(this.parameterTypes).map(([k, p]) => {
@@ -476,7 +476,7 @@ export class FunctionType extends TypeExpression {
476
476
  );
477
477
  }
478
478
 
479
- rootPrint(namespace: IntrospectedNamespace, options: OptionsBase): string {
479
+ rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
480
480
  const Parameters = Object.entries(this.parameterTypes)
481
481
  .map(([k, v]) => {
482
482
  return `${k}: ${v.rootPrint(namespace, options)}`;
@@ -486,7 +486,7 @@ export class FunctionType extends TypeExpression {
486
486
  return `(${Parameters}) => ${this.returnType.print(namespace, options)}`;
487
487
  }
488
488
 
489
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
489
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
490
490
  return `(${this.rootPrint(namespace, options)})`;
491
491
  }
492
492
  }
@@ -548,7 +548,7 @@ export class GenerifiedType extends TypeExpression {
548
548
  this.generic = generic;
549
549
  }
550
550
 
551
- resolve(namespace: IntrospectedNamespace, options: OptionsBase) {
551
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration) {
552
552
  return new GenerifiedType(this.type.resolve(namespace, options), this.generic);
553
553
  }
554
554
 
@@ -556,11 +556,11 @@ export class GenerifiedType extends TypeExpression {
556
556
  return this.type;
557
557
  }
558
558
 
559
- rootPrint(namespace: IntrospectedNamespace, options: OptionsBase) {
559
+ rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration) {
560
560
  return this.type.rootPrint(namespace, options);
561
561
  }
562
562
 
563
- print(namespace: IntrospectedNamespace, options: OptionsBase) {
563
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration) {
564
564
  return `${this.type.print(namespace, options)}<${this.generic.print()}>`;
565
565
  }
566
566
 
@@ -624,7 +624,7 @@ export class NullableType extends BinaryType {
624
624
  }
625
625
 
626
626
  rewrap(type: TypeExpression): TypeExpression {
627
- return new NullableType(this.a.rewrap(type));
627
+ return makeNullable(this.a.rewrap(type));
628
628
  }
629
629
 
630
630
  get type() {
@@ -632,6 +632,12 @@ export class NullableType extends BinaryType {
632
632
  }
633
633
  }
634
634
 
635
+ export function makeNullable(type: TypeExpression) {
636
+ if (type === RawPointerType) return NullType;
637
+ if (type === AnyType) return AnyType;
638
+ return new NullableType(type);
639
+ }
640
+
635
641
  export class PromiseType extends TypeExpression {
636
642
  type: TypeExpression;
637
643
 
@@ -652,11 +658,11 @@ export class PromiseType extends TypeExpression {
652
658
  return new PromiseType(this.type.rewrap(type));
653
659
  }
654
660
 
655
- resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression {
661
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
656
662
  return new PromiseType(this.type.resolve(namespace, options));
657
663
  }
658
664
 
659
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
665
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
660
666
  if (this.type.equals(VoidType)) {
661
667
  return "globalThis.Promise<void>";
662
668
  }
@@ -664,7 +670,7 @@ export class PromiseType extends TypeExpression {
664
670
  return `globalThis.Promise<${this.type.print(namespace, options)}>`;
665
671
  }
666
672
 
667
- rootPrint(namespace: IntrospectedNamespace, options: OptionsBase): string {
673
+ rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
668
674
  return this.print(namespace, options);
669
675
  }
670
676
  }
@@ -703,13 +709,13 @@ export class TypeConflict extends TypeExpression {
703
709
  return true;
704
710
  }
705
711
 
706
- resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression {
712
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
707
713
  const resolvedType = this.type.resolve(namespace, options);
708
714
  const typeString = resolvedType.print(namespace, options);
709
715
  throw new Error(`Type conflict was not resolved for ${typeString} in ${namespace.namespace}`);
710
716
  }
711
717
 
712
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
718
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
713
719
  const resolvedType = this.type.resolve(namespace, options);
714
720
  const typeString = resolvedType.print(namespace, options);
715
721
  throw new Error(`Type conflict was not resolved for ${typeString} in ${namespace.namespace}`);
@@ -750,7 +756,7 @@ export class ClosureType extends TypeExpression {
750
756
  return this;
751
757
  }
752
758
 
753
- resolve(namespace: IntrospectedNamespace, options: OptionsBase) {
759
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration) {
754
760
  const { user_data, type } = this;
755
761
 
756
762
  return ClosureType.new({
@@ -759,7 +765,7 @@ export class ClosureType extends TypeExpression {
759
765
  });
760
766
  }
761
767
 
762
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
768
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
763
769
  return this.type.print(namespace, options);
764
770
  }
765
771
 
@@ -807,7 +813,7 @@ export class ArrayType extends TypeExpression {
807
813
  return false;
808
814
  }
809
815
 
810
- resolve(namespace: IntrospectedNamespace, options: OptionsBase): TypeExpression {
816
+ resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
811
817
  const { type, arrayDepth, length } = this;
812
818
  return ArrayType.new({
813
819
  type: type.resolve(namespace, options),
@@ -816,7 +822,7 @@ export class ArrayType extends TypeExpression {
816
822
  });
817
823
  }
818
824
 
819
- print(namespace: IntrospectedNamespace, options: OptionsBase): string {
825
+ print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
820
826
  const depth = this.arrayDepth;
821
827
  let typeSuffix: string = "";
822
828
 
@@ -861,9 +867,13 @@ export const Uint8ArrayType = new NativeType("Uint8Array");
861
867
  export const BooleanType = new NativeType("boolean");
862
868
  export const StringType = new NativeType("string");
863
869
  export const NumberType = new NativeType("number");
870
+ export const BigintOrNumberType = new BinaryType(new NativeType("bigint"), NumberType);
864
871
  export const NullType = new NativeType("null");
865
872
  export const VoidType = new NativeType("void");
866
873
  export const UnknownType = new NativeType("unknown");
867
874
  export const AnyFunctionType = new NativeType("(...args: any[]) => any");
875
+ // Distinct from NeverType, so that we can transform it into NullType when
876
+ // marshalled from C to JS
877
+ export const RawPointerType = new NativeType("never");
868
878
 
869
879
  export type GirClassField = IntrospectedProperty | IntrospectedField;
@@ -23,6 +23,8 @@ export interface OptionsGeneration extends OptionsBase {
23
23
  npmScope: string;
24
24
  /** Uses the workspace protocol for the generated packages which can be used with package managers like Yarn and PNPM */
25
25
  workspace: boolean;
26
+ /** Format used for dependency version specifiers in generated package.json files */
27
+ depVersionFormat?: "workspace" | "caret" | "any" | "exact";
26
28
  /** Disable GLib.Variant class with string parsing */
27
29
  noAdvancedVariants: boolean;
28
30
  /**
@@ -62,4 +64,25 @@ export interface OptionsGeneration extends OptionsBase {
62
64
  merge?: boolean;
63
65
  /** Directory containing pre-generated TypeDoc JSON files for merge mode (from 'ts-for-gir json') */
64
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>;
65
88
  }
@@ -28,6 +28,16 @@ export interface UserConfig {
28
28
  npmScope: string;
29
29
  /** Uses the workspace protocol for the generated packages which can be used with package managers like Yarn and PNPM */
30
30
  workspace: boolean;
31
+ /**
32
+ * Format used for dependency version specifiers in generated package.json files.
33
+ * - `workspace`: emit `workspace:^` (yarn/pnpm; npm only when the ref matches a registered workspace)
34
+ * - `caret`: emit `^<version>` (all managers, falls back to registry for dangling refs)
35
+ * - `any`: emit `*` (all managers, most permissive)
36
+ * - `exact`: emit `<version>` (all managers, no range)
37
+ *
38
+ * If unset, defaults to `workspace` when `workspace: true`, else `caret`.
39
+ */
40
+ depVersionFormat?: "workspace" | "caret" | "any" | "exact";
31
41
  /**
32
42
  * Only use the version prefix for the ambient module exports.
33
43
  * This is useful if, for whatever reason, you want to use different library versions of the same library in your project.
@@ -67,4 +77,24 @@ export interface UserConfig {
67
77
  merge?: boolean;
68
78
  /** Directory containing pre-generated TypeDoc JSON files for merge mode (from 'ts-for-gir json') */
69
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>;
70
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);
@@ -37,7 +37,9 @@ export function addTSDocCommentLines(lines: string[], indentCount = 0): string[]
37
37
  }
38
38
 
39
39
  /**
40
- * Adds an info comment, is used for debugging the generated types
40
+ * Adds an info comment, is used for debugging the generated types.
41
+ * One blank line before the comment, one after — readers see a single
42
+ * blank-line separator on each side without a double-blank gap.
41
43
  */
42
44
  export function addInfoComment(comment?: string, indentCount = 0): string[] {
43
45
  const def: string[] = [];
@@ -45,7 +47,6 @@ export function addInfoComment(comment?: string, indentCount = 0): string[] {
45
47
  if (comment) {
46
48
  def.push("");
47
49
  def.push(`${indent}// ${comment}`);
48
- def.push("");
49
50
  }
50
51
  return def;
51
52
  }
@@ -58,7 +59,8 @@ export function mergeDescs(descs: string[], comment?: string, indentCount = 1):
58
59
  const indent = generateIndent(indentCount);
59
60
 
60
61
  for (const desc of descs) {
61
- def.push(`${indent}${desc}`);
62
+ // Empty separator strings stay empty; only real content gets indented.
63
+ def.push(desc.length === 0 ? desc : `${indent}${desc}`);
62
64
  }
63
65
 
64
66
  if (def.length > 0) {
@@ -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
+ }
@@ -14,10 +14,11 @@ import { LazyReporter } from "@ts-for-gir/reporter";
14
14
  import type { IntrospectedNamespace } from "../gir/namespace.ts";
15
15
  import {
16
16
  ArrayType,
17
+ BigintOrNumberType,
17
18
  ClosureType,
18
19
  GenerifiedTypeIdentifier,
20
+ makeNullable,
19
21
  NativeType,
20
- NullableType,
21
22
  type TypeExpression,
22
23
  TypeIdentifier,
23
24
  VoidType,
@@ -238,13 +239,14 @@ export function getType(
238
239
  parameter.$ &&
239
240
  (parameter.$.direction === GirDirection.Inout || parameter.$.direction === GirDirection.Out) &&
240
241
  (nullable || allowNone) &&
241
- !(variableType instanceof NativeType)
242
+ !(variableType instanceof NativeType) &&
243
+ variableType !== BigintOrNumberType
242
244
  ) {
243
- return new NullableType(variableType);
245
+ return makeNullable(variableType);
244
246
  }
245
247
 
246
248
  if ((!parameter.$?.direction || parameter.$.direction === GirDirection.In) && nullable) {
247
- return new NullableType(variableType);
249
+ return makeNullable(variableType);
248
250
  }
249
251
 
250
252
  variableType.isPointer = isPointer;
@@ -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";
package/src/utils/path.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import { dirname, resolve } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
 
4
- // Get __filename on ESM
5
- const __filename = fileURLToPath(import.meta.url);
6
- // Get __dirname on ESM, resolve to the root directory of this package
7
- export const __dirname = resolve(dirname(__filename), "../.."); // TODO: Bundled this must be '..' but unbundled it must be '../..'
4
+ // Resolves to the root directory of this package in dev mode.
5
+ // Wrapped in try/catch so a bundled consumer that lacks `import.meta.url`
6
+ // support degrades to "" instead of crashing at module load.
7
+ function resolvePackageDir(): string {
8
+ try {
9
+ return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
10
+ } catch {
11
+ return "";
12
+ }
13
+ }
14
+
15
+ export const __dirname = resolvePackageDir();
@@ -4,14 +4,21 @@ import type { IntrospectedNamespace } from "../gir/namespace.ts";
4
4
  import {
5
5
  AnyType,
6
6
  ArrayType,
7
+ BigintOrNumberType,
7
8
  BinaryType,
8
9
  BooleanType,
9
10
  NativeType,
10
11
  NeverType,
12
+ NullableType,
13
+ NullType,
11
14
  NumberType,
12
15
  ObjectType,
16
+ OrType,
17
+ PromiseType,
18
+ RawPointerType,
13
19
  StringType,
14
20
  ThisType,
21
+ TupleType,
15
22
  type TypeExpression,
16
23
  TypeIdentifier,
17
24
  Uint8ArrayType,
@@ -167,28 +174,33 @@ export function resolvePrimitiveType(name: string): TypeExpression | null {
167
174
  case "gfloat":
168
175
  case "gchar":
169
176
  case "guint":
170
- case "glong":
171
- case "gulong":
172
177
  case "gint":
173
178
  case "guint8":
179
+ case "gdouble":
180
+ return NumberType;
181
+ case "glong":
182
+ case "gulong":
174
183
  case "guint64":
175
184
  case "gint64":
176
- case "gdouble":
177
185
  case "gssize":
178
186
  case "gsize":
187
+ case "guintptr": // Integer of the same width as a pointer
179
188
  case "time_t": // C standard library time type (seconds since Unix epoch)
180
189
  case "ulong": // C standard library unsigned long type
181
- return NumberType;
190
+ return BigintOrNumberType;
182
191
  case "gboolean":
183
192
  return BooleanType;
184
- case "gpointer": // This is typically used in callbacks to pass data, so we'll allow anything.
185
- return AnyType;
193
+ case "gpointer":
194
+ // You can't use pointers. Pointer arguments are mostly not exposed
195
+ // in GJS, but any exposed pointer arguments are always marshalled
196
+ // as null pointers. If the argument is nullable, this will combine
197
+ // with `null` to produce `null`, but if the argument isn't nullable
198
+ // it's impossible to pass a valid parameter to the function.
199
+ return RawPointerType;
186
200
  case "object": // Support TS "object"
187
201
  return ObjectType;
188
202
  case "va_list":
189
203
  return AnyType;
190
- case "guintptr": // You can't use pointers in GJS! (at least that I'm aware of)
191
- return NeverType;
192
204
  case "never": // Support TS "never"
193
205
  return NeverType;
194
206
  case "unknown": // Support TS "unknown"
@@ -243,6 +255,30 @@ export function resolveDirectedType(type: TypeExpression, direction: GirDirectio
243
255
  return type;
244
256
  }
245
257
  }
258
+ } else if (type === BigintOrNumberType && direction === GirDirection.Out) {
259
+ // 64-bit integers accept number or bigint, but only return number to JS
260
+ return NumberType;
261
+ } else if (type === RawPointerType && direction === GirDirection.Out) {
262
+ // Raw pointers are always marshalled as JS null.
263
+ return NullType;
264
+ } else if (type instanceof PromiseType) {
265
+ // Propagate direction into the Promise's inner type so e.g. async
266
+ // functions returning 64-bit ints resolve to `Promise<number>` rather
267
+ // than `Promise<bigint | number>`.
268
+ const resolvedInner = resolveDirectedType(type.type, direction);
269
+ if (resolvedInner) return new PromiseType(resolvedInner);
270
+ } else if (type instanceof BinaryType && !(type instanceof NullableType)) {
271
+ // Walk through binary unions like `Promise<T> | void` (the dual-call
272
+ // async overload) so the inner types still get direction propagation.
273
+ // NullableType is skipped to preserve its subclass behaviour.
274
+ const a = resolveDirectedType(type.a, direction) ?? type.a;
275
+ const b = resolveDirectedType(type.b, direction) ?? type.b;
276
+ if (a !== type.a || b !== type.b) return new BinaryType(a, b);
277
+ } else if (type instanceof OrType && !(type instanceof BinaryType || type instanceof TupleType)) {
278
+ // flatten "bigint | number" out of another OR-type
279
+ const types = type.types.map((t) => resolveDirectedType(t, direction) ?? t);
280
+ if (types.length === 1) return types[0];
281
+ return new OrType(types[0], ...types.slice(1));
246
282
  }
247
283
 
248
284
  return null;
@@ -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) {
@@ -1,52 +0,0 @@
1
- import type { IntrospectedEnum } from "../gir/enum.ts";
2
- import type { IntrospectedFunction } from "../gir/function.ts";
3
- import type { IntrospectedBaseClass, IntrospectedClassFunction } from "../gir/introspected-classes.ts";
4
-
5
- import { NullableType, TypeIdentifier } from "../gir.ts";
6
- import { GirVisitor } from "../visitor.ts";
7
-
8
- export class FunctionParametersVisitor extends GirVisitor {
9
- /**
10
- * Marks all enum parameters of a function as nullable,
11
- * because GJS allows null values for enum parameters and treats them as a 0 value.
12
- * See issue [#207](https://github.com/gjsify/ts-for-gir/issues/207).
13
- */
14
- private makeEnumParamsNullable(node: IntrospectedFunction): IntrospectedFunction;
15
- private makeEnumParamsNullable<T extends IntrospectedBaseClass | IntrospectedEnum>(
16
- node: IntrospectedClassFunction<T>,
17
- ): IntrospectedClassFunction<T>;
18
- private makeEnumParamsNullable(
19
- node: IntrospectedFunction | IntrospectedClassFunction,
20
- ): IntrospectedFunction | IntrospectedClassFunction {
21
- return node.copy({
22
- parameters: node.parameters.map((param) => {
23
- const type = param.type.deepUnwrap();
24
- if (type instanceof TypeIdentifier) {
25
- // Get the namespace where this type should be defined
26
- const ns = node.namespace.assertInstalledImport(type.namespace);
27
-
28
- // Check if the type is an enum
29
- const isEnumType = !!ns.getEnum(type.name);
30
-
31
- // If it is, make the parameter nullable
32
- if (isEnumType) {
33
- return param.copy({
34
- type: new NullableType(param.type),
35
- });
36
- }
37
- }
38
- return param;
39
- }),
40
- });
41
- }
42
-
43
- visitFunction = (node: IntrospectedFunction): IntrospectedFunction => {
44
- return this.makeEnumParamsNullable(node);
45
- };
46
-
47
- visitClassFunction = <T extends IntrospectedBaseClass | IntrospectedEnum>(
48
- node: IntrospectedClassFunction<T>,
49
- ): IntrospectedClassFunction<T> => {
50
- return this.makeEnumParamsNullable(node);
51
- };
52
- }