@openwebf/webf 0.24.2 → 0.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,6 +100,26 @@ This will:
100
100
  - Generate `src/index.ts` and `src/types.ts` that wrap `webf.invokeModuleAsync('Share', ...)`
101
101
  - Generate Dart bindings in `../webf_modules/share/lib/src/share_module_bindings_generated.dart`
102
102
 
103
+ #### Module Events (Optional)
104
+
105
+ If your module emits events via Dart `moduleManager.emitModuleEvent(...)`, you can optionally declare them in the same `*.module.d.ts` file using:
106
+
107
+ ```ts
108
+ interface WebFMyModuleModuleEvents {
109
+ // Shorthand: only types the `event` parameter (extra defaults to `any`)
110
+ ready: Event;
111
+
112
+ // Tuple: [eventType, extraType] types both callback parameters
113
+ scanResult: [Event, { deviceId: string; rssi: number }];
114
+ click: [CustomEvent<string>, number[]];
115
+ }
116
+ ```
117
+
118
+ When present, `webf module-codegen` will generate:
119
+ - `WebFMyModuleModuleEventListener` (typed listener with correlated `event.type` ↔ `extra`)
120
+ - `WebFMyModule.addListener(eventType, listener)` (returns an unsubscribe function)
121
+ - `WebFMyModule.removeListener()` (removes all listeners for this module)
122
+
103
123
  ### Interactive Mode
104
124
 
105
125
  If you don't provide all required options, the CLI will prompt you interactively:
package/dist/module.js CHANGED
@@ -14,12 +14,12 @@ function parseModuleDefinition(modulePath) {
14
14
  const sourceFile = typescript_1.default.createSourceFile(modulePath, sourceText, typescript_1.default.ScriptTarget.ES2020, true, typescript_1.default.ScriptKind.TS);
15
15
  let interfaceDecl;
16
16
  const supporting = [];
17
+ const webfInterfaceDecls = [];
17
18
  for (const stmt of sourceFile.statements) {
18
19
  if (typescript_1.default.isInterfaceDeclaration(stmt)) {
19
20
  const name = stmt.name.text;
20
- if (!interfaceDecl && name.startsWith('WebF')) {
21
- interfaceDecl = stmt;
22
- }
21
+ if (name.startsWith('WebF'))
22
+ webfInterfaceDecls.push(stmt);
23
23
  supporting.push(stmt);
24
24
  }
25
25
  else if (typescript_1.default.isTypeAliasDeclaration(stmt) ||
@@ -28,6 +28,13 @@ function parseModuleDefinition(modulePath) {
28
28
  supporting.push(stmt);
29
29
  }
30
30
  }
31
+ // Prefer the "main module interface": first WebF* interface that declares methods.
32
+ if (!interfaceDecl) {
33
+ interfaceDecl = webfInterfaceDecls.find(decl => decl.members.some(member => typescript_1.default.isMethodSignature(member)));
34
+ }
35
+ if (!interfaceDecl) {
36
+ interfaceDecl = webfInterfaceDecls[0];
37
+ }
31
38
  if (!interfaceDecl) {
32
39
  throw new Error(`No interface starting with "WebF" found in module interface file: ${modulePath}`);
33
40
  }
@@ -70,10 +77,65 @@ function parseModuleDefinition(modulePath) {
70
77
  documentation,
71
78
  });
72
79
  }
