@openwebf/webf 0.23.2 → 0.23.10

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.
@@ -2,15 +2,20 @@
2
2
  jest.mock('fs');
3
3
  jest.mock('child_process');
4
4
  jest.mock('../src/generator');
5
+ jest.mock('glob', () => ({
6
+ globSync: jest.fn(),
7
+ }));
5
8
  jest.mock('inquirer');
6
9
  jest.mock('yaml');
7
10
 
8
11
  import fs from 'fs';
9
12
  import path from 'path';
10
13
  import { spawnSync } from 'child_process';
14
+ import { globSync } from 'glob';
11
15
 
12
16
  const mockFs = fs as jest.Mocked<typeof fs>;
13
17
  const mockSpawnSync = spawnSync as jest.MockedFunction<typeof spawnSync>;
18
+ const mockGlobSync = globSync as jest.MockedFunction<typeof globSync>;
14
19
 
15
20
  // Set up default mocks before importing commands
16
21
  mockFs.readFileSync = jest.fn().mockImplementation((filePath: any) => {
@@ -84,6 +89,7 @@ describe('Commands', () => {
84
89
  });
85
90
  // Default mock for readdirSync to avoid undefined
86
91
  mockFs.readdirSync.mockReturnValue([] as any);
92
+ mockGlobSync.mockReturnValue([]);
87
93
  });
88
94
 
89
95
 
@@ -220,11 +226,11 @@ describe('Commands', () => {
220
226
  // Mock that required files don't exist
221
227
  mockFs.existsSync.mockReturnValue(false);
222
228
 
223
- await generateCommand(target, options);
229
+ await generateCommand(target, options);
224
230
 
225
231
  expect(mockSpawnSync).toHaveBeenCalledWith(
226
232
  expect.stringMatching(/npm(\.cmd)?/),
227
- ['install', '--omit=peer'],
233
+ ['install'],
228
234
  { cwd: target, stdio: 'inherit' }
229
235
  );
230
236
  });
@@ -356,6 +362,36 @@ describe('Commands', () => {
356
362
  consoleSpy.mockRestore();
357
363
  });
358
364
 
