@openwebf/webf 0.23.2 → 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/bin/webf.js +1 -0
- package/dist/analyzer.js +65 -1
- package/dist/commands.js +198 -98
- package/dist/dart.js +91 -25
- package/dist/declaration.js +1 -0
- package/dist/generator.js +26 -17
- package/dist/react.js +272 -25
- package/dist/vue.js +74 -11
- package/package.json +1 -1
- package/src/analyzer.ts +58 -2
- package/src/commands.ts +300 -196
- package/src/dart.ts +95 -20
- package/src/declaration.ts +1 -0
- package/src/generator.ts +24 -16
- package/src/react.ts +288 -29
- package/src/vue.ts +85 -13
- package/templates/class.dart.tpl +1 -1
- package/templates/vue.components.d.ts.tpl +2 -0
- package/test/commands.test.ts +82 -2
- package/test/dart-nullable-props.test.ts +58 -0
- package/test/react-consts.test.ts +1 -1
- package/test/react-vue-nullable-props.test.ts +66 -0
- package/test/react.test.ts +46 -4
package/src/react.ts
CHANGED
|
@@ -4,22 +4,61 @@ import path from 'path';
|
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
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:
|
|
@@ -158,12 +197,20 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
|
|
|
158
197
|
// Include declare const values as ambient exports for type usage (e.g., unique symbol branding)
|
|
159
198
|
const constDeclarations = constObjects.map(c => `export declare const ${c.name}: ${c.type};`).join('\n');
|
|
160
199
|
|
|
161
|
-
// Include enums
|
|
200
|
+
// Include enums as concrete exports (no declare) so they are usable as values
|
|
162
201
|
const enumDeclarations = enumObjects.map(e => {
|
|
163
202
|
const members = e.members.map(m => m.initializer ? `${m.name} = ${m.initializer}` : `${m.name}`).join(', ');
|
|
164
|
-
return `export
|
|
203
|
+
return `export enum ${e.name} { ${members} }`;
|
|
165
204
|
}).join('\n');
|
|
166
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
|
+
|
|
167
214
|
const dependencies = [
|
|
168
215
|
typeAliasDeclarations,
|
|
169
216
|
constDeclarations,
|
|
@@ -174,28 +221,60 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
|
|
|
174
221
|
return generateMethodDeclarationWithDocs(method, ' ');
|
|
175
222
|
}).join('\n');
|
|
176
223
|
|
|
177
|
-
|
|
178
|
-
if (object.documentation) {
|
|
179
|
-
|
|
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(' */');
|
|
180
231
|
}
|
|
181
232
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
233
|
+
lines.push(`interface ${object.name} {`);
|
|
234
|
+
lines.push(methodDeclarations);
|
|
235
|
+
lines.push('}');
|
|
236
|
+
|
|
237
|
+
return lines.join('\n');
|
|
185
238
|
}).join('\n\n'),
|
|
186
239
|
others.map(object => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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(' */');
|
|
190
265
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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')
|
|
199
278
|
].filter(Boolean).join('\n\n');
|
|
200
279
|
|
|
201
280
|
// Generate all components from this file
|
|
@@ -262,17 +341,115 @@ interface ${object.name} {
|
|
|
262
341
|
createWebFComponentImport
|
|
263
342
|
);
|
|
264
343
|
|
|
265
|
-
|
|
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)({
|
|
266
443
|
className: className,
|
|
267
444
|
properties: component.properties,
|
|
268
445
|
events: component.events,
|
|
269
446
|
methods: component.methods,
|
|
270
447
|
classObjectDictionary,
|
|
271
|
-
dependencies,
|
|
448
|
+
dependencies: dependenciesWithImports,
|
|
272
449
|
blob,
|
|
273
450
|
toReactEventName,
|
|
274
451
|
toWebFTagName,
|
|
275
|
-
generateReturnType,
|
|
452
|
+
generateReturnType: genRT,
|
|
276
453
|
generateMethodDeclaration,
|
|
277
454
|
generateMethodDeclarationWithDocs,
|
|
278
455
|
generateEventHandlerType,
|
|
@@ -302,6 +479,80 @@ interface ${object.name} {
|
|
|
302
479
|
}
|
|
303
480
|
|
|
304
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
|
+
|
|
305
556
|
const content = _.template(readTemplate('react.component.tsx'))({
|
|
306
557
|
className: className,
|
|
307
558
|
properties: component.properties,
|
|
@@ -312,7 +563,7 @@ interface ${object.name} {
|
|
|
312
563
|
blob,
|
|
313
564
|
toReactEventName,
|
|
314
565
|
toWebFTagName,
|
|
315
|
-
generateReturnType,
|
|
566
|
+
generateReturnType: genRT,
|
|
316
567
|
generateMethodDeclaration,
|
|
317
568
|
generateMethodDeclarationWithDocs,
|
|
318
569
|
generateEventHandlerType,
|
|
@@ -329,15 +580,23 @@ interface ${object.name} {
|
|
|
329
580
|
});
|
|
330
581
|
|
|
331
582
|
// Combine with shared imports at the top
|
|
332
|
-
|
|
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 = [
|
|
333
591
|
'import React from "react";',
|
|
334
592
|
createWebFComponentImport,
|
|
593
|
+
typesImport,
|
|
335
594
|
'',
|
|
336
595
|
dependencies,
|
|
337
596
|
'',
|
|
338
597
|
...componentDefinitions
|
|
339
598
|
].filter(line => line !== undefined).join('\n');
|
|
340
|
-
|
|
599
|
+
|
|
341
600
|
return result.split('\n').filter(str => {
|
|
342
601
|
return str.trim().length > 0;
|
|
343
602
|
}).join('\n');
|
package/src/vue.ts
CHANGED
|
@@ -4,19 +4,63 @@ import path from 'path';
|
|
|
4
4
|
import {ParameterType} from "./analyzer";
|
|
5
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;
|
|
@@ -213,6 +280,10 @@ export function generateVueTypings(blobs: IDLBlob[]) {
|
|
|
213
280
|
return `export declare enum ${e.name} { ${members} }`;
|
|
214
281
|
}).join('\n');
|
|
215
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
|
+
|
|
216
287
|
// Build mapping of template tag names to class names for GlobalComponents
|
|
217
288
|
const componentMetas = componentNames.map(className => ({
|
|
218
289
|
className,
|
|
@@ -229,6 +300,7 @@ export function generateVueTypings(blobs: IDLBlob[]) {
|
|
|
229
300
|
components,
|
|
230
301
|
consts: constDeclarations,
|
|
231
302
|
enums: enumDeclarations,
|
|
303
|
+
typesImport,
|
|
232
304
|
});
|
|
233
305
|
|
|
234
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
|
);
|
package/test/commands.test.ts
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
jest.mock('fs');
|
|
3
3
|
jest.mock('child_process');
|
|
4
4
|
jest.mock('../src/generator');
|
|
5
|
+
jest.mock('glob');
|
|
5
6
|
jest.mock('inquirer');
|
|
6
7
|
jest.mock('yaml');
|
|
7
8
|
|
|
8
9
|
import fs from 'fs';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import { spawnSync } from 'child_process';
|
|
12
|
+
import { glob } from 'glob';
|
|
11
13
|
|
|
12
14
|
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
13
15
|
const mockSpawnSync = spawnSync as jest.MockedFunction<typeof spawnSync>;
|
|
16
|
+
const mockGlob = glob as jest.Mocked<typeof glob>;
|
|
14
17
|
|
|
15
18
|
// Set up default mocks before importing commands
|
|
16
19
|
mockFs.readFileSync = jest.fn().mockImplementation((filePath: any) => {
|
|
@@ -84,6 +87,7 @@ describe('Commands', () => {
|
|
|
84
87
|
});
|
|
85
88
|
// Default mock for readdirSync to avoid undefined
|
|
86
89
|
mockFs.readdirSync.mockReturnValue([] as any);
|
|
90
|
+
mockGlob.globSync.mockReturnValue([]);
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
|
|
@@ -356,6 +360,36 @@ describe('Commands', () => {
|
|
|
356
360
|
consoleSpy.mockRestore();
|
|
357
361
|
});
|
|
358
362
|
|
|
363
|
+
it('should generate only Dart bindings when dartOnly is set', async () => {
|
|
364
|
+
const options = {
|
|
365
|
+
flutterPackageSrc: '/flutter/src',
|
|
366
|
+
dartOnly: true,
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Mock TypeScript validation
|
|
370
|
+
mockTypeScriptValidation('/flutter/src');
|
|
371
|
+
|
|
372
|
+
await generateCommand('/dist', options as any);
|
|
373
|
+
|
|
374
|
+
// Should call dartGen for the Flutter package, but not reactGen/vueGen
|
|
375
|
+
expect(mockGenerator.dartGen).toHaveBeenCalledWith({
|
|
376
|
+
source: '/flutter/src',
|
|
377
|
+
target: '/flutter/src',
|
|
378
|
+
command: expect.stringContaining('webf codegen'),
|
|
379
|
+
exclude: undefined,
|
|
380
|
+
});
|
|
381
|
+
expect(mockGenerator.reactGen).not.toHaveBeenCalled();
|
|
382
|
+
expect(mockGenerator.vueGen).not.toHaveBeenCalled();
|
|
383
|
+
|
|
384
|
+
// Should not attempt to build or run npm scripts
|
|
385
|
+
const spawnCalls = (mockSpawnSync as jest.Mock).mock.calls;
|
|
386
|
+
const buildOrPublishCalls = spawnCalls.filter(call => {
|
|
387
|
+
const args = call[1] as any;
|
|
388
|
+
return Array.isArray(args) && (args.includes('run') || args.includes('publish') || args.includes('install'));
|
|
389
|
+
});
|
|
390
|
+
expect(buildOrPublishCalls).toHaveLength(0);
|
|
391
|
+
});
|
|
392
|
+
|
|
359
393
|
it('should show instructions when --flutter-package-src is missing', async () => {
|
|
360
394
|
const options = { framework: 'react' };
|
|
361
395
|
|
|
@@ -420,11 +454,57 @@ describe('Commands', () => {
|
|
|
420
454
|
|
|
421
455
|
await generateCommand('/dist', options);
|
|
422
456
|
|
|
423
|
-
expect(mockGenerator.reactGen).toHaveBeenCalledWith({
|
|
457
|
+
expect(mockGenerator.reactGen).toHaveBeenCalledWith(expect.objectContaining({
|
|
424
458
|
source: '/flutter/src',
|
|
425
459
|
target: path.resolve('/dist'),
|
|
426
460
|
command: expect.stringContaining('webf codegen')
|
|
461
|
+
}));
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should generate an aggregated README in dist from markdown docs', async () => {
|
|
465
|
+
const options = {
|
|
466
|
+
flutterPackageSrc: '/flutter/src',
|
|
467
|
+
framework: 'react',
|
|
468
|
+
packageName: 'test-package'
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// Mock TypeScript validation
|
|
472
|
+
mockTypeScriptValidation('/flutter/src');
|
|
473
|
+
|
|
474
|
+
// Mock .d.ts files so copyMarkdownDocsToDist sees at least one entry
|
|
475
|
+
mockGlob.globSync.mockReturnValue(['lib/src/alert.d.ts'] as any);
|
|
476
|
+
|
|
477
|
+
const originalExistsSync = mockFs.existsSync as jest.Mock;
|
|
478
|
+
mockFs.existsSync = jest.fn().mockImplementation((filePath: any) => {
|
|
479
|
+
const pathStr = filePath.toString();
|
|
480
|
+
const distRoot = path.join(path.resolve('/dist'), 'dist');
|
|
481
|
+
if (pathStr === distRoot) return true;
|
|
482
|
+
if (pathStr === path.join(path.resolve('/dist'), 'package.json')) return true;
|
|
483
|
+
if (pathStr === path.join(path.resolve('/dist'), 'global.d.ts')) return true;
|
|
484
|
+
if (pathStr === path.join(path.resolve('/dist'), 'tsconfig.json')) return true;
|
|
485
|
+
if (pathStr.endsWith('.md') && pathStr.includes('/flutter/src')) return true;
|
|
486
|
+
return originalExistsSync(filePath);
|
|
427
487
|
});
|
|
488
|
+
|
|
489
|
+
// Ensure that reading a markdown file returns some content
|
|
490
|
+
const originalReadFileSync = mockFs.readFileSync as jest.Mock;
|
|
491
|
+
mockFs.readFileSync = jest.fn().mockImplementation((filePath: any, encoding?: any) => {
|
|
492
|
+
const pathStr = filePath.toString();
|
|
493
|
+
if (pathStr.endsWith('.md')) {
|
|
494
|
+
return '# Test Component\n\nThis is a test component doc.';
|
|
495
|
+
}
|
|
496
|
+
return originalReadFileSync(filePath, encoding);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
await generateCommand('/dist', options);
|
|
500
|
+
|
|
501
|
+
// README.md should be written into dist directory
|
|
502
|
+
const writeCalls = (mockFs.writeFileSync as jest.Mock).mock.calls;
|
|
503
|
+
const readmeCall = writeCalls.find(call => {
|
|
504
|
+
const p = call[0].toString();
|
|
505
|
+
return p.endsWith(path.join('dist', 'README.md'));
|
|
506
|
+
});
|
|
507
|
+
expect(readmeCall).toBeDefined();
|
|
428
508
|
});
|
|
429
509
|
|
|
430
510
|
it('should create new project if package.json not found', async () => {
|
|
@@ -1245,4 +1325,4 @@ describe('Commands', () => {
|
|
|
1245
1325
|
});
|
|
1246
1326
|
});
|
|
1247
1327
|
});
|
|
1248
|
-
});
|
|
1328
|
+
});
|