@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 +14 -0
- package/README.md +46 -49
- package/docs/cli.md +154 -0
- package/package.json +4 -1
- package/src/cli/index.ts +368 -0
- package/src/core/decorators.ts +30 -0
- package/src/core/index.ts +1 -0
- package/src/core/reflector.ts +32 -0
- package/src/http/application.ts +10 -7
- package/tests/cli.test.ts +96 -0
- package/tests/phase5.test.ts +73 -0
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
|
|
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 **
|
|
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
|
-
##
|
|
19
|
+
## Getting Started (Quick Start)
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
|
|
25
|
+
bunx @martel/calyx new my-app
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
### 2. Run the Development Server (Watch Mode)
|
|
29
|
+
```bash
|
|
30
|
+
cd my-app
|
|
31
|
+
bun run start:dev
|
|
32
|
+
```
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
##
|
|
57
|
+
## Manual Installation
|
|
41
58
|
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
@
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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** | **
|
|
89
|
-
| **Raw Route Throughput** | **
|
|
90
|
-
| **Lifecycle Pipeline (Guards/Pipes)** | **
|
|
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.
|
|
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",
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|
package/src/core/decorators.ts
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
package/src/http/application.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { CalyxArgumentsHost, CalyxExecutionContext } from '../lifecycle/context.
|
|
|
8
8
|
|
|
9
9
|
export class CalyxResponse {
|
|
10
10
|
statusCode = 200;
|
|
11
|
-
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
|
|
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.
|
|
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
|
|
612
|
-
|
|
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
|
|
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
|
|
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
|
+
});
|