365
+ it('should generate only Dart bindings when dartOnly is set', async () => {
366
+ const options = {
367
+ flutterPackageSrc: '/flutter/src',
368
+ dartOnly: true,
369
+ };
370
+
371
+ // Mock TypeScript validation
372
+ mockTypeScriptValidation('/flutter/src');
373
+
374
+ await generateCommand('/dist', options as any);
375
+
376
+ // Should call dartGen for the Flutter package, but not reactGen/vueGen
377
+ expect(mockGenerator.dartGen).toHaveBeenCalledWith({
378
+ source: '/flutter/src',
379
+ target: '/flutter/src',
380
+ command: expect.stringContaining('webf codegen'),
381
+ exclude: undefined,
382
+ });
383
+ expect(mockGenerator.reactGen).not.toHaveBeenCalled();
384
+ expect(mockGenerator.vueGen).not.toHaveBeenCalled();
385
+
386
+ // Should not attempt to build or run npm scripts
387
+ const spawnCalls = (mockSpawnSync as jest.Mock).mock.calls;
388
+ const buildOrPublishCalls = spawnCalls.filter(call => {
389
+ const args = call[1] as any;
390
+ return Array.isArray(args) && (args.includes('run') || args.includes('publish') || args.includes('install'));
391
+ });
392
+ expect(buildOrPublishCalls).toHaveLength(0);
393
+ });
394
+
359
395
  it('should show instructions when --flutter-package-src is missing', async () => {
360
396
  const options = { framework: 'react' };
361
397
 
@@ -420,11 +456,57 @@ describe('Commands', () => {
420
456
 
421
457
  await generateCommand('/dist', options);
422
458
 
423
- expect(mockGenerator.reactGen).toHaveBeenCalledWith({
459
+ expect(mockGenerator.reactGen).toHaveBeenCalledWith(expect.objectContaining({
424
460
  source: '/flutter/src',
425
461
  target: path.resolve('/dist'),
426
462
  command: expect.stringContaining('webf codegen')
463
+ }));
464
+ });
465
+
466
+ it('should generate an aggregated README in dist from markdown docs', async () => {
467
+ const options = {
468
+ flutterPackageSrc: '/flutter/src',
469
+ framework: 'react',
470
+ packageName: 'test-package'
471
+ };
472
+
473
+ // Mock TypeScript validation
474
+ mockTypeScriptValidation('/flutter/src');
475
+
476
+ // Mock .d.ts files so copyMarkdownDocsToDist sees at least one entry
477
+ mockGlobSync.mockReturnValue(['lib/src/alert.d.ts'] as any);
478
+
479
+ const originalExistsSync = mockFs.existsSync as jest.Mock;
480
+ mockFs.existsSync = jest.fn().mockImplementation((filePath: any) => {
481
+ const pathStr = filePath.toString();
482
+ const distRoot = path.join(path.resolve('/dist'), 'dist');
483
+ if (pathStr === distRoot) return true;
484
+ if (pathStr === path.join(path.resolve('/dist'), 'package.json')) return true;
485
+ if (pathStr === path.join(path.resolve('/dist'), 'global.d.ts')) return true;
486
+ if (pathStr === path.join(path.resolve('/dist'), 'tsconfig.json')) return true;
487
+ if (pathStr.endsWith('.md') && pathStr.includes('/flutter/src')) return true;
488
+ return originalExistsSync(filePath);
489
+ });
490
+
491
+ // Ensure that reading a markdown file returns some content
492
+ const originalReadFileSync = mockFs.readFileSync as jest.Mock;
493
+ mockFs.readFileSync = jest.fn().mockImplementation((filePath: any, encoding?: any) => {
494
+ const pathStr = filePath.toString();
495
+ if (pathStr.endsWith('.md')) {
496
+ return '# Test Component\n\nThis is a test component doc.';
497
+ }
498
+ return originalReadFileSync(filePath, encoding);
427
499
  });
500
+
501
+ await generateCommand('/dist', options);
502
+
503
+ // README.md should be written into dist directory
504
+ const writeCalls = (mockFs.writeFileSync as jest.Mock).mock.calls;
505
+ const readmeCall = writeCalls.find(call => {
506
+ const p = call[0].toString();
507
+ return p.endsWith(path.join('dist', 'README.md'));
508
+ });
509
+ expect(readmeCall).toBeDefined();
428
510
  });
429
511
 
430
512
  it('should create new project if package.json not found', async () => {
@@ -1245,4 +1327,4 @@ describe('Commands', () => {
1245
1327
  });
1246
1328
  });
1247
1329
  });
1248
- });
1330
+ });
@@ -0,0 +1,58 @@
1
+ import { analyzer, clearCaches, UnionTypeCollector, ParameterType } from '../src/analyzer';
2
+ import { generateDartClass } from '../src/dart';
3
+ import { IDLBlob } from '../src/IDLBlob';
4
+
5
+ describe('Dart nullable union properties', () => {
6
+ beforeEach(() => {
7
+ clearCaches();
8
+ });
9
+
10
+ it('handles boolean | null properties and maps "null" attribute to null', () => {
11
+ const tsContent = `
12
+ interface FlutterCupertinoCheckboxProperties {
13
+ /**
14
+ * Whether the checkbox is checked.
15
+ * Default: false.
16
+ */
17
+ checked?: boolean | null;
18
+ }
19
+
20
+ interface FlutterCupertinoCheckboxEvents {
21
+ /**
22
+ * Fired when the checkbox value changes.
23
+ */
24
+ change: CustomEvent<boolean>;
25
+ }
26
+ `;
27
+
28
+ const blob = new IDLBlob('checkbox.d.ts', 'dist', 'checkbox', 'implement');
29
+ blob.raw = tsContent;
30
+
31
+ const definedPropertyCollector = {
32
+ properties: new Set<string>(),
33
+ files: new Set<string>(),
34
+ interfaces: new Set<string>(),
35
+ };
36
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
37
+
38
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
39
+
40
+ const dartCode = generateDartClass(blob, 'test');
41
+
42
+ // Should generate bindings class for FlutterCupertinoCheckbox
43
+ expect(dartCode).toContain('abstract class FlutterCupertinoCheckboxBindings extends WidgetElement {');
44
+
45
+ // The checked property should be a nullable bool in Dart bindings.
46
+ expect(dartCode).toContain('bool? get checked;');
47
+
48
+ // Attribute setter should treat the literal "null" as a Dart null.
49
+ expect(dartCode).toContain(
50
+ "setter: (value) => checked = value == 'null' ? null : (value == 'true' || value == ''),"
51
+ );
52
+
53
+ // Deleting the attribute should reset to the default `false`.
54
+ expect(dartCode).toContain(
55
+ "deleter: () => checked = false"
56
+ );
57
+ });
58
+ });
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { glob } from 'glob';
3
+ import { globSync } from 'glob';
4
4
  import { dartGen, reactGen, vueGen, clearAllCaches } from '../src/generator';
