@gtkx/codegen 1.2.0 → 1.2.1

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.
Files changed (47) hide show
  1. package/dist/docs/symbol-page.js +1 -1
  2. package/dist/docs/symbol-page.js.map +1 -1
  3. package/dist/gir/class.d.ts +7 -2
  4. package/dist/gir/class.d.ts.map +1 -1
  5. package/dist/gir/class.js +4 -1
  6. package/dist/gir/class.js.map +1 -1
  7. package/dist/store/gi/callables.d.ts +1 -1
  8. package/dist/store/gi/callables.d.ts.map +1 -1
  9. package/dist/store/gi/callables.js +3 -3
  10. package/dist/store/gi/callables.js.map +1 -1
  11. package/dist/store/gi/class.d.ts.map +1 -1
  12. package/dist/store/gi/class.js +15 -8
  13. package/dist/store/gi/class.js.map +1 -1
  14. package/dist/store/gi/gtype-binding.d.ts +2 -1
  15. package/dist/store/gi/gtype-binding.d.ts.map +1 -1
  16. package/dist/store/gi/gtype-binding.js +13 -1
  17. package/dist/store/gi/gtype-binding.js.map +1 -1
  18. package/dist/store/gi/interface.js +2 -2
  19. package/dist/store/gi/interface.js.map +1 -1
  20. package/dist/store/gi/record-constructor.d.ts.map +1 -1
  21. package/dist/store/gi/record-constructor.js +4 -2
  22. package/dist/store/gi/record-constructor.js.map +1 -1
  23. package/dist/store/gi/record-field-accessor.d.ts +3 -2
  24. package/dist/store/gi/record-field-accessor.d.ts.map +1 -1
  25. package/dist/store/gi/record-field-accessor.js +27 -9
  26. package/dist/store/gi/record-field-accessor.js.map +1 -1
  27. package/dist/store/gi/record.d.ts.map +1 -1
  28. package/dist/store/gi/record.js +5 -9
  29. package/dist/store/gi/record.js.map +1 -1
  30. package/dist/store/gi/signal.d.ts.map +1 -1
  31. package/dist/store/gi/signal.js +14 -14
  32. package/dist/store/gi/signal.js.map +1 -1
  33. package/dist/store/resolve-store.d.ts.map +1 -1
  34. package/dist/store/resolve-store.js +1 -0
  35. package/dist/store/resolve-store.js.map +1 -1
  36. package/package.json +3 -3
  37. package/src/docs/symbol-page.ts +1 -1
  38. package/src/gir/class.ts +12 -3
  39. package/src/store/gi/callables.ts +4 -4
  40. package/src/store/gi/class.ts +33 -14
  41. package/src/store/gi/gtype-binding.ts +16 -1
  42. package/src/store/gi/interface.ts +2 -2
  43. package/src/store/gi/record-constructor.ts +4 -2
  44. package/src/store/gi/record-field-accessor.ts +47 -10
  45. package/src/store/gi/record.ts +13 -17
  46. package/src/store/gi/signal.ts +20 -23
  47. package/src/store/resolve-store.ts +1 -0
package/src/gir/class.ts CHANGED
@@ -11,6 +11,12 @@ type GirVirtualMethod = GirCallable & {
11
11
  invoker: string | undefined;
12
12
  };
13
13
 
14
+ /** A `<glib:signal>` on a class or interface. */
15
+ type GirSignal = GirCallable & {
16
+ /** Whether the signal takes a `::detail` suffix, so `changed::key` reaches only that detail. */
17
+ isDetailed: boolean;
18
+ };
19
+
14
20
  /** A `<class>` or `<interface>` declared by a GIR namespace. */
