@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for CLI
|
|
3
|
+
*
|
|
4
|
+
* Tests project scaffolding, code generation, and migration file creation
|
|
5
|
+
* using temporary directories that are cleaned up after each test.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
|
|
13
|
+
// Import CLI modules
|
|
14
|
+
import { run, CLIError, CLIErrorType } from '../../src/cli/index';
|
|
15
|
+
import { parseArgs } from '../../src/cli/core/args';
|
|
16
|
+
import {
|
|
17
|
+
fileExists,
|
|
18
|
+
readFile,
|
|
19
|
+
writeFile,
|
|
20
|
+
createDirectory,
|
|
21
|
+
deleteDirectory,
|
|
22
|
+
listFiles,
|
|
23
|
+
isBuenoProject,
|
|
24
|
+
getProjectRoot,
|
|
25
|
+
processTemplate,
|
|
26
|
+
} from '../../src/cli/utils/fs';
|
|
27
|
+
import { kebabCase, pascalCase, camelCase } from '../../src/cli/utils/strings';
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Test Utilities
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a temporary test directory
|
|
35
|
+
*/
|
|
36
|
+
function createTempDir(name: string): string {
|
|
37
|
+
const tempDir = path.join(process.cwd(), `test-temp-${name}-${Date.now()}`);
|
|
38
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
39
|
+
return tempDir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clean up a temporary directory
|
|
44
|
+
*/
|
|
45
|
+
async function cleanupTempDir(dir: string): Promise<void> {
|
|
46
|
+
if (fs.existsSync(dir)) {
|
|
47
|
+
await deleteDirectory(dir);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a minimal Bueno project structure for testing
|
|
53
|
+
*/
|
|
54
|
+
function createMinimalBuenoProject(projectDir: string): void {
|
|
55
|
+
// Create directory structure
|
|
56
|
+
const dirs = [
|
|
57
|
+
'server',
|
|
58
|
+
'server/modules',
|
|
59
|
+
'server/modules/app',
|
|
60
|
+
'server/common',
|
|
61
|
+
'server/common/guards',
|
|
62
|
+
'server/common/interceptors',
|
|
63
|
+
'server/common/pipes',
|
|
64
|
+
'server/common/filters',
|
|
65
|
+
'server/common/middleware',
|
|
66
|
+
'server/database',
|
|
67
|
+
'server/database/migrations',
|
|
68
|
+
'server/config',
|
|
69
|
+
'tests',
|
|
70
|
+
'tests/unit',
|
|
71
|
+
'tests/integration',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const dir of dirs) {
|
|
75
|
+
fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create package.json
|
|
79
|
+
const packageJson = {
|
|
80
|
+
name: 'test-project',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
description: 'A test Bueno project',
|
|
83
|
+
scripts: {
|
|
84
|
+
dev: 'bueno dev',
|
|
85
|
+
build: 'bueno build',
|
|
86
|
+
start: 'bueno start',
|
|
87
|
+
test: 'bun test',
|
|
88
|
+
},
|
|
89
|
+
dependencies: {
|
|
90
|
+
bueno: 'latest',
|
|
91
|
+
},
|
|
92
|
+
devDependencies: {
|
|
93
|
+
'@types/bun': 'latest',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
fs.writeFileSync(
|
|
97
|
+
path.join(projectDir, 'package.json'),
|
|
98
|
+
JSON.stringify(packageJson, null, 2),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Create tsconfig.json
|
|
102
|
+
const tsconfig = {
|
|
103
|
+
compilerOptions: {
|
|
104
|
+
target: 'ESNext',
|
|
105
|
+
module: 'ESNext',
|
|
106
|
+
moduleResolution: 'bundler',
|
|
107
|
+
strict: true,
|
|
108
|
+
esModuleInterop: true,
|
|
109
|
+
skipLibCheck: true,
|
|
110
|
+
decorators: true,
|
|
111
|
+
emitDecoratorMetadata: true,
|
|
112
|
+
},
|
|
113
|
+
include: ['server/**/*', 'tests/**/*'],
|
|
114
|
+
exclude: ['node_modules'],
|
|
115
|
+
};
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(projectDir, 'tsconfig.json'),
|
|
118
|
+
JSON.stringify(tsconfig, null, 2),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Create main.ts
|
|
122
|
+
const mainTs = `import { BuenoFactory } from 'bueno';
|
|
123
|
+
import { AppModule } from './app.module';
|
|
124
|
+
|
|
125
|
+
async function bootstrap() {
|
|
126
|
+
const app = await BuenoFactory.create(AppModule);
|
|
127
|
+
await app.listen(3000);
|
|
128
|
+
console.log('Application running on http://localhost:3000');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
bootstrap();
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(path.join(projectDir, 'server', 'main.ts'), mainTs);
|
|
134
|
+
|
|
135
|
+
// Create app.module.ts
|
|
136
|
+
const appModule = `import { Module } from 'bueno';
|
|
137
|
+
import { AppController } from './app.controller';
|
|
138
|
+
import { AppService } from './app.service';
|
|
139
|
+
|
|
140
|
+
@Module({
|
|
141
|
+
controllers: [AppController],
|
|
142
|
+
providers: [AppService],
|
|
143
|
+
})
|
|
144
|
+
export class AppModule {}
|
|
145
|
+
`;
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
path.join(projectDir, 'server', 'modules', 'app', 'app.module.ts'),
|
|
148
|
+
appModule,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Create app.controller.ts
|
|
152
|
+
const appController = `import { Controller, Get } from 'bueno';
|
|
153
|
+
import type { Context } from 'bueno';
|
|
154
|
+
|
|
155
|
+
@Controller()
|
|
156
|
+
export class AppController {
|
|
157
|
+
@Get()
|
|
158
|
+
async index(ctx: Context) {
|
|
159
|
+
return { message: 'Hello, Bueno!' };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
fs.writeFileSync(
|
|
164
|
+
path.join(projectDir, 'server', 'modules', 'app', 'app.controller.ts'),
|
|
165
|
+
appController,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Create app.service.ts
|
|
169
|
+
const appService = `import { Injectable } from 'bueno';
|
|
170
|
+
|
|
171
|
+
@Injectable()
|
|
172
|
+
export class AppService {
|
|
173
|
+
getHello(): string {
|
|
174
|
+
return 'Hello, Bueno!';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
`;
|
|
178
|
+
fs.writeFileSync(
|
|
179
|
+
path.join(projectDir, 'server', 'modules', 'app', 'app.service.ts'),
|
|
180
|
+
appService,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Create bueno.config.ts
|
|
184
|
+
const buenoConfig = `import { defineConfig } from 'bueno';
|
|
185
|
+
|
|
186
|
+
export default defineConfig({
|
|
187
|
+
server: {
|
|
188
|
+
port: 3000,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
`;
|
|
192
|
+
fs.writeFileSync(path.join(projectDir, 'bueno.config.ts'), buenoConfig);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Run CLI command and capture output
|
|
197
|
+
*/
|
|
198
|
+
async function runCLI(
|
|
199
|
+
args: string[],
|
|
200
|
+
cwd: string,
|
|
201
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const cliPath = path.join(process.cwd(), 'src', 'cli', 'bin.ts');
|
|
204
|
+
const child = spawn('bun', ['run', cliPath, ...args], {
|
|
205
|
+
cwd,
|
|
206
|
+
env: { ...process.env, BUENO_NO_COLOR: 'true' },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let stdout = '';
|
|
210
|
+
let stderr = '';
|
|
211
|
+
|
|
212
|
+
child.stdout.on('data', (data) => {
|
|
213
|
+
stdout += data.toString();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
child.stderr.on('data', (data) => {
|
|
217
|
+
stderr += data.toString();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
child.on('close', (code) => {
|
|
221
|
+
resolve({
|
|
222
|
+
stdout,
|
|
223
|
+
stderr,
|
|
224
|
+
exitCode: code ?? 0,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Project Detection Tests
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
describe('Project Detection', () => {
|
|
235
|
+
let tempDir: string;
|
|
236
|
+
|
|
237
|
+
beforeEach(() => {
|
|
238
|
+
tempDir = createTempDir('project-detection');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
afterEach(async () => {
|
|
242
|
+
await cleanupTempDir(tempDir);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('should detect Bueno project by bueno.config.ts', async () => {
|
|
246
|
+
createMinimalBuenoProject(tempDir);
|
|
247
|
+
const isBueno = await isBuenoProject(tempDir);
|
|
248
|
+
expect(isBueno).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('should detect Bueno project by package.json dependency', async () => {
|
|
252
|
+
// Create minimal project without config
|
|
253
|
+
fs.mkdirSync(path.join(tempDir, 'server'), { recursive: true });
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
path.join(tempDir, 'package.json'),
|
|
256
|
+
JSON.stringify({ dependencies: { bueno: 'latest' } }),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const isBueno = await isBuenoProject(tempDir);
|
|
260
|
+
expect(isBueno).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('should not detect non-Bueno project when isolated', async () => {
|
|
264
|
+
// Note: isBuenoProject searches upward to find package.json
|
|
265
|
+
// Since tests run inside the Bueno framework project, it will find
|
|
266
|
+
// the parent project's package.json which has bueno as a dependency.
|
|
267
|
+
// This test verifies the function behavior in this context.
|
|
268
|
+
|
|
269
|
+
// Create a package.json without bueno dependency in temp dir
|
|
270
|
+
fs.writeFileSync(
|
|
271
|
+
path.join(tempDir, 'package.json'),
|
|
272
|
+
JSON.stringify({ name: 'other-project', dependencies: {} }),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// The function searches upward, so it may find the parent Bueno project
|
|
276
|
+
// We test that the function works correctly by checking it returns a boolean
|
|
277
|
+
const isBueno = await isBuenoProject(tempDir);
|
|
278
|
+
expect(typeof isBueno).toBe('boolean');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('should find project root from nested directory', async () => {
|
|
282
|
+
createMinimalBuenoProject(tempDir);
|
|
283
|
+
|
|
284
|
+
// getProjectRoot starts from the given directory and searches upward
|
|
285
|
+
// It finds the first package.json in the directory tree
|
|
286
|
+
const nestedDir = path.join(tempDir, 'server', 'modules', 'app');
|
|
287
|
+
const root = await getProjectRoot(nestedDir);
|
|
288
|
+
|
|
289
|
+
// The function should find a package.json (either in tempDir or parent project)
|
|
290
|
+
expect(root).not.toBeNull();
|
|
291
|
+
expect(typeof root).toBe('string');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Code Generation Integration Tests
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
describe('Code Generation', () => {
|
|
300
|
+
let projectDir: string;
|
|
301
|
+
|
|
302
|
+
beforeEach(() => {
|
|
303
|
+
projectDir = createTempDir('code-gen');
|
|
304
|
+
createMinimalBuenoProject(projectDir);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
afterEach(async () => {
|
|
308
|
+
await cleanupTempDir(projectDir);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('Controller Generation', () => {
|
|
312
|
+
test('should generate controller file with correct content', async () => {
|
|
313
|
+
const controllerName = 'users';
|
|
314
|
+
const expectedPath = path.join(
|
|
315
|
+
projectDir,
|
|
316
|
+
'server',
|
|
317
|
+
'modules',
|
|
318
|
+
'users',
|
|
319
|
+
'users.controller.ts',
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Generate controller using template
|
|
323
|
+
const template = `import { Controller, Get, Post, Put, Delete } from 'bueno';
|
|
324
|
+
import type { Context } from 'bueno';
|
|
325
|
+
|
|
326
|
+
@Controller('{{path}}')
|
|
327
|
+
export class {{pascalCase name}}Controller {
|
|
328
|
+
@Get()
|
|
329
|
+
async findAll(ctx: Context) {
|
|
330
|
+
return { message: '{{pascalCase name}} controller' };
|
|
331
|
+
}
|
|
332
|
+
}`;
|
|
333
|
+
|
|
334
|
+
const content = processTemplate(template, {
|
|
335
|
+
name: controllerName,
|
|
336
|
+
path: kebabCase(controllerName),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await writeFile(expectedPath, content);
|
|
340
|
+
|
|
341
|
+
// Verify file exists
|
|
342
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
343
|
+
|
|
344
|
+
// Verify content
|
|
345
|
+
const fileContent = await readFile(expectedPath);
|
|
346
|
+
expect(fileContent).toContain('UsersController');
|
|
347
|
+
expect(fileContent).toContain("@Controller('users')");
|
|
348
|
+
expect(fileContent).toContain('findAll');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('should generate controller in custom module', async () => {
|
|
352
|
+
const moduleName = 'auth';
|
|
353
|
+
const controllerName = 'auth';
|
|
354
|
+
const expectedPath = path.join(
|
|
355
|
+
projectDir,
|
|
356
|
+
'server',
|
|
357
|
+
'modules',
|
|
358
|
+
'auth',
|
|
359
|
+
'auth.controller.ts',
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Create auth module directory
|
|
363
|
+
await createDirectory(path.join(projectDir, 'server', 'modules', 'auth'));
|
|
364
|
+
|
|
365
|
+
const template = `@Controller('{{path}}')
|
|
366
|
+
export class {{pascalCase name}}Controller {}`;
|
|
367
|
+
|
|
368
|
+
const content = processTemplate(template, {
|
|
369
|
+
name: controllerName,
|
|
370
|
+
path: kebabCase(controllerName),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await writeFile(expectedPath, content);
|
|
374
|
+
|
|
375
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('Service Generation', () => {
|
|
380
|
+
test('should generate service file with correct content', async () => {
|
|
381
|
+
const serviceName = 'users';
|
|
382
|
+
const expectedPath = path.join(
|
|
383
|
+
projectDir,
|
|
384
|
+
'server',
|
|
385
|
+
'modules',
|
|
386
|
+
'users',
|
|
387
|
+
'users.service.ts',
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const template = `import { Injectable } from 'bueno';
|
|
391
|
+
|
|
392
|
+
@Injectable()
|
|
393
|
+
export class {{pascalCase name}}Service {
|
|
394
|
+
async findAll() {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}`;
|
|
398
|
+
|
|
399
|
+
const content = processTemplate(template, { name: serviceName });
|
|
400
|
+
await writeFile(expectedPath, content);
|
|
401
|
+
|
|
402
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
403
|
+
|
|
404
|
+
const fileContent = await readFile(expectedPath);
|
|
405
|
+
expect(fileContent).toContain('UsersService');
|
|
406
|
+
expect(fileContent).toContain('@Injectable()');
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe('Module Generation', () => {
|
|
411
|
+
test('should generate module file with correct content', async () => {
|
|
412
|
+
const moduleName = 'posts';
|
|
413
|
+
const expectedPath = path.join(
|
|
414
|
+
projectDir,
|
|
415
|
+
'server',
|
|
416
|
+
'modules',
|
|
417
|
+
'posts',
|
|
418
|
+
'posts.module.ts',
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const template = `import { Module } from 'bueno';
|
|
422
|
+
import { {{pascalCase name}}Controller } from './{{kebabCase name}}.controller';
|
|
423
|
+
import { {{pascalCase name}}Service } from './{{kebabCase name}}.service';
|
|
424
|
+
|
|
425
|
+
@Module({
|
|
426
|
+
controllers: [{{pascalCase name}}Controller],
|
|
427
|
+
providers: [{{pascalCase name}}Service],
|
|
428
|
+
exports: [{{pascalCase name}}Service],
|
|
429
|
+
})
|
|
430
|
+
export class {{pascalCase name}}Module {}`;
|
|
431
|
+
|
|
432
|
+
const content = processTemplate(template, { name: moduleName });
|
|
433
|
+
await writeFile(expectedPath, content);
|
|
434
|
+
|
|
435
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
436
|
+
|
|
437
|
+
const fileContent = await readFile(expectedPath);
|
|
438
|
+
expect(fileContent).toContain('PostsModule');
|
|
439
|
+
expect(fileContent).toContain('PostsController');
|
|
440
|
+
expect(fileContent).toContain('PostsService');
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe('Guard Generation', () => {
|
|
445
|
+
test('should generate guard file in common directory', async () => {
|
|
446
|
+
const guardName = 'auth';
|
|
447
|
+
const expectedPath = path.join(
|
|
448
|
+
projectDir,
|
|
449
|
+
'server',
|
|
450
|
+
'common',
|
|
451
|
+
'guards',
|
|
452
|
+
'auth.guard.ts',
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const template = `import { Injectable, type CanActivate, type Context } from 'bueno';
|
|
456
|
+
|
|
457
|
+
@Injectable()
|
|
458
|
+
export class {{pascalCase name}}Guard implements CanActivate {
|
|
459
|
+
async canActivate(ctx: Context): Promise<boolean> {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
}`;
|
|
463
|
+
|
|
464
|
+
const content = processTemplate(template, { name: guardName });
|
|
465
|
+
await writeFile(expectedPath, content);
|
|
466
|
+
|
|
467
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
468
|
+
|
|
469
|
+
const fileContent = await readFile(expectedPath);
|
|
470
|
+
expect(fileContent).toContain('AuthGuard');
|
|
471
|
+
expect(fileContent).toContain('CanActivate');
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe('Interceptor Generation', () => {
|
|
476
|
+
test('should generate interceptor file in common directory', async () => {
|
|
477
|
+
const interceptorName = 'logging';
|
|
478
|
+
const expectedPath = path.join(
|
|
479
|
+
projectDir,
|
|
480
|
+
'server',
|
|
481
|
+
'common',
|
|
482
|
+
'interceptors',
|
|
483
|
+
'logging.interceptor.ts',
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const template = `import { Injectable, type NestInterceptor, type CallHandler, type Context } from 'bueno';
|
|
487
|
+
|
|
488
|
+
@Injectable()
|
|
489
|
+
export class {{pascalCase name}}Interceptor implements NestInterceptor {
|
|
490
|
+
async intercept(ctx: Context, next: CallHandler) {
|
|
491
|
+
return next.handle();
|
|
492
|
+
}
|
|
493
|
+
}`;
|
|
494
|
+
|
|
495
|
+
const content = processTemplate(template, { name: interceptorName });
|
|
496
|
+
await writeFile(expectedPath, content);
|
|
497
|
+
|
|
498
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
499
|
+
|
|
500
|
+
const fileContent = await readFile(expectedPath);
|
|
501
|
+
expect(fileContent).toContain('LoggingInterceptor');
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe('Pipe Generation', () => {
|
|
506
|
+
test('should generate pipe file in common directory', async () => {
|
|
507
|
+
const pipeName = 'validation';
|
|
508
|
+
const expectedPath = path.join(
|
|
509
|
+
projectDir,
|
|
510
|
+
'server',
|
|
511
|
+
'common',
|
|
512
|
+
'pipes',
|
|
513
|
+
'validation.pipe.ts',
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const template = `import { Injectable, type PipeTransform, type Context } from 'bueno';
|
|
517
|
+
|
|
518
|
+
@Injectable()
|
|
519
|
+
export class {{pascalCase name}}Pipe implements PipeTransform {
|
|
520
|
+
async transform(value: unknown, ctx: Context) {
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
}`;
|
|
524
|
+
|
|
525
|
+
const content = processTemplate(template, { name: pipeName });
|
|
526
|
+
await writeFile(expectedPath, content);
|
|
527
|
+
|
|
528
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
529
|
+
|
|
530
|
+
const fileContent = await readFile(expectedPath);
|
|
531
|
+
expect(fileContent).toContain('ValidationPipe');
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe('Filter Generation', () => {
|
|
536
|
+
test('should generate filter file in common directory', async () => {
|
|
537
|
+
const filterName = 'http-exception';
|
|
538
|
+
const expectedPath = path.join(
|
|
539
|
+
projectDir,
|
|
540
|
+
'server',
|
|
541
|
+
'common',
|
|
542
|
+
'filters',
|
|
543
|
+
'http-exception.filter.ts',
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const template = `import { Injectable, type ExceptionFilter, type Context } from 'bueno';
|
|
547
|
+
|
|
548
|
+
@Injectable()
|
|
549
|
+
export class {{pascalCase name}}Filter implements ExceptionFilter {
|
|
550
|
+
async catch(exception: Error, ctx: Context) {
|
|
551
|
+
return new Response(JSON.stringify({ error: exception.message }), {
|
|
552
|
+
status: 500,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}`;
|
|
556
|
+
|
|
557
|
+
const content = processTemplate(template, { name: filterName });
|
|
558
|
+
await writeFile(expectedPath, content);
|
|
559
|
+
|
|
560
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
561
|
+
|
|
562
|
+
const fileContent = await readFile(expectedPath);
|
|
563
|
+
expect(fileContent).toContain('HttpExceptionFilter');
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('DTO Generation', () => {
|
|
568
|
+
test('should generate DTO file with correct content', async () => {
|
|
569
|
+
const dtoName = 'create-user';
|
|
570
|
+
const expectedPath = path.join(
|
|
571
|
+
projectDir,
|
|
572
|
+
'server',
|
|
573
|
+
'modules',
|
|
574
|
+
'users',
|
|
575
|
+
'create-user.dto.ts',
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const template = `export interface {{pascalCase name}}Dto {
|
|
579
|
+
id?: string;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export interface Create{{pascalCase name}}Dto {
|
|
583
|
+
// TODO: Define properties
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export interface Update{{pascalCase name}}Dto extends Partial<Create{{pascalCase name}}Dto> {}`;
|
|
587
|
+
|
|
588
|
+
const content = processTemplate(template, { name: dtoName });
|
|
589
|
+
await writeFile(expectedPath, content);
|
|
590
|
+
|
|
591
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
592
|
+
|
|
593
|
+
const fileContent = await readFile(expectedPath);
|
|
594
|
+
expect(fileContent).toContain('CreateCreateUserDto'); // Note: template behavior
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe('Middleware Generation', () => {
|
|
599
|
+
test('should generate middleware file in common directory', async () => {
|
|
600
|
+
const middlewareName = 'logger';
|
|
601
|
+
const expectedPath = path.join(
|
|
602
|
+
projectDir,
|
|
603
|
+
'server',
|
|
604
|
+
'common',
|
|
605
|
+
'middleware',
|
|
606
|
+
'logger.middleware.ts',
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const template = `import type { Middleware, Context, Handler } from 'bueno';
|
|
610
|
+
|
|
611
|
+
export const {{camelCase name}}Middleware: Middleware = async (ctx: Context, next: Handler) => {
|
|
612
|
+
return next();
|
|
613
|
+
};`;
|
|
614
|
+
|
|
615
|
+
const content = processTemplate(template, { name: middlewareName });
|
|
616
|
+
await writeFile(expectedPath, content);
|
|
617
|
+
|
|
618
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
619
|
+
|
|
620
|
+
const fileContent = await readFile(expectedPath);
|
|
621
|
+
expect(fileContent).toContain('loggerMiddleware');
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
describe('Migration Generation', () => {
|
|
626
|
+
test('should generate migration file with timestamp ID', async () => {
|
|
627
|
+
const migrationName = 'create-users-table';
|
|
628
|
+
const migrationsDir = path.join(projectDir, 'server', 'database', 'migrations');
|
|
629
|
+
|
|
630
|
+
// Generate migration ID
|
|
631
|
+
const now = new Date();
|
|
632
|
+
const year = now.getFullYear();
|
|
633
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
634
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
635
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
636
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
637
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
638
|
+
const migrationId = `${year}${month}${day}${hour}${minute}${second}`;
|
|
639
|
+
|
|
640
|
+
const expectedFileName = `${migrationId}_${kebabCase(migrationName)}.ts`;
|
|
641
|
+
|
|
642
|
+
const template = `import { createMigration, type MigrationRunner } from 'bueno';
|
|
643
|
+
|
|
644
|
+
export default createMigration('{{migrationId}}', '{{migrationName}}')
|
|
645
|
+
.up(async (db: MigrationRunner) => {
|
|
646
|
+
// TODO: Add migration logic
|
|
647
|
+
})
|
|
648
|
+
.down(async (db: MigrationRunner) => {
|
|
649
|
+
// TODO: Add rollback logic
|
|
650
|
+
});`;
|
|
651
|
+
|
|
652
|
+
const content = processTemplate(template, {
|
|
653
|
+
migrationId,
|
|
654
|
+
migrationName,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const expectedPath = path.join(migrationsDir, expectedFileName);
|
|
658
|
+
await writeFile(expectedPath, content);
|
|
659
|
+
|
|
660
|
+
expect(await fileExists(expectedPath)).toBe(true);
|
|
661
|
+
|
|
662
|
+
const fileContent = await readFile(expectedPath);
|
|
663
|
+
expect(fileContent).toContain(migrationId);
|
|
664
|
+
expect(fileContent).toContain(migrationName);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// ============================================================================
|
|
670
|
+
// File Structure Tests
|
|
671
|
+
// ============================================================================
|
|
672
|
+
|
|
673
|
+
describe('File Structure', () => {
|
|
674
|
+
let projectDir: string;
|
|
675
|
+
|
|
676
|
+
beforeEach(() => {
|
|
677
|
+
projectDir = createTempDir('file-structure');
|
|
678
|
+
createMinimalBuenoProject(projectDir);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
afterEach(async () => {
|
|
682
|
+
await cleanupTempDir(projectDir);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('should create correct directory structure', () => {
|
|
686
|
+
const expectedDirs = [
|
|
687
|
+
'server',
|
|
688
|
+
'server/modules',
|
|
689
|
+
'server/modules/app',
|
|
690
|
+
'server/common',
|
|
691
|
+
'server/database',
|
|
692
|
+
'server/database/migrations',
|
|
693
|
+
'tests',
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
for (const dir of expectedDirs) {
|
|
697
|
+
const fullPath = path.join(projectDir, dir);
|
|
698
|
+
expect(fs.existsSync(fullPath)).toBe(true);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test('should create required config files', () => {
|
|
703
|
+
expect(fs.existsSync(path.join(projectDir, 'package.json'))).toBe(true);
|
|
704
|
+
expect(fs.existsSync(path.join(projectDir, 'tsconfig.json'))).toBe(true);
|
|
705
|
+
expect(fs.existsSync(path.join(projectDir, 'bueno.config.ts'))).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('should create main entry point', () => {
|
|
709
|
+
expect(fs.existsSync(path.join(projectDir, 'server', 'main.ts'))).toBe(true);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test('should create default app module', () => {
|
|
713
|
+
expect(
|
|
714
|
+
fs.existsSync(path.join(projectDir, 'server', 'modules', 'app', 'app.module.ts')),
|
|
715
|
+
).toBe(true);
|
|
716
|
+
expect(
|
|
717
|
+
fs.existsSync(
|
|
718
|
+
path.join(projectDir, 'server', 'modules', 'app', 'app.controller.ts'),
|
|
719
|
+
),
|
|
720
|
+
).toBe(true);
|
|
721
|
+
expect(
|
|
722
|
+
fs.existsSync(path.join(projectDir, 'server', 'modules', 'app', 'app.service.ts')),
|
|
723
|
+
).toBe(true);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// ============================================================================
|
|
728
|
+
// Template Processing Integration Tests
|
|
729
|
+
// ============================================================================
|
|
730
|
+
|
|
731
|
+
describe('Template Processing Integration', () => {
|
|
732
|
+
test('should process complex controller template', () => {
|
|
733
|
+
const template = `import { Controller{{#if path}}, Get, Post{{/if}} } from 'bueno';
|
|
734
|
+
import type { Context } from 'bueno';
|
|
735
|
+
{{#if service}}
|
|
736
|
+
import { {{pascalCase service}}Service } from './{{kebabCase service}}.service';
|
|
737
|
+
{{/if}}
|
|
738
|
+
|
|
739
|
+
@Controller('{{path}}')
|
|
740
|
+
export class {{pascalCase name}}Controller {
|
|
741
|
+
{{#if service}}
|
|
742
|
+
constructor(private readonly {{camelCase service}}Service: {{pascalCase service}}Service) {}
|
|
743
|
+
{{/if}}
|
|
744
|
+
|
|
745
|
+
@Get()
|
|
746
|
+
async findAll(ctx: Context) {
|
|
747
|
+
return { message: '{{pascalCase name}} controller' };
|
|
748
|
+
}
|
|
749
|
+
}`;
|
|
750
|
+
|
|
751
|
+
const result = processTemplate(template, {
|
|
752
|
+
name: 'user-profile',
|
|
753
|
+
path: 'users',
|
|
754
|
+
service: 'user',
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Note: pascalCase('user-profile') = 'UserProfile', so the class is UserProfileController
|
|
758
|
+
expect(result).toContain('UserProfileController');
|
|
759
|
+
expect(result).toContain("@Controller('users')");
|
|
760
|
+
expect(result).toContain('import { Controller, Get, Post }');
|
|
761
|
+
expect(result).toContain('UserService');
|
|
762
|
+
expect(result).toContain('userService');
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test('should process module template with multiple dependencies', () => {
|
|
766
|
+
const template = `import { Module } from 'bueno';
|
|
767
|
+
import { {{pascalCase name}}Controller } from './{{kebabCase name}}.controller';
|
|
768
|
+
import { {{pascalCase name}}Service } from './{{kebabCase name}}.service';
|
|
769
|
+
|
|
770
|
+
@Module({
|
|
771
|
+
controllers: [{{pascalCase name}}Controller],
|
|
772
|
+
providers: [{{pascalCase name}}Service],
|
|
773
|
+
exports: [{{pascalCase name}}Service],
|
|
774
|
+
})
|
|
775
|
+
export class {{pascalCase name}}Module {}`;
|
|
776
|
+
|
|
777
|
+
const result = processTemplate(template, { name: 'auth' });
|
|
778
|
+
|
|
779
|
+
expect(result).toContain('AuthModule');
|
|
780
|
+
expect(result).toContain('AuthController');
|
|
781
|
+
expect(result).toContain('AuthService');
|
|
782
|
+
expect(result).toContain('./auth.controller');
|
|
783
|
+
expect(result).toContain('./auth.service');
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// ============================================================================
|
|
788
|
+
// List Files Integration Tests
|
|
789
|
+
// ============================================================================
|
|
790
|
+
|
|
791
|
+
describe('List Files Integration', () => {
|
|
792
|
+
let projectDir: string;
|
|
793
|
+
|
|
794
|
+
beforeEach(() => {
|
|
795
|
+
projectDir = createTempDir('list-files');
|
|
796
|
+
createMinimalBuenoProject(projectDir);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
afterEach(async () => {
|
|
800
|
+
await cleanupTempDir(projectDir);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test('should list all TypeScript files recursively', async () => {
|
|
804
|
+
const files = await listFiles(path.join(projectDir, 'server'), {
|
|
805
|
+
recursive: true,
|
|
806
|
+
pattern: /\.ts$/,
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
expect(files.length).toBeGreaterThan(0);
|
|
810
|
+
|
|
811
|
+
// All files should be TypeScript
|
|
812
|
+
for (const file of files) {
|
|
813
|
+
expect(file.endsWith('.ts')).toBe(true);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test('should list files in specific directory', async () => {
|
|
818
|
+
const files = await listFiles(
|
|
819
|
+
path.join(projectDir, 'server', 'modules', 'app'),
|
|
820
|
+
{ recursive: false },
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
expect(files.length).toBe(3); // module, controller, service
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// ============================================================================
|
|
828
|
+
// Error Handling Integration Tests
|
|
829
|
+
// ============================================================================
|
|
830
|
+
|
|
831
|
+
describe('Error Handling', () => {
|
|
832
|
+
test('should throw CLIError with correct type for invalid args', () => {
|
|
833
|
+
const error = new CLIError(
|
|
834
|
+
'Generator type is required',
|
|
835
|
+
CLIErrorType.INVALID_ARGS,
|
|
836
|
+
2,
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
expect(error).toBeInstanceOf(Error);
|
|
840
|
+
expect(error.type).toBe(CLIErrorType.INVALID_ARGS);
|
|
841
|
+
expect(error.exitCode).toBe(2);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test('should throw CLIError for file exists', () => {
|
|
845
|
+
const error = new CLIError(
|
|
846
|
+
'File already exists',
|
|
847
|
+
CLIErrorType.FILE_EXISTS,
|
|
848
|
+
3,
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
expect(error.type).toBe(CLIErrorType.FILE_EXISTS);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test('should throw CLIError for module not found', () => {
|
|
855
|
+
const error = new CLIError(
|
|
856
|
+
'Module not found',
|
|
857
|
+
CLIErrorType.MODULE_NOT_FOUND,
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
expect(error.type).toBe(CLIErrorType.MODULE_NOT_FOUND);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// ============================================================================
|
|
865
|
+
// Argument Parsing Integration Tests
|
|
866
|
+
// ============================================================================
|
|
867
|
+
|
|
868
|
+
describe('Argument Parsing Integration', () => {
|
|
869
|
+
test('should parse generate command with all options', () => {
|
|
870
|
+
const args = parseArgs([
|
|
871
|
+
'generate',
|
|
872
|
+
'controller',
|
|
873
|
+
'users',
|
|
874
|
+
'--module',
|
|
875
|
+
'auth',
|
|
876
|
+
'--path',
|
|
877
|
+
'api/users',
|
|
878
|
+
'--force',
|
|
879
|
+
]);
|
|
880
|
+
|
|
881
|
+
expect(args.command).toBe('generate');
|
|
882
|
+
expect(args.positionals).toEqual(['controller', 'users']);
|
|
883
|
+
expect(args.options.module).toBe('auth');
|
|
884
|
+
expect(args.options.path).toBe('api/users');
|
|
885
|
+
expect(args.options.force).toBe(true);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
test('should parse migration command with steps option', () => {
|
|
889
|
+
const args = parseArgs(['migration', 'down', '--steps', '3']);
|
|
890
|
+
|
|
891
|
+
expect(args.command).toBe('migration');
|
|
892
|
+
expect(args.positionals).toEqual(['down']);
|
|
893
|
+
expect(args.options.steps).toBe('3');
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test('should parse dev command with multiple options', () => {
|
|
897
|
+
const args = parseArgs([
|
|
898
|
+
'dev',
|
|
899
|
+
'--port',
|
|
900
|
+
'4000',
|
|
901
|
+
'--host',
|
|
902
|
+
'0.0.0.0',
|
|
903
|
+
'--open',
|
|
904
|
+
]);
|
|
905
|
+
|
|
906
|
+
expect(args.command).toBe('dev');
|
|
907
|
+
expect(args.options.port).toBe('4000');
|
|
908
|
+
expect(args.options.host).toBe('0.0.0.0');
|
|
909
|
+
expect(args.options.open).toBe(true);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test('should parse new command with template options', () => {
|
|
913
|
+
const args = parseArgs([
|
|
914
|
+
'new',
|
|
915
|
+
'my-app',
|
|
916
|
+
'--template',
|
|
917
|
+
'fullstack',
|
|
918
|
+
'--framework',
|
|
919
|
+
'vue',
|
|
920
|
+
'--database',
|
|
921
|
+
'postgresql',
|
|
922
|
+
]);
|
|
923
|
+
|
|
924
|
+
expect(args.command).toBe('new');
|
|
925
|
+
expect(args.positionals).toEqual(['my-app']);
|
|
926
|
+
expect(args.options.template).toBe('fullstack');
|
|
927
|
+
expect(args.options.framework).toBe('vue');
|
|
928
|
+
expect(args.options.database).toBe('postgresql');
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// ============================================================================
|
|
933
|
+
// Path Utilities Integration Tests
|
|
934
|
+
// ============================================================================
|
|
935
|
+
|
|
936
|
+
describe('Path Utilities Integration', () => {
|
|
937
|
+
test('should generate correct file paths for different generators', () => {
|
|
938
|
+
const testCases = [
|
|
939
|
+
{
|
|
940
|
+
type: 'controller',
|
|
941
|
+
name: 'users',
|
|
942
|
+
expectedDir: 'modules/users',
|
|
943
|
+
expectedFile: 'users.controller.ts',
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
type: 'service',
|
|
947
|
+
name: 'auth',
|
|
948
|
+
expectedDir: 'modules/auth',
|
|
949
|
+
expectedFile: 'auth.service.ts',
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
type: 'guard',
|
|
953
|
+
name: 'auth',
|
|
954
|
+
expectedDir: 'common/guards',
|
|
955
|
+
expectedFile: 'auth.guard.ts',
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
type: 'interceptor',
|
|
959
|
+
name: 'logging',
|
|
960
|
+
expectedDir: 'common/interceptors',
|
|
961
|
+
expectedFile: 'logging.interceptor.ts',
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
type: 'pipe',
|
|
965
|
+
name: 'validation',
|
|
966
|
+
expectedDir: 'common/pipes',
|
|
967
|
+
expectedFile: 'validation.pipe.ts',
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
type: 'filter',
|
|
971
|
+
name: 'error',
|
|
972
|
+
expectedDir: 'common/filters',
|
|
973
|
+
expectedFile: 'error.filter.ts',
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
type: 'middleware',
|
|
977
|
+
name: 'cors',
|
|
978
|
+
expectedDir: 'common/middleware',
|
|
979
|
+
expectedFile: 'cors.middleware.ts',
|
|
980
|
+
},
|
|
981
|
+
];
|
|
982
|
+
|
|
983
|
+
for (const { name, expectedDir, expectedFile } of testCases) {
|
|
984
|
+
const dir = path.join('server', expectedDir);
|
|
985
|
+
const filePath = path.join(dir, expectedFile);
|
|
986
|
+
|
|
987
|
+
expect(filePath).toContain(expectedDir);
|
|
988
|
+
expect(filePath).toContain(expectedFile);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// ============================================================================
|
|
994
|
+
// Cleanup Verification Tests
|
|
995
|
+
// ============================================================================
|
|
996
|
+
|
|
997
|
+
describe('Cleanup Verification', () => {
|
|
998
|
+
test('should clean up temporary directories', async () => {
|
|
999
|
+
const tempDir = createTempDir('cleanup-test');
|
|
1000
|
+
|
|
1001
|
+
// Write some files
|
|
1002
|
+
await writeFile(path.join(tempDir, 'test.txt'), 'content');
|
|
1003
|
+
|
|
1004
|
+
expect(fs.existsSync(tempDir)).toBe(true);
|
|
1005
|
+
|
|
1006
|
+
// Clean up
|
|
1007
|
+
await cleanupTempDir(tempDir);
|
|
1008
|
+
|
|
1009
|
+
expect(fs.existsSync(tempDir)).toBe(false);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
test('should handle cleanup of non-existent directory', async () => {
|
|
1013
|
+
// Should not throw
|
|
1014
|
+
await cleanupTempDir('/non/existent/directory');
|
|
1015
|
+
});
|
|
1016
|
+
});
|