5
5
  import * as analyzer from '../src/analyzer';
6
6
  import * as dartGenerator from '../src/dart';
@@ -10,7 +10,9 @@ import { ClassObject } from '../src/declaration';
10
10
 
11
11
  // Mock dependencies
12
12
  jest.mock('fs');
13
- jest.mock('glob');
13
+ jest.mock('glob', () => ({
14
+ globSync: jest.fn(),
15
+ }));
14
16
  jest.mock('../src/analyzer');
15
17
  jest.mock('../src/dart');
16
18
  jest.mock('../src/react');
@@ -47,7 +49,7 @@ jest.mock('../src/logger', () => ({
47
49
  }));
48
50
 
49
51
  const mockFs = fs as jest.Mocked<typeof fs>;
50
- const mockGlob = glob as jest.Mocked<typeof glob>;
52
+ const mockGlobSync = globSync as jest.MockedFunction<typeof globSync>;
51
53
  const mockAnalyzer = analyzer as jest.Mocked<typeof analyzer>;
52
54
  const mockDartGenerator = dartGenerator as jest.Mocked<typeof dartGenerator>;
53
55
  const mockReactGenerator = reactGenerator as jest.Mocked<typeof reactGenerator>;
@@ -65,7 +67,7 @@ describe('Generator', () => {
65
67
  mockFs.writeFileSync.mockImplementation(() => undefined);
66
68
  mockFs.mkdirSync.mockImplementation(() => undefined);
67
69
 
68
- mockGlob.globSync.mockReturnValue(['test.d.ts', 'component.d.ts']);
70
+ mockGlobSync.mockReturnValue(['test.d.ts', 'component.d.ts']);
69
71
 
70
72
  mockAnalyzer.analyzer.mockImplementation(() => undefined);
71
73
  mockAnalyzer.clearCaches.mockImplementation(() => undefined);
@@ -84,7 +86,7 @@ describe('Generator', () => {
84
86
  command: 'test command'
85
87
  });
86
88
 
87
- expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
89
+ expect(mockGlobSync).toHaveBeenCalledWith('**/*.d.ts', {
88
90
  cwd: '/test/source',
89
91
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**']
90
92
  });
@@ -101,14 +103,14 @@ describe('Generator', () => {
101
103
  command: 'test command'
102
104
  });
103
105
 
104
- expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
106
+ expect(mockGlobSync).toHaveBeenCalledWith('**/*.d.ts', {
105
107
  cwd: expect.stringContaining('relative/source'),
106
108
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**']
107
109
  });
108
110
  });
109
111
 
