@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 +20 -0
- package/dist/module.js +157 -5
- package/package.json +1 -1
- package/src/module.ts +222 -5
- package/test/module-events.test.ts +83 -0
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 (
|
|
21
|
-
|
|
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 {
|
|
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) &&
|
|
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
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 (
|
|
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 {
|
|
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 (
|
|
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
|
+
});
|