@openwebf/webf 0.24.1 → 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 +21 -1
- package/dist/analyzer.js +183 -121
- package/dist/commands.js +54 -9
- package/dist/generator.js +39 -16
- package/dist/module.js +157 -5
- package/package.json +1 -1
- package/src/analyzer.ts +186 -114
- package/src/commands.ts +65 -11
- package/src/generator.ts +32 -12
- package/src/module.ts +222 -5
- package/templates/module.package.json.tpl +17 -6
- package/templates/{module.tsup.config.ts.tpl → module.tsdown.config.ts.tpl} +2 -7
- package/templates/react.component.tsx.tpl +18 -2
- package/templates/react.package.json.tpl +17 -5
- package/templates/{react.tsup.config.ts.tpl → react.tsdown.config.ts.tpl} +2 -4
- package/templates/vue.package.json.tpl +1 -1
- package/test/analyzer.test.ts +45 -1
- package/test/commands.test.ts +76 -4
- package/test/generator.test.ts +37 -0
- package/test/module-events.test.ts +83 -0
- package/test/react.test.ts +5 -0
package/src/commands.ts
CHANGED
|
@@ -177,8 +177,8 @@ const moduleTsConfig = fs.readFileSync(
|
|
|
177
177
|
'utf-8'
|
|
178
178
|
);
|
|
179
179
|
|
|
180
|
-
const
|
|
181
|
-
path.resolve(__dirname, '../templates/module.
|
|
180
|
+
const moduleTsDownConfig = fs.readFileSync(
|
|
181
|
+
path.resolve(__dirname, '../templates/module.tsdown.config.ts.tpl'),
|
|
182
182
|
'utf-8'
|
|
183
183
|
);
|
|
184
184
|
const reactPackageJson = fs.readFileSync(
|
|
@@ -191,8 +191,8 @@ const reactTsConfig = fs.readFileSync(
|
|
|
191
191
|
'utf-8'
|
|
192
192
|
);
|
|
193
193
|
|
|
194
|
-
const
|
|
195
|
-
path.resolve(__dirname, '../templates/react.
|
|
194
|
+
const reactTsDownConfig = fs.readFileSync(
|
|
195
|
+
path.resolve(__dirname, '../templates/react.tsdown.config.ts.tpl'),
|
|
196
196
|
'utf-8'
|
|
197
197
|
);
|
|
198
198
|
|
|
@@ -239,6 +239,40 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
function copyReadmeToPackageRoot(params: {
|
|
243
|
+
sourceRoot: string;
|
|
244
|
+
targetRoot: string;
|
|
245
|
+
}): { copied: boolean; sourcePath?: string; targetPath: string } {
|
|
246
|
+
const { sourceRoot, targetRoot } = params;
|
|
247
|
+
const targetPath = path.join(targetRoot, 'README.md');
|
|
248
|
+
|
|
249
|
+
if (fs.existsSync(targetPath)) {
|
|
250
|
+
return { copied: false, targetPath };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const candidateNames = ['README.md', 'Readme.md', 'readme.md'];
|
|
254
|
+
let sourcePath: string | null = null;
|
|
255
|
+
for (const candidate of candidateNames) {
|
|
256
|
+
const abs = path.join(sourceRoot, candidate);
|
|
257
|
+
if (fs.existsSync(abs)) {
|
|
258
|
+
sourcePath = abs;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!sourcePath) {
|
|
264
|
+
return { copied: false, targetPath };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
269
|
+
writeFileIfChanged(targetPath, content);
|
|
270
|
+
return { copied: true, sourcePath, targetPath };
|
|
271
|
+
} catch {
|
|
272
|
+
return { copied: false, targetPath };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
242
276
|
// Copy markdown docs that match .d.ts basenames from source to the built dist folder,
|
|
243
277
|
// and generate an aggregated README.md in the dist directory.
|
|
244
278
|
async function copyMarkdownDocsToDist(params: {
|
|
@@ -419,9 +453,9 @@ function createCommand(target: string, options: { framework: string; packageName
|
|
|
419
453
|
const tsConfigContent = _.template(reactTsConfig)({});
|
|
420
454
|
writeFileIfChanged(tsConfigPath, tsConfigContent);
|
|
421
455
|
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
writeFileIfChanged(
|
|
456
|
+
const tsdownConfigPath = path.join(target, 'tsdown.config.ts');
|
|
457
|
+
const tsdownConfigContent = _.template(reactTsDownConfig)({});
|
|
458
|
+
writeFileIfChanged(tsdownConfigPath, tsdownConfigContent);
|
|
425
459
|
|
|
426
460
|
const gitignorePath = path.join(target, '.gitignore');
|
|
427
461
|
const gitignoreContent = _.template(gitignore)({});
|
|
@@ -498,9 +532,9 @@ function createModuleProject(target: string, options: { packageName: string; met
|
|
|
498
532
|
const tsConfigContent = _.template(moduleTsConfig)({});
|
|
499
533
|
writeFileIfChanged(tsConfigPath, tsConfigContent);
|
|
500
534
|
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
writeFileIfChanged(
|
|
535
|
+
const tsdownConfigPath = path.join(target, 'tsdown.config.ts');
|
|
536
|
+
const tsdownConfigContent = _.template(moduleTsDownConfig)({});
|
|
537
|
+
writeFileIfChanged(tsdownConfigPath, tsdownConfigContent);
|
|
504
538
|
|
|
505
539
|
if (!skipGitignore) {
|
|
506
540
|
const gitignorePath = path.join(target, '.gitignore');
|
|
@@ -780,6 +814,17 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
|
|
|
780
814
|
// Auto-initialize typings in the output directory if needed
|
|
781
815
|
ensureInitialized(resolvedDistPath);
|
|
782
816
|
|
|
817
|
+
// Copy README.md from the source Flutter package into the npm package root (so `npm publish` includes it).
|
|
818
|
+
if (options.flutterPackageSrc) {
|
|
819
|
+
const { copied } = copyReadmeToPackageRoot({
|
|
820
|
+
sourceRoot: options.flutterPackageSrc,
|
|
821
|
+
targetRoot: resolvedDistPath,
|
|
822
|
+
});
|
|
823
|
+
if (copied) {
|
|
824
|
+
console.log('📄 Copied README.md to package root');
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
783
828
|
console.log(`\nGenerating ${framework} code from ${options.flutterPackageSrc}...`);
|
|
784
829
|
|
|
785
830
|
await dartGen({
|
|
@@ -1019,7 +1064,7 @@ async function generateModuleCommand(distPath: string, options: GenerateOptions)
|
|
|
1019
1064
|
packageName = packageNameAnswer.packageName;
|
|
1020
1065
|
}
|
|
1021
1066
|
|
|
1022
|
-
// Prevent npm scaffolding (package.json,
|
|
1067
|
+
// Prevent npm scaffolding (package.json, tsdown.config.ts, etc.) from being written into
|
|
1023
1068
|
// the Flutter package itself. Force users to choose a separate output directory.
|
|
1024
1069
|
if (resolvedDistPath === flutterPackageSrc) {
|
|
1025
1070
|
console.error('\n❌ Output directory must not be the Flutter package root.');
|
|
@@ -1070,6 +1115,15 @@ async function generateModuleCommand(distPath: string, options: GenerateOptions)
|
|
|
1070
1115
|
command,
|
|
1071
1116
|
});
|
|
1072
1117
|
|
|
1118
|
+
// Copy README.md from the source Flutter package into the npm package root
|
|
1119
|
+
const { copied } = copyReadmeToPackageRoot({
|
|
1120
|
+
sourceRoot: flutterPackageSrc,
|
|
1121
|
+
targetRoot: resolvedDistPath,
|
|
1122
|
+
});
|
|
1123
|
+
if (copied) {
|
|
1124
|
+
console.log('📄 Copied README.md to package root');
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1073
1127
|
console.log('\nModule code generation completed successfully!');
|
|
1074
1128
|
|
|
1075
1129
|
try {
|
package/src/generator.ts
CHANGED
|
@@ -329,7 +329,8 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
329
329
|
const newExports = generateReactIndex(blobs);
|
|
330
330
|
|
|
331
331
|
// Build desired export map: moduleSpecifier -> Set of names
|
|
332
|
-
const
|
|
332
|
+
const desiredValueExports = new Map<string, Set<string>>();
|
|
333
|
+
const desiredTypeExports = new Map<string, Set<string>>();
|
|
333
334
|
const components = blobs.flatMap(blob => {
|
|
334
335
|
const classObjects = blob.objects.filter(obj => obj instanceof ClassObject) as ClassObject[];
|
|
335
336
|
const properties = classObjects.filter(object => object.name.endsWith('Properties'));
|
|
@@ -351,10 +352,10 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
351
352
|
}
|
|
352
353
|
for (const { className, fileName, relativeDir } of unique.values()) {
|
|
353
354
|
const spec = `./${relativeDir ? `${relativeDir}/` : ''}${fileName}`;
|
|
354
|
-
if (!
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
if (!desiredValueExports.has(spec)) desiredValueExports.set(spec, new Set());
|
|
356
|
+
if (!desiredTypeExports.has(spec)) desiredTypeExports.set(spec, new Set());
|
|
357
|
+
desiredValueExports.get(spec)!.add(className);
|
|
358
|
+
desiredTypeExports.get(spec)!.add(`${className}Element`);
|
|
358
359
|
}
|
|
359
360
|
|
|
360
361
|
if (!fs.existsSync(indexFilePath)) {
|
|
@@ -376,22 +377,41 @@ export async function reactGen({ source, target, exclude, packageName }: Generat
|
|
|
376
377
|
? stmt.moduleSpecifier.text
|
|
377
378
|
: undefined;
|
|
378
379
|
if (!moduleSpecifier) continue;
|
|
379
|
-
const
|
|
380
|
-
|
|
380
|
+
const desiredValues = desiredValueExports.get(moduleSpecifier);
|
|
381
|
+
const desiredTypes = desiredTypeExports.get(moduleSpecifier);
|
|
382
|
+
if (!desiredValues && !desiredTypes) continue;
|
|
383
|
+
const declIsTypeOnly = Boolean((stmt as unknown as { isTypeOnly?: boolean }).isTypeOnly);
|
|
381
384
|
for (const el of stmt.exportClause.elements) {
|
|
382
385
|
const name = el.name.getText(sourceFile);
|
|
383
|
-
|
|
386
|
+
const specIsTypeOnly = Boolean((el as unknown as { isTypeOnly?: boolean }).isTypeOnly);
|
|
387
|
+
const isTypeOnly = declIsTypeOnly || specIsTypeOnly;
|
|
388
|
+
if (isTypeOnly) {
|
|
389
|
+
if (desiredTypes?.has(name)) desiredTypes.delete(name);
|
|
390
|
+
} else {
|
|
391
|
+
if (desiredValues?.has(name)) desiredValues.delete(name);
|
|
392
|
+
}
|
|
384
393
|
}
|
|
385
394
|
}
|
|
386
395
|
}
|
|
387
396
|
|
|
388
397
|
// Prepare new export lines for any remaining names
|
|
389
398
|
const lines: string[] = [];
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
399
|
+
const specs = new Set<string>([
|
|
400
|
+
...desiredValueExports.keys(),
|
|
401
|
+
...desiredTypeExports.keys()
|
|
402
|
+
]);
|
|
403
|
+
|
|
404
|
+
for (const spec of specs) {
|
|
405
|
+
const missingValues = Array.from(desiredValueExports.get(spec) ?? []);
|
|
406
|
+
const missingTypes = Array.from(desiredTypeExports.get(spec) ?? []);
|
|
407
|
+
if (missingValues.length === 0 && missingTypes.length === 0) continue;
|
|
393
408
|
const specEscaped = spec.replace(/\\/g, '/');
|
|
394
|
-
|
|
409
|
+
if (missingValues.length > 0) {
|
|
410
|
+
lines.push(`export { ${missingValues.join(', ')} } from "${specEscaped}";`);
|
|
411
|
+
}
|
|
412
|
+
if (missingTypes.length > 0) {
|
|
413
|
+
lines.push(`export type { ${missingTypes.join(', ')} } from "${specEscaped}";`);
|
|
414
|
+
}
|
|
395
415
|
}
|
|
396
416
|
|
|
397
417
|
if (lines.length > 0) {
|
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);
|
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
"name": "<%= packageName %>",
|
|
3
3
|
"version": "<%= version %>",
|
|
4
4
|
"description": "<%= description %>",
|
|
5
|
-
"main": "dist/index.
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
|
-
"types": "dist/index.d.
|
|
7
|
+
"types": "dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
8
20
|
"files": ["dist"],
|
|
9
21
|
"scripts": {
|
|
10
|
-
"build": "
|
|
11
|
-
"dev": "
|
|
22
|
+
"build": "tsdown",
|
|
23
|
+
"dev": "tsdown --watch",
|
|
12
24
|
"clean": "rimraf dist",
|
|
13
25
|
"prepublishOnly": "npm run build"
|
|
14
26
|
},
|
|
@@ -26,11 +38,10 @@
|
|
|
26
38
|
},
|
|
27
39
|
"devDependencies": {
|
|
28
40
|
"rimraf": "^5.0.0",
|
|
29
|
-
"
|
|
41
|
+
"tsdown": "^0.19.0",
|
|
30
42
|
"typescript": "^5.8.3"
|
|
31
43
|
},
|
|
32
44
|
"publishConfig": {
|
|
33
45
|
"access": "public"
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
|
-
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
1
|
+
module.exports = {
|
|
4
2
|
entry: ['src/index.ts'],
|
|
5
3
|
format: ['cjs', 'esm'],
|
|
6
4
|
dts: true,
|
|
7
5
|
clean: true,
|
|
8
|
-
splitting: false,
|
|
9
6
|
sourcemap: true,
|
|
10
|
-
minify: false,
|
|
11
7
|
external: [],
|
|
12
|
-
}
|
|
13
|
-
|
|
8
|
+
};
|
|
@@ -63,12 +63,16 @@ export interface <%= className %>Props {
|
|
|
63
63
|
|
|
64
64
|
<% if (methods && methods.methods.length > 0) { %>
|
|
65
65
|
/**
|
|
66
|
-
* Element interface with methods accessible via ref
|
|
66
|
+
* Element interface with methods/properties accessible via ref
|
|
67
67
|
* @example
|
|
68
68
|
* ```tsx
|
|
69
69
|
* const ref = useRef<<%= className %>Element>(null);
|
|
70
70
|
* // Call methods on the element
|
|
71
71
|
* ref.current?.finishRefresh('success');
|
|
72
|
+
<% if (properties && properties.props && properties.props.length > 0) { %>
|
|
73
|
+
* // Access properties
|
|
74
|
+
* console.log(ref.current?.<%= _.camelCase(properties.props[0].name || 'someProp') %>);
|
|
75
|
+
<% } %>
|
|
72
76
|
* ```
|
|
73
77
|
*/
|
|
74
78
|
<% } %>
|
|
@@ -76,7 +80,19 @@ export interface <%= className %>Element extends WebFElementWithMethods<{
|
|
|
76
80
|
<% _.forEach(methods?.methods, function(method, index) { %>
|
|
77
81
|
<%= generateMethodDeclarationWithDocs(method, ' ') %>
|
|
78
82
|
<% }); %>
|
|
79
|
-
}> {
|
|
83
|
+
}> {
|
|
84
|
+
<% _.forEach(properties?.props, function(prop, index) { %>
|
|
85
|
+
<% var propName = _.camelCase(prop.name); %>
|
|
86
|
+
<% if (prop.documentation) { %>
|
|
87
|
+
/** <%= prop.documentation.split('\n')[0] %> */
|
|
88
|
+
<% } %>
|
|
89
|
+
<% if (prop.readonly) { %>
|
|
90
|
+
readonly <%= propName %><% if (prop.optional) { %>?<% } %>: <%= generateReturnType(prop.type) %>;
|
|
91
|
+
<% } else { %>
|
|
92
|
+
<%= propName %><% if (prop.optional) { %>?<% } %>: <%= generateReturnType(prop.type) %>;
|
|
93
|
+
<% } %>
|
|
94
|
+
<% }); %>
|
|
95
|
+
}
|
|
80
96
|
|
|
81
97
|
<% if (properties?.documentation || methods?.documentation || events?.documentation) { %>
|
|
82
98
|
<% const docs = properties?.documentation || methods?.documentation || events?.documentation; %>
|
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
"name": "<%= packageName %>",
|
|
3
3
|
"version": "<%= version %>",
|
|
4
4
|
"description": "<%= description %>",
|
|
5
|
-
"main": "dist/index.
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
|
-
"types": "dist/index.d.
|
|
8
|
-
"
|
|
7
|
+
"types": "dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": ["dist", "README.md"],
|
|
9
21
|
"scripts": {
|
|
10
|
-
"build": "
|
|
22
|
+
"build": "tsdown"
|
|
11
23
|
},
|
|
12
24
|
"keywords": [],
|
|
13
25
|
"author": "",
|
|
@@ -23,7 +35,7 @@
|
|
|
23
35
|
"@types/react": "^19.1.0",
|
|
24
36
|
"@types/react-dom": "^19.1.2",
|
|
25
37
|
"picomatch": "^4.0.2",
|
|
26
|
-
"
|
|
38
|
+
"tsdown": "^0.19.0",
|
|
27
39
|
"typescript": "^5.8.3"
|
|
28
40
|
}
|
|
29
41
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
1
|
+
module.exports = {
|
|
4
2
|
entry: ['src/index.ts'],
|
|
5
3
|
format: ['esm', 'cjs'],
|
|
6
4
|
dts: true,
|
|
7
5
|
sourcemap: true,
|
|
8
6
|
clean: true,
|
|
9
7
|
external: ['react', 'react-dom', '@openwebf/react-core-ui'],
|
|
10
|
-
}
|
|
8
|
+
};
|