80
+ // Optional module events declaration:
81
+ // `interface WebF<ModuleName>ModuleEvents { scanResult: [Event, Payload]; }`
82
+ const eventsInterfaceName = `${interfaceName}ModuleEvents`;
83
+ const eventsDecl = sourceFile.statements.find(stmt => typescript_1.default.isInterfaceDeclaration(stmt) && stmt.name.text === eventsInterfaceName);
84
+ let events;
85
+ if (eventsDecl) {
86
+ const eventSpecs = [];
87
+ for (const member of eventsDecl.members) {
88
+ if (!typescript_1.default.isPropertySignature(member) || !member.name)
89
+ continue;
90
+ const rawName = member.name.getText(sourceFile);
91
+ const eventName = rawName.replace(/['"]/g, '');
92
+ let eventTypeText = 'Event';
93
+ let extraTypeText = 'any';
94
+ if (member.type) {
95
+ if (typescript_1.default.isTupleTypeNode(member.type) && member.type.elements.length === 2) {
96
+ eventTypeText = printer.printNode(typescript_1.default.EmitHint.Unspecified, member.type.elements[0], sourceFile);
97
+ extraTypeText = printer.printNode(typescript_1.default.EmitHint.Unspecified, member.type.elements[1], sourceFile);
98
+ }
99
+ else {
100
+ eventTypeText = printer.printNode(typescript_1.default.EmitHint.Unspecified, member.type, sourceFile);
101
+ }
102
+ }
103
+ let documentation;
104
+ const jsDocs = member.jsDoc;
105
+ if (jsDocs && jsDocs.length > 0) {
106
+ documentation = jsDocs
107
+ .map(doc => doc.comment)
108
+ .filter(Boolean)
109
+ .join('\n');
110
+ }
111
+ eventSpecs.push({
112
+ name: eventName,
113
+ eventTypeText,
114
+ extraTypeText,
115
+ documentation,
116
+ });
117
+ }
118
+ if (eventSpecs.length > 0) {
119
+ events = {
120
+ interfaceName: eventsInterfaceName,
121
+ eventNameTypeName: `${interfaceName}ModuleEventName`,
122
+ eventArgsTypeName: `${interfaceName}ModuleEventArgs`,
123
+ listenerTypeName: `${interfaceName}ModuleEventListener`,
124
+ events: eventSpecs,
125
+ };
126
+ }
127
+ }
73
128
  if (methods.length === 0) {
74
129
  throw new Error(`Interface ${interfaceName} in ${modulePath} does not declare any methods`);
75
130
  }
76
- return { interfaceName, moduleName, methods, supportingStatements: supporting, sourceFile };
131
+ return {
132
+ interfaceName,
133
+ moduleName,
134
+ methods,
135
+ events,
136
+ supportingStatements: supporting,
137
+ sourceFile,
138
+ };
77
139
  }
78
140
  function buildTypesFile(def) {
79
141
  const printer = typescript_1.default.createPrinter();
@@ -102,6 +164,19 @@ function buildTypesFile(def) {
102
164
  lines.push(printed);
103
165
  }
104
166
  lines.push('');
167
+ if (def.events) {
168
+ const { interfaceName, eventNameTypeName, eventArgsTypeName, listenerTypeName } = def.events;
169
+ lines.push(`export type ${eventNameTypeName} = Extract<keyof ${interfaceName}, string>;`);
170
+ lines.push(`export type ${eventArgsTypeName}<K extends ${eventNameTypeName} = ${eventNameTypeName}> =`);
171
+ lines.push(` ${interfaceName}[K] extends readonly [infer E, infer X]`);
172
+ lines.push(` ? [event: (E & { type: K }), extra: X]`);
173
+ lines.push(` : [event: (${interfaceName}[K] & { type: K }), extra: any];`);
174
+ lines.push('');
175
+ lines.push(`export type ${listenerTypeName} = (...args: {`);
176
+ lines.push(` [K in ${eventNameTypeName}]: ${eventArgsTypeName}<K>;`);
177
+ lines.push(`}[${eventNameTypeName}]) => any;`);
178
+ lines.push('');
179
+ }
105
180
  // Ensure file is treated as a module even if no declarations were emitted.
106
181
  lines.push('export {};');
107
182
  return lines.join('\n');
@@ -128,6 +203,10 @@ function buildIndexFile(def) {
128
203
  typeImportNames.add(name);
129
204
  }
130
205
  }
206
+ if (def.events) {
207
+ typeImportNames.add(def.events.eventNameTypeName);
208
+ typeImportNames.add(def.events.eventArgsTypeName);
209
+ }
131
210
  const typeImportsSorted = Array.from(typeImportNames).sort();
132
211
  if (typeImportsSorted.length > 0) {
133
212
  lines.push(`import type { ${typeImportsSorted.join(', ')} } from './types';`);
@@ -138,6 +217,49 @@ function buildIndexFile(def) {
138
217
  lines.push(" return typeof webf !== 'undefined' && typeof (webf as any).invokeModuleAsync === 'function';");
139
218
  lines.push(' }');
140
219
  lines.push('');
220
+ if (def.events) {
221
+ lines.push(' private static _moduleListenerInstalled = false;');
222
+ lines.push(' private static _listeners: Record<string, Set<(event: Event, extra: any) => any>> = Object.create(null);');
223
+ lines.push('');
224
+ lines.push(` static addListener<K extends ${def.events.eventNameTypeName}>(type: K, listener: (...args: ${def.events.eventArgsTypeName}<K>) => any): () => void {`);
225
+ lines.push(" if (typeof webf === 'undefined' || typeof (webf as any).addWebfModuleListener !== 'function') {");
226
+ lines.push(" throw new Error('WebF module event API is not available. Make sure you are running in WebF runtime.');");
227
+ lines.push(' }');
228
+ lines.push('');
229
+ lines.push(' if (!this._moduleListenerInstalled) {');
230
+ lines.push(` (webf as any).addWebfModuleListener('${def.moduleName}', (event: Event, extra: any) => {`);
231
+ lines.push(' const set = this._listeners[event.type];');
232
+ lines.push(' if (!set) return;');
233
+ lines.push(' for (const fn of set) { fn(event, extra); }');
234
+ lines.push(' });');
235
+ lines.push(' this._moduleListenerInstalled = true;');
236
+ lines.push(' }');
237
+ lines.push('');
238
+ lines.push(' (this._listeners[type] ??= new Set()).add(listener as any);');
239
+ lines.push('');
240
+ lines.push(' const cls = this;');
241
+ lines.push(' return () => {');
242
+ lines.push(' const set = cls._listeners[type];');
243
+ lines.push(' if (!set) return;');
244
+ lines.push(' set.delete(listener as any);');
245
+ lines.push(' if (set.size === 0) { delete cls._listeners[type]; }');
246
+ lines.push('');
247
+ lines.push(' if (Object.keys(cls._listeners).length === 0) {');
248
+ lines.push(' cls.removeListener();');
249
+ lines.push(' }');
250
+ lines.push(' };');
251
+ lines.push(' }');
252
+ lines.push('');
253
+ lines.push(' static removeListener(): void {');
254
+ lines.push(' this._listeners = Object.create(null);');
255
+ lines.push(' this._moduleListenerInstalled = false;');
256
+ lines.push(" if (typeof webf === 'undefined' || typeof (webf as any).removeWebfModuleListener !== 'function') {");
257
+ lines.push(' return;');
258
+ lines.push(' }');
259
+ lines.push(` (webf as any).removeWebfModuleListener('${def.moduleName}');`);
260
+ lines.push(' }');
261
+ lines.push('');
262
+ }
141
263
  for (const method of def.methods) {
142
264
  if (method.documentation) {
143
265
  lines.push(' /**');
@@ -173,6 +295,11 @@ function buildIndexFile(def) {
173
295
  typeExportNames.add(stmt.name.text);
174
296
  }
175
297
  }
298
+ if (def.events) {
299
+ typeExportNames.add(def.events.eventNameTypeName);
300
+ typeExportNames.add(def.events.eventArgsTypeName);
301
+ typeExportNames.add(def.events.listenerTypeName);
302
+ }
176
303
  const sorted = Array.from(typeExportNames).sort();
177
304
  if (sorted.length) {
178
305
  lines.push(' ' + sorted.join(','));
@@ -260,6 +387,7 @@ function mapTsPropertyTypeToDart(type, optional) {
260
387
  }
261
388
  }
262
389
  function buildDartBindings(def, command) {
390
+ var _a;
263
391
  const dartClassBase = `${def.moduleName}Module`;
264
392
  const dartBindingsClass = `${dartClassBase}Bindings`;
265
393
  const lines = [];
@@ -268,6 +396,9 @@ function buildDartBindings(def, command) {
268
396
  lines.push('// Generated by `webf module-codegen`');
269
397
  lines.push('');
270
398
  lines.push("import 'package:webf/module.dart';");
399
+ if (def.events) {
400
+ lines.push("import 'package:webf/dom.dart';");
401
+ }
271
402
  if (def.methods.some(m => m.params.some(p => isTsByteArrayUnion(p.typeText)))) {
272
403
  lines.push("import 'package:webf/bridge.dart';");
273
404
  }
@@ -275,7 +406,9 @@ function buildDartBindings(def, command) {
275
406
  // Generate Dart classes for supporting TS interfaces (compound option types).
276
407
  const optionInterfaces = [];
277
408
  for (const stmt of def.supportingStatements) {
278
- if (typescript_1.default.isInterfaceDeclaration(stmt) && stmt.name.text !== def.interfaceName) {
409
+ if (typescript_1.default.isInterfaceDeclaration(stmt) &&
410
+ stmt.name.text !== def.interfaceName &&
411
+ stmt.name.text !== ((_a = def.events) === null || _a === void 0 ? void 0 : _a.interfaceName)) {
279
412
  optionInterfaces.push(stmt);
280
413
  }
281
414
  }
@@ -365,6 +498,25 @@ function buildDartBindings(def, command) {
365
498
  lines.push(` @override`);
366
499
  lines.push(` String get name => '${def.moduleName}';`);
367
500
  lines.push('');
501
+ if (def.events) {
502
+ for (const evt of def.events.events) {
503
+ const methodName = `emit${lodash_1.default.upperFirst(lodash_1.default.camelCase(evt.name))}`;
504
+ const mappedExtra = mapTsParamTypeToDart(evt.extraTypeText, optionTypeNames);
505
+ const dataParamType = mappedExtra.optionClassName
506
+ ? `${mappedExtra.optionClassName}?`
507
+ : 'dynamic';
508
+ lines.push(` dynamic ${methodName}({Event? event, ${dataParamType} data}) {`);
509
+ if (mappedExtra.optionClassName) {
510
+ lines.push(' final mapped = data?.toMap();');
511
+ lines.push(` return dispatchEvent(event: event ?? Event('${evt.name}'), data: mapped);`);
512
+ }
513
+ else {
514
+ lines.push(` return dispatchEvent(event: event ?? Event('${evt.name}'), data: data);`);
515
+ }
516
+ lines.push(' }');
517
+ lines.push('');
518
+ }
519
+ }
368
520
  for (const method of def.methods) {
369
521
  const dartMethodName = lodash_1.default.camelCase(method.name);
370
522
  let dartReturnType = mapTsReturnTypeToDart(method.returnTypeText);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwebf/webf",
3
- "version": "0.24.2",
3
+ "version": "0.24.3",
4
4
  "description": "Command line tools for WebF",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/module.ts CHANGED
@@ -12,10 +12,24 @@ interface ModuleMethodSpec {
12
12
  documentation?: string;
13
13
  }
14
14
 
15
+ interface ModuleEventSpec {
16
+ name: string;
17
+ eventTypeText: string;
18
+ extraTypeText: string;
19
+ documentation?: string;
20
+ }
21
+
15
22
  interface ModuleDefinition {
16
23
  interfaceName: string;
17
24
  moduleName: string;
18
25
  methods: ModuleMethodSpec[];
26
+ events?: {
27
+ interfaceName: string;
28
+ eventNameTypeName: string;
29
+ eventArgsTypeName: string;
30
+ listenerTypeName: string;
31
+ events: ModuleEventSpec[];
32
+ };
19
33
  supportingStatements: ts.Statement[];
20
34
  sourceFile: ts.SourceFile;
21
35
  }
@@ -32,13 +46,12 @@ function parseModuleDefinition(modulePath: string): ModuleDefinition {
32
46
 
33
47
  let interfaceDecl: ts.InterfaceDeclaration | undefined;
34
48
  const supporting: ts.Statement[] = [];
49
+ const webfInterfaceDecls: ts.InterfaceDeclaration[] = [];
35
50
 
36
51
  for (const stmt of sourceFile.statements) {
37
52
  if (ts.isInterfaceDeclaration(stmt)) {
38
53
  const name = stmt.name.text;
39
- if (!interfaceDecl && name.startsWith('WebF')) {
40
- interfaceDecl = stmt;
41
- }
54
+ if (name.startsWith('WebF')) webfInterfaceDecls.push(stmt);
42
55
  supporting.push(stmt);
43
56
  } else if (
44
57
  ts.isTypeAliasDeclaration(stmt) ||
@@ -49,6 +62,17 @@ function parseModuleDefinition(modulePath: string): ModuleDefinition {
49
62
  }
50
63
  }
51
64
 
65
+ // Prefer the "main module interface": first WebF* interface that declares methods.
66
+ if (!interfaceDecl) {
67
+ interfaceDecl = webfInterfaceDecls.find(decl =>
68
+ decl.members.some(member => ts.isMethodSignature(member))
69
+ );
70
+ }
71
+
72
+ if (!interfaceDecl) {
73
+ interfaceDecl = webfInterfaceDecls[0];
74
+ }
75
+
52
76
  if (!interfaceDecl) {
53
77
  throw new Error(
54
78
  `No interface starting with "WebF" found in module interface file: ${modulePath}`
@@ -100,13 +124,93 @@ function parseModuleDefinition(modulePath: string): ModuleDefinition {
100
124
  });
101
125
  }
102
126
 
127
+ // Optional module events declaration:
128
+ // `interface WebF<ModuleName>ModuleEvents { scanResult: [Event, Payload]; }`
129
+ const eventsInterfaceName = `${interfaceName}ModuleEvents`;
130
+ const eventsDecl = sourceFile.statements.find(
131
+ stmt => ts.isInterfaceDeclaration(stmt) && stmt.name.text === eventsInterfaceName
132
+ ) as ts.InterfaceDeclaration | undefined;
133
+
134
+ let events:
135
+ | {
136
+ interfaceName: string;
137
+ eventNameTypeName: string;
138
+ eventArgsTypeName: string;
139
+ listenerTypeName: string;
140
+ events: ModuleEventSpec[];
141
+ }
142
+ | undefined;
143
+
144
+ if (eventsDecl) {
145
+ const eventSpecs: ModuleEventSpec[] = [];
146
+ for (const member of eventsDecl.members) {
147
+ if (!ts.isPropertySignature(member) || !member.name) continue;
148
+
149
+ const rawName = member.name.getText(sourceFile);
150
+ const eventName = rawName.replace(/['"]/g, '');
151
+
152
+ let eventTypeText = 'Event';
153
+ let extraTypeText = 'any';
154
+
155
+ if (member.type) {
156
+ if (ts.isTupleTypeNode(member.type) && member.type.elements.length === 2) {
157
+ eventTypeText = printer.printNode(
158
+ ts.EmitHint.Unspecified,
159
+ member.type.elements[0],
160
+ sourceFile
161
+ );
162
+ extraTypeText = printer.printNode(
163
+ ts.EmitHint.Unspecified,
164
+ member.type.elements[1],
165
+ sourceFile
166
+ );
167
+ } else {
168
+ eventTypeText = printer.printNode(ts.EmitHint.Unspecified, member.type, sourceFile);
169
+ }
170
+ }
171
+
172
+ let documentation: string | undefined;
173
+ const jsDocs = (member as any).jsDoc as ts.JSDoc[] | undefined;
174
+ if (jsDocs && jsDocs.length > 0) {
175
+ documentation = jsDocs
176
+ .map(doc => doc.comment)
177
+ .filter(Boolean)
178
+ .join('\n');
179
+ }
180
+
181
+ eventSpecs.push({
182
+ name: eventName,
183
+ eventTypeText,
184
+ extraTypeText,
185
+ documentation,
186
+ });
187
+ }
188
+
189
+ if (eventSpecs.length > 0) {
190
+ events = {
191
+ interfaceName: eventsInterfaceName,
192
+ eventNameTypeName: `${interfaceName}ModuleEventName`,
193
+ eventArgsTypeName: `${interfaceName}ModuleEventArgs`,
194
+ listenerTypeName: `${interfaceName}ModuleEventListener`,
195
+ events: eventSpecs,
196
+ };
197
+ }
198
+ }
199
+
103
200
  if (methods.length === 0) {
104
201
  throw new Error(
105
202
  `Interface ${interfaceName} in ${modulePath} does not declare any methods`
106
203
  );
107
204
  }
108
205
 
109
- return { interfaceName, moduleName, methods, supportingStatements: supporting, sourceFile };
206
+ return {
207
+ interfaceName,
208
+ moduleName,
209
+ methods,
210
+ events,
211
+ supportingStatements: supporting,
212
+ sourceFile,
213
+ };
110
214
  }
111
215
 
112
216
  function buildTypesFile(def: ModuleDefinition): string {
@@ -144,6 +248,24 @@ function buildTypesFile(def: ModuleDefinition): string {
144
248
  }
145
249
 
146
250
  lines.push('');
251
+
252
+ if (def.events) {
253
+ const { interfaceName, eventNameTypeName, eventArgsTypeName, listenerTypeName } = def.events;
254
+
255
+ lines.push(`export type ${eventNameTypeName} = Extract<keyof ${interfaceName}, string>;`);
256
+ lines.push(
257
+ `export type ${eventArgsTypeName}<K extends ${eventNameTypeName} = ${eventNameTypeName}> =`
258
+ );
259
+ lines.push(` ${interfaceName}[K] extends readonly [infer E, infer X]`);
260
+ lines.push(` ? [event: (E & { type: K }), extra: X]`);
261
+ lines.push(` : [event: (${interfaceName}[K] & { type: K }), extra: any];`);
262
+ lines.push('');
263
+ lines.push(`export type ${listenerTypeName} = (...args: {`);
264
+ lines.push(` [K in ${eventNameTypeName}]: ${eventArgsTypeName}<K>;`);
265
+ lines.push(`}[${eventNameTypeName}]) => any;`);
266
+ lines.push('');
267
+ }
268
+
147
269
  // Ensure file is treated as a module even if no declarations were emitted.
148
270
  lines.push('export {};');
149
271
  return lines.join('\n');
@@ -180,6 +302,10 @@ function buildIndexFile(def: ModuleDefinition): string {
180
302
  typeImportNames.add(name);
181
303
  }
182
304
  }
305
+ if (def.events) {
306
+ typeImportNames.add(def.events.eventNameTypeName);
307
+ typeImportNames.add(def.events.eventArgsTypeName);
308
+ }
183
309
  const typeImportsSorted = Array.from(typeImportNames).sort();
184
310
  if (typeImportsSorted.length > 0) {
185
311
  lines.push(
@@ -200,6 +326,64 @@ function buildIndexFile(def: ModuleDefinition): string {
200
326
  lines.push(' }');
201
327
  lines.push('');
202
328
 
329
+ if (def.events) {
330
+ lines.push(' private static _moduleListenerInstalled = false;');
331
+ lines.push(
332
+ ' private static _listeners: Record<string, Set<(event: Event, extra: any) => any>> = Object.create(null);'
333
+ );
334
+ lines.push('');
335
+
336
+ lines.push(
337
+ ` static addListener<K extends ${def.events.eventNameTypeName}>(type: K, listener: (...args: ${def.events.eventArgsTypeName}<K>) => any): () => void {`
338
+ );
339
+ lines.push(
340
+ " if (typeof webf === 'undefined' || typeof (webf as any).addWebfModuleListener !== 'function') {"
341
+ );
342
+ lines.push(
343
+ " throw new Error('WebF module event API is not available. Make sure you are running in WebF runtime.');"
344
+ );
345
+ lines.push(' }');
346
+ lines.push('');
347
+ lines.push(' if (!this._moduleListenerInstalled) {');
348
+ lines.push(
349
+ ` (webf as any).addWebfModuleListener('${def.moduleName}', (event: Event, extra: any) => {`
350
+ );
351
+ lines.push(' const set = this._listeners[event.type];');
352
+ lines.push(' if (!set) return;');
353
+ lines.push(' for (const fn of set) { fn(event, extra); }');
354
+ lines.push(' });');
355
+ lines.push(' this._moduleListenerInstalled = true;');
356
+ lines.push(' }');
357
+ lines.push('');
358
+ lines.push(' (this._listeners[type] ??= new Set()).add(listener as any);');
359
+ lines.push('');
360
+ lines.push(' const cls = this;');
361
+ lines.push(' return () => {');
362
+ lines.push(' const set = cls._listeners[type];');
363
+ lines.push(' if (!set) return;');
364
+ lines.push(' set.delete(listener as any);');
365
+ lines.push(' if (set.size === 0) { delete cls._listeners[type]; }');
366
+ lines.push('');
367
+ lines.push(' if (Object.keys(cls._listeners).length === 0) {');
368
+ lines.push(' cls.removeListener();');
369
+ lines.push(' }');
370
+ lines.push(' };');
371
+ lines.push(' }');
372
+ lines.push('');
373
+
374
+ lines.push(' static removeListener(): void {');
375
+ lines.push(' this._listeners = Object.create(null);');
376
+ lines.push(' this._moduleListenerInstalled = false;');
377
+ lines.push(
378
+ " if (typeof webf === 'undefined' || typeof (webf as any).removeWebfModuleListener !== 'function') {"
379
+ );
380
+ lines.push(' return;');
381
+ lines.push(' }');
382
+ lines.push(` (webf as any).removeWebfModuleListener('${def.moduleName}');`);
383
+ lines.push(' }');
384
+ lines.push('');
385
+ }
386
+
203
387
  for (const method of def.methods) {
204
388
  if (method.documentation) {
205
389
  lines.push(' /**');
@@ -246,6 +430,11 @@ function buildIndexFile(def: ModuleDefinition): string {
246
430
  typeExportNames.add(stmt.name.text);
247
431
  }
248
432
  }
433
+ if (def.events) {
434
+ typeExportNames.add(def.events.eventNameTypeName);
435
+ typeExportNames.add(def.events.eventArgsTypeName);
436
+ typeExportNames.add(def.events.listenerTypeName);
437
+ }
249
438
  const sorted = Array.from(typeExportNames).sort();
250
439
  if (sorted.length) {
251
440
  lines.push(' ' + sorted.join(','));
@@ -359,6 +548,9 @@ function buildDartBindings(def: ModuleDefinition, command: string): string {
359
548
  lines.push('// Generated by `webf module-codegen`');
360
549
  lines.push('');
361
550
  lines.push("import 'package:webf/module.dart';");
551
+ if (def.events) {
552
+ lines.push("import 'package:webf/dom.dart';");
553
+ }
362
554
  if (
363
555
  def.methods.some(m =>
364
556
  m.params.some(p => isTsByteArrayUnion(p.typeText))
@@ -371,7 +563,11 @@ function buildDartBindings(def: ModuleDefinition, command: string): string {
371
563
  // Generate Dart classes for supporting TS interfaces (compound option types).
372
564
  const optionInterfaces: ts.InterfaceDeclaration[] = [];
373
565
  for (const stmt of def.supportingStatements) {
374
- if (ts.isInterfaceDeclaration(stmt) && stmt.name.text !== def.interfaceName) {
566
+ if (
567
+ ts.isInterfaceDeclaration(stmt) &&
568
+ stmt.name.text !== def.interfaceName &&
569
+ stmt.name.text !== def.events?.interfaceName
570
+ ) {
375
571
  optionInterfaces.push(stmt);
376
572
  }
377
573
  }
@@ -477,6 +673,27 @@ function buildDartBindings(def: ModuleDefinition, command: string): string {
477
673
  lines.push(` String get name => '${def.moduleName}';`);
478
674
  lines.push('');
479
675
 
676
+ if (def.events) {
677
+ for (const evt of def.events.events) {
678
+ const methodName = `emit${_.upperFirst(_.camelCase(evt.name))}`;
679
+
680
+ const mappedExtra = mapTsParamTypeToDart(evt.extraTypeText, optionTypeNames);
681
+ const dataParamType = mappedExtra.optionClassName
682
+ ? `${mappedExtra.optionClassName}?`
683
+ : 'dynamic';
684
+
685
+ lines.push(` dynamic ${methodName}({Event? event, ${dataParamType} data}) {`);
686
+ if (mappedExtra.optionClassName) {
687
+ lines.push(' final mapped = data?.toMap();');
688
+ lines.push(` return dispatchEvent(event: event ?? Event('${evt.name}'), data: mapped);`);
689
+ } else {
690
+ lines.push(` return dispatchEvent(event: event ?? Event('${evt.name}'), data: data);`);
691
+ }
692
+ lines.push(' }');
693
+ lines.push('');
694
+ }
695
+ }
696
+
480
697
  for (const method of def.methods) {
481
698
  const dartMethodName = _.camelCase(method.name);
482
699
  let dartReturnType = mapTsReturnTypeToDart(method.returnTypeText);
@@ -0,0 +1,83 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { generateModuleArtifacts } from '../src/module';
4
+ import { writeFileIfChanged } from '../src/generator';
5
+
6
+ jest.mock('fs');
7
+ jest.mock('../src/generator', () => ({
8
+ writeFileIfChanged: jest.fn(),
9
+ }));
10
+
11
+ const mockFs = fs as jest.Mocked<typeof fs>;
12
+ const mockWriteFileIfChanged = writeFileIfChanged as jest.MockedFunction<typeof writeFileIfChanged>;
13
+
14
+ describe('module-codegen events', () => {
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ mockFs.existsSync.mockReturnValue(true);
18
+ mockFs.mkdirSync.mockImplementation(() => undefined);
19
+ });
20
+
21
+ it('generates typed module event listener + Dart emit helpers', () => {
22
+ const moduleInterfacePath = '/tmp/demo.module.d.ts';
23
+ const npmTargetDir = '/out/npm';
24
+ const flutterPackageDir = '/out/flutter';
25
+
26
+ mockFs.readFileSync.mockReturnValue(`
27
+ interface Payload {
28
+ id: string;
29
+ }
30
+
31
+ interface WebFDemoModuleEvents {
32
+ scanResult: [Event, Payload];
33
+ click: CustomEvent<string>;
34
+ }
35
+
36
+ interface WebFDemo {
37
+ ping(): Promise<boolean>;
38
+ }
39
+ `);
40
+
41
+ generateModuleArtifacts({
42
+ moduleInterfacePath,
43
+ npmTargetDir,
44
+ flutterPackageDir,
45
+ command: 'webf module-codegen',
46
+ });
47
+
48
+ const typesPath = path.join(npmTargetDir, 'src', 'types.ts');
49
+ const indexPath = path.join(npmTargetDir, 'src', 'index.ts');
50
+ const dartBindingsPath = path.join(
51
+ flutterPackageDir,
52
+ 'lib',
53
+ 'src',
54
+ 'demo_module_bindings_generated.dart'
55
+ );
56
+
57
+ const writes = mockWriteFileIfChanged.mock.calls.reduce<Record<string, string>>(
58
+ (acc, [filePath, content]) => {
59
+ acc[filePath] = content;
60
+ return acc;
61
+ },
62
+ {}
63
+ );
64
+
65
+ expect(writes[typesPath]).toContain('export interface WebFDemoModuleEvents');
66
+ expect(writes[typesPath]).toContain(
67
+ 'export type WebFDemoModuleEventName = Extract<keyof WebFDemoModuleEvents, string>;'
68
+ );
69
+ expect(writes[typesPath]).toContain('export type WebFDemoModuleEventListener');
70
+
71
+ expect(writes[indexPath]).toContain(
72
+ 'static addListener<K extends WebFDemoModuleEventName>(type: K, listener: (...args: WebFDemoModuleEventArgs<K>) => any): () => void'
73
+ );
74
+ expect(writes[indexPath]).toContain('private static _moduleListenerInstalled = false;');
75
+ expect(writes[indexPath]).toContain("addWebfModuleListener('Demo'");
76
+ expect(writes[indexPath]).toContain('static removeListener(): void');
77
+
78
+ expect(writes[dartBindingsPath]).toContain("import 'package:webf/dom.dart';");
79
+ expect(writes[dartBindingsPath]).toContain('dynamic emitScanResult({Event? event, Payload? data})');
80
+ expect(writes[dartBindingsPath]).toContain('final mapped = data?.toMap();');
81
+ expect(writes[dartBindingsPath]).not.toContain('class WebFDemoModuleEvents');
82
+ });
83
+ });