@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.
- package/CLAUDE.md +206 -0
- package/README-zhCN.md +256 -0
- package/README.md +232 -0
- package/bin/webf.js +25 -0
- package/coverage/clover.xml +1295 -0
- package/coverage/coverage-final.json +12 -0
- package/coverage/lcov-report/IDLBlob.ts.html +142 -0
- package/coverage/lcov-report/analyzer.ts.html +2158 -0
- package/coverage/lcov-report/analyzer_original.ts.html +1450 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/commands.ts.html +700 -0
- package/coverage/lcov-report/dart.ts.html +490 -0
- package/coverage/lcov-report/declaration.ts.html +337 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/generator.ts.html +1171 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/logger.ts.html +424 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/react.ts.html +619 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/utils.ts.html +466 -0
- package/coverage/lcov-report/vue.ts.html +613 -0
- package/coverage/lcov.info +2149 -0
- package/global.d.ts +2 -0
- package/jest.config.js +24 -0
- package/package.json +36 -0
- package/src/IDLBlob.ts +20 -0
- package/src/analyzer.ts +692 -0
- package/src/commands.ts +645 -0
- package/src/dart.ts +170 -0
- package/src/declaration.ts +84 -0
- package/src/generator.ts +454 -0
- package/src/logger.ts +114 -0
- package/src/react.ts +186 -0
- package/src/utils.ts +127 -0
- package/src/vue.ts +176 -0
- package/templates/class.dart.tpl +86 -0
- package/templates/gitignore.tpl +2 -0
- package/templates/react.component.tsx.tpl +53 -0
- package/templates/react.createComponent.tpl +286 -0
- package/templates/react.index.ts.tpl +8 -0
- package/templates/react.package.json.tpl +26 -0
- package/templates/react.tsconfig.json.tpl +16 -0
- package/templates/react.tsup.config.ts.tpl +10 -0
- package/templates/tsconfig.json.tpl +8 -0
- package/templates/vue.component.partial.tpl +31 -0
- package/templates/vue.components.d.ts.tpl +49 -0
- package/templates/vue.package.json.tpl +11 -0
- package/templates/vue.tsconfig.json.tpl +15 -0
- package/test/IDLBlob.test.ts +75 -0
- package/test/analyzer.test.ts +370 -0
- package/test/commands.test.ts +1253 -0
- package/test/generator.test.ts +460 -0
- package/test/logger.test.ts +215 -0
- package/test/react.test.ts +49 -0
- package/test/utils.test.ts +316 -0
- 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
|
+
});
|