@openwebf/webf 0.1.0

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.
Files changed (60) hide show
  1. package/CLAUDE.md +206 -0
  2. package/README-zhCN.md +256 -0
  3. package/README.md +232 -0
  4. package/bin/webf.js +25 -0
  5. package/coverage/clover.xml +1295 -0
  6. package/coverage/coverage-final.json +12 -0
  7. package/coverage/lcov-report/IDLBlob.ts.html +142 -0
  8. package/coverage/lcov-report/analyzer.ts.html +2158 -0
  9. package/coverage/lcov-report/analyzer_original.ts.html +1450 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/commands.ts.html +700 -0
  13. package/coverage/lcov-report/dart.ts.html +490 -0
  14. package/coverage/lcov-report/declaration.ts.html +337 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/generator.ts.html +1171 -0
  17. package/coverage/lcov-report/index.html +266 -0
  18. package/coverage/lcov-report/logger.ts.html +424 -0
  19. package/coverage/lcov-report/prettify.css +1 -0
  20. package/coverage/lcov-report/prettify.js +2 -0
  21. package/coverage/lcov-report/react.ts.html +619 -0
  22. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  23. package/coverage/lcov-report/sorter.js +196 -0
  24. package/coverage/lcov-report/utils.ts.html +466 -0
  25. package/coverage/lcov-report/vue.ts.html +613 -0
  26. package/coverage/lcov.info +2149 -0
  27. package/global.d.ts +2 -0
  28. package/jest.config.js +24 -0
  29. package/package.json +36 -0
  30. package/src/IDLBlob.ts +20 -0
  31. package/src/analyzer.ts +692 -0
  32. package/src/commands.ts +645 -0
  33. package/src/dart.ts +170 -0
  34. package/src/declaration.ts +84 -0
  35. package/src/generator.ts +454 -0
  36. package/src/logger.ts +114 -0
  37. package/src/react.ts +186 -0
  38. package/src/utils.ts +127 -0
  39. package/src/vue.ts +176 -0
  40. package/templates/class.dart.tpl +86 -0
  41. package/templates/gitignore.tpl +2 -0
  42. package/templates/react.component.tsx.tpl +53 -0
  43. package/templates/react.createComponent.tpl +286 -0
  44. package/templates/react.index.ts.tpl +8 -0
  45. package/templates/react.package.json.tpl +26 -0
  46. package/templates/react.tsconfig.json.tpl +16 -0
  47. package/templates/react.tsup.config.ts.tpl +10 -0
  48. package/templates/tsconfig.json.tpl +8 -0
  49. package/templates/vue.component.partial.tpl +31 -0
  50. package/templates/vue.components.d.ts.tpl +49 -0
  51. package/templates/vue.package.json.tpl +11 -0
  52. package/templates/vue.tsconfig.json.tpl +15 -0
  53. package/test/IDLBlob.test.ts +75 -0
  54. package/test/analyzer.test.ts +370 -0
  55. package/test/commands.test.ts +1253 -0
  56. package/test/generator.test.ts +460 -0
  57. package/test/logger.test.ts +215 -0
  58. package/test/react.test.ts +49 -0
  59. package/test/utils.test.ts +316 -0
  60. package/tsconfig.json +30 -0
