@martel/calyx 1.0.0 → 1.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.2.0](https://github.com/bmartel/calyx/compare/v1.1.0...v1.2.0) (2026-07-01)
2
+
3
+
4
+ ### Features
5
+
6
+ * enhance CLI new command with Dockerfile and Bun-native lifecycle scripts ([8037a35](https://github.com/bmartel/calyx/commit/8037a35a771e222398168b19cb77ad9ccfa2c49a))
7
+
8
+ # [1.1.0](https://github.com/bmartel/calyx/compare/v1.0.0...v1.1.0) (2026-07-01)
9
+
10
+
11
+ ### Features
12
+
13
+ * implement reflector, setMetadata, headers tuning, and Bun-native CLI ([5ebb132](https://github.com/bmartel/calyx/commit/5ebb132a05aea49da9a11fda9dff60e651393884))
14
+
1
15
  # 1.0.0 (2026-07-01)
2
16
 
3
17
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Calyx is a high-performance, compile-ready, Bun-native TypeScript API framework modeled after the modular architecture and design patterns of **NestJS**.
4
4
 
5
- It provides 100% conceptual and syntax compatibility, letting developers easily migrate NestJS applications to a native Bun runtime for **~2.5x higher throughput** and **3.5x lower latency**, with the option to compile the server directly into a single self-contained binary file.
5
+ It provides 100% conceptual and syntax compatibility, letting developers easily migrate NestJS applications to a native Bun runtime for **up to 4.7x higher throughput** and **5.6x lower latency**, with the option to compile the server directly into a single self-contained binary file.
6
6
 
7
7
  ---
8
8
 
@@ -10,71 +10,67 @@ It provides 100% conceptual and syntax compatibility, letting developers easily
10
10
 
11
11
  * **⚡ Blazing Fast**: Engineered natively on `Bun.serve`, utilizing a fast radix routing engine and optimized request lifecycle executions.
12
12
  * **🧩 NestJS Decorators Compatibility**: Direct mappings of `@Module()`, `@Injectable()`, `@Controller()`, `@UseGuards()`, `@UseInterceptors()`, `@UsePipes()`, and `@UseFilters()`.
13
- * **📦 High Performance DI**: Fully featured Dependency Injection container with circular dependency protection, supporting custom (`useValue`/`useClass`/`useFactory`) and dynamic modules with **29x lower bootstrapping overhead**.
13
+ * **📦 High Performance DI**: Fully featured Dependency Injection container with circular dependency protection, supporting custom (`useValue`/`useClass`/`useFactory`) and dynamic modules with **18x lower bootstrapping overhead**.
14
14
  * **🛡️ Complete Request Lifecycle**: Clean request pipelines with Guards, Interceptors, Pipe transforms (with metadata), and target exception Filters.
15
15
  * **🚀 Binary Compilation**: Compiles down to a single standalone executable binary with zero external runtime dependencies using `bun build --compile`.
16
16
 
17
17
  ---
18
18
 
19
- ## Installation
19
+ ## Getting Started (Quick Start)
20
20
 
21
- Initialize your project and install Calyx with your package manager:
21
+ The easiest way to get started with Calyx is using the Bun-native CLI tool:
22
22
 
23
+ ### 1. Scaffold a New Project
23
24
  ```bash
24
- bun add @martel/calyx reflect-metadata
25
+ bunx @martel/calyx new my-app
25
26
  ```
26
27
 
27
- Ensure your `tsconfig.json` has legacy decorators enabled:
28
+ ### 2. Run the Development Server (Watch Mode)
29
+ ```bash
30
+ cd my-app
31
+ bun run start:dev
32
+ ```
28
33
 
29
- ```json
30
- {
31
- "compilerOptions": {
32
- "experimentalDecorators": true,
33
- "emitDecoratorMetadata": true
34
- }
35
- }
34
+ ### 3. Generate Components
35
+ Generate modules, controllers, or service providers with automatic parent module registration:
36
+ ```bash
37
+ bunx @martel/calyx g controller users
38
+ bunx @martel/calyx g service users
39
+ ```
40
+
41
+ ### 4. Build and Compile Standalone Binary
42
+ Calyx compiles your entire application into a single self-contained binary file with zero dependencies:
43
+ ```bash
44
+ bun run build:compile
45
+ ./dist/server
46
+ ```
47
+
48
+ ### 5. Deploy with Docker
49
+ Run your compiled binary in a secure, minimal multi-stage Docker container (under **40MB** total image size, containing no `node_modules` or runtime dependencies):
50
+ ```bash
51
+ docker build -t my-app .
52
+ docker run -p 3000:3000 my-app
36
53
  ```
37
54
 
38
55
  ---
39
56
 
40
- ## Quick Start
57
+ ## Manual Installation
41
58
 
42
- ```typescript
43
- import 'reflect-metadata';
44
- import { Module, Injectable, Controller, Get, Query, UseGuards, CanActivate, ExecutionContext, VerdantFactory } from '@martel/calyx';
59
+ If you prefer to set up Calyx manually in an existing project:
45
60
 
46
- // 1. Define a Guard
47
- @Injectable()
48
- class AuthGuard implements CanActivate {
49
- canActivate(context: ExecutionContext): boolean {
50
- const req = context.switchToHttp().getRequest<Request>();
51
- return req.headers.get('Authorization') === 'secret-token';
52
- }
53
- }
61
+ ```bash
62
+ bun add @martel/calyx reflect-metadata
63
+ ```
54
64
 
55
- // 2. Define a Controller
56
- @Controller('items')
57
- class ItemsController {
58
- @Get()
59
- @UseGuards(AuthGuard)
60
- getItems(@Query('limit') limit?: string) {
61
- return { data: ['item1', 'item2'], limit: limit ? Number(limit) : 10 };
62
- }
63
- }
65
+ Ensure your `tsconfig.json` has legacy decorators enabled:
64
66
 
65
- // 3. Define a Module
66
- @Module({
67
- controllers: [ItemsController],
68
- })
69
- class AppModule {}
70
-
71
- // 4. Bootstrap Server
72
- async function bootstrap() {
73
- const app = await CalyxFactory.create(AppModule);
74
- await app.listen(3000);
75
- console.log('Calyx application is running on port 3000');
67
+ ```json
68
+ {
69
+ "compilerOptions": {
70
+ "experimentalDecorators": true,
71
+ "emitDecoratorMetadata": true
72
+ }
76
73
  }
77
- bootstrap();
78
74
  ```
79
75
 
80
76
  ---
@@ -85,9 +81,9 @@ Measured using process-isolated `autocannon` benchmarks (100 connections, 8s, no
85
81
 
86
82
  | Benchmark | Calyx (Bun Native) | NestJS (Express on Bun) | Speedup | Latency Improvement |
87
83
  | :--- | :--- | :--- | :--- | :--- |
88
- | **DI Bootstrapping** | **7.81 μs** | 227.56 μs | **~29x faster** | N/A |
89
- | **Raw Route Throughput** | **59,408 req/sec** | 23,148 req/sec | **2.57x faster** | **3.5x lower** (1.10ms vs 3.88ms) |
90
- | **Lifecycle Pipeline (Guards/Pipes)** | **25,152 req/sec** | 10,274 req/sec | **2.45x faster** | **2.8x lower** (3.25ms vs 9.28ms) |
84
+ | **DI Bootstrapping** | **11.73 μs** | 220.39 μs | **18.79x faster** | N/A |
85
+ | **Raw Route Throughput** | **63,356 req/sec** | 23,796 req/sec | **2.66x faster** | **3.4x lower** (1.05ms vs 3.61ms) |
86
+ | **Lifecycle Pipeline (Guards/Pipes)** | **47,568 req/sec** | 9,961 req/sec | **4.78x faster** | **5.6x lower** (1.70ms vs 9.57ms) |
91
87
 
92
88
  ---
93
89
 
@@ -95,6 +91,7 @@ Measured using process-isolated `autocannon` benchmarks (100 connections, 8s, no
95
91
 
96
92
  For detailed guides, API reference, and examples, refer to the documentation in the repository:
97
93
 
94
+ * 📖 **[Calyx CLI Reference](file:///e:/code/verdant/docs/cli.md)**: Scaffolding, schematic boilerplate generation, and watch modes.
98
95
  * 📖 **[Dependency Injection & Module System](file:///e:/code/verdant/docs/dependency-injection.md)**: Custom providers, modular structures, and resolution visibility.
99
96
  * 📖 **[Controllers & Routing](file:///e:/code/verdant/docs/controllers.md)**: HTTP request handlers, parameters, redirects, and manual response wrappers.
100
97
  * 📖 **[Request Lifecycle Pipeline](file:///e:/code/verdant/docs/lifecycle.md)**: How Guards, Interceptors, Pipes, and Exception Filters interact.
package/docs/cli.md ADDED
@@ -0,0 +1,154 @@
1
+ # Calyx CLI
2
+
3
+ Calyx CLI is a lightweight, Bun-native command-line utility built to replace the standard Nest CLI. It is written in TypeScript and executes natively using Bun, enabling fast project scaffolding, code generation, hot-reloading dev servers, and single-file bundling.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ The CLI is bundled directly into the `@martel/calyx` package. You can run it on-demand using `bunx`:
10
+
11
+ ```bash
12
+ bunx @martel/calyx --help
13
+ ```
14
+
15
+ To install the CLI globally:
16
+
17
+ ```bash
18
+ bun install -g @martel/calyx
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Core Commands
24
+
25
+ ### 1. `calyx new <project-name>`
26
+ Scaffolds a new, fully configured Calyx application with the recommended folder structure, initializes `tsconfig.json` with decorator metadata settings, and runs `bun install` automatically.
27
+
28
+ ```bash
29
+ calyx new my-awesome-app
30
+ ```
31
+
32
+ #### Generated Project Structure
33
+ ```text
34
+ my-awesome-app/
35
+ ├── src/
36
+ │ ├── app.controller.ts
37
+ │ ├── app.module.ts
38
+ │ ├── app.service.ts
39
+ │ └── main.ts
40
+ ├── package.json
41
+ └── tsconfig.json
42
+ ```
43
+
44
+ ---
45
+
46
+ ### 2. `calyx generate` (or `g`)
47
+ Generates boilerplate files for core Calyx components and automatically registers them in the nearest parent Module (e.g., updating the `controllers` or `providers` metadata array).
48
+
49
+ **Usage:**
50
+ ```bash
51
+ calyx g <schematic> <name>
52
+ ```
53
+
54
+ #### Supported Schematics
55
+
56
+ | Schematic | Shortcut | Description |
57
+ | :--- | :--- | :--- |
58
+ | `module` | `module` | Creates a new module file |
59
+ | `controller` | `controller` | Creates a controller and registers it in the module |
60
+ | `service` | `service` | Creates a service provider and registers it in the module |
61
+
62
+ #### Examples
63
+ ```bash
64
+ # Generate a new users module
65
+ calyx g module users
66
+
67
+ # Generate a new users controller and register it in UsersModule
68
+ calyx g controller users
69
+
70
+ # Generate a new users service provider and register it in UsersModule
71
+ calyx g service users
72
+ ```
73
+
74
+ ---
75
+
76
+ ### 3. `calyx start`
77
+ Runs the application from the entrypoint `src/main.ts`.
78
+
79
+ ```bash
80
+ # Start standard execution
81
+ calyx start
82
+
83
+ # Start dev server with built-in hot reloading (Watch Mode)
84
+ calyx start --watch
85
+ # or shortcut:
86
+ calyx start -w
87
+ ```
88
+
89
+ ---
90
+
91
+ ### 4. `calyx build`
92
+ Bundles the application into a single production file (`dist/main.js`) using Bun's native compiler.
93
+
94
+ ```bash
95
+ calyx build
96
+ ```
97
+
98
+ ---
99
+
100
+ ### 5. `calyx info`
101
+ Displays current environment and system details, including Calyx version, Bun version, and OS.
102
+
103
+ ```bash
104
+ calyx info
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Scaffolding Lifecycle & Deployments
110
+
111
+ When a new project is created with `calyx new`, it is pre-configured with industry-best practices for development, testing, compiling, and container deployment.
112
+
113
+ ### Bun-Native NPM Scripts
114
+ The generated `package.json` includes the following scripts:
115
+
116
+ | Command | Action | Description |
117
+ | :--- | :--- | :--- |
118
+ | `bun run start` | `calyx start` | Starts the production server |
119
+ | `bun run start:dev` | `calyx start --watch` | Runs the server with hot-reloading |
120
+ | `bun run build` | `calyx build` | Bundles TS assets into `dist/main.js` |
121
+ | `bun run build:compile` | `bun build --compile` | Compiles application into a single standalone binary |
122
+ | `bun run test` | `bun test` | Runs the Bun-native test runner |
123
+ | `bun run test:watch` | `bun test --watch` | Runs tests in interactive watch mode |
124
+ | `bun run test:cov` | `bun test --coverage` | Outputs test coverage details |
125
+ | `bun run format` | `bunx prettier --write` | Standardizes format across project files |
126
+
127
+ ### Portability & Production Deployments (Docker)
128
+ Calyx supports high-performance Docker builds out-of-the-box. A multi-stage `Dockerfile` is automatically scaffolded in the root folder.
129
+
130
+ It compiles your TypeScript application into a **single self-contained binary** in the builder stage, and copies *only* the binary into a minimal Alpine linux image:
131
+
132
+ ```dockerfile
133
+ # Multistage build for high performance, portability, and minimal size
134
+ FROM oven/bun:1.1-alpine AS builder
135
+ WORKDIR /app
136
+ COPY package.json bun.lockb* ./
137
+ RUN bun install --frozen-lockfile || bun install
138
+ COPY . .
139
+ RUN bun run build:compile
140
+
141
+ # Production runner (Distroless-style minimal Alpine)
142
+ FROM alpine:latest
143
+ RUN apk --no-cache add ca-certificates
144
+ WORKDIR /app
145
+ COPY --from=builder /app/dist/server ./server
146
+ EXPOSE 3000
147
+ CMD ["./server"]
148
+ ```
149
+
150
+ #### Why This is Optimized for Deployments:
151
+ * **Size:** Under **40MB** total image size (compared to ~300MB+ for standard Node/Nest Docker images).
152
+ * **Security:** Contains **no node_modules, no Bun/Node runtimes, and no source code files** in the final production stage. It only has your single executable.
153
+ * **Speed:** The binary is pre-compiled for production, yielding faster startup times and maximum throughput.
154
+
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@martel/calyx",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "High-performance Bun-native NestJS-compatible framework",
5
5
  "main": "src/index.ts",
6
+ "bin": {
7
+ "calyx": "./src/cli/index.ts"
8
+ },
6
9
  "type": "module",
7
10
  "scripts": {
8
11
  "test": "bun test",
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env bun
2
+ import { spawnSync } from 'child_process';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+
6
+ const args = Bun.argv.slice(2);
7
+ const command = args[0];
8
+
9
+ if (!command) {
10
+ printHelp();
11
+ process.exit(0);
12
+ }
13
+
14
+ switch (command) {
15
+ case 'info':
16
+ runInfo();
17
+ break;
18
+ case 'start':
19
+ runStart(args.slice(1));
20
+ break;
21
+ case 'build':
22
+ runBuild(args.slice(1));
23
+ break;
24
+ case 'new':
25
+ runNew(args[1]);
26
+ break;
27
+ case 'generate':
28
+ case 'g':
29
+ runGenerate(args[1], args[2]);
30
+ break;
31
+ default:
32
+ console.log(`Unknown command: ${command}`);
33
+ printHelp();
34
+ process.exit(1);
35
+ }
36
+
37
+ function printHelp() {
38
+ console.log(`
39
+ Calyx CLI - Bun-native NestJS-compatible Framework CLI
40
+
41
+ Usage:
42
+ calyx <command> [options]
43
+
44
+ Commands:
45
+ new <name> Scaffold a new Calyx application
46
+ generate, g <type> <name> Generate a new schematic element (module, controller, service)
47
+ start Start the application
48
+ -w, --watch Enable hot reload / watch mode
49
+ build Bundle the application using Bun compiler
50
+ info Display system and environment details
51
+ `);
52
+ }
53
+
54
+ function runInfo() {
55
+ console.log(`
56
+ Calyx CLI Information:
57
+ Calyx Version: 0.1.0
58
+ Bun Version: ${Bun.version}
59
+ OS Platform: ${process.platform}
60
+ Node Compatibility: Yes
61
+ `);
62
+ }
63
+
64
+ function runStart(cmdArgs: string[]) {
65
+ const isWatch = cmdArgs.includes('--watch') || cmdArgs.includes('-w');
66
+ const mainPath = 'src/main.ts';
67
+
68
+ if (!existsSync(mainPath)) {
69
+ console.error(`Error: Cannot find entrypoint "${mainPath}". Make sure you are in a Calyx project root.`);
70
+ process.exit(1);
71
+ }
72
+
73
+ const spawnArgs = isWatch ? ['--watch', mainPath] : [mainPath];
74
+ console.log(`Starting Calyx application... (${isWatch ? 'Watch Mode' : 'Standard Mode'})`);
75
+
76
+ const proc = spawnSync('bun', spawnArgs, { stdio: 'inherit' });
77
+ process.exit(proc.status ?? 0);
78
+ }
79
+
80
+ function runBuild(cmdArgs: string[]) {
81
+ const mainPath = 'src/main.ts';
82
+ if (!existsSync(mainPath)) {
83
+ console.error(`Error: Cannot find entrypoint "${mainPath}". Make sure you are in a Calyx project root.`);
84
+ process.exit(1);
85
+ }
86
+
87
+ console.log('Building Calyx application using bun build...');
88
+ const proc = spawnSync('bun', ['build', mainPath, '--outdir', './dist', '--target', 'bun'], { stdio: 'inherit' });
89
+ if (proc.status === 0) {
90
+ console.log('Build completed successfully. Output at ./dist/main.js');
91
+ }
92
+ process.exit(proc.status ?? 0);
93
+ }
94
+
95
+ function runNew(name: string) {
96
+ if (!name) {
97
+ console.error('Error: Please specify the project name. Example: calyx new my-app');
98
+ process.exit(1);
99
+ }
100
+
101
+ if (existsSync(name)) {
102
+ console.error(`Error: Directory "${name}" already exists.`);
103
+ process.exit(1);
104
+ }
105
+
106
+ console.log(`Scaffolding new Calyx application in "${name}"...`);
107
+
108
+ mkdirSync(name, { recursive: true });
109
+ mkdirSync(join(name, 'src'), { recursive: true });
110
+
111
+ // Write package.json
112
+ const isDevMode = existsSync(join(import.meta.dir, '../../package.json'));
113
+ const calyxDependency = isDevMode ? "link:../.." : "^0.1.0";
114
+
115
+ const packageJson = {
116
+ name,
117
+ version: '0.0.1',
118
+ description: 'Calyx application',
119
+ type: 'module',
120
+ scripts: {
121
+ "start": "calyx start",
122
+ "start:dev": "calyx start --watch",
123
+ "build": "calyx build",
124
+ "build:compile": "bun build src/main.ts --compile --outfile dist/server",
125
+ "test": "bun test",
126
+ "test:watch": "bun test --watch",
127
+ "test:cov": "bun test --coverage",
128
+ "format": "bunx prettier --write \"src/**/*.ts\""
129
+ },
130
+ dependencies: {
131
+ "@martel/calyx": calyxDependency,
132
+ "reflect-metadata": "^0.2.2"
133
+ }
134
+ };
135
+ writeFileSync(join(name, 'package.json'), JSON.stringify(packageJson, null, 2));
136
+
137
+ // Write tsconfig.json
138
+ const tsconfigJson = {
139
+ compilerOptions: {
140
+ module: "ESNext",
141
+ target: "ESNext",
142
+ moduleResolution: "bundler",
143
+ esModuleInterop: true,
144
+ experimentalDecorators: true,
145
+ emitDecoratorMetadata: true,
146
+ strict: true,
147
+ skipLibCheck: true
148
+ }
149
+ };
150
+ writeFileSync(join(name, 'tsconfig.json'), JSON.stringify(tsconfigJson, null, 2));
151
+
152
+ // Write Dockerfile
153
+ const dockerfile = `# Multistage build for high performance, portability, and minimal size
154
+ FROM oven/bun:1.1-alpine AS builder
155
+ WORKDIR /app
156
+ COPY package.json bun.lockb* ./
157
+ RUN bun install --frozen-lockfile || bun install
158
+ COPY . .
159
+ RUN bun run build:compile
160
+
161
+ # Production runner (Distroless-style minimal Alpine)
162
+ FROM alpine:latest
163
+ RUN apk --no-cache add ca-certificates
164
+ WORKDIR /app
165
+ COPY --from=builder /app/dist/server ./server
166
+ EXPOSE 3000
167
+ CMD ["./server"]
168
+ `;
169
+ writeFileSync(join(name, 'Dockerfile'), dockerfile);
170
+
171
+ // Write .dockerignore
172
+ const dockerignore = `node_modules
173
+ dist
174
+ .git
175
+ .gitignore
176
+ Dockerfile
177
+ README.md
178
+ `;
179
+ writeFileSync(join(name, '.dockerignore'), dockerignore);
180
+
181
+ // Write .gitignore
182
+ const gitignore = `node_modules
183
+ dist
184
+ *.log
185
+ .env
186
+ `;
187
+ writeFileSync(join(name, '.gitignore'), gitignore);
188
+
189
+ // Write src/app.service.ts
190
+ const appService = `import { Injectable } from '@martel/calyx';
191
+
192
+ @Injectable()
193
+ export class AppService {
194
+ getHello(): string {
195
+ return 'Hello World!';
196
+ }
197
+ }
198
+ `;
199
+ writeFileSync(join(name, 'src/app.service.ts'), appService);
200
+
201
+ // Write src/app.controller.ts
202
+ const appController = `import { Controller, Get } from '@martel/calyx';
203
+ import { AppService } from './app.service';
204
+
205
+ @Controller()
206
+ export class AppController {
207
+ constructor(private readonly appService: AppService) {}
208
+
209
+ @Get()
210
+ getHello(): string {
211
+ return this.appService.getHello();
212
+ }
213
+ }
214
+ `;
215
+ writeFileSync(join(name, 'src/app.controller.ts'), appController);
216
+
217
+ // Write src/app.module.ts
218
+ const appModule = `import { Module } from '@martel/calyx';
219
+ import { AppController } from './app.controller';
220
+ import { AppService } from './app.service';
221
+
222
+ @Module({
223
+ controllers: [AppController],
224
+ providers: [AppService],
225
+ })
226
+ export class AppModule {}
227
+ `;
228
+ writeFileSync(join(name, 'src/app.module.ts'), appModule);
229
+
230
+ // Write src/main.ts
231
+ const mainTs = `import 'reflect-metadata';
232
+ import { CalyxFactory } from '@martel/calyx';
233
+ import { AppModule } from './app.module';
234
+
235
+ async function bootstrap() {
236
+ const app = await CalyxFactory.create(AppModule);
237
+ await app.listen(3000);
238
+ console.log('Application is running on http://localhost:3000');
239
+ }
240
+ bootstrap();
241
+ `;
242
+ writeFileSync(join(name, 'src/main.ts'), mainTs);
243
+
244
+ console.log('Installing dependencies...');
245
+ spawnSync('bun', ['install'], { cwd: name, stdio: 'inherit' });
246
+
247
+ console.log(`
248
+ Calyx application successfully created!
249
+
250
+ To start running your app:
251
+ cd ${name}
252
+ bun run start:dev
253
+ `);
254
+ }
255
+
256
+ function runGenerate(schematic: string, rawName: string) {
257
+ if (!schematic || !rawName) {
258
+ console.error('Error: Please specify schematic type and name. Example: calyx g controller users');
259
+ process.exit(1);
260
+ }
261
+
262
+ const name = rawName.toLowerCase();
263
+ const pascalName = rawName.charAt(0).toUpperCase() + rawName.slice(1);
264
+ const type = schematic.toLowerCase();
265
+
266
+ const srcDir = existsSync('src') ? 'src' : '.';
267
+ const targetDir = join(srcDir, name);
268
+
269
+ if (!existsSync(targetDir)) {
270
+ mkdirSync(targetDir, { recursive: true });
271
+ }
272
+
273
+ switch (type) {
274
+ case 'module': {
275
+ const filePath = join(targetDir, `${name}.module.ts`);
276
+ const content = `import { Module } from '@martel/calyx';
277
+
278
+ @Module({})
279
+ export class ${pascalName}Module {}
280
+ `;
281
+ writeFileSync(filePath, content);
282
+ console.log(`CREATE ${filePath}`);
283
+ break;
284
+ }
285
+ case 'controller': {
286
+ const filePath = join(targetDir, `${name}.controller.ts`);
287
+ const content = `import { Controller, Get } from '@martel/calyx';
288
+
289
+ @Controller('${name}')
290
+ export class ${pascalName}Controller {
291
+ @Get()
292
+ findAll() {
293
+ return 'This action returns all ${name}';
294
+ }
295
+ }
296
+ `;
297
+ writeFileSync(filePath, content);
298
+ console.log(`CREATE ${filePath}`);
299
+ autoRegisterInModule(name, pascalName, 'controller');
300
+ break;
301
+ }
302
+ case 'service': {
303
+ const filePath = join(targetDir, `${name}.service.ts`);
304
+ const content = `import { Injectable } from '@martel/calyx';
305
+
306
+ @Injectable()
307
+ export class ${pascalName}Service {}
308
+ `;
309
+ writeFileSync(filePath, content);
310
+ console.log(`CREATE ${filePath}`);
311
+ autoRegisterInModule(name, pascalName, 'service');
312
+ break;
313
+ }
314
+ default:
315
+ console.error(`Error: Unknown schematic type "${type}". Supported: module, controller, service`);
316
+ process.exit(1);
317
+ }
318
+ }
319
+
320
+ function autoRegisterInModule(name: string, pascalName: string, type: 'controller' | 'service') {
321
+ const srcDir = existsSync('src') ? 'src' : '.';
322
+ const modulePath = join(srcDir, name, `${name}.module.ts`);
323
+ const rootModulePath = join(srcDir, `app.module.ts`);
324
+
325
+ const pathToCheck = existsSync(modulePath) ? modulePath : (existsSync(rootModulePath) ? rootModulePath : null);
326
+ if (!pathToCheck) return;
327
+
328
+ let content = readFileSync(pathToCheck, 'utf-8');
329
+
330
+ // Insert import statement
331
+ const importName = `${pascalName}${type === 'controller' ? 'Controller' : 'Service'}`;
332
+ const importRelativePath = pathToCheck === rootModulePath ? `./${name}/${name}.${type}` : `./${name}.${type}`;
333
+ const importStatement = `import { ${importName} } from '${importRelativePath}';\n`;
334
+
335
+ // Find last import line and insert after it
336
+ const lines = content.split('\n');
337
+ let lastImportIdx = -1;
338
+ for (let i = 0; i < lines.length; i++) {
339
+ if (lines[i].trim().startsWith('import ')) {
340
+ lastImportIdx = i;
341
+ }
342
+ }
343
+ lines.splice(lastImportIdx + 1, 0, importStatement.trim());
344
+ content = lines.join('\n');
345
+
346
+ // Add item into metadata decorator
347
+ const arrayName = type === 'controller' ? 'controllers' : 'providers';
348
+ const arrayRegex = new RegExp(`(${arrayName}\\s*:\\s*\\[)([^\\]]*)(\\])`);
349
+
350
+ if (arrayRegex.test(content)) {
351
+ content = content.replace(arrayRegex, (match, prefix, list, suffix) => {
352
+ const trimmedList = list.trim();
353
+ const newList = trimmedList ? `${trimmedList}, ${importName}` : importName;
354
+ return `${prefix}${newList}${suffix}`;
355
+ });
356
+ } else {
357
+ // Add the metadata property if it doesn't exist
358
+ const decoratorRegex = /(@Module\(\s*\{)([\s\S]*?)(\}\s*\))/;
359
+ content = content.replace(decoratorRegex, (match, prefix, body, suffix) => {
360
+ const trimmedBody = body.trim();
361
+ const newBody = trimmedBody ? `${trimmedBody},\n ${arrayName}: [${importName}]` : ` ${arrayName}: [${importName}]`;
362
+ return `${prefix}\n${newBody}\n${suffix}`;
363
+ });
364
+ }
365
+
366
+ writeFileSync(pathToCheck, content);
367
+ console.log(`UPDATE ${pathToCheck} (Registered ${importName})`);
368
+ }
@@ -67,3 +67,33 @@ export function Global(): ClassDecorator {
67
67
  export function forwardRef(fn: () => any): ForwardReference {
68
68
  return { forwardRef: fn };
69
69
  }
70
+
71
+ export interface CustomDecorator<TKey = any> {
72
+ (target: object, key?: string | symbol, descriptor?: any): any;
73
+ KEY: TKey;
74
+ }
75
+
76
+ export function SetMetadata<K = any, V = any>(metadataKey: K, metadataValue: V): CustomDecorator<K> {
77
+ const decoratorFn = (target: any, key?: string | symbol, descriptor?: any) => {
78
+ if (descriptor) {
79
+ Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
80
+ return descriptor;
81
+ }
82
+ Reflect.defineMetadata(metadataKey, metadataValue, target);
83
+ return target;
84
+ };
85
+ decoratorFn.KEY = metadataKey;
86
+ return decoratorFn;
87
+ }
88
+
89
+ export function applyDecorators(...decorators: any[]) {
90
+ return (target: any, propertyKey?: string | symbol, descriptor?: any) => {
91
+ for (const decorator of decorators) {
92
+ if (target instanceof Function && !propertyKey) {
93
+ decorator(target);
94
+ } else {
95
+ decorator(target, propertyKey, descriptor);
96
+ }
97
+ }
98
+ };
99
+ }
package/src/core/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from './metadata.ts';
2
2
  export * from './decorators.ts';
3
3
  export * from './module-ref.ts';
4
4
  export * from './container.ts';
5
+ export * from './reflector.ts';
@@ -0,0 +1,32 @@
1
+ import { Injectable } from './decorators.ts';
2
+ import { Type } from './metadata.ts';
3
+
4
+ @Injectable()
5
+ export class Reflector {
6
+ get<T = any>(metadataKey: any, target: Function | Type<any>): T {
7
+ return Reflect.getMetadata(metadataKey, target);
8
+ }
9
+
10
+ getAllAndOverride<T = any>(metadataKey: any, targets: (Function | Type<any>)[]): T {
11
+ for (const target of targets) {
12
+ if (!target) continue;
13
+ const val = Reflect.getMetadata(metadataKey, target);
14
+ if (val !== undefined) return val;
15
+ }
16
+ return undefined as any;
17
+ }
18
+
19
+ getAllAndMerge<T = any>(metadataKey: any, targets: (Function | Type<any>)[]): T {
20
+ const result: any[] = [];
21
+ for (const target of targets) {
22
+ if (!target) continue;
23
+ const val = Reflect.getMetadata(metadataKey, target);
24
+ if (Array.isArray(val)) {
25
+ result.push(...val);
26
+ } else if (val !== undefined) {
27
+ result.push(val);
28
+ }
29
+ }
30
+ return result as any as T;
31
+ }
32
+ }
@@ -8,7 +8,7 @@ import { CalyxArgumentsHost, CalyxExecutionContext } from '../lifecycle/context.
8
8
 
9
9
  export class CalyxResponse {
10
10
  statusCode = 200;
11
- headers = new Headers();
11
+ headers: Record<string, string> = {};
12
12
  body: any = null;
13
13
  sent = false;
14
14
 
@@ -24,14 +24,14 @@ export class CalyxResponse {
24
24
  }
25
25
 
26
26
  json(body: any) {
27
- this.headers.set('content-type', 'application/json');
27
+ this.headers['content-type'] = 'application/json';
28
28
  this.body = JSON.stringify(body);
29
29
  this.sent = true;
30
30
  return this;
31
31
  }
32
32
 
33
33
  set(name: string, value: string) {
34
- this.headers.set(name, value);
34
+ this.headers[name.toLowerCase()] = value;
35
35
  return this;
36
36
  }
37
37
  }
@@ -608,10 +608,13 @@ export class CalyxApplication {
608
608
  ? resWrapper.statusCode
609
609
  : (httpCode ?? (req.method === 'POST' ? 201 : 200));
610
610
 
611
- // Build headers only if we have custom headers or content-type requirements
612
- let responseHeaders = resWrapper && isPassthrough ? resWrapper.headers : new Headers();
611
+ // Build headers as a plain object Record<string, string>
612
+ const responseHeaders: Record<string, string> = resWrapper && isPassthrough
613
+ ? { ...resWrapper.headers }
614
+ : {};
615
+
613
616
  for (const header of headers) {
614
- responseHeaders.set(header.name, header.value);
617
+ responseHeaders[header.name.toLowerCase()] = header.value;
615
618
  }
616
619
 
617
620
  if (result === undefined || result === null) {
@@ -623,7 +626,7 @@ export class CalyxApplication {
623
626
  }
624
627
 
625
628
  if (typeof result === 'object') {
626
- responseHeaders.set('content-type', 'application/json');
629
+ responseHeaders['content-type'] = 'application/json';
627
630
  return new Response(JSON.stringify(result), { status, headers: responseHeaders });
628
631
  }
629
632
 
@@ -0,0 +1,96 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2
+ import { spawnSync } from 'child_process';
3
+ import { existsSync, rmSync, readFileSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ const testAppName = 'scratch/my-cli-test-app';
7
+
8
+ describe('Calyx CLI Integration Tests', () => {
9
+ beforeAll(() => {
10
+ if (existsSync(testAppName)) {
11
+ rmSync(testAppName, { recursive: true, force: true });
12
+ }
13
+ });
14
+
15
+ afterAll(() => {
16
+ if (existsSync(testAppName)) {
17
+ rmSync(testAppName, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ test('should display version details on info command', () => {
22
+ const proc = spawnSync('bun', ['./src/cli/index.ts', 'info'], { encoding: 'utf-8' });
23
+ expect(proc.status).toBe(0);
24
+ expect(proc.stdout).toContain('Calyx CLI Information');
25
+ expect(proc.stdout).toContain('Calyx Version: 0.1.0');
26
+ });
27
+
28
+ test('should scaffold a new application on new command', () => {
29
+ console.log('Scaffolding app, this may take a few seconds to run bun install...');
30
+ const proc = spawnSync('bun', ['./src/cli/index.ts', 'new', testAppName], { stdio: 'inherit' });
31
+ expect(proc.status).toBe(0);
32
+
33
+ expect(existsSync(join(testAppName, 'package.json'))).toBe(true);
34
+ expect(existsSync(join(testAppName, 'tsconfig.json'))).toBe(true);
35
+ expect(existsSync(join(testAppName, 'src/main.ts'))).toBe(true);
36
+ expect(existsSync(join(testAppName, 'src/app.module.ts'))).toBe(true);
37
+ expect(existsSync(join(testAppName, 'src/app.controller.ts'))).toBe(true);
38
+ expect(existsSync(join(testAppName, 'src/app.service.ts'))).toBe(true);
39
+ expect(existsSync(join(testAppName, 'Dockerfile'))).toBe(true);
40
+ expect(existsSync(join(testAppName, '.dockerignore'))).toBe(true);
41
+ expect(existsSync(join(testAppName, '.gitignore'))).toBe(true);
42
+ expect(existsSync(join(testAppName, 'node_modules'))).toBe(true);
43
+ });
44
+
45
+ test('should generate controllers, services, and modules with auto-registration', () => {
46
+ // Generate a controller
47
+ const genCtrl = spawnSync('bun', ['../../src/cli/index.ts', 'g', 'controller', 'users'], {
48
+ cwd: testAppName,
49
+ encoding: 'utf-8',
50
+ });
51
+ expect(genCtrl.status).toBe(0);
52
+ expect(existsSync(join(testAppName, 'src/users/users.controller.ts'))).toBe(true);
53
+
54
+ // Verify registration inside app.module.ts
55
+ const moduleContent = readFileSync(join(testAppName, 'src/app.module.ts'), 'utf-8');
56
+ expect(moduleContent).toContain("import { UsersController } from './users/users.controller';");
57
+ expect(moduleContent).toContain('controllers: [AppController, UsersController]');
58
+
59
+ // Generate a service
60
+ const genSvc = spawnSync('bun', ['../../src/cli/index.ts', 'g', 'service', 'users'], {
61
+ cwd: testAppName,
62
+ encoding: 'utf-8',
63
+ });
64
+ expect(genSvc.status).toBe(0);
65
+ expect(existsSync(join(testAppName, 'src/users/users.service.ts'))).toBe(true);
66
+
67
+ const moduleContent2 = readFileSync(join(testAppName, 'src/app.module.ts'), 'utf-8');
68
+ expect(moduleContent2).toContain("import { UsersService } from './users/users.service';");
69
+ expect(moduleContent2).toContain('providers: [AppService, UsersService]');
70
+ });
71
+
72
+ test('should build the application using Bun compiler', () => {
73
+ // Manually modify package.json to point to local CLI script for Windows/Link test compatibility
74
+ const pkgPath = join(testAppName, 'package.json');
75
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
76
+ pkg.scripts.build = "bun ../../src/cli/index.ts build";
77
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
78
+
79
+ // Manually add path mapping to tsconfig.json for TypeScript source resolution during tests
80
+ const tsconfigPath = join(testAppName, 'tsconfig.json');
81
+ const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf-8'));
82
+ tsconfig.compilerOptions.paths = {
83
+ "@martel/calyx": ["../../src/index.ts"]
84
+ };
85
+ writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
86
+
87
+ const buildProc = spawnSync('bun', ['run', 'build'], {
88
+ cwd: testAppName,
89
+ encoding: 'utf-8',
90
+ });
91
+ console.log('Build STDOUT:', buildProc.stdout);
92
+ console.log('Build STDERR:', buildProc.stderr);
93
+ expect(buildProc.status).toBe(0);
94
+ expect(existsSync(join(testAppName, 'dist/main.js'))).toBe(true);
95
+ });
96
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ SetMetadata,
4
+ Reflector,
5
+ applyDecorators,
6
+ Controller,
7
+ Get,
8
+ } from '../src/index.ts';
9
+
10
+ describe('Phase 5: NestJS Parity Extensions (Reflector & Metadata)', () => {
11
+ test('should set and get metadata using SetMetadata and Reflector', () => {
12
+ const reflector = new Reflector();
13
+
14
+ @SetMetadata('roles', ['admin'])
15
+ class SecuredController {
16
+ @SetMetadata('permission', 'write')
17
+ handler() {}
18
+ }
19
+
20
+ const roles = reflector.get<string[]>('roles', SecuredController);
21
+ expect(roles).toEqual(['admin']);
22
+
23
+ const controllerInstance = new SecuredController();
24
+ const permission = reflector.get<string>('permission', controllerInstance.handler);
25
+ expect(permission).toBe('write');
26
+ });
27
+
28
+ test('should support Reflector.getAllAndOverride and getAllAndMerge', () => {
29
+ const reflector = new Reflector();
30
+
31
+ @SetMetadata('roles', ['user'])
32
+ class TestController {
33
+ @SetMetadata('roles', ['admin'])
34
+ handler() {}
35
+ }
36
+
37
+ const controllerInstance = new TestController();
38
+
39
+ // getAllAndOverride: returns first non-undefined (reverses targets parameter order in Nest, e.g. [handler, class])
40
+ const overridenRoles = reflector.getAllAndOverride<string[]>('roles', [
41
+ controllerInstance.handler,
42
+ TestController,
43
+ ]);
44
+ expect(overridenRoles).toEqual(['admin']);
45
+
46
+ // getAllAndMerge: merges values from targets
47
+ const mergedRoles = reflector.getAllAndMerge<string[]>('roles', [
48
+ controllerInstance.handler,
49
+ TestController,
50
+ ]);
51
+ expect(mergedRoles).toEqual(['admin', 'user']);
52
+ });
53
+
54
+ test('should support applyDecorators to group multiple decorators', () => {
55
+ const reflector = new Reflector();
56
+
57
+ function CustomSecured(permission: string, roles: string[]) {
58
+ return applyDecorators(
59
+ SetMetadata('permission', permission),
60
+ SetMetadata('roles', roles)
61
+ );
62
+ }
63
+
64
+ @CustomSecured('write', ['admin'])
65
+ class GroupSecuredController {}
66
+
67
+ const permission = reflector.get<string>('permission', GroupSecuredController);
68
+ const roles = reflector.get<string[]>('roles', GroupSecuredController);
69
+
70
+ expect(permission).toBe('write');
71
+ expect(roles).toEqual(['admin']);
72
+ });
73
+ });