@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/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
- return (type.value as ParameterType[]).map(v => `'${v.value}'`).join(' | ');
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
- return `${getPointerType(type.value)}[]`;
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 declare enum ${e.name} { ${members} }`;
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
- let interfaceDoc = '';
178
- if (object.documentation) {
179
- interfaceDoc = `/**\n${object.documentation.split('\n').map(line => ` * ${line}`).join('\n')}\n */\n`;
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
- return `${interfaceDoc}interface ${object.name} {
183
- ${methodDeclarations}
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
- const props = object.props.map(prop => {
188
- if (prop.optional) {
189
- return `${prop.name}?: ${generateReturnType(prop.type)};`;
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
- return `${prop.name}: ${generateReturnType(prop.type)};`;
192
- }).join('\n ');
193
-
194
- return `
195
- interface ${object.name} {
196
- ${props}
197
- }`;
198
- }).join('\n\n')
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
- const content = _.template(templateContent)({
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
- const result = [
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 {getPointerType, isPointerType} from "./utils";
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
- return `${getPointerType(type.value)}[]`;
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
- const props = object.props.map(prop => {
101
- if (prop.optional) {
102
- return `${prop.name}?: ${generateReturnType(prop.type)};`;
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
- return `
108
- interface ${object.name} {
109
- ${props}
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 => {
@@ -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
  }
@@ -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
+ });