@hazeljs/cli 0.2.0-rc.7 → 0.2.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/@template/src/hello.controller.ts +1 -1
- package/README.md +40 -3
- package/dist/commands/generate-agent.d.ts +2 -0
- package/dist/commands/generate-agent.js +13 -7
- package/dist/commands/generate-ai-service.d.ts +2 -0
- package/dist/commands/generate-ai-service.js +8 -2
- package/dist/commands/generate-app.d.ts +7 -0
- package/dist/commands/generate-app.interactive-packages.test.d.ts +1 -0
- package/dist/commands/generate-app.interactive-packages.test.js +107 -0
- package/dist/commands/generate-app.js +141 -0
- package/dist/commands/generate-app.new-command.test.d.ts +1 -0
- package/dist/commands/generate-app.new-command.test.js +86 -0
- package/dist/commands/generate-app.skeleton.test.d.ts +1 -0
- package/dist/commands/generate-app.skeleton.test.js +51 -0
- package/dist/commands/generate-auth.d.ts +2 -0
- package/dist/commands/generate-auth.js +39 -40
- package/dist/commands/generate-cache.d.ts +2 -0
- package/dist/commands/generate-cache.js +14 -12
- package/dist/commands/generate-config.d.ts +2 -0
- package/dist/commands/generate-config.js +14 -12
- package/dist/commands/generate-controller.d.ts +2 -0
- package/dist/commands/generate-controller.js +8 -2
- package/dist/commands/generate-cron.d.ts +2 -0
- package/dist/commands/generate-cron.js +14 -12
- package/dist/commands/generate-crud.d.ts +2 -0
- package/dist/commands/generate-crud.js +44 -33
- package/dist/commands/generate-discovery.d.ts +2 -0
- package/dist/commands/generate-discovery.js +14 -12
- package/dist/commands/generate-dto.d.ts +2 -0
- package/dist/commands/generate-dto.js +15 -6
- package/dist/commands/generate-exception-filter.d.ts +2 -0
- package/dist/commands/generate-exception-filter.js +8 -2
- package/dist/commands/generate-guard.d.ts +2 -0
- package/dist/commands/generate-guard.js +8 -2
- package/dist/commands/generate-interceptor.d.ts +2 -0
- package/dist/commands/generate-interceptor.js +8 -2
- package/dist/commands/generate-middleware.d.ts +2 -0
- package/dist/commands/generate-middleware.js +11 -10
- package/dist/commands/generate-module.d.ts +2 -0
- package/dist/commands/generate-module.js +35 -31
- package/dist/commands/generate-pipe.d.ts +2 -0
- package/dist/commands/generate-pipe.js +8 -2
- package/dist/commands/generate-rag.d.ts +2 -0
- package/dist/commands/generate-rag.js +14 -12
- package/dist/commands/generate-repository.d.ts +2 -0
- package/dist/commands/generate-repository.js +8 -2
- package/dist/commands/generate-serverless-handler.d.ts +2 -0
- package/dist/commands/generate-serverless-handler.js +14 -3
- package/dist/commands/generate-service.d.ts +2 -0
- package/dist/commands/generate-service.js +8 -2
- package/dist/commands/generate-setup.d.ts +4 -0
- package/dist/commands/generate-setup.js +187 -0
- package/dist/commands/generate-setup.test.d.ts +1 -0
- package/dist/commands/generate-setup.test.js +50 -0
- package/dist/commands/generate-websocket-gateway.d.ts +2 -0
- package/dist/commands/generate-websocket-gateway.js +8 -2
- package/dist/commands/info.test.d.ts +1 -0
- package/dist/commands/info.test.js +58 -0
- package/dist/commands/pdf-to-audio.test.d.ts +1 -0
- package/dist/commands/pdf-to-audio.test.js +92 -0
- package/dist/commands/utility-commands.test.d.ts +1 -0
- package/dist/commands/utility-commands.test.js +95 -0
- package/dist/index.js +32 -3
- package/dist/index.list.test.d.ts +0 -0
- package/dist/index.list.test.js +70 -0
- package/dist/utils/generator-registry.d.ts +15 -0
- package/dist/utils/generator-registry.js +97 -0
- package/dist/utils/generator-registry.test.d.ts +1 -0
- package/dist/utils/generator-registry.test.js +10 -0
- package/dist/utils/generator.d.ts +21 -1
- package/dist/utils/generator.js +23 -4
- package/dist/utils/generator.test.js +5 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,13 +44,20 @@ The CLI provides commands to generate various HazelJS components, create new app
|
|
|
44
44
|
|
|
45
45
|
### Project Management
|
|
46
46
|
|
|
47
|
-
#### Create
|
|
47
|
+
#### Create new application
|
|
48
48
|
|
|
49
|
+
**Skeleton app (quick start, like create-next-app):**
|
|
50
|
+
```bash
|
|
51
|
+
hazel g app <appName>
|
|
52
|
+
# Then: cd <appName> && npm install && npm run dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Full setup with interactive package selection:**
|
|
49
56
|
```bash
|
|
50
57
|
hazel new <appName> [options]
|
|
51
58
|
```
|
|
52
59
|
|
|
53
|
-
Creates a new HazelJS application
|
|
60
|
+
Creates a new HazelJS application. Use `hazel g app <name>` for a minimal skeleton; use `hazel new <name> -i` for interactive setup with optional packages.
|
|
54
61
|
|
|
55
62
|
**Options:**
|
|
56
63
|
- `-d, --dest <path>` - Destination path (default: current directory)
|
|
@@ -209,8 +216,35 @@ Or using the shorter alias:
|
|
|
209
216
|
hazel g <component> <name> [options]
|
|
210
217
|
```
|
|
211
218
|
|
|
219
|
+
#### Generation options
|
|
220
|
+
|
|
221
|
+
- **One pattern:** `hazel g <type> <name> [--path <path>] [--dry-run] [--json]`
|
|
222
|
+
- **List types:** `hazel g --list` — list all generator types
|
|
223
|
+
- **List as JSON:** `hazel g --list --list-json` — output `{ "generators": [ ... ] }`
|
|
224
|
+
- **Result as JSON:** add `--json` to any generator to get `{ "ok", "created", "nextSteps" }` on stdout
|
|
225
|
+
- **Stable options:** `-p, --path`, `--dry-run`, and `--json` work the same for every generator.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# See what you can generate
|
|
229
|
+
hazel g --list
|
|
230
|
+
hazel g --list --list-json
|
|
231
|
+
|
|
232
|
+
# Generate with JSON result
|
|
233
|
+
hazel g controller users --json
|
|
234
|
+
hazel g crud product --path src/products --json
|
|
235
|
+
|
|
236
|
+
# Dry run (no files written)
|
|
237
|
+
hazel g module orders --dry-run
|
|
238
|
+
```
|
|
239
|
+
|
|
212
240
|
### Available Generators
|
|
213
241
|
|
|
242
|
+
#### Skeleton app
|
|
243
|
+
- `app` - Generate a skeleton HazelJS application (minimal template, like create-next-app). Use `hazel g app my-app` then `cd my-app && npm install && npm run dev`.
|
|
244
|
+
|
|
245
|
+
#### Package setup
|
|
246
|
+
- `setup` / `st` - Generate a minimal setup starter file for a HazelJS package (e.g. `hazel g setup swagger`).
|
|
247
|
+
|
|
214
248
|
#### Core Components
|
|
215
249
|
- `controller` / `c` - Generate a new controller
|
|
216
250
|
- `service` / `s` - Generate a new service
|
|
@@ -232,6 +266,8 @@ hazel g <component> <name> [options]
|
|
|
232
266
|
### Generator Options
|
|
233
267
|
|
|
234
268
|
- `-p, --path <path>` - Specify the path where the component should be generated (default: 'src')
|
|
269
|
+
- `--dry-run` - Preview files without writing them
|
|
270
|
+
- `--json` - Output result as JSON (created paths and next steps)
|
|
235
271
|
- `-r, --route <route>` - Specify the route path (for CRUD generator)
|
|
236
272
|
- `--platform <platform>` - For serverless, specify platform: `lambda` or `cloud-function` (default: 'lambda')
|
|
237
273
|
|
|
@@ -532,7 +568,8 @@ hazel start [-d] [-p <port>] # Start application
|
|
|
532
568
|
hazel test [pattern] [-w] [-c] # Run tests
|
|
533
569
|
|
|
534
570
|
# Code Generation (alias: g)
|
|
535
|
-
hazel g
|
|
571
|
+
hazel g app <name> # Skeleton application
|
|
572
|
+
hazel g crud <name> # Complete CRUD resource
|
|
536
573
|
hazel g controller <name> # Controller
|
|
537
574
|
hazel g service <name> # Service
|
|
538
575
|
hazel g module <name> # Module
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { GenerateResult, GenerateCLIOptions } from '../utils/generator';
|
|
3
|
+
export declare function runAgent(name: string, options: GenerateCLIOptions): Promise<GenerateResult>;
|
|
2
4
|
export declare function generateAgent(program: Command): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAgent = runAgent;
|
|
3
4
|
exports.generateAgent = generateAgent;
|
|
4
5
|
const generator_1 = require("../utils/generator");
|
|
5
6
|
const AGENT_TEMPLATE = `import { Agent, Tool } from '@hazeljs/agent';
|
|
@@ -40,19 +41,24 @@ class AgentGenerator extends generator_1.Generator {
|
|
|
40
41
|
return AGENT_TEMPLATE;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
async function runAgent(name, options) {
|
|
45
|
+
const generator = new AgentGenerator();
|
|
46
|
+
return generator.generate({
|
|
47
|
+
name,
|
|
48
|
+
path: options.path,
|
|
49
|
+
dryRun: options.dryRun,
|
|
50
|
+
data: { description: `A ${name} agent` },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
43
53
|
function generateAgent(program) {
|
|
44
54
|
program
|
|
45
55
|
.command('agent <name>')
|
|
46
56
|
.description('Generate a new AI agent with @Agent and @Tool decorators')
|
|
47
57
|
.option('-p, --path <path>', 'Path where the agent should be generated')
|
|
48
58
|
.option('--dry-run', 'Preview files without writing them')
|
|
59
|
+
.option('--json', 'Output result as JSON')
|
|
49
60
|
.action(async (name, options) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
name,
|
|
53
|
-
path: options.path,
|
|
54
|
-
dryRun: options.dryRun,
|
|
55
|
-
data: { description: `A ${name} agent` },
|
|
56
|
-
});
|
|
61
|
+
const result = await runAgent(name, options);
|
|
62
|
+
(0, generator_1.printGenerateResult)(result, { json: options.json });
|
|
57
63
|
});
|
|
58
64
|
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { GenerateResult, GenerateCLIOptions } from '../utils/generator';
|
|
3
|
+
export declare function runAIService(name: string, options: GenerateCLIOptions): Promise<GenerateResult>;
|
|
2
4
|
export declare function generateAIService(program: Command): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAIService = runAIService;
|
|
3
4
|
exports.generateAIService = generateAIService;
|
|
4
5
|
const generator_1 = require("../utils/generator");
|
|
5
6
|
const AI_SERVICE_TEMPLATE = `import { Service } from '@hazeljs/core';
|
|
@@ -34,6 +35,10 @@ class AIServiceGenerator extends generator_1.Generator {
|
|
|
34
35
|
return AI_SERVICE_TEMPLATE;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
async function runAIService(name, options) {
|
|
39
|
+
const generator = new AIServiceGenerator();
|
|
40
|
+
return generator.generate({ name, path: options.path, dryRun: options.dryRun });
|
|
41
|
+
}
|
|
37
42
|
function generateAIService(program) {
|
|
38
43
|
program
|
|
39
44
|
.command('ai-service <name>')
|
|
@@ -41,8 +46,9 @@ function generateAIService(program) {
|
|
|
41
46
|
.alias('ai')
|
|
42
47
|
.option('-p, --path <path>', 'Path where the AI service should be generated')
|
|
43
48
|
.option('--dry-run', 'Preview files without writing them')
|
|
49
|
+
.option('--json', 'Output result as JSON')
|
|
44
50
|
.action(async (name, options) => {
|
|
45
|
-
const
|
|
46
|
-
|
|
51
|
+
const result = await runAIService(name, options);
|
|
52
|
+
(0, generator_1.printGenerateResult)(result, { json: options.json });
|
|
47
53
|
});
|
|
48
54
|
}
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import type { GenerateResult, GenerateCLIOptions } from '../utils/generator';
|
|
2
3
|
export declare function generateApp(program: Command): void;
|
|
4
|
+
/** Run the skeleton app generator (used by `hazel g app <name>`). Creates a minimal app, no install/git. */
|
|
5
|
+
export declare function runApp(name: string, options: GenerateCLIOptions & {
|
|
6
|
+
path?: string;
|
|
7
|
+
}): Promise<GenerateResult>;
|
|
8
|
+
/** Register `app <name>` under the generate command (skeleton app, like create-next-app). */
|
|
9
|
+
export declare function registerGenerateApp(generateCommand: Command): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const generate_app_1 = require("./generate-app");
|
|
12
|
+
jest.mock('fs');
|
|
13
|
+
jest.mock('child_process', () => ({ execSync: jest.fn() }));
|
|
14
|
+
jest.mock('inquirer');
|
|
15
|
+
describe('generateApp interactive package wiring', () => {
|
|
16
|
+
const mockFs = fs_1.default;
|
|
17
|
+
const mockExec = child_process_1.execSync;
|
|
18
|
+
const mockInquirer = inquirer_1.default;
|
|
19
|
+
let exitSpy;
|
|
20
|
+
let logSpy;
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
23
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
24
|
+
});
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
exitSpy.mockRestore();
|
|
27
|
+
logSpy.mockRestore();
|
|
28
|
+
});
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
mockExec.mockImplementation(() => undefined);
|
|
32
|
+
mockFs.mkdirSync.mockImplementation(() => undefined);
|
|
33
|
+
mockFs.copyFileSync.mockImplementation(() => undefined);
|
|
34
|
+
mockFs.writeFileSync.mockImplementation(() => undefined);
|
|
35
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ name: 'x', description: 'y' }));
|
|
36
|
+
const allPackages = [
|
|
37
|
+
'@hazeljs/config',
|
|
38
|
+
'@hazeljs/swagger',
|
|
39
|
+
'@hazeljs/prisma',
|
|
40
|
+
'@hazeljs/typeorm',
|
|
41
|
+
'@hazeljs/audit',
|
|
42
|
+
'@hazeljs/auth',
|
|
43
|
+
'@hazeljs/oauth',
|
|
44
|
+
'@hazeljs/cache',
|
|
45
|
+
'@hazeljs/cron',
|
|
46
|
+
'@hazeljs/websocket',
|
|
47
|
+
'@hazeljs/ai',
|
|
48
|
+
'@hazeljs/agent',
|
|
49
|
+
'@hazeljs/rag',
|
|
50
|
+
'@hazeljs/pdf-to-audio',
|
|
51
|
+
'@hazeljs/data',
|
|
52
|
+
'@hazeljs/event-emitter',
|
|
53
|
+
'@hazeljs/gateway',
|
|
54
|
+
'@hazeljs/graphql',
|
|
55
|
+
'@hazeljs/grpc',
|
|
56
|
+
'@hazeljs/kafka',
|
|
57
|
+
'@hazeljs/messaging',
|
|
58
|
+
'@hazeljs/ml',
|
|
59
|
+
];
|
|
60
|
+
mockInquirer.prompt.mockResolvedValue({
|
|
61
|
+
description: 'My app',
|
|
62
|
+
author: 'Me',
|
|
63
|
+
license: 'Apache-2.0',
|
|
64
|
+
packages: allPackages,
|
|
65
|
+
});
|
|
66
|
+
// Simulate template exists and has some files/dirs
|
|
67
|
+
mockFs.existsSync.mockImplementation((p) => {
|
|
68
|
+
const s = String(p);
|
|
69
|
+
if (s.includes(path_1.default.join('@template')))
|
|
70
|
+
return true; // template exists
|
|
71
|
+
if (s.endsWith(path_1.default.join('.', 'my-app')) || s.endsWith(path_1.default.join(process.cwd(), '.', 'my-app')))
|
|
72
|
+
return false; // dest doesn't exist
|
|
73
|
+
if (s.endsWith(path_1.default.join('my-app', 'package.json')))
|
|
74
|
+
return true; // updatePackageJson path
|
|
75
|
+
if (s.endsWith(path_1.default.join('my-app', 'src')))
|
|
76
|
+
return true;
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
mockFs.readdirSync.mockImplementation((p) => {
|
|
80
|
+
const s = String(p);
|
|
81
|
+
if (s.includes('@template') && s.endsWith('@template'))
|
|
82
|
+
return ['package.json', 'src'];
|
|
83
|
+
if (s.includes('@template') && s.endsWith(path_1.default.join('@template', 'src')))
|
|
84
|
+
return ['index.ts', 'app.module.ts', 'hello.controller.ts'];
|
|
85
|
+
return [];
|
|
86
|
+
});
|
|
87
|
+
mockFs.lstatSync.mockImplementation((p) => {
|
|
88
|
+
const s = String(p);
|
|
89
|
+
if (s.endsWith('src'))
|
|
90
|
+
return { isDirectory: () => true };
|
|
91
|
+
return { isDirectory: () => false };
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('covers interactive selection and scaffolding boilerplate', async () => {
|
|
95
|
+
const program = new commander_1.Command();
|
|
96
|
+
(0, generate_app_1.generateApp)(program);
|
|
97
|
+
await program.parseAsync(['new', 'my-app', '--dest', '.', '--interactive'], { from: 'user' });
|
|
98
|
+
expect(mockInquirer.prompt).toHaveBeenCalled();
|
|
99
|
+
// git init + npm install should be attempted (mocked)
|
|
100
|
+
expect(mockExec).toHaveBeenCalled();
|
|
101
|
+
// boilerplate writes (app.module/index/env files) should happen
|
|
102
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
103
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
104
|
+
const out = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
105
|
+
expect(out).toContain('Project created successfully');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -4,11 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.generateApp = generateApp;
|
|
7
|
+
exports.runApp = runApp;
|
|
8
|
+
exports.registerGenerateApp = registerGenerateApp;
|
|
7
9
|
const fs_1 = __importDefault(require("fs"));
|
|
8
10
|
const path_1 = __importDefault(require("path"));
|
|
9
11
|
const child_process_1 = require("child_process");
|
|
10
12
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
13
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
14
|
+
const generator_1 = require("../utils/generator");
|
|
12
15
|
function copyRecursiveSync(src, dest) {
|
|
13
16
|
if (fs_1.default.existsSync(src)) {
|
|
14
17
|
fs_1.default.mkdirSync(dest, { recursive: true });
|
|
@@ -34,6 +37,94 @@ function updatePackageJson(destPath, appName, description) {
|
|
|
34
37
|
fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
/** Create a minimal skeleton app at destPath (template copy + package.json). No git, no install. */
|
|
41
|
+
function createSkeletonAtDest(destPath, appName, description) {
|
|
42
|
+
const templatePath = path_1.default.join(__dirname, '../../@template');
|
|
43
|
+
if (fs_1.default.existsSync(templatePath)) {
|
|
44
|
+
copyRecursiveSync(templatePath, destPath);
|
|
45
|
+
updatePackageJson(destPath, appName, description);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
fs_1.default.mkdirSync(destPath, { recursive: true });
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.join(destPath, 'src'), { recursive: true });
|
|
50
|
+
const packageJson = {
|
|
51
|
+
name: appName,
|
|
52
|
+
version: '0.1.0',
|
|
53
|
+
description,
|
|
54
|
+
main: 'dist/index.js',
|
|
55
|
+
scripts: {
|
|
56
|
+
build: 'tsc',
|
|
57
|
+
start: 'node dist/index.js',
|
|
58
|
+
dev: 'ts-node-dev --respawn --transpile-only src/index.ts',
|
|
59
|
+
test: 'jest',
|
|
60
|
+
lint: 'eslint "src/**/*.ts"',
|
|
61
|
+
'lint:fix': 'eslint "src/**/*.ts" --fix',
|
|
62
|
+
format: 'prettier --write "src/**/*.ts"',
|
|
63
|
+
},
|
|
64
|
+
dependencies: { '@hazeljs/core': '^0.2.0', 'reflect-metadata': '^0.2.2' },
|
|
65
|
+
devDependencies: {
|
|
66
|
+
'@types/jest': '^29.5.12',
|
|
67
|
+
'@types/node': '^20.0.0',
|
|
68
|
+
'@typescript-eslint/eslint-plugin': '^8.18.2',
|
|
69
|
+
'@typescript-eslint/parser': '^8.18.2',
|
|
70
|
+
eslint: '^8.56.0',
|
|
71
|
+
'eslint-config-prettier': '^9.1.0',
|
|
72
|
+
'eslint-plugin-prettier': '^5.1.3',
|
|
73
|
+
jest: '^29.7.0',
|
|
74
|
+
prettier: '^3.2.5',
|
|
75
|
+
'ts-jest': '^29.1.2',
|
|
76
|
+
'ts-node-dev': '^2.0.0',
|
|
77
|
+
typescript: '^5.3.3',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(destPath, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
81
|
+
const indexContent = `import 'reflect-metadata';
|
|
82
|
+
import { HazelApp, HazelModule, Controller, Get } from '@hazeljs/core';
|
|
83
|
+
|
|
84
|
+
@Controller('/')
|
|
85
|
+
export class AppController {
|
|
86
|
+
@Get()
|
|
87
|
+
hello() {
|
|
88
|
+
return { message: 'Hello from HazelJS!' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@HazelModule({
|
|
93
|
+
controllers: [AppController],
|
|
94
|
+
})
|
|
95
|
+
export class AppModule {}
|
|
96
|
+
|
|
97
|
+
async function bootstrap() {
|
|
98
|
+
const app = new HazelApp(AppModule);
|
|
99
|
+
app.enableCors({ origin: '*', methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] });
|
|
100
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
101
|
+
await app.listen(port);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bootstrap();
|
|
105
|
+
`;
|
|
106
|
+
fs_1.default.writeFileSync(path_1.default.join(destPath, 'src', 'index.ts'), indexContent);
|
|
107
|
+
const tsConfig = {
|
|
108
|
+
compilerOptions: {
|
|
109
|
+
target: 'ES2020',
|
|
110
|
+
module: 'commonjs',
|
|
111
|
+
lib: ['ES2020'],
|
|
112
|
+
outDir: './dist',
|
|
113
|
+
rootDir: './src',
|
|
114
|
+
strict: true,
|
|
115
|
+
esModuleInterop: true,
|
|
116
|
+
skipLibCheck: true,
|
|
117
|
+
forceConsistentCasingInFileNames: true,
|
|
118
|
+
experimentalDecorators: true,
|
|
119
|
+
emitDecoratorMetadata: true,
|
|
120
|
+
},
|
|
121
|
+
include: ['src/**/*'],
|
|
122
|
+
exclude: ['node_modules', 'dist'],
|
|
123
|
+
};
|
|
124
|
+
fs_1.default.writeFileSync(path_1.default.join(destPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
|
|
125
|
+
fs_1.default.writeFileSync(path_1.default.join(destPath, '.gitignore'), 'node_modules/\ndist/\n.env\n.DS_Store\ncoverage/\n*.log\n');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
37
128
|
function scaffoldPackageBoilerplate(destPath, packages) {
|
|
38
129
|
const srcPath = path_1.default.join(destPath, 'src');
|
|
39
130
|
// Build up enhanced app.module.ts imports based on selected packages
|
|
@@ -446,3 +537,53 @@ coverage/
|
|
|
446
537
|
}
|
|
447
538
|
});
|
|
448
539
|
}
|
|
540
|
+
/** Run the skeleton app generator (used by `hazel g app <name>`). Creates a minimal app, no install/git. */
|
|
541
|
+
async function runApp(name, options) {
|
|
542
|
+
const parentDir = options.path || '.';
|
|
543
|
+
const destPath = path_1.default.join(process.cwd(), parentDir, name);
|
|
544
|
+
if (options.dryRun) {
|
|
545
|
+
return {
|
|
546
|
+
ok: true,
|
|
547
|
+
created: [destPath],
|
|
548
|
+
dryRun: true,
|
|
549
|
+
nextSteps: [`cd ${name}`, 'npm install', 'npm run dev'],
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (fs_1.default.existsSync(destPath)) {
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
created: [],
|
|
556
|
+
error: `Destination already exists: ${destPath}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
createSkeletonAtDest(destPath, name, 'A HazelJS application');
|
|
561
|
+
return {
|
|
562
|
+
ok: true,
|
|
563
|
+
created: [destPath],
|
|
564
|
+
nextSteps: [`cd ${name}`, 'npm install', 'npm run dev'],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
return {
|
|
569
|
+
ok: false,
|
|
570
|
+
created: [],
|
|
571
|
+
error: error instanceof Error ? error.message : String(error),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** Register `app <name>` under the generate command (skeleton app, like create-next-app). */
|
|
576
|
+
function registerGenerateApp(generateCommand) {
|
|
577
|
+
generateCommand
|
|
578
|
+
.command('app <name>')
|
|
579
|
+
.description('Generate a skeleton HazelJS application (minimal template, no install)')
|
|
580
|
+
.option('-p, --path <path>', 'Parent directory for the app', '.')
|
|
581
|
+
.option('--dry-run', 'Preview without writing files')
|
|
582
|
+
.option('--json', 'Output result as JSON')
|
|
583
|
+
.action(async (name, options) => {
|
|
584
|
+
const result = await runApp(name, options);
|
|
585
|
+
(0, generator_1.printGenerateResult)(result, { json: options.json });
|
|
586
|
+
if (!result.ok)
|
|
587
|
+
process.exit(1);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
9
|
+
const commander_1 = require("commander");
|
|
10
|
+
const generate_app_1 = require("./generate-app");
|
|
11
|
+
jest.mock('fs');
|
|
12
|
+
jest.mock('child_process', () => ({ execSync: jest.fn() }));
|
|
13
|
+
jest.mock('inquirer');
|
|
14
|
+
describe('generateApp (hazel new)', () => {
|
|
15
|
+
const mockFs = fs_1.default;
|
|
16
|
+
const mockExec = child_process_1.execSync;
|
|
17
|
+
const mockInquirer = inquirer_1.default;
|
|
18
|
+
let exitSpy;
|
|
19
|
+
let logSpy;
|
|
20
|
+
let errSpy;
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
23
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
24
|
+
errSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
25
|
+
});
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
exitSpy.mockRestore();
|
|
28
|
+
logSpy.mockRestore();
|
|
29
|
+
errSpy.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
mockExec.mockImplementation(() => undefined);
|
|
34
|
+
mockFs.mkdirSync.mockImplementation(() => undefined);
|
|
35
|
+
mockFs.writeFileSync.mockImplementation(() => undefined);
|
|
36
|
+
mockFs.copyFileSync.mockImplementation(() => undefined);
|
|
37
|
+
mockFs.readdirSync.mockReturnValue([]);
|
|
38
|
+
mockFs.lstatSync.mockReturnValue({ isDirectory: () => false });
|
|
39
|
+
mockInquirer.prompt.mockResolvedValue({});
|
|
40
|
+
});
|
|
41
|
+
it('exits if destination already exists', async () => {
|
|
42
|
+
// dest exists
|
|
43
|
+
mockFs.existsSync.mockImplementation((p) => String(p).endsWith('my-app'));
|
|
44
|
+
const program = new commander_1.Command();
|
|
45
|
+
(0, generate_app_1.generateApp)(program);
|
|
46
|
+
await program.parseAsync(['new', 'my-app', '--dest', '.'], { from: 'user' });
|
|
47
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
48
|
+
});
|
|
49
|
+
it('creates basic structure when template missing and skip install/git', async () => {
|
|
50
|
+
mockFs.existsSync.mockImplementation((p) => {
|
|
51
|
+
const s = String(p);
|
|
52
|
+
if (s.includes('@template'))
|
|
53
|
+
return false;
|
|
54
|
+
if (s.endsWith('my-app'))
|
|
55
|
+
return false;
|
|
56
|
+
return false;
|
|
57
|
+
});
|
|
58
|
+
const program = new commander_1.Command();
|
|
59
|
+
(0, generate_app_1.generateApp)(program);
|
|
60
|
+
await program.parseAsync(['new', 'my-app', '--dest', '.', '--skip-install', '--skip-git'], { from: 'user' });
|
|
61
|
+
expect(mockFs.mkdirSync).toHaveBeenCalled();
|
|
62
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
63
|
+
expect(mockExec).not.toHaveBeenCalled(); // skipped install + git
|
|
64
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
it('runs interactive prompt when -i is set', async () => {
|
|
67
|
+
mockFs.existsSync.mockImplementation((p) => {
|
|
68
|
+
const s = String(p);
|
|
69
|
+
if (s.includes('@template'))
|
|
70
|
+
return false;
|
|
71
|
+
if (s.endsWith('my-app'))
|
|
72
|
+
return false;
|
|
73
|
+
return false;
|
|
74
|
+
});
|
|
75
|
+
mockInquirer.prompt.mockResolvedValue({
|
|
76
|
+
description: 'My app',
|
|
77
|
+
author: 'Me',
|
|
78
|
+
license: 'MIT',
|
|
79
|
+
packages: [],
|
|
80
|
+
});
|
|
81
|
+
const program = new commander_1.Command();
|
|
82
|
+
(0, generate_app_1.generateApp)(program);
|
|
83
|
+
await program.parseAsync(['new', 'my-app', '--dest', '.', '--skip-install', '--skip-git', '--interactive'], { from: 'user' });
|
|
84
|
+
expect(mockInquirer.prompt).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const generate_app_1 = require("./generate-app");
|
|
9
|
+
jest.mock('fs');
|
|
10
|
+
describe('runApp (skeleton app)', () => {
|
|
11
|
+
const mockFs = fs_1.default;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
mockFs.mkdirSync.mockImplementation(() => undefined);
|
|
15
|
+
mockFs.writeFileSync.mockImplementation(() => undefined);
|
|
16
|
+
mockFs.copyFileSync.mockImplementation(() => undefined);
|
|
17
|
+
mockFs.readdirSync.mockReturnValue([]);
|
|
18
|
+
mockFs.lstatSync.mockReturnValue({ isDirectory: () => false });
|
|
19
|
+
});
|
|
20
|
+
it('should return dry-run result without touching fs', async () => {
|
|
21
|
+
const result = await (0, generate_app_1.runApp)('my-app', { path: '.', dryRun: true });
|
|
22
|
+
expect(result.ok).toBe(true);
|
|
23
|
+
expect(result.dryRun).toBe(true);
|
|
24
|
+
expect(result.created[0]).toContain(path_1.default.join(process.cwd(), 'my-app'));
|
|
25
|
+
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
|
|
26
|
+
expect(mockFs.writeFileSync).not.toHaveBeenCalled();
|
|
27
|
+
});
|
|
28
|
+
it('should fail if destination already exists', async () => {
|
|
29
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
30
|
+
const result = await (0, generate_app_1.runApp)('my-app', { path: '.', dryRun: false });
|
|
31
|
+
expect(result.ok).toBe(false);
|
|
32
|
+
expect(result.error).toContain('Destination already exists');
|
|
33
|
+
});
|
|
34
|
+
it('should create skeleton when destination does not exist', async () => {
|
|
35
|
+
// destPath doesn't exist; templatePath also doesn't exist -> fallback basic structure
|
|
36
|
+
mockFs.existsSync.mockImplementation((p) => {
|
|
37
|
+
const s = String(p);
|
|
38
|
+
if (s.endsWith(path_1.default.join('.', 'my-app')) || s.endsWith(path_1.default.join(process.cwd(), '.', 'my-app')))
|
|
39
|
+
return false;
|
|
40
|
+
if (s.includes('@template'))
|
|
41
|
+
return false;
|
|
42
|
+
return false;
|
|
43
|
+
});
|
|
44
|
+
const result = await (0, generate_app_1.runApp)('my-app', { path: '.', dryRun: false });
|
|
45
|
+
expect(result.ok).toBe(true);
|
|
46
|
+
expect(result.created).toHaveLength(1);
|
|
47
|
+
expect(mockFs.mkdirSync).toHaveBeenCalled();
|
|
48
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
49
|
+
expect(result.nextSteps).toEqual(['cd my-app', 'npm install', 'npm run dev']);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { GenerateResult, GenerateCLIOptions } from '../utils/generator';
|
|
3
|
+
export declare function runAuth(_name: string, options: GenerateCLIOptions): Promise<GenerateResult>;
|
|
2
4
|
export declare function generateAuth(command: Command): void;
|