@martel/calyx 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +4 -1
- package/src/cli/index.ts +323 -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 +93 -0
- package/tests/phase5.test.ts +73 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.1.0](https://github.com/bmartel/calyx/compare/v1.0.0...v1.1.0) (2026-07-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* implement reflector, setMetadata, headers tuning, and Bun-native CLI ([5ebb132](https://github.com/bmartel/calyx/commit/5ebb132a05aea49da9a11fda9dff60e651393884))
|
|
7
|
+
|
|
1
8
|
# 1.0.0 (2026-07-01)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martel/calyx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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,323 @@
|
|
|
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 packageJson = {
|
|
113
|
+
name,
|
|
114
|
+
version: '0.0.1',
|
|
115
|
+
description: 'Calyx application',
|
|
116
|
+
type: 'module',
|
|
117
|
+
scripts: {
|
|
118
|
+
"start": "calyx start",
|
|
119
|
+
"start:dev": "calyx start --watch",
|
|
120
|
+
"build": "calyx build"
|
|
121
|
+
},
|
|
122
|
+
dependencies: {
|
|
123
|
+
"@martel/calyx": "link:../..", // link back to Calyx package parent root for local tests
|
|
124
|
+
"reflect-metadata": "^0.2.2"
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
writeFileSync(join(name, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
128
|
+
|
|
129
|
+
// Write tsconfig.json
|
|
130
|
+
const tsconfigJson = {
|
|
131
|
+
compilerOptions: {
|
|
132
|
+
module: "ESNext",
|
|
133
|
+
target: "ESNext",
|
|
134
|
+
moduleResolution: "bundler",
|
|
135
|
+
esModuleInterop: true,
|
|
136
|
+
experimentalDecorators: true,
|
|
137
|
+
emitDecoratorMetadata: true,
|
|
138
|
+
strict: true,
|
|
139
|
+
skipLibCheck: true
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
writeFileSync(join(name, 'tsconfig.json'), JSON.stringify(tsconfigJson, null, 2));
|
|
143
|
+
|
|
144
|
+
// Write src/app.service.ts
|
|
145
|
+
const appService = `import { Injectable } from '@martel/calyx';
|
|
146
|
+
|
|
147
|
+
@Injectable()
|
|
148
|
+
export class AppService {
|
|
149
|
+
getHello(): string {
|
|
150
|
+
return 'Hello World!';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
writeFileSync(join(name, 'src/app.service.ts'), appService);
|
|
155
|
+
|
|
156
|
+
// Write src/app.controller.ts
|
|
157
|
+
const appController = `import { Controller, Get } from '@martel/calyx';
|
|
158
|
+
import { AppService } from './app.service';
|
|
159
|
+
|
|
160
|
+
@Controller()
|
|
161
|
+
export class AppController {
|
|
162
|
+
constructor(private readonly appService: AppService) {}
|
|
163
|
+
|
|
164
|
+
@Get()
|
|
165
|
+
getHello(): string {
|
|
166
|
+
return this.appService.getHello();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
writeFileSync(join(name, 'src/app.controller.ts'), appController);
|
|
171
|
+
|
|
172
|
+
// Write src/app.module.ts
|
|
173
|
+
const appModule = `import { Module } from '@martel/calyx';
|
|
174
|
+
import { AppController } from './app.controller';
|
|
175
|
+
import { AppService } from './app.service';
|
|
176
|
+
|
|
177
|
+
@Module({
|
|
178
|
+
controllers: [AppController],
|
|
179
|
+
providers: [AppService],
|
|
180
|
+
})
|
|
181
|
+
export class AppModule {}
|
|
182
|
+
`;
|
|
183
|
+
writeFileSync(join(name, 'src/app.module.ts'), appModule);
|
|
184
|
+
|
|
185
|
+
// Write src/main.ts
|
|
186
|
+
const mainTs = `import 'reflect-metadata';
|
|
187
|
+
import { CalyxFactory } from '@martel/calyx';
|
|
188
|
+
import { AppModule } from './app.module';
|
|
189
|
+
|
|
190
|
+
async function bootstrap() {
|
|
191
|
+
const app = await CalyxFactory.create(AppModule);
|
|
192
|
+
await app.listen(3000);
|
|
193
|
+
console.log('Application is running on http://localhost:3000');
|
|
194
|
+
}
|
|
195
|
+
bootstrap();
|
|
196
|
+
`;
|
|
197
|
+
writeFileSync(join(name, 'src/main.ts'), mainTs);
|
|
198
|
+
|
|
199
|
+
console.log('Installing dependencies...');
|
|
200
|
+
spawnSync('bun', ['install'], { cwd: name, stdio: 'inherit' });
|
|
201
|
+
|
|
202
|
+
console.log(`
|
|
203
|
+
Calyx application successfully created!
|
|
204
|
+
|
|
205
|
+
To start running your app:
|
|
206
|
+
cd ${name}
|
|
207
|
+
bun run start:dev
|
|
208
|
+
`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function runGenerate(schematic: string, rawName: string) {
|
|
212
|
+
if (!schematic || !rawName) {
|
|
213
|
+
console.error('Error: Please specify schematic type and name. Example: calyx g controller users');
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const name = rawName.toLowerCase();
|
|
218
|
+
const pascalName = rawName.charAt(0).toUpperCase() + rawName.slice(1);
|
|
219
|
+
const type = schematic.toLowerCase();
|
|
220
|
+
|
|
221
|
+
const srcDir = existsSync('src') ? 'src' : '.';
|
|
222
|
+
const targetDir = join(srcDir, name);
|
|
223
|
+
|
|
224
|
+
if (!existsSync(targetDir)) {
|
|
225
|
+
mkdirSync(targetDir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
switch (type) {
|
|
229
|
+
case 'module': {
|
|
230
|
+
const filePath = join(targetDir, `${name}.module.ts`);
|
|
231
|
+
const content = `import { Module } from '@martel/calyx';
|
|
232
|
+
|
|
233
|
+
@Module({})
|
|
234
|
+
export class ${pascalName}Module {}
|
|
235
|
+
`;
|
|
236
|
+
writeFileSync(filePath, content);
|
|
237
|
+
console.log(`CREATE ${filePath}`);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'controller': {
|
|
241
|
+
const filePath = join(targetDir, `${name}.controller.ts`);
|
|
242
|
+
const content = `import { Controller, Get } from '@martel/calyx';
|
|
243
|
+
|
|
244
|
+
@Controller('${name}')
|
|
245
|
+
export class ${pascalName}Controller {
|
|
246
|
+
@Get()
|
|
247
|
+
findAll() {
|
|
248
|
+
return 'This action returns all ${name}';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
writeFileSync(filePath, content);
|
|
253
|
+
console.log(`CREATE ${filePath}`);
|
|
254
|
+
autoRegisterInModule(name, pascalName, 'controller');
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'service': {
|
|
258
|
+
const filePath = join(targetDir, `${name}.service.ts`);
|
|
259
|
+
const content = `import { Injectable } from '@martel/calyx';
|
|
260
|
+
|
|
261
|
+
@Injectable()
|
|
262
|
+
export class ${pascalName}Service {}
|
|
263
|
+
`;
|
|
264
|
+
writeFileSync(filePath, content);
|
|
265
|
+
console.log(`CREATE ${filePath}`);
|
|
266
|
+
autoRegisterInModule(name, pascalName, 'service');
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
console.error(`Error: Unknown schematic type "${type}". Supported: module, controller, service`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function autoRegisterInModule(name: string, pascalName: string, type: 'controller' | 'service') {
|
|
276
|
+
const srcDir = existsSync('src') ? 'src' : '.';
|
|
277
|
+
const modulePath = join(srcDir, name, `${name}.module.ts`);
|
|
278
|
+
const rootModulePath = join(srcDir, `app.module.ts`);
|
|
279
|
+
|
|
280
|
+
const pathToCheck = existsSync(modulePath) ? modulePath : (existsSync(rootModulePath) ? rootModulePath : null);
|
|
281
|
+
if (!pathToCheck) return;
|
|
282
|
+
|
|
283
|
+
let content = readFileSync(pathToCheck, 'utf-8');
|
|
284
|
+
|
|
285
|
+
// Insert import statement
|
|
286
|
+
const importName = `${pascalName}${type === 'controller' ? 'Controller' : 'Service'}`;
|
|
287
|
+
const importRelativePath = pathToCheck === rootModulePath ? `./${name}/${name}.${type}` : `./${name}.${type}`;
|
|
288
|
+
const importStatement = `import { ${importName} } from '${importRelativePath}';\n`;
|
|
289
|
+
|
|
290
|
+
// Find last import line and insert after it
|
|
291
|
+
const lines = content.split('\n');
|
|
292
|
+
let lastImportIdx = -1;
|
|
293
|
+
for (let i = 0; i < lines.length; i++) {
|
|
294
|
+
if (lines[i].trim().startsWith('import ')) {
|
|
295
|
+
lastImportIdx = i;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
lines.splice(lastImportIdx + 1, 0, importStatement.trim());
|
|
299
|
+
content = lines.join('\n');
|
|
300
|
+
|
|
301
|
+
// Add item into metadata decorator
|
|
302
|
+
const arrayName = type === 'controller' ? 'controllers' : 'providers';
|
|
303
|
+
const arrayRegex = new RegExp(`(${arrayName}\\s*:\\s*\\[)([^\\]]*)(\\])`);
|
|
304
|
+
|
|
305
|
+
if (arrayRegex.test(content)) {
|
|
306
|
+
content = content.replace(arrayRegex, (match, prefix, list, suffix) => {
|
|
307
|
+
const trimmedList = list.trim();
|
|
308
|
+
const newList = trimmedList ? `${trimmedList}, ${importName}` : importName;
|
|
309
|
+
return `${prefix}${newList}${suffix}`;
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
// Add the metadata property if it doesn't exist
|
|
313
|
+
const decoratorRegex = /(@Module\(\s*\{)([\s\S]*?)(\}\s*\))/;
|
|
314
|
+
content = content.replace(decoratorRegex, (match, prefix, body, suffix) => {
|
|
315
|
+
const trimmedBody = body.trim();
|
|
316
|
+
const newBody = trimmedBody ? `${trimmedBody},\n ${arrayName}: [${importName}]` : ` ${arrayName}: [${importName}]`;
|
|
317
|
+
return `${prefix}\n${newBody}\n${suffix}`;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
writeFileSync(pathToCheck, content);
|
|
322
|
+
console.log(`UPDATE ${pathToCheck} (Registered ${importName})`);
|
|
323
|
+
}
|
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,93 @@
|
|
|
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, 'node_modules'))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should generate controllers, services, and modules with auto-registration', () => {
|
|
43
|
+
// Generate a controller
|
|
44
|
+
const genCtrl = spawnSync('bun', ['../../src/cli/index.ts', 'g', 'controller', 'users'], {
|
|
45
|
+
cwd: testAppName,
|
|
46
|
+
encoding: 'utf-8',
|
|
47
|
+
});
|
|
48
|
+
expect(genCtrl.status).toBe(0);
|
|
49
|
+
expect(existsSync(join(testAppName, 'src/users/users.controller.ts'))).toBe(true);
|
|
50
|
+
|
|
51
|
+
// Verify registration inside app.module.ts
|
|
52
|
+
const moduleContent = readFileSync(join(testAppName, 'src/app.module.ts'), 'utf-8');
|
|
53
|
+
expect(moduleContent).toContain("import { UsersController } from './users/users.controller';");
|
|
54
|
+
expect(moduleContent).toContain('controllers: [AppController, UsersController]');
|
|
55
|
+
|
|
56
|
+
// Generate a service
|
|
57
|
+
const genSvc = spawnSync('bun', ['../../src/cli/index.ts', 'g', 'service', 'users'], {
|
|
58
|
+
cwd: testAppName,
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
});
|
|
61
|
+
expect(genSvc.status).toBe(0);
|
|
62
|
+
expect(existsSync(join(testAppName, 'src/users/users.service.ts'))).toBe(true);
|
|
63
|
+
|
|
64
|
+
const moduleContent2 = readFileSync(join(testAppName, 'src/app.module.ts'), 'utf-8');
|
|
65
|
+
expect(moduleContent2).toContain("import { UsersService } from './users/users.service';");
|
|
66
|
+
expect(moduleContent2).toContain('providers: [AppService, UsersService]');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should build the application using Bun compiler', () => {
|
|
70
|
+
// Manually modify package.json to point to local CLI script for Windows/Link test compatibility
|
|
71
|
+
const pkgPath = join(testAppName, 'package.json');
|
|
72
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
73
|
+
pkg.scripts.build = "bun ../../src/cli/index.ts build";
|
|
74
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
75
|
+
|
|
76
|
+
// Manually add path mapping to tsconfig.json for TypeScript source resolution during tests
|
|
77
|
+
const tsconfigPath = join(testAppName, 'tsconfig.json');
|
|
78
|
+
const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf-8'));
|
|
79
|
+
tsconfig.compilerOptions.paths = {
|
|
80
|
+
"@martel/calyx": ["../../src/index.ts"]
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
83
|
+
|
|
84
|
+
const buildProc = spawnSync('bun', ['run', 'build'], {
|
|
85
|
+
cwd: testAppName,
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
});
|
|
88
|
+
console.log('Build STDOUT:', buildProc.stdout);
|
|
89
|
+
console.log('Build STDERR:', buildProc.stderr);
|
|
90
|
+
expect(buildProc.status).toBe(0);
|
|
91
|
+
expect(existsSync(join(testAppName, 'dist/main.js'))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -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
|
+
});
|