15
21
  type GirClass = {
16
22
  /** Name GIR gives the type, without its namespace prefix. */
@@ -54,7 +60,7 @@ type GirClass = {
54
60
  /** GObject properties the type declares. */
55
61
  properties: GirProperty[];
56
62
  /** GObject signals the type declares. */
57
- signals: GirCallable[];
63
+ signals: GirSignal[];
58
64
  /** Virtual methods the type declares, carrying the documentation their vtable slots are generated from. */
59
65
  vfuncs: GirVirtualMethod[];
60
66
  };
@@ -88,8 +94,11 @@ const classFromNode = (node: RawNode, isInterface: boolean, context: ParseContex
88
94
  constructors: getChildren(node, GIR_CONSTRUCTOR_TAG).map((ctor) => functionFromNode(ctor, context)),
89
95
  functions: getChildren(node, "function").map((fn) => functionFromNode(fn, context)),
90
96
  properties: getChildren(node, "property").map((property) => propertyFromNode(property, context)),
91
- signals: getChildren(node, "glib:signal").map((signal) => parseCallable(signal, context)),
97
+ signals: getChildren(node, "glib:signal").map((signal) => ({
98
+ ...parseCallable(signal, context),
99
+ isDetailed: isAttrTrue(signal, "detailed"),
100
+ })),
92
101
  vfuncs: virtualMethodsFromNode(node, context),
93
102
  });
94
103
 
95
- export { classFromNode, type GirClass, type GirVirtualMethod };
104
+ export { classFromNode, type GirClass, type GirSignal, type GirVirtualMethod };
@@ -7,7 +7,7 @@ import { renderBlock } from "../../writer/emit.js";
7
7
  import { matchAsyncFinish } from "./async.js";
8
8
  import { callableDoc, callableSpec } from "./callable-doc.js";
9
9
  import { renderFnExpression } from "./function.js";