@@ -0,0 +1,1253 @@
1
+ // Mock fs before importing commands
2
+ jest.mock('fs');
3
+ jest.mock('child_process');
4
+ jest.mock('../src/generator');
5
+ jest.mock('inquirer');
6
+ jest.mock('yaml');
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { spawnSync } from 'child_process';
11
+
12
+ const mockFs = fs as jest.Mocked<typeof fs>;
13
+ const mockSpawnSync = spawnSync as jest.MockedFunction<typeof spawnSync>;
14
+
15
+ // Set up default mocks before importing commands
16
+ mockFs.readFileSync = jest.fn().mockImplementation((filePath: any) => {
17
+ const pathStr = filePath.toString();
18
+ if (pathStr.includes('global.d.ts')) return 'global.d.ts content';
19
+ if (pathStr.includes('gitignore.tpl')) return 'gitignore template';
20
+ if (pathStr.includes('react.package.json.tpl')) return '<%= packageName %> <%= version %> <%= description %>';
21
+ if (pathStr.includes('react.tsconfig.json.tpl')) return 'react tsconfig';
22
+ if (pathStr.includes('react.tsup.config.ts.tpl')) return 'tsup config';
23
+ if (pathStr.includes('react.createComponent.tpl')) return 'create component';
24
+ if (pathStr.includes('react.index.ts.tpl')) return 'index template';
25
+ if (pathStr.includes('vue.package.json.tpl')) return '<%= packageName %> <%= version %> <%= description %>';
26
+ if (pathStr.includes('vue.tsconfig.json.tpl')) return 'vue tsconfig';
27
+ // This should come after more specific checks
28
+ if (pathStr.includes('tsconfig.json.tpl')) return 'tsconfig template';
29
+ if (pathStr.includes('pubspec.yaml')) return 'name: test\nversion: 1.0.0\ndescription: Test description';
30
+ return '';
31
+ });
32
+
33
+ // Now import commands after mocks are set up
34
+ import { generateCommand } from '../src/commands';
35
+ import * as generator from '../src/generator';
36
+ import inquirer from 'inquirer';
37
+ import yaml from 'yaml';
38
+
39
+ const mockGenerator = generator as jest.Mocked<typeof generator>;
40
+ const mockInquirer = inquirer as jest.Mocked<typeof inquirer>;
41
+ const mockYaml = yaml as jest.Mocked<typeof yaml>;
42
+
43
+ describe('Commands', () => {
44
+ // Helper function to mock TypeScript environment validation
45
+ const mockTypeScriptValidation = (path: string) => {
46
+ mockFs.existsSync.mockImplementation((filePath) => {
47
+ const pathStr = filePath.toString();
48
+ if (pathStr === `${path}/tsconfig.json`) return true;
49
+ if (pathStr === `${path}/lib`) return true;
50
+ if (pathStr.includes('pubspec.yaml')) return true;
51
+ return false;
52
+ });
53
+
54
+ mockFs.readdirSync.mockImplementation((dirPath: any) => {
55
+ if (dirPath.toString() === `${path}/lib`) {
56
+ return ['component.d.ts'] as any;
57
+ }
58
+ return [] as any;
59
+ });
60
+
61
+ mockFs.statSync.mockReturnValue({ isDirectory: () => false } as any);
62
+ };
63
+
64
+ beforeEach(() => {
65
+ jest.clearAllMocks();
66
+ // Setup default mocks
67
+ mockFs.existsSync.mockReturnValue(false);
68
+ mockFs.mkdirSync.mockImplementation(() => undefined);
69
+ mockFs.writeFileSync.mockImplementation(() => undefined);
70
+ mockSpawnSync.mockReturnValue({
71
+ pid: 1234,
72
+ output: [],
73
+ stdout: Buffer.from(''),
74
+ stderr: Buffer.from(''),
75
+ status: 0,
76
+ signal: null,
77
+ });
78
+ // Default mock for inquirer
79
+ mockInquirer.prompt.mockResolvedValue({});
80
+ // Default mock for yaml
81
+ mockYaml.parse.mockReturnValue({
82
+ name: 'test_package',
83
+ version: '1.0.0',
84
+ description: 'Test Flutter package description'
85
+ });
86
+ // Default mock for readdirSync to avoid undefined
87
+ mockFs.readdirSync.mockReturnValue([] as any);
88
+ });
89
+
90
+
91
+ describe('generateCommand with auto-creation', () => {
92
+ let mockExit: jest.SpyInstance;
93
+ let consoleSpy: jest.SpyInstance;
94
+
95
+ beforeEach(() => {
96
+ mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
97
+ throw new Error('process.exit called');
98
+ });
99
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
100
+ });
101
+
102
+ afterEach(() => {
103
+ mockExit.mockRestore();
104
+ consoleSpy.mockRestore();
105
+ });
106
+
107
+ describe('React framework - new project', () => {
108
+ it('should create React project structure when package.json is missing', async () => {
109
+ const target = '/test/react-project';
110
+ const options = { framework: 'react', packageName: 'test-package' };
111
+
112
+ // Mock that required files don't exist
113
+ mockFs.existsSync.mockImplementation((filePath) => {
114
+ const pathStr = filePath.toString();
115
+ // The target directory exists but the required files don't
116
+ if (pathStr === path.resolve(target)) return true;
117
+ return false;
118
+ });
119
+
120
+ await generateCommand(target, options);
121
+
122
+ // Should log creation message
123
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Creating new react project'));
124
+
125
+ // Check directory creation for src folders (target already exists)
126
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(
127
+ path.join(path.resolve(target), 'src'),
128
+ { recursive: true }
129
+ );
130
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(
131
+ path.join(path.resolve(target), 'src', 'utils'),
132
+ { recursive: true }
133
+ );
134
+ });
135
+
136
+ it('should prompt for framework and package name when missing', async () => {
137
+ const target = '/test/react-project';
138
+ const options = {};
139
+
140
+ // Mock that required files don't exist
141
+ mockFs.existsSync.mockReturnValue(false);
142
+
143
+ // Mock inquirer prompts
144
+ mockInquirer.prompt
145
+ .mockResolvedValueOnce({ framework: 'react' })
146
+ .mockResolvedValueOnce({ packageName: 'test-package' });
147
+
148
+ await generateCommand(target, options);
149
+
150
+ // Should have prompted for framework and package name
151
+ expect(mockInquirer.prompt).toHaveBeenCalledTimes(2);
152
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([{
153
+ type: 'list',
154
+ name: 'framework',
155
+ message: 'Which framework would you like to use?',
156
+ choices: ['react', 'vue']
157
+ }]);
158
+ expect(mockInquirer.prompt).toHaveBeenNthCalledWith(2, [{
159
+ type: 'input',
160
+ name: 'packageName',
161
+ message: 'What is your package name?',
162
+ default: 'react-project',
163
+ validate: expect.any(Function)
164
+ }]);
165
+
166
+ // Check package.json was written with processed content
167
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
168
+ path.join(target, 'package.json'),
169
+ 'test-package 0.0.1 ',
170
+ 'utf-8'
171
+ );
172
+ });
173
+
174
+ it('should use Flutter package name as default when available', async () => {
175
+ const target = '/test/react-project';
176
+ const options = { flutterPackageSrc: '/flutter/src' };
177
+
178
+ // Mock that required files don't exist except pubspec.yaml and TypeScript files
179
+ mockFs.existsSync.mockImplementation((filePath) => {
180
+ const pathStr = filePath.toString();
181
+ if (pathStr.includes('pubspec.yaml')) return true;
182
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
183
+ if (pathStr === '/flutter/src/lib') return true;
184
+ return false;
185
+ });
186
+
187
+ // Mock .d.ts files exist
188
+ mockFs.readdirSync.mockImplementation((dirPath: any) => {
189
+ if (dirPath.toString() === '/flutter/src/lib') {
190
+ return ['component.d.ts'] as any;
191
+ }
192
+ return [] as any;
193
+ });
194
+
195
+ mockFs.statSync.mockReturnValue({ isDirectory: () => false } as any);
196
+
197
+ // Mock yaml parse to return Flutter package info
198
+ mockYaml.parse.mockReturnValue({
199
+ name: 'flutter_awesome_widget',
200
+ version: '2.0.0',
201
+ description: 'An awesome Flutter widget'
202
+ });
203
+
204
+ // Mock inquirer prompts
205
+ mockInquirer.prompt
206
+ .mockResolvedValueOnce({ framework: 'react' })
207
+ .mockResolvedValueOnce({ packageName: 'flutter_awesome_widget' });
208
+
209
+ await generateCommand(target, options);
210
+
211
+ // Should have prompted with Flutter package name as default
212
+ expect(mockInquirer.prompt).toHaveBeenNthCalledWith(2, [{
213
+ type: 'input',
214
+ name: 'packageName',
215
+ message: 'What is your package name?',
216
+ default: 'flutter_awesome_widget',
217
+ validate: expect.any(Function)
218
+ }]);
219
+ });
220
+
221
+ it('should run npm install when creating new project', async () => {
222
+ const target = '/test/react-project';
223
+ const options = { framework: 'react', packageName: 'test-package' };
224
+
225
+ // Mock that required files don't exist
226
+ mockFs.existsSync.mockReturnValue(false);
227
+
228
+ await generateCommand(target, options);
229
+
230
+ expect(mockSpawnSync).toHaveBeenCalledWith(
231
+ expect.stringMatching(/npm(\.cmd)?/),
232
+ ['install', '--omit=peer'],
233
+ { cwd: target, stdio: 'inherit' }
234
+ );
235
+ });
236
+ });
237
+
238
+ describe('Vue framework - new project', () => {
239
+ it('should create Vue project structure when files are missing', async () => {
240
+ const target = '/test/vue-project';
241
+ const options = { framework: 'vue', packageName: 'test-vue-package' };
242
+
243
+ // Mock that required files don't exist
244
+ mockFs.existsSync.mockReturnValue(false);
245
+
246
+ await generateCommand(target, options);
247
+
248
+ // Check directory creation
249
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(path.resolve(target), { recursive: true });
250
+ });
251
+
252
+ it('should write Vue configuration files', async () => {
253
+ const target = '/test/vue-project';
254
+ const options = { framework: 'vue', packageName: 'test-vue-package' };
255
+
256
+ // Mock that required files don't exist
257
+ mockFs.existsSync.mockReturnValue(false);
258
+
259
+ await generateCommand(target, options);
260
+
261
+ // Check package.json was written with processed content
262
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
263
+ path.join(target, 'package.json'),
264
+ 'test-vue-package 0.0.1 ',
265
+ 'utf-8'
266
+ );
267
+
268
+ // Check tsconfig.json
269
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
270
+ path.join(target, 'tsconfig.json'),
271
+ 'vue tsconfig',
272
+ 'utf-8'
273
+ );
274
+ });
275
+
276
+ it('should run npm install commands for Vue', async () => {
277
+ const target = '/test/vue-project';
278
+ const options = { framework: 'vue', packageName: 'test-vue-package' };
279
+
280
+ // Mock that required files don't exist
281
+ mockFs.existsSync.mockReturnValue(false);
282
+
283
+ await generateCommand(target, options);
284
+
285
+ // Should install WebF typings
286
+ expect(mockSpawnSync).toHaveBeenCalledWith(
287
+ expect.stringMatching(/npm(\.cmd)?/),
288
+ ['install', '@openwebf/webf-enterprise-typings'],
289
+ { cwd: target, stdio: 'inherit' }
290
+ );
291
+
292
+ // Should install Vue types as dev dependency
293
+ expect(mockSpawnSync).toHaveBeenCalledWith(
294
+ expect.stringMatching(/npm(\.cmd)?/),
295
+ ['install', '@types/vue', '-D'],
296
+ { cwd: target, stdio: 'inherit' }
297
+ );
298
+ });
299
+ });
300
+
301
+ it('should detect existing project and skip creation', async () => {
302
+ const target = '/test/project';
303
+ const options = { framework: 'react', flutterPackageSrc: '/flutter/src' };
304
+
305
+ // Mock TypeScript validation
306
+ mockTypeScriptValidation('/flutter/src');
307
+
308
+ // Mock all required files exist
309
+ mockFs.existsSync.mockImplementation((filePath) => {
310
+ const pathStr = filePath.toString();
311
+ // Project files exist
312
+ if (pathStr.includes(target)) return true;
313
+ // TypeScript validation files
314
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
315
+ if (pathStr === '/flutter/src/lib') return true;
316
+ if (pathStr.includes('pubspec.yaml')) return true;
317
+ return true;
318
+ });
319
+
320
+ mockFs.readFileSync.mockImplementation((filePath) => {
321
+ const pathStr = filePath.toString();
322
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
323
+ return JSON.stringify({ dependencies: { react: '^18.0.0' } });
324
+ }
325
+ return '';
326
+ });
327
+
328
+ await generateCommand(target, options);
329
+
330
+ // Should detect existing React project
331
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Detected existing react project'));
332
+
333
+ // Should not create new files
334
+ const writeFileCallsForExisting = (mockFs.writeFileSync as jest.Mock).mock.calls;
335
+ const projectFileCalls = writeFileCallsForExisting.filter(call => {
336
+ const path = call[0].toString();
337
+ return path.includes('package.json') && !path.includes('.tpl');
338
+ });
339
+ expect(projectFileCalls).toHaveLength(0);
340
+
341
+ });
342
+
343
+ });
344
+
345
+ describe('generateCommand with code generation', () => {
346
+ let mockExit: jest.SpyInstance;
347
+ let consoleSpy: jest.SpyInstance;
348
+
349
+ beforeEach(() => {
350
+ mockGenerator.dartGen.mockResolvedValue(undefined);
351
+ mockGenerator.reactGen.mockResolvedValue(undefined);
352
+ mockGenerator.vueGen.mockResolvedValue(undefined);
353
+ mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
354
+ throw new Error('process.exit called');
355
+ });
356
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
357
+ });
358
+
359
+ afterEach(() => {
360
+ mockExit.mockRestore();
361
+ consoleSpy.mockRestore();
362
+ });
363
+
364
+ it('should show instructions when --flutter-package-src is missing', async () => {
365
+ const options = { framework: 'react' };
366
+
367
+ // Don't auto-detect Flutter package for this test
368
+ const cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/non-flutter-dir');
369
+
370
+ // Mock all required files exist except pubspec.yaml
371
+ mockFs.existsSync.mockImplementation((filePath) => {
372
+ const pathStr = filePath.toString();
373
+ // No pubspec.yaml files should exist
374
+ if (pathStr.includes('pubspec.yaml')) return false;
375
+ // But other project files exist
376
+ if (pathStr.includes('/dist')) return true;
377
+ return true;
378
+ });
379
+
380
+ mockFs.readFileSync.mockImplementation((filePath) => {
381
+ const pathStr = filePath.toString();
382
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
383
+ return JSON.stringify({ dependencies: { react: '^18.0.0' } });
384
+ }
385
+ return '';
386
+ });
387
+
388
+ await generateCommand('/dist', options);
389
+
390
+ expect(consoleSpy).toHaveBeenCalledWith('\nProject is ready for code generation.');
391
+ expect(consoleSpy).toHaveBeenCalledWith('To generate code, run:');
392
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('webf codegen'));
393
+
394
+ cwdSpy.mockRestore();
395
+ });
396
+
397
+ it('should always call dartGen', async () => {
398
+ const options = {
399
+ flutterPackageSrc: '/flutter/src',
400
+ framework: 'react',
401
+ packageName: 'test-package'
402
+ };
403
+
404
+ // Mock TypeScript validation
405
+ mockTypeScriptValidation('/flutter/src');
406
+
407
+ await generateCommand('/dist', options);
408
+
409
+ expect(mockGenerator.dartGen).toHaveBeenCalledWith({
410
+ source: '/flutter/src',
411
+ target: '/flutter/src',
412
+ command: expect.stringContaining('webf codegen')
413
+ });
414
+ });
415
+
416
+ it('should call reactGen for React framework', async () => {
417
+ const options = {
418
+ flutterPackageSrc: '/flutter/src',
419
+ framework: 'react',
420
+ packageName: 'test-package'
421
+ };
422
+
423
+ // Mock TypeScript validation
424
+ mockTypeScriptValidation('/flutter/src');
425
+
426
+ await generateCommand('/dist', options);
427
+
428
+ expect(mockGenerator.reactGen).toHaveBeenCalledWith({
429
+ source: '/flutter/src',
430
+ target: path.resolve('/dist'),
431
+ command: expect.stringContaining('webf codegen')
432
+ });
433
+ });
434
+
435
+ it('should create new project if package.json not found', async () => {
436
+ const options = {
437
+ flutterPackageSrc: '/flutter/src',
438
+ framework: 'react',
439
+ packageName: 'new-project'
440
+ };
441
+
442
+ // Mock TypeScript validation
443
+ mockTypeScriptValidation('/flutter/src');
444
+
445
+ await generateCommand('/dist', options);
446
+
447
+ // Should create project files
448
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
449
+ expect.stringContaining('package.json'),
450
+ expect.any(String),
451
+ 'utf-8'
452
+ );
453
+
454
+ // Should still run code generation after creation
455
+ expect(mockGenerator.dartGen).toHaveBeenCalled();
456
+ expect(mockGenerator.reactGen).toHaveBeenCalled();
457
+ });
458
+
459
+ it('should detect framework from existing package.json', async () => {
460
+ const options = { flutterPackageSrc: '/flutter/src' };
461
+
462
+ // Mock TypeScript validation
463
+ mockTypeScriptValidation('/flutter/src');
464
+
465
+ // Mock all required files exist
466
+ mockFs.existsSync.mockImplementation((filePath) => {
467
+ const pathStr = filePath.toString();
468
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
469
+ if (pathStr === '/flutter/src/lib') return true;
470
+ if (pathStr.includes('pubspec.yaml')) return true;
471
+ return true;
472
+ });
473
+
474
+ mockFs.readFileSync.mockImplementation((filePath) => {
475
+ const pathStr = filePath.toString();
476
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
477
+ return JSON.stringify({ dependencies: { vue: '^3.0.0' } });
478
+ }
479
+ return '';
480
+ });
481
+
482
+ await generateCommand('/dist', options);
483
+
484
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Detected existing vue project'));
485
+ expect(mockGenerator.vueGen).toHaveBeenCalled();
486
+ });
487
+
488
+ it('should prompt for framework if cannot detect from package.json', async () => {
489
+ const options = { flutterPackageSrc: '/flutter/src' };
490
+
491
+ // Mock TypeScript validation
492
+ mockTypeScriptValidation('/flutter/src');
493
+
494
+ // Mock all required files exist
495
+ mockFs.existsSync.mockImplementation((filePath) => {
496
+ const pathStr = filePath.toString();
497
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
498
+ if (pathStr === '/flutter/src/lib') return true;
499
+ if (pathStr.includes('pubspec.yaml')) return true;
500
+ return true;
501
+ });
502
+
503
+ mockFs.readFileSync.mockImplementation((filePath) => {
504
+ const pathStr = filePath.toString();
505
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
506
+ return JSON.stringify({ name: 'test-project' });
507
+ }
508
+ return '';
509
+ });
510
+
511
+ // Mock inquirer prompt
512
+ mockInquirer.prompt.mockResolvedValueOnce({ framework: 'react' });
513
+
514
+ await generateCommand('/dist', options);
515
+
516
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([{
517
+ type: 'list',
518
+ name: 'framework',
519
+ message: 'Which framework are you using?',
520
+ choices: ['react', 'vue']
521
+ }]);
522
+ expect(mockGenerator.reactGen).toHaveBeenCalled();
523
+ });
524
+
525
+ it('should call vueGen for Vue framework', async () => {
526
+ const options = {
527
+ flutterPackageSrc: '/flutter/src',
528
+ framework: 'vue',
529
+ packageName: 'test-package'
530
+ };
531
+
532
+ // Mock TypeScript validation
533
+ mockTypeScriptValidation('/flutter/src');
534
+
535
+ await generateCommand('/dist', options);
536
+
537
+ expect(mockGenerator.vueGen).toHaveBeenCalledWith({
538
+ source: '/flutter/src',
539
+ target: path.resolve('/dist'),
540
+ command: expect.stringContaining('webf codegen')
541
+ });
542
+ });
543
+
544
+ it('should auto-initialize typings if not present', async () => {
545
+ const options = {
546
+ flutterPackageSrc: '/flutter/src',
547
+ framework: 'react',
548
+ packageName: 'test-package'
549
+ };
550
+
551
+ // Mock TypeScript validation
552
+ mockTypeScriptValidation('/flutter/src');
553
+
554
+ // Mock that init files don't exist in dist but TypeScript validation passes
555
+ mockFs.existsSync.mockImplementation((filePath) => {
556
+ const pathStr = filePath.toString();
557
+ // TypeScript validation files
558
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
559
+ if (pathStr === '/flutter/src/lib') return true;
560
+ if (pathStr.includes('pubspec.yaml')) return true;
561
+ // Dist files don't exist
562
+ if (pathStr.includes('/dist') && (pathStr.includes('global.d.ts') || pathStr.includes('tsconfig.json'))) {
563
+ return false;
564
+ }
565
+ return pathStr.includes('package.json');
566
+ });
567
+
568
+ await generateCommand('/dist', options);
569
+
570
+ // Should create directory
571
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(path.resolve('/dist'), { recursive: true });
572
+
573
+ // Should write init files
574
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
575
+ path.join(path.resolve('/dist'), 'global.d.ts'),
576
+ 'global.d.ts content',
577
+ 'utf-8'
578
+ );
579
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
580
+ path.join(path.resolve('/dist'), 'tsconfig.json'),
581
+ 'tsconfig template',
582
+ 'utf-8'
583
+ );
584
+ });
585
+
586
+ it('should not re-initialize if typings already exist', async () => {
587
+ const options = {
588
+ flutterPackageSrc: '/flutter/src',
589
+ framework: 'react'
590
+ };
591
+
592
+ // Mock TypeScript validation
593
+ mockTypeScriptValidation('/flutter/src');
594
+
595
+ // Mock that all files exist
596
+ mockFs.existsSync.mockImplementation((filePath) => {
597
+ const pathStr = filePath.toString();
598
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
599
+ if (pathStr === '/flutter/src/lib') return true;
600
+ return true;
601
+ });
602
+
603
+ mockFs.readFileSync.mockImplementation((filePath) => {
604
+ const pathStr = filePath.toString();
605
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
606
+ return JSON.stringify({ dependencies: { react: '^18.0.0' } });
607
+ }
608
+ return '';
609
+ });
610
+
611
+ await generateCommand('/dist', options);
612
+
613
+ // Should not create directory or write init files
614
+ const writeFileCalls = (mockFs.writeFileSync as jest.Mock).mock.calls;
615
+ const initFileCalls = writeFileCalls.filter(call => {
616
+ const path = call[0].toString();
617
+ return path.includes('global.d.ts') || (path.includes('tsconfig.json') && !path.includes('.tpl'));
618
+ });
619
+
620
+ expect(initFileCalls).toHaveLength(0);
621
+ });
622
+
623
+ it('should use Flutter package metadata when creating project', async () => {
624
+ const options = {
625
+ flutterPackageSrc: '/flutter/src',
626
+ framework: 'react',
627
+ packageName: 'test-package'
628
+ };
629
+
630
+ // Mock TypeScript validation
631
+ mockTypeScriptValidation('/flutter/src');
632
+
633
+ // Mock that required files don't exist to trigger creation
634
+ mockFs.existsSync.mockImplementation((filePath) => {
635
+ const pathStr = filePath.toString();
636
+ if (pathStr.includes('pubspec.yaml')) return true;
637
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
638
+ if (pathStr === '/flutter/src/lib') return true;
639
+ return false;
640
+ });
641
+
642
+ await generateCommand('/dist', options);
643
+
644
+ // Check that package.json was written with metadata
645
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
646
+ expect.stringContaining('package.json'),
647
+ expect.stringContaining('test-package 1.0.0 Test Flutter package description'),
648
+ 'utf-8'
649
+ );
650
+ });
651
+
652
+ describe('npm publishing', () => {
653
+ it('should build and publish package when --publish-to-npm is set', async () => {
654
+ const options = {
655
+ flutterPackageSrc: '/flutter/src',
656
+ framework: 'react',
657
+ packageName: 'test-package',
658
+ publishToNpm: true
659
+ };
660
+
661
+ // Mock TypeScript validation
662
+ mockTypeScriptValidation('/flutter/src');
663
+
664
+ // Mock all required files exist
665
+ mockFs.existsSync.mockImplementation((filePath) => {
666
+ const pathStr = filePath.toString();
667
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
668
+ if (pathStr === '/flutter/src/lib') return true;
669
+ if (pathStr.includes('pubspec.yaml')) return true;
670
+ if (pathStr.includes('package.json')) return true;
671
+ if (pathStr.includes('global.d.ts')) return true;
672
+ if (pathStr.includes('tsconfig.json')) return true;
673
+ return false;
674
+ });
675
+
676
+ // Mock package.json with build script
677
+ mockFs.readFileSync.mockImplementation((filePath) => {
678
+ const pathStr = filePath.toString();
679
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
680
+ return JSON.stringify({
681
+ name: 'test-package',
682
+ version: '1.0.0',
683
+ dependencies: { react: '^18.0.0' },
684
+ scripts: { build: 'tsup' }
685
+ });
686
+ }
687
+ return '';
688
+ });
689
+
690
+ // Mock npm whoami success
691
+ mockSpawnSync.mockImplementation((command, args) => {
692
+ if (args && args[0] === 'whoami') {
693
+ return {
694
+ pid: 1234,
695
+ output: ['testuser'],
696
+ stdout: Buffer.from('testuser'),
697
+ stderr: Buffer.from(''),
698
+ status: 0,
699
+ signal: null,
700
+ };
701
+ }
702
+ // Default mock for other commands
703
+ return {
704
+ pid: 1234,
705
+ output: [],
706
+ stdout: Buffer.from(''),
707
+ stderr: Buffer.from(''),
708
+ status: 0,
709
+ signal: null,
710
+ };
711
+ });
712
+
713
+ await generateCommand('/dist', options);
714
+
715
+ // Should run build
716
+ expect(mockSpawnSync).toHaveBeenCalledWith(
717
+ expect.stringMatching(/npm(\.cmd)?/),
718
+ ['run', 'build'],
719
+ expect.objectContaining({ cwd: path.resolve('/dist') })
720
+ );
721
+
722
+ // Should check whoami
723
+ expect(mockSpawnSync).toHaveBeenCalledWith(
724
+ expect.stringMatching(/npm(\.cmd)?/),
725
+ ['whoami'],
726
+ expect.objectContaining({ cwd: path.resolve('/dist') })
727
+ );
728
+
729
+ // Should publish
730
+ expect(mockSpawnSync).toHaveBeenCalledWith(
731
+ expect.stringMatching(/npm(\.cmd)?/),
732
+ ['publish'],
733
+ expect.objectContaining({ cwd: path.resolve('/dist') })
734
+ );
735
+ });
736
+
737
+ it('should use custom npm registry when provided', async () => {
738
+ const options = {
739
+ flutterPackageSrc: '/flutter/src',
740
+ framework: 'react',
741
+ packageName: 'test-package',
742
+ publishToNpm: true,
743
+ npmRegistry: 'https://custom.registry.com/'
744
+ };
745
+
746
+ // Mock TypeScript validation
747
+ mockTypeScriptValidation('/flutter/src');
748
+
749
+ // Mock all required files exist
750
+ mockFs.existsSync.mockImplementation((filePath) => {
751
+ const pathStr = filePath.toString();
752
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
753
+ if (pathStr === '/flutter/src/lib') return true;
754
+ return true;
755
+ });
756
+ mockFs.readFileSync.mockImplementation((filePath) => {
757
+ const pathStr = filePath.toString();
758
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
759
+ return JSON.stringify({
760
+ name: 'test-package',
761
+ version: '1.0.0',
762
+ dependencies: { react: '^18.0.0' }
763
+ });
764
+ }
765
+ return '';
766
+ });
767
+
768
+ // Mock npm commands success
769
+ mockSpawnSync.mockReturnValue({
770
+ pid: 1234,
771
+ output: ['testuser'],
772
+ stdout: Buffer.from('testuser'),
773
+ stderr: Buffer.from(''),
774
+ status: 0,
775
+ signal: null,
776
+ });
777
+
778
+ await generateCommand('/dist', options);
779
+
780
+ // Should set custom registry
781
+ expect(mockSpawnSync).toHaveBeenCalledWith(
782
+ expect.stringMatching(/npm(\.cmd)?/),
783
+ ['config', 'set', 'registry', 'https://custom.registry.com/'],
784
+ expect.objectContaining({ cwd: path.resolve('/dist') })
785
+ );
786
+
787
+ // Should delete registry config after publish
788
+ expect(mockSpawnSync).toHaveBeenCalledWith(
789
+ expect.stringMatching(/npm(\.cmd)?/),
790
+ ['config', 'delete', 'registry'],
791
+ expect.objectContaining({ cwd: path.resolve('/dist') })
792
+ );
793
+ });
794
+
795
+ it('should handle npm publish errors gracefully', async () => {
796
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
797
+ throw new Error('process.exit called');
798
+ });
799
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
800
+
801
+ const options = {
802
+ flutterPackageSrc: '/flutter/src',
803
+ framework: 'react',
804
+ packageName: 'test-package',
805
+ publishToNpm: true
806
+ };
807
+
808
+ // Mock TypeScript validation
809
+ mockTypeScriptValidation('/flutter/src');
810
+
811
+ // Mock all required files exist
812
+ mockFs.existsSync.mockImplementation((filePath) => {
813
+ const pathStr = filePath.toString();
814
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
815
+ if (pathStr === '/flutter/src/lib') return true;
816
+ return true;
817
+ });
818
+ mockFs.readFileSync.mockImplementation((filePath) => {
819
+ const pathStr = filePath.toString();
820
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
821
+ return JSON.stringify({
822
+ name: 'test-package',
823
+ version: '1.0.0',
824
+ dependencies: { react: '^18.0.0' }
825
+ });
826
+ }
827
+ return '';
828
+ });
829
+
830
+ // Mock npm whoami failure (not logged in)
831
+ mockSpawnSync.mockImplementation((command, args) => {
832
+ if (args && args[0] === 'whoami') {
833
+ return {
834
+ pid: 1234,
835
+ output: [],
836
+ stdout: Buffer.from(''),
837
+ stderr: Buffer.from('npm ERR! not logged in'),
838
+ status: 1,
839
+ signal: null,
840
+ };
841
+ }
842
+ return {
843
+ pid: 1234,
844
+ output: [],
845
+ stdout: Buffer.from(''),
846
+ stderr: Buffer.from(''),
847
+ status: 0,
848
+ signal: null,
849
+ };
850
+ });
851
+
852
+ await expect(async () => {
853
+ await generateCommand('/dist', options);
854
+ }).rejects.toThrow('process.exit called');
855
+
856
+ expect(consoleSpy).toHaveBeenCalledWith(
857
+ '\nError during npm publish:',
858
+ expect.any(Error)
859
+ );
860
+ expect(mockExit).toHaveBeenCalledWith(1);
861
+
862
+ mockExit.mockRestore();
863
+ consoleSpy.mockRestore();
864
+ });
865
+
866
+ it('should prompt for npm publishing when not specified in options', async () => {
867
+ const options = {
868
+ flutterPackageSrc: '/flutter/src',
869
+ framework: 'react',
870
+ packageName: 'test-package'
871
+ };
872
+
873
+ // Mock TypeScript validation
874
+ mockTypeScriptValidation('/flutter/src');
875
+
876
+ // Mock all required files exist
877
+ mockFs.existsSync.mockImplementation((filePath) => {
878
+ const pathStr = filePath.toString();
879
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
880
+ if (pathStr === '/flutter/src/lib') return true;
881
+ return true;
882
+ });
883
+
884
+ mockFs.readFileSync.mockImplementation((filePath) => {
885
+ const pathStr = filePath.toString();
886
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
887
+ return JSON.stringify({
888
+ name: 'test-package',
889
+ version: '1.0.0',
890
+ dependencies: { react: '^18.0.0' }
891
+ });
892
+ }
893
+ return '';
894
+ });
895
+
896
+ // Mock user says yes to publish
897
+ mockInquirer.prompt
898
+ .mockResolvedValueOnce({ publish: true })
899
+ .mockResolvedValueOnce({ registry: 'https://custom.registry.com/' });
900
+
901
+ // Mock npm commands success
902
+ mockSpawnSync.mockReturnValue({
903
+ pid: 1234,
904
+ output: ['testuser'],
905
+ stdout: Buffer.from('testuser'),
906
+ stderr: Buffer.from(''),
907
+ status: 0,
908
+ signal: null,
909
+ });
910
+
911
+ await generateCommand('/dist', options);
912
+
913
+ // Should have prompted for publishing
914
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([{
915
+ type: 'confirm',
916
+ name: 'publish',
917
+ message: 'Would you like to publish this package to npm?',
918
+ default: false
919
+ }]);
920
+
921
+ // Should have prompted for registry
922
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([{
923
+ type: 'input',
924
+ name: 'registry',
925
+ message: 'NPM registry URL (leave empty for default npm registry):',
926
+ default: '',
927
+ validate: expect.any(Function)
928
+ }]);
929
+
930
+ // Should have published with custom registry
931
+ expect(mockSpawnSync).toHaveBeenCalledWith(
932
+ expect.stringMatching(/npm(\.cmd)?/),
933
+ ['config', 'set', 'registry', 'https://custom.registry.com/'],
934
+ expect.objectContaining({ cwd: path.resolve('/dist') })
935
+ );
936
+ });
937
+
938
+ it('should skip publishing if user says no', async () => {
939
+ const options = {
940
+ flutterPackageSrc: '/flutter/src',
941
+ framework: 'react',
942
+ packageName: 'test-package'
943
+ };
944
+
945
+ // Mock TypeScript validation
946
+ mockTypeScriptValidation('/flutter/src');
947
+
948
+ // Mock all required files exist
949
+ mockFs.existsSync.mockImplementation((filePath) => {
950
+ const pathStr = filePath.toString();
951
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
952
+ if (pathStr === '/flutter/src/lib') return true;
953
+ return true;
954
+ });
955
+
956
+ mockFs.readFileSync.mockImplementation((filePath) => {
957
+ const pathStr = filePath.toString();
958
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
959
+ return JSON.stringify({
960
+ name: 'test-package',
961
+ version: '1.0.0',
962
+ dependencies: { react: '^18.0.0' }
963
+ });
964
+ }
965
+ return '';
966
+ });
967
+
968
+ // Mock user says no to publish
969
+ mockInquirer.prompt.mockResolvedValueOnce({ publish: false });
970
+
971
+ await generateCommand('/dist', options);
972
+
973
+ // Should have prompted for publishing
974
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([{
975
+ type: 'confirm',
976
+ name: 'publish',
977
+ message: 'Would you like to publish this package to npm?',
978
+ default: false
979
+ }]);
980
+
981
+ // Should not have prompted for registry
982
+ expect(mockInquirer.prompt).toHaveBeenCalledTimes(1);
983
+
984
+ // Should not have published
985
+ const publishCalls = (mockSpawnSync as jest.Mock).mock.calls.filter(
986
+ call => call[1] && call[1].includes('publish')
987
+ );
988
+ expect(publishCalls).toHaveLength(0);
989
+ });
990
+ });
991
+
992
+ describe('Flutter package detection and TypeScript validation', () => {
993
+ it('should auto-detect Flutter package from current directory', async () => {
994
+ const cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/test/flutter-package');
995
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
996
+
997
+ const options = {
998
+ framework: 'react',
999
+ packageName: 'test-package'
1000
+ };
1001
+
1002
+ // Mock Flutter package structure
1003
+ mockFs.existsSync.mockImplementation((filePath) => {
1004
+ const pathStr = filePath.toString();
1005
+ if (pathStr === '/test/flutter-package/pubspec.yaml') return true;
1006
+ if (pathStr.includes('tsconfig.json')) return true;
1007
+ if (pathStr.includes('/lib')) return true;
1008
+ return false;
1009
+ });
1010
+
1011
+ // Mock .d.ts files exist
1012
+ mockFs.readdirSync.mockImplementation((dirPath: any) => {
1013
+ if (dirPath.toString().includes('/lib')) {
1014
+ return ['component.d.ts'] as any;
1015
+ }
1016
+ return [] as any;
1017
+ });
1018
+
1019
+ mockFs.statSync.mockReturnValue({ isDirectory: () => false } as any);
1020
+
1021
+ await generateCommand('/dist', options);
1022
+
1023
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Detected Flutter package at: /test/flutter-package'));
1024
+
1025
+ cwdSpy.mockRestore();
1026
+ consoleSpy.mockRestore();
1027
+ });
1028
+
1029
+ it('should prompt to create tsconfig.json if missing', async () => {
1030
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
1031
+ throw new Error('process.exit called');
1032
+ });
1033
+
1034
+ const options = {
1035
+ flutterPackageSrc: '/flutter/src',
1036
+ framework: 'react',
1037
+ packageName: 'test-package'
1038
+ };
1039
+
1040
+ // Mock tsconfig.json doesn't exist in Flutter package but everything else is valid
1041
+ mockFs.existsSync.mockImplementation((filePath) => {
1042
+ const pathStr = filePath.toString();
1043
+ // tsconfig.json missing in Flutter package
1044
+ if (pathStr === '/flutter/src/tsconfig.json') return false;
1045
+ // But exists in dist (after creation)
1046
+ if (pathStr.includes('/dist') && pathStr.includes('tsconfig.json')) return true;
1047
+ if (pathStr.includes('/lib')) return true;
1048
+ if (pathStr.includes('package.json')) return true;
1049
+ if (pathStr.includes('global.d.ts')) return true;
1050
+ return true;
1051
+ });
1052
+
1053
+ // Mock .d.ts files exist
1054
+ mockFs.readdirSync.mockImplementation((dirPath: any) => {
1055
+ if (dirPath.toString().includes('/lib')) {
1056
+ return ['component.d.ts'] as any;
1057
+ }
1058
+ return [] as any;
1059
+ });
1060
+
1061
+ mockFs.statSync.mockReturnValue({ isDirectory: () => false } as any);
1062
+
1063
+ // Mock user says yes to create tsconfig
1064
+ mockInquirer.prompt.mockResolvedValueOnce({ createTsConfig: true });
1065
+
1066
+ // After tsconfig is created, mock it exists
1067
+ let tsconfigCreated = false;
1068
+ const originalWriteFileSync = mockFs.writeFileSync;
1069
+ mockFs.writeFileSync = jest.fn().mockImplementation((path, content, encoding) => {
1070
+ originalWriteFileSync(path, content, encoding);
1071
+ if (path.toString().includes('tsconfig.json')) {
1072
+ tsconfigCreated = true;
1073
+ }
1074
+ });
1075
+
1076
+ // Update existsSync to return true for tsconfig after it's created
1077
+ const originalExistsSync = mockFs.existsSync as jest.Mock;
1078
+ mockFs.existsSync = jest.fn().mockImplementation((filePath) => {
1079
+ const pathStr = filePath.toString();
1080
+ if (pathStr === '/flutter/src/tsconfig.json' && tsconfigCreated) return true;
1081
+ return originalExistsSync(filePath);
1082
+ });
1083
+
1084
+ await generateCommand('/dist', options);
1085
+
1086
+ // Should have prompted about tsconfig
1087
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([{
1088
+ type: 'confirm',
1089
+ name: 'createTsConfig',
1090
+ message: 'No tsconfig.json found. Would you like me to create one for you?',
1091
+ default: true
1092
+ }]);
1093
+
1094
+ // Should have created tsconfig.json
1095
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
1096
+ '/flutter/src/tsconfig.json',
1097
+ expect.stringContaining('"target": "ES2020"'),
1098
+ 'utf-8'
1099
+ );
1100
+
1101
+ mockExit.mockRestore();
1102
+ });
1103
+
1104
+ it('should fail validation if no .d.ts files found', async () => {
1105
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
1106
+ throw new Error('process.exit called');
1107
+ });
1108
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1109
+
1110
+ const options = {
1111
+ flutterPackageSrc: '/flutter/src',
1112
+ framework: 'react',
1113
+ packageName: 'test-package'
1114
+ };
1115
+
1116
+ // Mock everything exists except .d.ts files
1117
+ mockFs.existsSync.mockReturnValue(true);
1118
+ mockFs.readdirSync.mockReturnValue(['file.ts', 'package.json'] as any);
1119
+ mockFs.statSync.mockReturnValue({ isDirectory: () => false } as any);
1120
+
1121
+ await expect(async () => {
1122
+ await generateCommand('/dist', options);
1123
+ }).rejects.toThrow('process.exit called');
1124
+
1125
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No TypeScript definition files (.d.ts) found'));
1126
+
1127
+ mockExit.mockRestore();
1128
+ consoleSpy.mockRestore();
1129
+ });
1130
+ });
1131
+
1132
+ describe('Automatic build after generation', () => {
1133
+ it('should automatically run npm run build after code generation', async () => {
1134
+ const options = {
1135
+ flutterPackageSrc: '/flutter/src',
1136
+ framework: 'react',
1137
+ packageName: 'test-package'
1138
+ };
1139
+
1140
+ // Mock TypeScript validation
1141
+ mockTypeScriptValidation('/flutter/src');
1142
+
1143
+ // Mock package.json exists
1144
+ mockFs.existsSync.mockImplementation((filePath) => {
1145
+ const pathStr = filePath.toString();
1146
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
1147
+ if (pathStr === '/flutter/src/lib') return true;
1148
+ if (pathStr.includes('pubspec.yaml')) return true;
1149
+ if (pathStr.includes('package.json')) return true;
1150
+ return true;
1151
+ });
1152
+
1153
+ // Mock package.json with build script
1154
+ mockFs.readFileSync.mockImplementation((filePath) => {
1155
+ const pathStr = filePath.toString();
1156
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
1157
+ return JSON.stringify({
1158
+ name: 'test-package',
1159
+ version: '1.0.0',
1160
+ scripts: {
1161
+ build: 'tsup'
1162
+ },
1163
+ dependencies: { react: '^18.0.0' }
1164
+ });
1165
+ }
1166
+ return '';
1167
+ });
1168
+
1169
+ await generateCommand('/dist', options);
1170
+
1171
+ // Should have called npm run build
1172
+ const buildCalls = (mockSpawnSync as jest.Mock).mock.calls.filter(
1173
+ call => call[1] && call[1].includes('build') && call[1].includes('run')
1174
+ );
1175
+ expect(buildCalls).toHaveLength(1);
1176
+ expect(buildCalls[0][0]).toMatch(/npm(\.cmd)?$/);
1177
+ expect(buildCalls[0][1]).toEqual(['run', 'build']);
1178
+ });
1179
+
1180
+ it('should handle build failure gracefully', async () => {
1181
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1182
+
1183
+ const options = {
1184
+ flutterPackageSrc: '/flutter/src',
1185
+ framework: 'react',
1186
+ packageName: 'test-package'
1187
+ };
1188
+
1189
+ // Mock TypeScript validation
1190
+ mockTypeScriptValidation('/flutter/src');
1191
+
1192
+ // Mock package.json exists
1193
+ mockFs.existsSync.mockImplementation((filePath) => {
1194
+ const pathStr = filePath.toString();
1195
+ if (pathStr === '/flutter/src/tsconfig.json') return true;
1196
+ if (pathStr === '/flutter/src/lib') return true;
1197
+ if (pathStr.includes('pubspec.yaml')) return true;
1198
+ if (pathStr.includes('package.json')) return true;
1199
+ return true;
1200
+ });
1201
+
1202
+ // Mock package.json with build script
1203
+ mockFs.readFileSync.mockImplementation((filePath) => {
1204
+ const pathStr = filePath.toString();
1205
+ if (pathStr.includes('package.json') && !pathStr.includes('.tpl')) {
1206
+ return JSON.stringify({
1207
+ name: 'test-package',
1208
+ version: '1.0.0',
1209
+ scripts: {
1210
+ build: 'tsup'
1211
+ },
1212
+ dependencies: { react: '^18.0.0' }
1213
+ });
1214
+ }
1215
+ return '';
1216
+ });
1217
+
1218
+ // Mock build failure
1219
+ mockSpawnSync.mockImplementation((command: any, args: any) => {
1220
+ if (args && args.includes('build')) {
1221
+ return {
1222
+ pid: 1234,
1223
+ output: [],
1224
+ stdout: Buffer.from(''),
1225
+ stderr: Buffer.from('Build error'),
1226
+ status: 1,
1227
+ signal: null,
1228
+ };
1229
+ }
1230
+ return {
1231
+ pid: 1234,
1232
+ output: [],
1233
+ stdout: Buffer.from(''),
1234
+ stderr: Buffer.from(''),
1235
+ status: 0,
1236
+ signal: null,
1237
+ };
1238
+ });
1239
+
1240
+ await generateCommand('/dist', options);
1241
+
1242
+ // Should have logged warning about build failure
1243
+ expect(consoleSpy).toHaveBeenCalledWith('\nWarning: Build failed:', expect.any(Error));
1244
+
1245
+ // Should still complete successfully (generation worked)
1246
+ expect(mockGenerator.dartGen).toHaveBeenCalled();
1247
+ expect(mockGenerator.reactGen).toHaveBeenCalled();
1248
+
1249
+ consoleSpy.mockRestore();
1250
+ });
1251
+ });
1252
+ });
1253
+ });