110
112
  it('should filter out global.d.ts files', async () => {
111
- mockGlob.globSync.mockReturnValue(['test.d.ts', 'global.d.ts', 'component.d.ts']);
113
+ mockGlobSync.mockReturnValue(['test.d.ts', 'global.d.ts', 'component.d.ts']);
112
114
 
113
115
  await dartGen({
114
116
  source: '/test/source',
@@ -122,7 +124,7 @@ describe('Generator', () => {
122
124
  });
123
125
 
124
126
  it('should handle empty type files', async () => {
125
- mockGlob.globSync.mockReturnValue([]);
127
+ mockGlobSync.mockReturnValue([]);
126
128
 
127
129
  await dartGen({
128
130
  source: '/test/source',
@@ -135,7 +137,7 @@ describe('Generator', () => {
135
137
  });
136
138
 
137
139
  it('should continue processing if one file fails', async () => {
138
- mockGlob.globSync.mockReturnValue(['test1.d.ts', 'test2.d.ts']);
140
+ mockGlobSync.mockReturnValue(['test1.d.ts', 'test2.d.ts']);
139
141
  mockAnalyzer.analyzer
140
142
  .mockImplementationOnce(() => { throw new Error('Parse error'); })
141
143
  .mockImplementationOnce(() => undefined);
@@ -216,7 +218,7 @@ describe('Generator', () => {
216
218
  });
217
219
 
218
220
  it('should generate index.d.ts with references and exports', async () => {
219
- mockGlob.globSync.mockReturnValue(['components/button.d.ts', 'widgets/card.d.ts']);
221
+ mockGlobSync.mockReturnValue(['components/button.d.ts', 'widgets/card.d.ts']);
220
222
  mockFs.readFileSync.mockReturnValue('interface Test {}');
221
223
  mockFs.existsSync.mockImplementation((path) => {
222
224
  // Source directory exists
@@ -247,7 +249,7 @@ describe('Generator', () => {
247
249
  });
248
250
 
249
251
  it('should copy original .d.ts files to output directory', async () => {
250
- mockGlob.globSync.mockReturnValue(['test.d.ts']);
252
+ mockGlobSync.mockReturnValue(['test.d.ts']);
251
253
  const originalContent = 'interface Original {}';
252
254
  mockFs.readFileSync.mockReturnValue(originalContent);
253
255
  mockFs.existsSync.mockImplementation((path) => {
@@ -279,7 +281,7 @@ describe('Generator', () => {
279
281
  exclude: ['**/test/**', '**/docs/**']
280
282
  });
281
283
 
282
- expect(mockGlob.globSync).toHaveBeenCalledWith('**/*.d.ts', {
284
+ expect(mockGlobSync).toHaveBeenCalledWith('**/*.d.ts', {
283
285
  cwd: '/test/source',
284
286
  ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/example/**', '**/test/**', '**/docs/**']
285
287
  });
@@ -419,7 +421,7 @@ describe('Generator', () => {
419
421
  });
420
422
 
421
423
  it('should only generate one index.d.ts file', async () => {
422
- mockGlob.globSync.mockReturnValue(['comp1.d.ts', 'comp2.d.ts', 'comp3.d.ts']);
424
+ mockGlobSync.mockReturnValue(['comp1.d.ts', 'comp2.d.ts', 'comp3.d.ts']);
423
425
 
424
426
  await vueGen({
425
427
  source: '/test/source',
@@ -436,7 +438,7 @@ describe('Generator', () => {
436
438
 
437
439
  describe('Error handling', () => {
438
440
  it('should handle glob errors', async () => {
439
- mockGlob.globSync.mockImplementation(() => {
441
+ mockGlobSync.mockImplementation(() => {
440
442
  throw new Error('Glob error');
441
443
  });
442
444
 
@@ -25,6 +25,6 @@ describe('React generator - declare const support', () => {
25
25
  blob.objects = [eo as any];
26
26
 
27
27
  const output = generateReactComponent(blob);
28
- expect(output).toContain("export declare enum CupertinoColors { 'red' = 0x0f, 'bbb' = 0x00 }");
28
+ expect(output).toContain("export enum CupertinoColors { 'red' = 0x0f, 'bbb' = 0x00 }");
29
29
  });
30
30
  });
@@ -0,0 +1,66 @@
1
+ import { analyzer, clearCaches, UnionTypeCollector, ParameterType } from '../src/analyzer';
2
+ import { generateReactComponent } from '../src/react';
3
+ import { generateVueTypings } from '../src/vue';
4
+ import { IDLBlob } from '../src/IDLBlob';
5
+
6
+ describe('React/Vue nullable union props', () => {
7
+ beforeEach(() => {
8
+ clearCaches();
9
+ });
10
+
11
+ const tsContent = `
12
+ interface FlutterCupertinoCheckboxProperties {
13
+ /**
14
+ * Whether the checkbox is checked.
15
+ * Default: false.
16
+ */
17
+ checked?: boolean | null;
18
+ }
19
+
20
+ interface FlutterCupertinoCheckboxEvents {
21
+ /**
22
+ * Fired when the checkbox value changes.
23
+ */
24
+ change: CustomEvent<boolean>;
25
+ }
26
+ `;
27
+
28
+ it('emits boolean | null for React props', () => {
29
+ const blob = new IDLBlob('checkbox.d.ts', 'dist', 'checkbox', 'implement');
30
+ blob.raw = tsContent;
31
+
32
+ const definedPropertyCollector = {
33
+ properties: new Set<string>(),
34
+ files: new Set<string>(),
35
+ interfaces: new Set<string>(),
36
+ };
37
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
38
+
39
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
40
+
41
+ const reactCode = generateReactComponent(blob);
42
+
43
+ // Prop should allow both undefined and null
44
+ expect(reactCode).toContain('checked?: boolean | null;');
45
+ });
46
+
47
+ it('emits boolean | null for Vue props', () => {
48
+ const blob = new IDLBlob('checkbox.d.ts', 'dist', 'checkbox', 'implement');
49
+ blob.raw = tsContent;
50
+
51
+ const definedPropertyCollector = {
52
+ properties: new Set<string>(),
53
+ files: new Set<string>(),
54
+ interfaces: new Set<string>(),
55
+ };
56
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
57
+
58
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
59
+
60
+ const vueCode = generateVueTypings([blob]);
61
+
62
+ // Vue prop name is kebab-cased and should allow null explicitly
63
+ expect(vueCode).toContain(`'checked'?: boolean | null;`);
64
+ });
65
+ });
66
+
@@ -181,9 +181,9 @@ describe('React Generator', () => {
181
181
 
182
182
  const result = generateReactComponent(blob);
183
183
 
184
- // Should include custom props (dom_string is not converted in raw output)
185
- expect(result).toContain('title: dom_string;');
186
- expect(result).toContain('disabled?: boolean;');
184
+ // Should include custom props with generated TS types
185
+ expect(result).toContain('title: __webfTypes.dom_string;');
186
+ expect(result).toContain('disabled?: __webfTypes.boolean;');
187
187
 
188
188
  // And still include standard HTML props
189
189
  expect(result).toContain('id?: string;');
@@ -191,5 +191,47 @@ describe('React Generator', () => {
191
191
  expect(result).toContain('children?: React.ReactNode;');
192
192
  expect(result).toContain('className?: string;');
193
193
  });
194
+
195
+ it('should preserve JSDoc for supporting option interfaces', () => {
196
+ const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', '');
197
+
198
+ const properties = new ClassObject();
199
+ properties.name = 'TestComponentProperties';
200
+ properties.kind = ClassObjectKind.interface;
201
+
202
+ const options = new ClassObject();
203
+ options.name = 'TestComponentOptions';
204
+ options.kind = ClassObjectKind.interface;
205
+
206
+ const titleProp = new PropsDeclaration();
207
+ titleProp.name = 'title';
208
+ titleProp.type = { value: 'string' } as any;
209
+ titleProp.optional = true;
210
+ titleProp.documentation = 'Optional override title for this show() call.';
211
+ titleProp.readonly = false;
212
+ titleProp.typeMode = {};
213
+
214
+ const messageProp = new PropsDeclaration();
215
+ messageProp.name = 'message';
216
+ messageProp.type = { value: 'string' } as any;
217
+ messageProp.optional = true;
218
+ messageProp.documentation = 'Optional override message for this show() call.';
219
+ messageProp.readonly = false;
220
+ messageProp.typeMode = {};
221
+
222
+ options.props = [titleProp, messageProp];
223
+
224
+ blob.objects = [properties, options];
225
+
226
+ const result = generateReactComponent(blob);
227
+
228
+ expect(result).toContain('interface TestComponentOptions');
229
+ expect(result).toMatch(
230
+ /interface TestComponentOptions[\s\S]*?\/\*\*[\s\S]*?Optional override title for this show\(\) call\.[\s\S]*?\*\/\s*\n\s*title\?:/
231
+ );
232
+ expect(result).toMatch(
233
+ /interface TestComponentOptions[\s\S]*?\/\*\*[\s\S]*?Optional override message for this show\(\) call\.[\s\S]*?\*\/\s*\n\s*message\?:/
234
+ );
235
+ });
194
236
  });
195
- });
237
+ });