10
- import { gtypeMemberDeclaration } from "./gtype-binding.js";
10
+ import { gtypeMemberDeclaration, gtypeStaticMember } from "./gtype-binding.js";
11
11
  import {
12
12
  finishCallExpression,
13
13
  memberName,
@@ -66,7 +66,7 @@ type PlainTypeMembersOptions = {
66
66
  context: ModuleContext;
67
67
  className: string;
68
68
  callables: Callables;
69
- hasGtype: boolean;
69
+ gtypeExpr: string | undefined;
70
70
  };
71
71
 
72
72
  const RUNTIME_OWNED_LIFETIME_METHODS: Set<string> = new Set([
@@ -514,11 +514,11 @@ const renderPlainInstanceMethods = (
514
514
  const renderPlainTypeMembers = (
515
515
  options: PlainTypeMembersOptions,
516
516
  ): { members: string[]; claimedNames: Set<string> } => {
517
- const { context, className, callables, hasGtype } = options;
517
+ const { context, className, callables, gtypeExpr } = options;
518
518
  const claimedNames: Set<string> = new Set();
519
519
 
520
520
  const members: string[] = [
521
- ...(hasGtype ? [gtypeMemberDeclaration(context)] : []),
521
+ ...(gtypeExpr === undefined ? [] : [gtypeMemberDeclaration(context), gtypeStaticMember(context)]),
522
522
  ...renderStaticHead(context, callables, className),
523
523
  ...renderPlainInstanceMethods(context, callables.methods, claimedNames),
524
524
  ];
@@ -34,7 +34,7 @@ import {
34
34
  } from "./callables.js";
35
35
  import { renderClassConstructor, renderConstructorPropsInterface } from "./constructor-props.js";
36
36
  import { getDoc } from "./doc-spec.js";
37
- import { gtypeMemberDeclaration, renderSourceGtype } from "./gtype-binding.js";
37
+ import { gtypeMemberDeclaration, gtypeStaticMember, renderSourceGtype } from "./gtype-binding.js";
38
38
  import { methodExportName } from "./method.js";
39
39
  import { renderPropertyDeclarations } from "./properties.js";
40
40
  import { renderResolvedPropertyAccessor, resolveAccessor, type ResolvedAccessor } from "./property-accessor.js";
@@ -60,6 +60,14 @@ type MemberDeclarationsOptions = {
60
60
 
61
61
  type ClassMembers = { members: string[]; accessors: ResolvedAccessor[] };
62
62
 
63
+ type ClassMembersOptions = {
64
+ context: ModuleContext;
65
+ klass: GirClass;
66
+ callables: Callables;
67
+ gtypeExpr: string | undefined;
68
+ hasParent: boolean;
69
+ };
70
+
63
71
  type ImplementedRefOptions = {
64
72
  context: ModuleContext;
65
73
  klass: GirClass;
@@ -97,7 +105,16 @@ const generateClass = (context: ModuleContext, klass: GirClass): void => {
97
105
  const implemented = resolveImplementedRefs(context, klass);
98
106
  const typeRefs = implemented.map((ref) => omittedTypeRef(ref.typeRef, ref.conflicts));
99
107
  const implementsClause = typeRefs.length === 0 ? "" : ` implements ${typeRefs.join(", ")}`;
100
- const { members, accessors } = renderClassMembers(context, klass, callables, parentExpression !== undefined);
108
+ const gtypeExpr = renderSourceGtype(context, klass);
109
+
110
+ const { members, accessors } = renderClassMembers({
111
+ context,
112
+ klass,
113
+ callables,
114
+ gtypeExpr,
115
+ hasParent: parentExpression !== undefined,
116
+ });
117
+
101
118
  const body = indentMembers(members);
102
119
  const doc = getDoc(klass);
103
120
  const modifier = klass.isAbstract ? "abstract " : "";
@@ -115,7 +132,7 @@ const generateClass = (context: ModuleContext, klass: GirClass): void => {
115
132
 
116
133
  appendMemberDeclarations({ context, klass, className, accessors, implemented });
117
134
  appendInstallMixins(context, className, implemented);
118
- appendClassRegistrations(context, klass, className);
135
+ appendClassRegistrations(context, klass, className, gtypeExpr);
119
136
  };
120
137
 
121
138
  const appendMemberDeclarations = (options: MemberDeclarationsOptions): void => {
@@ -150,15 +167,14 @@ const appendInterfaceMerge = (
150
167
  });
151
168
  };
152
169
 
153
- const renderClassMembers = (
154
- context: ModuleContext,
155
- klass: GirClass,
156
- callables: Callables,
157
- hasParent: boolean,
158
- ): ClassMembers => {
170
+ const renderClassMembers = (options: ClassMembersOptions): ClassMembers => {
171
+ const { context, klass, callables, gtypeExpr } = options;
159
172
  const className = sanitizeTypeIdentifier(klass.name);
160
- const members: string[] = [gtypeMemberDeclaration(context)];
161
- const constructorBlock = renderClassConstructor(context, klass, className, hasParent);
173
+
174
+ const members: string[] =
175
+ gtypeExpr === undefined ? [] : [gtypeMemberDeclaration(context), gtypeStaticMember(context)];
176
+
177
+ const constructorBlock = renderClassConstructor(context, klass, className, options.hasParent);
162
178
 
163
179
  if (constructorBlock !== undefined) {
164
180
  members.push(constructorBlock);
@@ -239,9 +255,12 @@ const appendInstallMixins = (context: ModuleContext, className: string, implemen
239
255
  ]);
240
256
  };
241
257
 
242
- const appendClassRegistrations = (context: ModuleContext, klass: GirClass, className: string): void => {
243
- const gtypeExpr = renderSourceGtype(context, klass);
244
-
258
+ const appendClassRegistrations = (
259
+ context: ModuleContext,
260
+ klass: GirClass,
261
+ className: string,
262
+ gtypeExpr: string | undefined,
263
+ ): void => {
245
264
  appendWrapperClassRegistration(context, {
246
265
  className,
247
266
  gtypeExpr,
@@ -1,17 +1,32 @@
1
1
  import { sourceStringLiteral } from "@gtkx/utils";
2
2
  import type { ModuleContext } from "../../writer/context.js";
3
3
  import { PRIMITIVE_TS_TYPE } from "../../gir/primitives.js";
4
+ import { renderJsDoc } from "../../writer/doc.js";
5
+ import { renderBlock } from "../../writer/emit.js";
4
6
 
5
7
  type TypeSource = {
6
8
  glibGetType: string | undefined;
7
9
  glibTypeName: string | undefined;
8
10
  };
9
11
 
12
+ const GTYPE_NOTE = [
13
+ "The GType this class is registered under, read off the class it is accessed on rather than",
14
+ "inherited, so a subclass `registerClass` created reports its own type and one that never went",
15
+ "through `registerClass` reports the invalid type.",
16
+ ].join("\n");
17
+
10
18
  const gtypeTsType = (context: ModuleContext): string =>
11
19
  context.namespace.name === "GObject" ? "Type" : PRIMITIVE_TS_TYPE.gtype;
12
20
 
13
21
  const gtypeMemberDeclaration = (context: ModuleContext): string => `declare __type__: ${gtypeTsType(context)};`;
14
22
 
23
+ const gtypeStaticMember = (context: ModuleContext): string => {
24
+ context.addRuntimeImport("getClassType");
25
+ const block = renderBlock(`static get __gtype__(): ${gtypeTsType(context)}`, "return getClassType(this);");
26
+
27
+ return `${renderJsDoc(undefined, GTYPE_NOTE)}${block}`;
28
+ };
29
+
15
30
  const renderInternGtype = (context: ModuleContext, typeName: string | undefined): string | undefined => {
16
31
  if (typeName === undefined) {
17
32
  return undefined;
@@ -48,4 +63,4 @@ const renderSourceGtype = (context: ModuleContext, source: TypeSource): string |
48
63
  ? undefined
49
64
  : renderGtypeExpression(context, source.glibGetType, source.glibTypeName);
50
65
 
51
- export { gtypeTsType, gtypeMemberDeclaration, renderSourceGtype };
66
+ export { gtypeTsType, gtypeMemberDeclaration, gtypeStaticMember, renderSourceGtype };
@@ -21,7 +21,7 @@ import {
21
21
  renderStaticHead,
22
22
  } from "./callables.js";
23
23
  import { annotationSpec, getDoc } from "./doc-spec.js";
24
- import { renderSourceGtype } from "./gtype-binding.js";
24
+ import { gtypeStaticMember, renderSourceGtype } from "./gtype-binding.js";
25
25
  import { methodExportName } from "./method.js";
26
26
  import {
27
27
  type PropertyAccessorArgs,
@@ -378,7 +378,7 @@ const renderInterfaceClass = (context: ModuleContext, options: InterfaceClassOpt
378
378
  const members: string[] = [];
379
379
 
380
380
  if (gtypeExpr !== undefined) {
381
- members.push(renderInterfaceHasInstance(context, className, gtypeExpr));
381
+ members.push(renderInterfaceHasInstance(context, className, gtypeExpr), gtypeStaticMember(context));
382
382
  }
383
383
 
384
384
  members.push(...renderStaticHead(context, callables, className), renderInterfaceBrand(context, implRef));
@@ -11,7 +11,7 @@ import { renderBlock, renderBracedOrEmpty } from "../../writer/emit.js";
11
11
  import { type Callables, staticMembers } from "./callables.js";
12
12
  import { getDoc } from "./doc-spec.js";
13
13
  import { renderSourceGtype } from "./gtype-binding.js";
14
- import { emitFieldWrite, isEmittableField, isInlineField } from "./record-field-accessor.js";
14
+ import { emitFieldWrite, isEmittableField, isInlineField, isStorableFieldType } from "./record-field-accessor.js";
15
15
  import { computeRecordFieldSlots, type RecordFieldSlot } from "./record-layout.js";
16
16
  import { isConstructibleRecord } from "./value-marshalable.js";
17
17
 
@@ -25,7 +25,9 @@ type RecordConstructorSpec = {
25
25
  };
26
26
 
27
27
  const isWritableFieldSlot = (context: ModuleContext, entry: RecordFieldSlot): entry is WritableFieldSlot =>
28
- entry.field.writable && isEmittableField(context, entry.field);
28
+ entry.field.writable &&
29
+ isEmittableField(context, entry.field) &&
30
+ isStorableFieldType(context, entry.field.type);
29
31
 
30
32
  const renderRecordConstructorPropsInterface = (
31
33
  context: ModuleContext,
@@ -9,6 +9,7 @@ import type { ModuleContext } from "../../writer/context.js";
9
9
  import { isInlineCallbackRef, renderDescriptor } from "../../analysis/descriptor-render.js";
10
10
  import { tStruct } from "../../analysis/descriptor.js";
11
11
  import { renderTsType } from "../../analysis/ts-type.js";
12
+ import { isUnboundedArray } from "../../analysis/type-shape.js";
12
13
  import { indent, renderBlock } from "../../writer/emit.js";
13
14
  import { getDoc } from "./doc-spec.js";
14
15
  import { bitMask, computeRecordFieldSlots, mergeBitfield, type RecordFieldSlot } from "./record-layout.js";
@@ -27,6 +28,10 @@ type AdmittedField = {
27
28
  jsName: string;
28
29
  };
29
30
 
31
+ type AccessibleField = AdmittedField & {
32
+ isSettable: boolean;
33
+ };
34
+
30
35
  type RecordFieldEntry = {
31
36
  jsName: string;
32
37
  tsType: string;
@@ -167,12 +172,49 @@ const admitField = (
167
172
  return { field, jsName };
168
173
  };
169
174
 
175
+ const isNullTerminatedArrayField = (context: ModuleContext, ref: TypeId): boolean => {
176
+ const type = context.library.typeFor(ref);
177
+
178
+ return type?.kind === "carray" && isUnboundedArray(type) && type.elementCType?.endsWith("*") === true;
179
+ };
180
+
181
+ const isAccessibleFieldType = (context: ModuleContext, ref: TypeId): boolean =>
182
+ isAccessorEligibleType(context, ref) || isNullTerminatedArrayField(context, ref);
183
+
184
+ const isStorableFieldType = (context: ModuleContext, ref: TypeId): boolean => {
185
+ const kind = context.library.typeFor(ref)?.kind;
186
+
187
+ return kind !== "carray" && kind !== "list" && kind !== "hashtable";
188
+ };
189
+
190
+ const admitAccessibleField = (
191
+ context: ModuleContext,
192
+ slot: RecordFieldSlot,
193
+ claimedNames: Set<string>,
194
+ siblingFields: GirField[],
195
+ ): AccessibleField | undefined => {
196
+ const admitted = admitField(context, slot, claimedNames);
197
+
198
+ if (admitted === undefined) {
199
+ return undefined;
200
+ }
201
+
202
+ const { field, jsName } = admitted;
203
+ const target: StructArrayTarget = { field, jsName, slot: slot.slot, siblingFields };
204
+ const hasStructArray = resolveStructArray(context, target) !== undefined;
205
+ const hasAccessor = hasStructArray || isAccessibleFieldType(context, field.type);
206
+ const isSettable = field.writable && (hasStructArray || isStorableFieldType(context, field.type));
207
+
208
+ return hasAccessor ? { ...admitted, isSettable } : undefined;
209
+ };
210
+
170
211
  const resolveRecordFieldEntry = (
171
212
  context: ModuleContext,
172
213
  slot: RecordFieldSlot,
173
214
  claimedNames: Set<string>,
215
+ siblingFields: GirField[],
174
216
  ): RecordFieldEntry | undefined => {
175
- const admitted = admitField(context, slot, claimedNames);
217
+ const admitted = admitAccessibleField(context, slot, claimedNames, siblingFields);
176
218
 
177
219
  if (admitted === undefined) {
178
220
  return undefined;
@@ -181,7 +223,7 @@ const resolveRecordFieldEntry = (
181
223
  return {
182
224
  jsName: admitted.jsName,
183
225
  tsType: renderTsType(context, admitted.field.type, false),
184
- isWritable: admitted.field.writable,
226
+ isWritable: admitted.isSettable,
185
227
  doc: admitted.field.doc,
186
228
  annotations: admitted.field.annotations,
187
229
  };
@@ -193,7 +235,7 @@ const renderRecordFieldAccessor = (
193
235
  claimedNames: Set<string>,
194
236
  siblingFields: GirField[],
195
237
  ): string | undefined => {
196
- const admitted = admitField(context, slot, claimedNames);
238
+ const admitted = admitAccessibleField(context, slot, claimedNames, siblingFields);
197
239
 
198
240
  if (admitted === undefined) {
199
241
  return undefined;
@@ -207,12 +249,6 @@ const renderRecordFieldAccessor = (
207
249
  return `${doc}${structArray}`;
208
250
  }
209
251
 
210
- if (!isAccessorEligibleType(context, field.type)) {
211
- const tsType = renderTsType(context, field.type, false);
212
-
213
- return `${doc}declare ${jsName}: ${tsType};`;
214
- }
215
-
216
252
  const descriptor = context.hoistDescriptor(
217
253
  renderDescriptor(context, field.type, "none", { isInline: isInlineField(context, field) }),
218
254
  );
@@ -230,7 +266,7 @@ const renderRecordFieldAccessor = (
230
266
 
231
267
  const blocks: string[] = [getterBlock(accessorOptions)];
232
268
 
233
- if (field.writable) {
269
+ if (admitted.isSettable) {
234
270
  blocks.push(setterBlock(accessorOptions));
235
271
  }
236
272
 
@@ -654,6 +690,7 @@ const setterBlock = (options: AccessorOptions): string =>
654
690
  export {
655
691
  isEmittableField,
656
692
  isInlineField,
693
+ isStorableFieldType,
657
694
  emitFieldWrite,
658
695
  resolveRecordFieldEntry,
659
696
  renderRecordFieldAccessor,
@@ -12,6 +12,14 @@ import { computeRecordFieldSlots } from "./record-layout.js";
12
12
  import { appendWrapperClassRegistration } from "./registration.js";
13
13
  import { isConstructibleRecord } from "./value-marshalable.js";
14
14
 
15
+ type RecordMembersOptions = {
16
+ context: ModuleContext;
17
+ record: GirRecord;
18
+ className: string;
19
+ callables: Callables;
20
+ gtypeExpr: string | undefined;
21
+ };
22
+
15
23
  const isGErrorRecord = (context: ModuleContext, record: GirRecord): boolean =>
16
24
  context.namespace.name === "GLib" && record.glibGetType === "g_error_get_type";
17
25
 
@@ -30,7 +38,8 @@ const generateRecord = (context: ModuleContext, record: GirRecord): void => {
30
38
  };
31
39
 
32
40
  generateBindings(context, callables);
33
- const members = renderRecordMembers(context, record, className, callables);
41
+ const gtypeExpr = renderSourceGtype(context, record);
42
+ const members = renderRecordMembers({ context, record, className, callables, gtypeExpr });
34
43
  const body = indentMembers(members);
35
44
  const heritage = isErrorSubclass ? " extends globalThis.Error" : "";
36
45
  const doc = getDoc(record);
@@ -50,29 +59,16 @@ const generateRecord = (context: ModuleContext, record: GirRecord): void => {
50
59
  });
51
60
  }
52
61
 
53
- const gtypeExpr = renderSourceGtype(context, record);
54
-
55
62
  appendWrapperClassRegistration(context, {
56
63
  className,
57
64
  gtypeExpr,
58
65
  });
59
66
  };
60
67
 
61
- const renderRecordMembers = (
62
- context: ModuleContext,
63
- record: GirRecord,
64
- className: string,
65
- callables: Callables,
66
- ): string[] => {
68
+ const renderRecordMembers = (options: RecordMembersOptions): string[] => {
69
+ const { context, record, className, callables, gtypeExpr } = options;
67
70
  const isErrorSubclass = isGErrorRecord(context, record);
68
-
69
- const { members, claimedNames } = renderPlainTypeMembers({
70
- context,
71
- className,
72
- callables,
73
- hasGtype: record.glibGetType !== undefined,
74
- });
75
-
71
+ const { members, claimedNames } = renderPlainTypeMembers({ context, className, callables, gtypeExpr });
76
72
  members.unshift(renderRecordConstructor(context, { record, className, callables, isErrorSubclass }));
77
73
  const { slots } = computeRecordFieldSlots(context, record.fields, record.isUnion);
78
74
 
@@ -1,5 +1,5 @@
1
1
  import { camelCase, sourceStringLiteral } from "@gtkx/utils";
2
- import type { GirClass } from "../../gir/class.js";
2
+ import type { GirClass, GirSignal } from "../../gir/class.js";
3
3
  import type { GirCallable, GirParameter, GirReturnValue } from "../../gir/parameter.js";
4
4
  import type { GirProperty } from "../../gir/property.js";
5
5
  import type { ModuleContext } from "../../writer/context.js";
@@ -139,16 +139,20 @@ const renderSignalMap = (spec: SignalMapSpec): string => {
139
139
  const { context, klass, className, isParentlessObjectSubclass, suffix, renderEntry } = spec;
140
140
  const extendsRefs = signalMapParentRefs(context, klass, isParentlessObjectSubclass, suffix);
141
141
  const extendsClause = extendsRefs.length === 0 ? "" : ` extends ${extendsRefs.join(", ")}`;
142
+ const signals = collectClassSignals(context, klass);
142
143
 
143
- const signalEntries = collectClassSignals(context, klass).map(
144
- (signal) => `${signalDoc(signal)}${sourceStringLiteral(signal.name)}: ${renderEntry(context, signal)};`,
145
- );
144
+ const signalEntries = signals.flatMap((signal) => {
145
+ const value = renderEntry(context, signal);
146
+ const entry = `${signalDoc(signal)}${sourceStringLiteral(signal.name)}: ${value};`;
146
147
 
147
- const entries = [
148
- ...signalEntries,
149
- ...renderNotifyDetailEntries(context, klass, suffix),
150
- ...renderCustomNotifyEntry(context, klass, suffix),
151
- ];
148
+ if (!signal.isDetailed) {
149
+ return [entry];
150
+ }
151
+
152
+ return [entry, `[detail: \`${signal.name}::\${string}\`]: ${value};`];
153
+ });
154
+
155
+ const entries = [...signalEntries, ...renderNotifyDetailEntries(context, klass, suffix)];
152
156
 
153
157
  return renderBracedOrEmpty(`export interface ${className}${suffix}${extendsClause}`, entries.join("\n"));
154
158
  };
@@ -156,14 +160,6 @@ const renderSignalMap = (spec: SignalMapSpec): string => {
156
160
  const signalDoc = (signal: GirCallable): string =>
157
161
  renderJsDoc(signal.doc, undefined, handlerSpec(signal, signal.parameters));
158
162
 
159
- const renderCustomNotifyEntry = (context: ModuleContext, klass: GirClass, suffix: string): string[] => {
160
- if (context.namespace.name !== "GObject" || klass.name !== "Object") {
161
- return [];
162
- }
163
-
164
- return [`[detail: \`notify::\${string}\`]: ${gobjectObjectMapRef(context, suffix)}["notify"];`];
165
- };
166
-
167
163
  const renderNotifyDetailEntries = (context: ModuleContext, klass: GirClass, suffix: string): string[] => {
168
164
  const notifyValue = `${gobjectObjectMapRef(context, suffix)}["notify"]`;
169
165
 
@@ -219,10 +215,11 @@ const renderSignalConnectInterface = (className: string, isRootObject: boolean):
219
215
  `__signals__?: ${map};`,
220
216
  `connect<K extends keyof ${map}>(signal: K, handler: ${map}[K], isAfter?: boolean): number;`,
221
217
  `emit<K extends keyof ${emitMap}>(sigName: K, ...args: ${emitMap}[K]["args"]): ${emitMap}[K]["result"];`,
222
- "emit(sigName: string, ...args: unknown[]): unknown;",
223
218
  ];
224
219
 
225
- if (!isRootObject) {
220
+ if (isRootObject) {
221
+ lines.push("emit(sigName: string, ...args: unknown[]): unknown;");
222
+ } else {
226
223
  const chainable = (methods: string[], trailing: string): void => {
227
224
  for (const method of methods) {
228
225
  lines.push(
@@ -382,7 +379,7 @@ const renderCallback = (context: ModuleContext, signal: GirCallable): string =>
382
379
  const forEachInterfaceSignal = (
383
380
  context: ModuleContext,
384
381
  klass: GirClass,
385
- consider: (signal: GirCallable) => void,
382
+ consider: (signal: GirSignal) => void,
386
383
  ): void => {
387
384
  for (const iface of resolveInterfaces(context.library, context.namespace.name, klass.implements)) {
388
385
  for (const signal of iface.klass.signals) {
@@ -391,12 +388,12 @@ const forEachInterfaceSignal = (
391
388
  }
392
389
  };
393
390
 
394
- const collectClassSignals = (context: ModuleContext, klass: GirClass): GirCallable[] => {
391
+ const collectClassSignals = (context: ModuleContext, klass: GirClass): GirSignal[] => {
395
392
  const inheritedNames = collectInheritedSignalNames(context, klass);
396
393
  const seen: Set<string> = new Set();
397
- const result: GirCallable[] = [];
394
+ const result: GirSignal[] = [];
398
395
 
399
- const consider = (signal: GirCallable): void => {
396
+ const consider = (signal: GirSignal): void => {
400
397
  const name = camelCase(signal.name);
401
398
 
402
399
  if (!signal.introspectable || inheritedNames.has(name) || seen.has(name)) {
@@ -28,6 +28,7 @@ const STORE_CONSUMERS: string[] = [
28
28
  "@gtkx/components",
29
29
  "@gtkx/css",
30
30
  "@gtkx/react",
31
+ "@gtkx/runtime",
31
32
  "@gtkx/testing",
32
33
  ];
33
34