@openwebf/webf 0.23.0 → 0.23.7
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 +5 -1
- package/TYPING_GUIDE.md +17 -1
- package/bin/webf.js +1 -0
- package/dist/analyzer.js +135 -17
- package/dist/commands.js +251 -99
- package/dist/constants.js +242 -0
- package/dist/dart.js +91 -25
- package/dist/declaration.js +14 -1
- package/dist/generator.js +80 -12
- package/dist/react.js +281 -23
- package/dist/vue.js +114 -11
- package/package.json +1 -1
- package/src/IDLBlob.ts +2 -2
- package/src/analyzer.ts +142 -28
- package/src/commands.ts +363 -197
- package/src/dart.ts +95 -20
- package/src/declaration.ts +16 -0
- package/src/generator.ts +81 -13
- package/src/react.ts +300 -28
- package/src/vue.ts +131 -14
- package/templates/class.dart.tpl +1 -1
- package/templates/react.package.json.tpl +1 -0
- package/templates/vue.components.d.ts.tpl +9 -4
- package/test/commands.test.ts +82 -2
- package/test/dart-nullable-props.test.ts +58 -0
- package/test/react-consts.test.ts +30 -0
- package/test/react-vue-nullable-props.test.ts +66 -0
- package/test/react.test.ts +46 -4
- package/test/vue.test.ts +34 -2
package/src/react.ts
CHANGED
|
@@ -2,24 +2,63 @@ import _ from "lodash";
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
|
-
import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject} from "./declaration";
|
|
5
|
+
import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject, ConstObject, EnumObject} from "./declaration";
|
|
6
6
|
import {IDLBlob} from "./IDLBlob";
|
|
7
|
-
import {getPointerType, isPointerType, isUnionType} from "./utils";
|
|
7
|
+
import {getPointerType, isPointerType, isUnionType, trimNullTypeFromType} from "./utils";
|
|
8
|
+
import { debug } from './logger';
|
|
8
9
|
|
|
9
10
|
function readTemplate(name: string) {
|
|
10
11
|
return fs.readFileSync(path.join(__dirname, '../templates/' + name + '.tpl'), {encoding: 'utf-8'});
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
function generateReturnType(type: ParameterType) {
|
|
14
|
+
function generateReturnType(type: ParameterType): string {
|
|
14
15
|
if (isUnionType(type)) {
|
|
15
|
-
|
|
16
|
+
const values = type.value as ParameterType[];
|
|
17
|
+
return values.map(v => {
|
|
18
|
+
if (v.value === FunctionArgumentType.null) {
|
|
19
|
+
return 'null';
|
|
20
|
+
}
|
|
21
|
+
// String literal unions: 'left' | 'center' | 'right'
|
|
22
|
+
if (typeof v.value === 'string') {
|
|
23
|
+
return `'${v.value}'`;
|
|
24
|
+
}
|
|
25
|
+
return 'any';
|
|
26
|
+
}).join(' | ');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle non-literal unions such as boolean | null, number | null, CustomType | null
|
|
30
|
+
if (Array.isArray(type.value)) {
|
|
31
|
+
const values = type.value as ParameterType[];
|
|
32
|
+
const hasNull = values.some(v => v.value === FunctionArgumentType.null);
|
|
33
|
+
if (hasNull) {
|
|
34
|
+
const nonNulls = values.filter(v => v.value !== FunctionArgumentType.null);
|
|
35
|
+
if (nonNulls.length === 0) {
|
|
36
|
+
return 'null';
|
|
37
|
+
}
|
|
38
|
+
const parts: string[] = nonNulls.map(v => generateReturnType(v));
|
|
39
|
+
// Deduplicate and append null
|
|
40
|
+
const unique: string[] = Array.from(new Set(parts));
|
|
41
|
+
unique.push('null');
|
|
42
|
+
return unique.join(' | ');
|
|
43
|
+
}
|
|
44
|
+
// Complex non-null unions are rare for React typings; fall back to any
|
|
45
|
+
return 'any';
|
|
16
46
|
}
|
|
47
|
+
|
|
17
48
|
if (isPointerType(type)) {
|
|
18
49
|
const pointerType = getPointerType(type);
|
|
50
|
+
// Map Dart's `Type` (from TS typeof) to TS `any`
|
|
51
|
+
if (pointerType === 'Type') return 'any';
|
|
19
52
|
return pointerType;
|
|
20
53
|
}
|
|
21
54
|
if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
|
|
22
|
-
|
|
55
|
+
const elemType = getPointerType(type.value);
|
|
56
|
+
// Map arrays of Dart `Type` to `any[]` in TS; parenthesize typeof
|
|
57
|
+
if (elemType === 'Type') return 'any[]';
|
|
58
|
+
if (typeof elemType === 'string' && elemType.startsWith('typeof ')) {
|
|
59
|
+
return `(${elemType})[]`;
|
|
60
|
+
}
|
|
61
|
+
return `${elemType}[]`;
|
|
23
62
|
}
|
|
24
63
|
switch (type.value) {
|
|
25
64
|
case FunctionArgumentType.int:
|
|
@@ -125,6 +164,8 @@ export function toWebFTagName(className: string): string {
|
|
|
125
164
|
export function generateReactComponent(blob: IDLBlob, packageName?: string, relativeDir?: string) {
|
|
126
165
|
const classObjects = blob.objects.filter(obj => obj instanceof ClassObject) as ClassObject[];
|
|
127
166
|
const typeAliases = blob.objects.filter(obj => obj instanceof TypeAliasObject) as TypeAliasObject[];
|
|
167
|
+
const constObjects = blob.objects.filter(obj => obj instanceof ConstObject) as ConstObject[];
|
|
168
|
+
const enumObjects = blob.objects.filter(obj => obj instanceof EnumObject) as EnumObject[];
|
|
128
169
|
|
|
129
170
|
const classObjectDictionary = Object.fromEntries(
|
|
130
171
|
classObjects.map(object => {
|
|
@@ -153,36 +194,87 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
|
|
|
153
194
|
return `type ${typeAlias.name} = ${typeAlias.type};`;
|
|
154
195
|
}).join('\n');
|
|
155
196
|
|
|
197
|
+
// Include declare const values as ambient exports for type usage (e.g., unique symbol branding)
|
|
198
|
+
const constDeclarations = constObjects.map(c => `export declare const ${c.name}: ${c.type};`).join('\n');
|
|
199
|
+
|
|
200
|
+
// Include enums as concrete exports (no declare) so they are usable as values
|
|
201
|
+
const enumDeclarations = enumObjects.map(e => {
|
|
202
|
+
const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
|
|
203
|
+
return `export enum ${e.name} { ${members} }`;
|
|
204
|
+
}).join('\n');
|
|
205
|
+
|
|
206
|
+
// Names declared within this blob (so we shouldn't prefix them with __webfTypes)
|
|
207
|
+
const localTypeNames = new Set<string>([
|
|
208
|
+
...others.map(o => o.name),
|
|
209
|
+
...typeAliases.map(t => t.name),
|
|
210
|
+
...constObjects.map(c => c.name),
|
|
211
|
+
...enumObjects.map(e => e.name),
|
|
212
|
+
]);
|
|
213
|
+
|
|
156
214
|
const dependencies = [
|
|
157
215
|
typeAliasDeclarations,
|
|
216
|
+
constDeclarations,
|
|
217
|
+
enumDeclarations,
|
|
158
218
|
// Include Methods interfaces as dependencies
|
|
159
219
|
methods.map(object => {
|
|
160
220
|
const methodDeclarations = object.methods.map(method => {
|
|
161
221
|
return generateMethodDeclarationWithDocs(method, ' ');
|
|
162
222
|
}).join('\n');
|
|
163
223
|
|
|
164
|
-
|
|
165
|
-
if (object.documentation) {
|
|
166
|
-
|
|
224
|
+
const lines: string[] = [];
|
|
225
|
+
if (object.documentation && object.documentation.trim().length > 0) {
|
|
226
|
+
lines.push('/**');
|
|
227
|
+
object.documentation.split('\n').forEach(line => {
|
|
228
|
+
lines.push(` * ${line}`);
|
|
229
|
+
});
|
|
230
|
+
lines.push(' */');
|
|
167
231
|
}
|
|
168
232
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
233
|
+
lines.push(`interface ${object.name} {`);
|
|
234
|
+
lines.push(methodDeclarations);
|
|
235
|
+
lines.push('}');
|
|
236
|
+
|
|
237
|
+
return lines.join('\n');
|
|
172
238
|
}).join('\n\n'),
|
|
173
239
|
others.map(object => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
240
|
+
if (!object || !object.props || object.props.length === 0) {
|
|
241
|
+
return '';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const interfaceLines: string[] = [];
|
|
245
|
+
|
|
246
|
+
if (object.documentation && object.documentation.trim().length > 0) {
|
|
247
|
+
interfaceLines.push('/**');
|
|
248
|
+
object.documentation.split('\n').forEach(line => {
|
|
249
|
+
interfaceLines.push(` * ${line}`);
|
|
250
|
+
});
|
|
251
|
+
interfaceLines.push(' */');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interfaceLines.push(`interface ${object.name} {`);
|
|
255
|
+
|
|
256
|
+
const propLines = object.props.map(prop => {
|
|
257
|
+
const lines: string[] = [];
|
|
258
|
+
|
|
259
|
+
if (prop.documentation && prop.documentation.trim().length > 0) {
|
|
260
|
+
lines.push(' /**');
|
|
261
|
+
prop.documentation.split('\n').forEach(line => {
|
|
262
|
+
lines.push(` * ${line}`);
|
|
263
|
+
});
|
|
264
|
+
lines.push(' */');
|
|
177
265
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
266
|
+
|
|
267
|
+
const optionalToken = prop.optional ? '?' : '';
|
|
268
|
+
lines.push(` ${prop.name}${optionalToken}: ${generateReturnType(prop.type)};`);
|
|
269
|
+
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
interfaceLines.push(propLines.join('\n'));
|
|
274
|
+
interfaceLines.push('}');
|
|
275
|
+
|
|
276
|
+
return interfaceLines.join('\n');
|
|
277
|
+
}).filter(Boolean).join('\n\n')
|
|
186
278
|
].filter(Boolean).join('\n\n');
|
|
187
279
|
|
|
188
280
|
// Generate all components from this file
|
|
@@ -249,17 +341,115 @@ interface ${object.name} {
|
|
|
249
341
|
createWebFComponentImport
|
|
250
342
|
);
|
|
251
343
|
|
|
252
|
-
|
|
344
|
+
// Generate return type mapping; always use __webfTypes namespace for typeof
|
|
345
|
+
const genRT = (type: ParameterType): string => {
|
|
346
|
+
if (isUnionType(type)) {
|
|
347
|
+
const values = type.value as ParameterType[];
|
|
348
|
+
return values.map(v => {
|
|
349
|
+
if (v.value === FunctionArgumentType.null) {
|
|
350
|
+
return 'null';
|
|
351
|
+
}
|
|
352
|
+
if (typeof v.value === 'string') {
|
|
353
|
+
return `'${v.value}'`;
|
|
354
|
+
}
|
|
355
|
+
return 'any';
|
|
356
|
+
}).join(' | ');
|
|
357
|
+
}
|
|
358
|
+
if (Array.isArray(type.value)) {
|
|
359
|
+
const values = type.value as ParameterType[];
|
|
360
|
+
const hasNull = values.some(v => v.value === FunctionArgumentType.null);
|
|
361
|
+
if (hasNull) {
|
|
362
|
+
const nonNulls = values.filter(v => v.value !== FunctionArgumentType.null);
|
|
363
|
+
if (nonNulls.length === 0) {
|
|
364
|
+
return 'null';
|
|
365
|
+
}
|
|
366
|
+
const parts: string[] = nonNulls.map(v => genRT(v));
|
|
367
|
+
const unique: string[] = Array.from(new Set(parts));
|
|
368
|
+
unique.push('null');
|
|
369
|
+
return unique.join(' | ');
|
|
370
|
+
}
|
|
371
|
+
return 'any';
|
|
372
|
+
}
|
|
373
|
+
if (isPointerType(type)) {
|
|
374
|
+
const pointerType = getPointerType(type);
|
|
375
|
+
if (pointerType === 'Type') return 'any';
|
|
376
|
+
if (typeof pointerType === 'string' && pointerType.startsWith('typeof ')) {
|
|
377
|
+
const ident = pointerType.substring('typeof '.length).trim();
|
|
378
|
+
return `typeof __webfTypes.${ident}`;
|
|
379
|
+
}
|
|
380
|
+
// Prefix external pointer types with __webfTypes unless locally declared
|
|
381
|
+
if (typeof pointerType === 'string' && /^(?:[A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(pointerType)) {
|
|
382
|
+
const base = pointerType.split('.')[0];
|
|
383
|
+
if (!localTypeNames.has(base)) {
|
|
384
|
+
return `__webfTypes.${pointerType}`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return pointerType;
|
|
388
|
+
}
|
|
389
|
+
if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
|
|
390
|
+
const elemType = getPointerType(type.value);
|
|
391
|
+
if (elemType === 'Type') return 'any[]';
|
|
392
|
+
if (typeof elemType === 'string' && elemType.startsWith('typeof ')) {
|
|
393
|
+
const ident = elemType.substring('typeof '.length).trim();
|
|
394
|
+
return `(typeof __webfTypes.${ident})[]`;
|
|
395
|
+
}
|
|
396
|
+
if (typeof elemType === 'string' && /^(?:[A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(elemType)) {
|
|
397
|
+
const base = elemType.split('.')[0];
|
|
398
|
+
if (!localTypeNames.has(base)) {
|
|
399
|
+
return `__webfTypes.${elemType}[]`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return `${elemType}[]`;
|
|
403
|
+
}
|
|
404
|
+
switch (type.value) {
|
|
405
|
+
case FunctionArgumentType.int:
|
|
406
|
+
case FunctionArgumentType.double:
|
|
407
|
+
return 'number';
|
|
408
|
+
case FunctionArgumentType.any:
|
|
409
|
+
return 'any';
|
|
410
|
+
case FunctionArgumentType.boolean:
|
|
411
|
+
return 'boolean';
|
|
412
|
+
case FunctionArgumentType.dom_string:
|
|
413
|
+
return 'string';
|
|
414
|
+
case FunctionArgumentType.void:
|
|
415
|
+
default:
|
|
416
|
+
return 'void';
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Compute relative import path to src/types
|
|
421
|
+
const depth = (relativeDir || '').split('/').filter(p => p).length;
|
|
422
|
+
const upPath = '../'.repeat(depth);
|
|
423
|
+
// Always import the types namespace for typeof references
|
|
424
|
+
const typesImport = `import * as __webfTypes from "${upPath}types";\n\n`;
|
|
425
|
+
|
|
426
|
+
// Debug: collect typeof references from props for this component
|
|
427
|
+
const typeofRefs = new Set<string>();
|
|
428
|
+
if (component.properties) {
|
|
429
|
+
component.properties.props.forEach(p => {
|
|
430
|
+
const t = p.type;
|
|
431
|
+
if (!t) return;
|
|
432
|
+
if (!t.isArray && typeof (t.value as any) === 'string' && String((t.value as any)).startsWith('typeof ')) {
|
|
433
|
+
const ident = String((t.value as any)).substring('typeof '.length).trim();
|
|
434
|
+
typeofRefs.add(ident);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
debug(`[react] Generating ${className} (${blob.relativeDir}/${blob.filename}.tsx) typeof refs: ${Array.from(typeofRefs).join(', ') || '(none)'}; types import: ${upPath}types`);
|
|
439
|
+
|
|
440
|
+
const dependenciesWithImports = `${typesImport}${dependencies}`;
|
|
441
|
+
|
|
442
|
+
let content = _.template(templateContent)({
|
|
253
443
|
className: className,
|
|
254
444
|
properties: component.properties,
|
|
255
445
|
events: component.events,
|
|
256
446
|
methods: component.methods,
|
|
257
447
|
classObjectDictionary,
|
|
258
|
-
dependencies,
|
|
448
|
+
dependencies: dependenciesWithImports,
|
|
259
449
|
blob,
|
|
260
450
|
toReactEventName,
|
|
261
451
|
toWebFTagName,
|
|
262
|
-
generateReturnType,
|
|
452
|
+
generateReturnType: genRT,
|
|
263
453
|
generateMethodDeclaration,
|
|
264
454
|
generateMethodDeclarationWithDocs,
|
|
265
455
|
generateEventHandlerType,
|
|
@@ -289,6 +479,80 @@ interface ${object.name} {
|
|
|
289
479
|
}
|
|
290
480
|
|
|
291
481
|
componentEntries.forEach(([className, component]) => {
|
|
482
|
+
const genRT = (type: ParameterType): string => {
|
|
483
|
+
if (isUnionType(type)) {
|
|
484
|
+
const values = type.value as ParameterType[];
|
|
485
|
+
return values.map(v => {
|
|
486
|
+
if (v.value === FunctionArgumentType.null) {
|
|
487
|
+
return 'null';
|
|
488
|
+
}
|
|
489
|
+
if (typeof v.value === 'string') {
|
|
490
|
+
return `'${v.value}'`;
|
|
491
|
+
}
|
|
492
|
+
return 'any';
|
|
493
|
+
}).join(' | ');
|
|
494
|
+
}
|
|
495
|
+
if (Array.isArray(type.value)) {
|
|
496
|
+
const values = type.value as ParameterType[];
|
|
497
|
+
const hasNull = values.some(v => v.value === FunctionArgumentType.null);
|
|
498
|
+
if (hasNull) {
|
|
499
|
+
const nonNulls = values.filter(v => v.value !== FunctionArgumentType.null);
|
|
500
|
+
if (nonNulls.length === 0) {
|
|
501
|
+
return 'null';
|
|
502
|
+
}
|
|
503
|
+
const parts: string[] = nonNulls.map(v => genRT(v));
|
|
504
|
+
const unique: string[] = Array.from(new Set(parts));
|
|
505
|
+
unique.push('null');
|
|
506
|
+
return unique.join(' | ');
|
|
507
|
+
}
|
|
508
|
+
return 'any';
|
|
509
|
+
}
|
|
510
|
+
if (isPointerType(type)) {
|
|
511
|
+
const pointerType = getPointerType(type);
|
|
512
|
+
if (pointerType === 'Type') return 'any';
|
|
513
|
+
if (typeof pointerType === 'string' && pointerType.startsWith('typeof ')) {
|
|
514
|
+
const ident = pointerType.substring('typeof '.length).trim();
|
|
515
|
+
return `typeof __webfTypes.${ident}`;
|
|
516
|
+
}
|
|
517
|
+
if (typeof pointerType === 'string' && /^(?:[A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(pointerType)) {
|
|
518
|
+
const base = pointerType.split('.')[0];
|
|
519
|
+
if (!localTypeNames.has(base)) {
|
|
520
|
+
return `__webfTypes.${pointerType}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return pointerType;
|
|
524
|
+
}
|
|
525
|
+
if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
|
|
526
|
+
const elemType = getPointerType(type.value);
|
|
527
|
+
if (elemType === 'Type') return 'any[]';
|
|
528
|
+
if (typeof elemType === 'string' && elemType.startsWith('typeof ')) {
|
|
529
|
+
const ident = elemType.substring('typeof '.length).trim();
|
|
530
|
+
return `(typeof __webfTypes.${ident})[]`;
|
|
531
|
+
}
|
|
532
|
+
if (typeof elemType === 'string' && /^(?:[A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(elemType)) {
|
|
533
|
+
const base = elemType.split('.')[0];
|
|
534
|
+
if (!localTypeNames.has(base)) {
|
|
535
|
+
return `__webfTypes.${elemType}[]`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return `${elemType}[]`;
|
|
539
|
+
}
|
|
540
|
+
switch (type.value) {
|
|
541
|
+
case FunctionArgumentType.int:
|
|
542
|
+
case FunctionArgumentType.double:
|
|
543
|
+
return 'number';
|
|
544
|
+
case FunctionArgumentType.any:
|
|
545
|
+
return 'any';
|
|
546
|
+
case FunctionArgumentType.boolean:
|
|
547
|
+
return 'boolean';
|
|
548
|
+
case FunctionArgumentType.dom_string:
|
|
549
|
+
return 'string';
|
|
550
|
+
case FunctionArgumentType.void:
|
|
551
|
+
default:
|
|
552
|
+
return 'void';
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
292
556
|
const content = _.template(readTemplate('react.component.tsx'))({
|
|
293
557
|
className: className,
|
|
294
558
|
properties: component.properties,
|
|
@@ -299,7 +563,7 @@ interface ${object.name} {
|
|
|
299
563
|
blob,
|
|
300
564
|
toReactEventName,
|
|
301
565
|
toWebFTagName,
|
|
302
|
-
generateReturnType,
|
|
566
|
+
generateReturnType: genRT,
|
|
303
567
|
generateMethodDeclaration,
|
|
304
568
|
generateMethodDeclarationWithDocs,
|
|
305
569
|
generateEventHandlerType,
|
|
@@ -316,15 +580,23 @@ interface ${object.name} {
|
|
|
316
580
|
});
|
|
317
581
|
|
|
318
582
|
// Combine with shared imports at the top
|
|
319
|
-
|
|
583
|
+
// Compute relative import path to src/types and always include namespace import
|
|
584
|
+
const depth = (relativeDir || '').split('/').filter(p => p).length;
|
|
585
|
+
const upPath = '../'.repeat(depth);
|
|
586
|
+
const typesImport = `import * as __webfTypes from "${upPath}types";`;
|
|
587
|
+
|
|
588
|
+
debug(`[react] Generating combined components for ${blob.filename}.tsx; types import: ${upPath}types`);
|
|
589
|
+
|
|
590
|
+
let result = [
|
|
320
591
|
'import React from "react";',
|
|
321
592
|
createWebFComponentImport,
|
|
593
|
+
typesImport,
|
|
322
594
|
'',
|
|
323
595
|
dependencies,
|
|
324
596
|
'',
|
|
325
597
|
...componentDefinitions
|
|
326
598
|
].filter(line => line !== undefined).join('\n');
|
|
327
|
-
|
|
599
|
+
|
|
328
600
|
return result.split('\n').filter(str => {
|
|
329
601
|
return str.trim().length > 0;
|
|
330
602
|
}).join('\n');
|
package/src/vue.ts
CHANGED
|
@@ -2,21 +2,65 @@ import _ from "lodash";
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
|
-
import {ClassObject, FunctionArgumentType, FunctionDeclaration} from "./declaration";
|
|
5
|
+
import {ClassObject, FunctionArgumentType, FunctionDeclaration, ConstObject, EnumObject} from "./declaration";
|
|
6
6
|
import {IDLBlob} from "./IDLBlob";
|
|
7
|
-
import {
|
|
7
|
+
import { debug } from './logger';
|
|
8
|
+
import {getPointerType, isPointerType, isUnionType, trimNullTypeFromType} from "./utils";
|
|
8
9
|
|
|
9
10
|
function readTemplate(name: string) {
|
|
10
11
|
return fs.readFileSync(path.join(__dirname, '../templates/' + name + '.tpl'), {encoding: 'utf-8'});
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
function generateReturnType(type: ParameterType) {
|
|
14
|
+
function generateReturnType(type: ParameterType): string {
|
|
15
|
+
if (isUnionType(type)) {
|
|
16
|
+
const values = type.value as ParameterType[];
|
|
17
|
+
return values.map(v => {
|
|
18
|
+
if (v.value === FunctionArgumentType.null) {
|
|
19
|
+
return 'null';
|
|
20
|
+
}
|
|
21
|
+
if (typeof v.value === 'string') {
|
|
22
|
+
return `'${v.value}'`;
|
|
23
|
+
}
|
|
24
|
+
return 'any';
|
|
25
|
+
}).join(' | ');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle unions like boolean | null, number | null, CustomType | null
|
|
29
|
+
if (Array.isArray(type.value)) {
|
|
30
|
+
const values = type.value as ParameterType[];
|
|
31
|
+
const hasNull = values.some(v => v.value === FunctionArgumentType.null);
|
|
32
|
+
if (hasNull) {
|
|
33
|
+
const nonNulls = values.filter(v => v.value !== FunctionArgumentType.null);
|
|
34
|
+
if (nonNulls.length === 0) {
|
|
35
|
+
return 'null';
|
|
36
|
+
}
|
|
37
|
+
const parts: string[] = nonNulls.map(v => generateReturnType(v));
|
|
38
|
+
const unique: string[] = Array.from(new Set(parts));
|
|
39
|
+
unique.push('null');
|
|
40
|
+
return unique.join(' | ');
|
|
41
|
+
}
|
|
42
|
+
// Complex non-null unions are rare; fall back to any
|
|
43
|
+
return 'any';
|
|
44
|
+
}
|
|
45
|
+
|
|
14
46
|
if (isPointerType(type)) {
|
|
15
47
|
const pointerType = getPointerType(type);
|
|
48
|
+
// Map Dart's `Type` (from TS typeof) to TS `any`
|
|
49
|
+
if (pointerType === 'Type') return 'any';
|
|
50
|
+
if (typeof pointerType === 'string' && pointerType.startsWith('typeof ')) {
|
|
51
|
+
const ident = pointerType.substring('typeof '.length).trim();
|
|
52
|
+
return `typeof __webfTypes.${ident}`;
|
|
53
|
+
}
|
|
16
54
|
return pointerType;
|
|
17
55
|
}
|
|
18
56
|
if (type.isArray && typeof type.value === 'object' && !Array.isArray(type.value)) {
|
|
19
|
-
|
|
57
|
+
const elemType = getPointerType(type.value);
|
|
58
|
+
if (elemType === 'Type') return 'any[]';
|
|
59
|
+
if (typeof elemType === 'string' && elemType.startsWith('typeof ')) {
|
|
60
|
+
const ident = elemType.substring('typeof '.length).trim();
|
|
61
|
+
return `(typeof __webfTypes.${ident})[]`;
|
|
62
|
+
}
|
|
63
|
+
return `${elemType}[]`;
|
|
20
64
|
}
|
|
21
65
|
switch (type.value) {
|
|
22
66
|
case FunctionArgumentType.int:
|
|
@@ -94,20 +138,43 @@ function generateVueComponent(blob: IDLBlob) {
|
|
|
94
138
|
});
|
|
95
139
|
|
|
96
140
|
const dependencies = others.map(object => {
|
|
97
|
-
if (!object || !object.props) {
|
|
141
|
+
if (!object || !object.props || object.props.length === 0) {
|
|
98
142
|
return '';
|
|
99
143
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
144
|
+
|
|
145
|
+
const interfaceLines: string[] = [];
|
|
146
|
+
|
|
147
|
+
if (object.documentation && object.documentation.trim().length > 0) {
|
|
148
|
+
interfaceLines.push('/**');
|
|
149
|
+
object.documentation.split('\n').forEach(line => {
|
|
150
|
+
interfaceLines.push(` * ${line}`);
|
|
151
|
+
});
|
|
152
|
+
interfaceLines.push(' */');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interfaceLines.push(`interface ${object.name} {`);
|
|
156
|
+
|
|
157
|
+
const propLines = object.props.map(prop => {
|
|
158
|
+
const lines: string[] = [];
|
|
159
|
+
|
|
160
|
+
if (prop.documentation && prop.documentation.trim().length > 0) {
|
|
161
|
+
lines.push(' /**');
|
|
162
|
+
prop.documentation.split('\n').forEach(line => {
|
|
163
|
+
lines.push(` * ${line}`);
|
|
164
|
+
});
|
|
165
|
+
lines.push(' */');
|
|
103
166
|
}
|
|
104
|
-
return `${prop.name}: ${generateReturnType(prop.type)};`;
|
|
105
|
-
}).join('\n ');
|
|
106
167
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
const optionalToken = prop.optional ? '?' : '';
|
|
169
|
+
lines.push(` ${prop.name}${optionalToken}: ${generateReturnType(prop.type)};`);
|
|
170
|
+
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
interfaceLines.push(propLines.join('\n'));
|
|
175
|
+
interfaceLines.push('}');
|
|
176
|
+
|
|
177
|
+
return interfaceLines.join('\n');
|
|
111
178
|
}).filter(dep => dep.trim() !== '').join('\n\n');
|
|
112
179
|
|
|
113
180
|
const componentProperties = properties.length > 0 ? properties[0] : undefined;
|
|
@@ -145,6 +212,17 @@ interface ${object.name} {
|
|
|
145
212
|
return result;
|
|
146
213
|
}
|
|
147
214
|
|
|
215
|
+
function toVueTagName(className: string): string {
|
|
216
|
+
if (className.startsWith('WebF')) {
|
|
217
|
+
const withoutPrefix = className.substring(4);
|
|
218
|
+
return 'web-f-' + _.kebabCase(withoutPrefix);
|
|
219
|
+
} else if (className.startsWith('Flutter')) {
|
|
220
|
+
const withoutPrefix = className.substring(7);
|
|
221
|
+
return 'flutter-' + _.kebabCase(withoutPrefix);
|
|
222
|
+
}
|
|
223
|
+
return _.kebabCase(className);
|
|
224
|
+
}
|
|
225
|
+
|
|
148
226
|
export function generateVueTypings(blobs: IDLBlob[]) {
|
|
149
227
|
const componentNames = blobs.map(blob => {
|
|
150
228
|
const classObjects = blob.objects as ClassObject[];
|
|
@@ -177,13 +255,52 @@ export function generateVueTypings(blobs: IDLBlob[]) {
|
|
|
177
255
|
return component.length > 0;
|
|
178
256
|
}).join('\n\n');
|
|
179
257
|
|
|
258
|
+
// Collect declare consts across blobs and render as exported ambient declarations
|
|
259
|
+
const consts = blobs
|
|
260
|
+
.flatMap(blob => blob.objects)
|
|
261
|
+
.filter(obj => obj instanceof ConstObject) as ConstObject[];
|
|
262
|
+
|
|
263
|
+
// Deduplicate by name keeping first occurrence
|
|
264
|
+
const uniqueConsts = new Map<string, ConstObject>();
|
|
265
|
+
consts.forEach(c => {
|
|
266
|
+
if (!uniqueConsts.has(c.name)) uniqueConsts.set(c.name, c);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const constDeclarations = Array.from(uniqueConsts.values())
|
|
270
|
+
.map(c => `export declare const ${c.name}: ${c.type};`)
|
|
271
|
+
.join('\n');
|
|
272
|
+
|
|
273
|
+
// Collect declare enums across blobs
|
|
274
|
+
const enums = blobs
|
|
275
|
+
.flatMap(blob => blob.objects)
|
|
276
|
+
.filter(obj => obj instanceof EnumObject) as EnumObject[];
|
|
277
|
+
|
|
278
|
+
const enumDeclarations = enums.map(e => {
|
|
279
|
+
const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
|
|
280
|
+
return `export declare enum ${e.name} { ${members} }`;
|
|
281
|
+
}).join('\n');
|
|
282
|
+
|
|
283
|
+
// Always import the types namespace to support typeof references
|
|
284
|
+
const typesImport = `import * as __webfTypes from './src/types';`;
|
|
285
|
+
debug(`[vue] Generating typings; importing types from ./src/types`);
|
|
286
|
+
|
|
287
|
+
// Build mapping of template tag names to class names for GlobalComponents
|
|
288
|
+
const componentMetas = componentNames.map(className => ({
|
|
289
|
+
className,
|
|
290
|
+
tagName: toVueTagName(className),
|
|
291
|
+
}));
|
|
292
|
+
|
|
180
293
|
const content = _.template(readTemplate('vue.components.d.ts'), {
|
|
181
294
|
interpolate: /<%=([\s\S]+?)%>/g,
|
|
182
295
|
evaluate: /<%([\s\S]+?)%>/g,
|
|
183
296
|
escape: /<%-([\s\S]+?)%>/g
|
|
184
297
|
})({
|
|
185
298
|
componentNames,
|
|
299
|
+
componentMetas,
|
|
186
300
|
components,
|
|
301
|
+
consts: constDeclarations,
|
|
302
|
+
enums: enumDeclarations,
|
|
303
|
+
typesImport,
|
|
187
304
|
});
|
|
188
305
|
|
|
189
306
|
return content.split('\n').filter(str => {
|
package/templates/class.dart.tpl
CHANGED
|
@@ -38,7 +38,7 @@ abstract class <%= className %>Bindings extends WidgetElement {
|
|
|
38
38
|
<% var attributeName = _.kebabCase(prop.name); %>
|
|
39
39
|
<% var propName = _.camelCase(prop.name); %>
|
|
40
40
|
attributes['<%= attributeName %>'] = ElementAttributeProperty(
|
|
41
|
-
getter: () => <%= generateAttributeGetter(propName, prop.type, prop.optional) %>,
|
|
41
|
+
getter: () => <%= generateAttributeGetter(propName, prop.type, prop.optional, prop) %>,
|
|
42
42
|
setter: (value) => <%= generateAttributeSetter(propName, prop.type) %>,
|
|
43
43
|
deleter: () => <%= generateAttributeDeleter(propName, prop.type, prop.optional) %>
|
|
44
44
|
);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// https://vuejs.org/guide/extras/web-components
|
|
6
6
|
import { EmitFn, PublicProps, HTMLAttributes } from 'vue';
|
|
7
7
|
|
|
8
|
+
<%= typesImport %>
|
|
9
|
+
|
|
8
10
|
type EventMap = {
|
|
9
11
|
[event: string]: Event
|
|
10
12
|
}
|
|
@@ -19,6 +21,9 @@ type VueEventListeners<T extends EventMap> = {
|
|
|
19
21
|
[K in keyof T as `on${Capitalize<string & K>}`]?: (event: T[K]) => any
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
<%= consts %>
|
|
25
|
+
<%= enums %>
|
|
26
|
+
|
|
22
27
|
type DefineCustomElement<
|
|
23
28
|
ElementType,
|
|
24
29
|
Events extends EventMap = {},
|
|
@@ -44,10 +49,10 @@ type DefineCustomElement<
|
|
|
44
49
|
|
|
45
50
|
declare module 'vue' {
|
|
46
51
|
interface GlobalComponents {
|
|
47
|
-
<%
|
|
48
|
-
'<%=
|
|
49
|
-
<%=
|
|
50
|
-
<%=
|
|
52
|
+
<% componentMetas.forEach(comp => { %>
|
|
53
|
+
'<%= comp.tagName %>': DefineCustomElement<
|
|
54
|
+
<%= comp.className %>Props,
|
|
55
|
+
<%= comp.className %>Events
|
|
51
56
|
>
|
|
52
57
|
<% }) %>
|
|
53
58
|
}
|