@avleon/cli 0.0.44 → 0.0.45
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/dist/cli.js +49 -49
- package/dist/commands.js +10 -10
- package/dist/create.js +413 -263
- package/dist/serve.js +22 -16
- package/package.json +61 -51
- package/readme.md +66 -66
- package/.github/workflows/publish.yml +0 -69
- package/stubs/app/app-controller.stub +0 -11
- package/stubs/app/app-minimal.stub +0 -17
- package/stubs/app/app-withdb.stub +0 -0
- package/stubs/basic-controller.stub +0 -7
- package/stubs/config.stub +0 -11
- package/stubs/controllers/basic-controller.stub +0 -12
- package/stubs/controllers/orm-rest-controller.stub +0 -52
- package/stubs/controllers/rest-controller.stub +0 -34
- package/stubs/model.stub +0 -0
- package/stubs/models/basic-model.stub +0 -6
- package/stubs/models/entity-model.stub +0 -23
- package/stubs/serve.stub +0 -10
- package/stubs/services/basic-service.stub +0 -8
- package/stubs/services/orm-rest-service.stub +0 -73
- package/stubs/services/rest-service.stub +0 -71
- package/tsconfig.json +0 -16
package/dist/create.js
CHANGED
|
@@ -44,305 +44,455 @@ const path_1 = __importDefault(require("path"));
|
|
|
44
44
|
const os_1 = __importDefault(require("os"));
|
|
45
45
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
46
46
|
const child_process_1 = require("child_process");
|
|
47
|
-
const util_1 = __importDefault(require("util"));
|
|
48
47
|
const lodash_1 = require("lodash");
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
// ─── Colors ───────────────────────────────────────────────────────────────────
|
|
49
|
+
const C = {
|
|
50
|
+
reset: '\x1b[0m',
|
|
51
|
+
dim: '\x1b[90m',
|
|
52
|
+
bold: '\x1b[1m',
|
|
53
|
+
cyan: '\x1b[36m',
|
|
54
|
+
green: '\x1b[32m',
|
|
55
|
+
yellow: '\x1b[33m',
|
|
56
|
+
red: '\x1b[31m',
|
|
57
|
+
white: '\x1b[37m',
|
|
58
|
+
};
|
|
59
|
+
const icon = {
|
|
60
|
+
success: `${C.green}✔${C.reset}`,
|
|
61
|
+
error: `${C.red}✖${C.reset}`,
|
|
62
|
+
warn: `${C.yellow}⚠${C.reset}`,
|
|
63
|
+
arrow: `${C.dim}→${C.reset}`,
|
|
55
64
|
};
|
|
65
|
+
function step(label, detail) {
|
|
66
|
+
const pad = label.padEnd(30, ' ');
|
|
67
|
+
console.log(` ${icon.arrow} ${C.white}${pad}${C.reset}${detail ? C.dim + detail + C.reset : ''}`);
|
|
68
|
+
}
|
|
69
|
+
function success(msg) {
|
|
70
|
+
console.log(` ${icon.success} ${C.green}${msg}${C.reset}`);
|
|
71
|
+
}
|
|
72
|
+
function fail(msg) {
|
|
73
|
+
console.log(` ${icon.error} ${C.red}${msg}${C.reset}`);
|
|
74
|
+
}
|
|
75
|
+
function section(title) {
|
|
76
|
+
console.log(`\n${C.bold}${C.cyan} ${title}${C.reset}`);
|
|
77
|
+
console.log(` ${'─'.repeat(40)}`);
|
|
78
|
+
}
|
|
79
|
+
// ─── Version resolver ─────────────────────────────────────────────────────────
|
|
80
|
+
function resolveDepVersion(pkgName, fallback = 'latest') {
|
|
81
|
+
try {
|
|
82
|
+
const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
|
|
83
|
+
const pkg = JSON.parse(fs_extra_1.default.readFileSync(pkgJsonPath, 'utf8'));
|
|
84
|
+
return `^${pkg.version}`;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function getAvleonVersions() {
|
|
91
|
+
try {
|
|
92
|
+
// Read CLI's own package.json (this file lives inside @avleon/cli)
|
|
93
|
+
const cliPkgPath = path_1.default.resolve(__dirname, '../../package.json');
|
|
94
|
+
const cliPkg = JSON.parse(fs_extra_1.default.readFileSync(cliPkgPath, 'utf8'));
|
|
95
|
+
// Core version — resolve from installed node_modules
|
|
96
|
+
const corePkgPath = require.resolve('@avleon/core/package.json');
|
|
97
|
+
const corePkg = JSON.parse(fs_extra_1.default.readFileSync(corePkgPath, 'utf8'));
|
|
98
|
+
return {
|
|
99
|
+
core: `^${corePkg.version}`,
|
|
100
|
+
cli: `^${cliPkg.version}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { core: 'latest', cli: 'latest' };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
108
|
+
function banner(fname) {
|
|
109
|
+
const { core } = getAvleonVersions();
|
|
110
|
+
console.clear();
|
|
111
|
+
console.log();
|
|
112
|
+
console.log(`${C.bold}${C.cyan} ╔═══════════════════════════════════════╗${C.reset}`);
|
|
113
|
+
console.log(`${C.bold}${C.cyan} ║ AVLEON APPLICATION CLI ║${C.reset}`);
|
|
114
|
+
console.log(`${C.bold}${C.cyan} ╚═══════════════════════════════════════╝${C.reset}`);
|
|
115
|
+
console.log();
|
|
116
|
+
console.log(` ${C.dim}Creating project :${C.reset} ${C.bold}${C.white}${fname}${C.reset}`);
|
|
117
|
+
console.log(` ${C.dim}Avleon version :${C.reset} ${C.bold}${C.cyan}${core}${C.reset}`);
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
// ─── Shell ────────────────────────────────────────────────────────────────────
|
|
121
|
+
const getShell = () => os_1.default.platform() === 'win32'
|
|
122
|
+
? process.env.ComSpec || 'cmd.exe'
|
|
123
|
+
: process.env.SHELL || '/bin/sh';
|
|
124
|
+
// ─── Prompts ──────────────────────────────────────────────────────────────────
|
|
56
125
|
async function askForPackageManager() {
|
|
57
126
|
const { packageManager } = await inquirer_1.default.prompt([
|
|
58
127
|
{
|
|
59
128
|
type: 'list',
|
|
60
129
|
name: 'packageManager',
|
|
61
|
-
message: '
|
|
62
|
-
choices: [
|
|
63
|
-
|
|
64
|
-
|
|
130
|
+
message: 'Package manager:',
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: 'npm', value: 'npm' },
|
|
133
|
+
{ name: 'yarn', value: 'yarn' },
|
|
134
|
+
{ name: 'pnpm (recommended)', value: 'pnpm' },
|
|
135
|
+
],
|
|
136
|
+
default: 'npm',
|
|
137
|
+
},
|
|
65
138
|
]);
|
|
66
139
|
return packageManager;
|
|
67
140
|
}
|
|
68
|
-
async function
|
|
141
|
+
async function askForFeatures() {
|
|
142
|
+
const { features } = await inquirer_1.default.prompt([
|
|
143
|
+
{
|
|
144
|
+
type: 'checkbox',
|
|
145
|
+
name: 'features',
|
|
146
|
+
message: 'Select features to include:',
|
|
147
|
+
choices: [
|
|
148
|
+
{ name: 'OpenAPI / Swagger docs', value: 'openapi', checked: true },
|
|
149
|
+
{ name: 'CORS support', value: 'cors', checked: false },
|
|
150
|
+
{ name: 'TypeORM database', value: 'typeorm', checked: false },
|
|
151
|
+
{ name: 'Scalar UI (replaces Swagger UI)', value: 'scalar', checked: false },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
return features;
|
|
156
|
+
}
|
|
157
|
+
// ─── Installer ────────────────────────────────────────────────────────────────
|
|
158
|
+
async function installDependencies(pm, cwd) {
|
|
69
159
|
return new Promise((resolve, reject) => {
|
|
70
160
|
const child = (0, child_process_1.spawn)(pm, ['install'], {
|
|
71
|
-
cwd
|
|
161
|
+
cwd,
|
|
72
162
|
shell: getShell(),
|
|
73
163
|
stdio: 'inherit',
|
|
74
164
|
});
|
|
75
165
|
child.on('exit', (code) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
console.log(pm == 'npm' ? `npm run start:dev` : `${pm} start:dev`);
|
|
80
|
-
resolve();
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
console.error(`❌ Installation failed with exit code ${code}`);
|
|
84
|
-
reject(new Error(`Install failed with code ${code}`));
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
child.on('error', (err) => {
|
|
88
|
-
console.error('❌ Failed to start install process:', err);
|
|
89
|
-
reject(err);
|
|
166
|
+
code === 0
|
|
167
|
+
? resolve()
|
|
168
|
+
: reject(new Error(`Install failed with exit code ${code}`));
|
|
90
169
|
});
|
|
170
|
+
child.on('error', reject);
|
|
91
171
|
});
|
|
92
172
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// const dirs = Object.values(appPaths);
|
|
97
|
-
// for (let i = 0; i < dirs.length; i++) {
|
|
98
|
-
// await fs.ensureDir(dirs[i]);
|
|
99
|
-
// }
|
|
100
|
-
await fs_extra_1.default.ensureDir(appPaths.root + "/src");
|
|
101
|
-
await fs_extra_1.default.ensureDir(appPaths.root + "/src/controllers");
|
|
102
|
-
await fs_extra_1.default.ensureDir(appPaths.root + "/src/config");
|
|
103
|
-
await fs_extra_1.default.ensureDir(appPaths.root + "/src/services");
|
|
104
|
-
// await fs.ensureDir(appPaths.root + "/src/models");
|
|
105
|
-
await fs_extra_1.default.ensureDir(appPaths.root + "/test");
|
|
106
|
-
await fs_extra_1.default.ensureDir(appPaths.root + "/public");
|
|
107
|
-
// copy
|
|
108
|
-
// home controller
|
|
109
|
-
const homeService = `
|
|
110
|
-
import { AppService } from '@avleon/core';
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@AppService
|
|
114
|
-
export class HomeService{
|
|
115
|
-
|
|
116
|
-
sayHello(){
|
|
117
|
-
return "Hello World!";
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
`;
|
|
121
|
-
const formatHomeService = await prettier.format(homeService, {
|
|
173
|
+
// ─── Format helper ────────────────────────────────────────────────────────────
|
|
174
|
+
async function fmt(source, parser = 'typescript') {
|
|
175
|
+
return prettier.format(source, {
|
|
122
176
|
singleQuote: true,
|
|
123
177
|
singleAttributePerLine: true,
|
|
124
|
-
parser
|
|
178
|
+
parser,
|
|
125
179
|
});
|
|
126
|
-
await fs_extra_1.default.writeFile(path_1.default.join(appPaths.services, "home.service.ts"), formatHomeService);
|
|
127
|
-
// home controller
|
|
128
|
-
const homeContrlelr = `
|
|
129
|
-
import { ApiController, Get } from '@avleon/core';
|
|
130
|
-
import { HomeService } from '../services/home.service';
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
@ApiController
|
|
134
|
-
export class HomeController{
|
|
135
|
-
|
|
136
|
-
constructor(
|
|
137
|
-
private readonly homeService: HomeService
|
|
138
|
-
){}
|
|
139
|
-
|
|
140
|
-
@Get()
|
|
141
|
-
sayHello(){
|
|
142
|
-
return this.homeService.sayHello();
|
|
143
|
-
}
|
|
144
180
|
}
|
|
181
|
+
// ─── File writer ──────────────────────────────────────────────────────────────
|
|
182
|
+
async function writeFile(filePath, content, label) {
|
|
183
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
|
|
184
|
+
await fs_extra_1.default.writeFile(filePath, content);
|
|
185
|
+
if (label)
|
|
186
|
+
step(label, path_1.default.relative(process.cwd(), filePath));
|
|
187
|
+
}
|
|
188
|
+
// ─── Templates ────────────────────────────────────────────────────────────────
|
|
189
|
+
function homeServiceTemplate() {
|
|
190
|
+
return `
|
|
191
|
+
import { AppService } from '@avleon/core';
|
|
192
|
+
|
|
193
|
+
@AppService
|
|
194
|
+
export class HomeService {
|
|
195
|
+
sayHello() {
|
|
196
|
+
return { message: 'Hello World!' };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
145
199
|
`;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('should be defined', () => {
|
|
164
|
-
expect(controller).toBeDefined()
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('should have sayhello function', () => {
|
|
168
|
-
expect(controller.sayHello).toBeDefined
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('should return sayhello', () => {
|
|
172
|
-
expect(controller.sayHello()).toBe("Hello World!");
|
|
173
|
-
})
|
|
174
|
-
})
|
|
200
|
+
}
|
|
201
|
+
function homeControllerTemplate() {
|
|
202
|
+
return `
|
|
203
|
+
import { ApiController, Get } from '@avleon/core';
|
|
204
|
+
import { HomeService } from '../services/home.service';
|
|
205
|
+
|
|
206
|
+
@ApiController('/')
|
|
207
|
+
export class HomeController {
|
|
208
|
+
constructor(private readonly homeService: HomeService) {}
|
|
209
|
+
|
|
210
|
+
@Get()
|
|
211
|
+
sayHello() {
|
|
212
|
+
return this.homeService.sayHello();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
175
215
|
`;
|
|
176
|
-
const formatHomeControllerTest = await prettier.format(homeContrlelrTest, {
|
|
177
|
-
singleQuote: true,
|
|
178
|
-
singleAttributePerLine: true,
|
|
179
|
-
parser: "typescript",
|
|
180
|
-
});
|
|
181
|
-
await fs_extra_1.default.writeFile(path_1.default.join(appPaths.controllers, "home.controller.spec.ts"), formatHomeControllerTest);
|
|
182
|
-
// openapi config file
|
|
183
|
-
const openapiConfigFile = `
|
|
184
|
-
import { AppConfig, Environment, IConfig } from "@avleon/core";
|
|
185
|
-
|
|
186
|
-
@AppConfig
|
|
187
|
-
export class OpenApiConfig implements IConfig {
|
|
188
|
-
|
|
189
|
-
config(env: Environment) {
|
|
190
|
-
return {
|
|
191
|
-
info: {
|
|
192
|
-
title: '${(0, lodash_1.capitalize)(fname)}',
|
|
193
|
-
version: '0.0.1',
|
|
194
|
-
},
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
216
|
}
|
|
217
|
+
function homeControllerTestTemplate() {
|
|
218
|
+
return `
|
|
219
|
+
import { AvleonTest } from '@avleon/core';
|
|
220
|
+
import { HomeController } from './home.controller';
|
|
221
|
+
|
|
222
|
+
describe('HomeController', () => {
|
|
223
|
+
let controller!: HomeController;
|
|
224
|
+
|
|
225
|
+
beforeAll(() => {
|
|
226
|
+
controller = AvleonTest.getController(HomeController);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should be defined', () => {
|
|
230
|
+
expect(controller).toBeDefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should have sayHello method', () => {
|
|
234
|
+
expect(typeof controller.sayHello).toBe('function');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should return hello message', () => {
|
|
238
|
+
expect(controller.sayHello()).toEqual({ message: 'Hello World!' });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
198
241
|
`;
|
|
199
|
-
const formatConfigFile = await prettier.format(openapiConfigFile, {
|
|
200
|
-
singleQuote: true,
|
|
201
|
-
singleAttributePerLine: true,
|
|
202
|
-
parser: "typescript",
|
|
203
|
-
});
|
|
204
|
-
await fs_extra_1.default.writeFile(path_1.default.join(appPaths.configs, "openapi.config.ts"), formatConfigFile);
|
|
205
|
-
// main file
|
|
206
|
-
const mainFunc = `
|
|
207
|
-
import { Avleon } from '@avleon/core';
|
|
208
|
-
import { OpenApiConfig } from './config/openapi.config';
|
|
209
|
-
import { HomeController } from './controllers/home.controller';
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const app = Avleon.createApplication();
|
|
213
|
-
|
|
214
|
-
if(app.isDevelopment()){
|
|
215
|
-
app.useOpenApi(OpenApiConfig);
|
|
216
242
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
function openApiConfigTemplate(fname, useScalar) {
|
|
244
|
+
return `
|
|
245
|
+
import { AppConfig, Environment, IConfig } from '@avleon/core';
|
|
246
|
+
|
|
247
|
+
@AppConfig
|
|
248
|
+
export class OpenApiConfig implements IConfig {
|
|
249
|
+
config(env: Environment) {
|
|
250
|
+
return {
|
|
251
|
+
info: {
|
|
252
|
+
title: '${(0, lodash_1.capitalize)(fname)} API',
|
|
253
|
+
version: '0.0.1',
|
|
254
|
+
description: 'API documentation for ${(0, lodash_1.capitalize)(fname)}',
|
|
255
|
+
},
|
|
256
|
+
${useScalar ? "provider: 'scalar'," : ''}
|
|
257
|
+
routePrefix: '/docs',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
222
261
|
`;
|
|
223
|
-
const formatMainFunc = await prettier.format(mainFunc, {
|
|
224
|
-
singleQuote: true,
|
|
225
|
-
singleAttributePerLine: true,
|
|
226
|
-
parser: "typescript",
|
|
227
|
-
});
|
|
228
|
-
await fs_extra_1.default.writeFile(path_1.default.join(appPaths.src, "app.ts"), formatMainFunc);
|
|
229
|
-
// create serve.ts
|
|
230
|
-
const serveFunc = `
|
|
231
|
-
import app from "./app";
|
|
232
|
-
|
|
233
|
-
async function start(){
|
|
234
|
-
app.run(); // default run on :4000
|
|
235
262
|
}
|
|
236
|
-
|
|
237
|
-
|
|
263
|
+
function appTemplate(features) {
|
|
264
|
+
const hasOpenApi = features.includes('openapi');
|
|
265
|
+
const hasCors = features.includes('cors');
|
|
266
|
+
const hasTypeorm = features.includes('typeorm');
|
|
267
|
+
const imports = [`import { Avleon } from '@avleon/core';`];
|
|
268
|
+
imports.push(`import { HomeController } from './controllers/home.controller';`);
|
|
269
|
+
if (hasOpenApi) {
|
|
270
|
+
imports.push(`import { OpenApiConfig } from './config/openapi.config';`);
|
|
271
|
+
}
|
|
272
|
+
const body = [`const app = Avleon.createApplication();`, ''];
|
|
273
|
+
if (hasCors) {
|
|
274
|
+
body.push(`app.useCors({ origin: '*' });`);
|
|
275
|
+
}
|
|
276
|
+
if (hasOpenApi) {
|
|
277
|
+
body.push(`if (app.isDevelopment()) {`);
|
|
278
|
+
body.push(` app.useOpenApi(OpenApiConfig);`);
|
|
279
|
+
body.push(`}`);
|
|
280
|
+
body.push('');
|
|
281
|
+
}
|
|
282
|
+
if (hasTypeorm) {
|
|
283
|
+
body.push(`// TODO: configure datasource in src/config/database.config.ts`);
|
|
284
|
+
body.push(`// app.useDatasource(AppDataSource);`);
|
|
285
|
+
body.push('');
|
|
286
|
+
}
|
|
287
|
+
body.push(`app.useControllers([HomeController]);`);
|
|
288
|
+
body.push('');
|
|
289
|
+
body.push(`export default app;`);
|
|
290
|
+
return `${imports.join('\n')}\n\n${body.join('\n')}\n`;
|
|
291
|
+
}
|
|
292
|
+
function serveTemplate() {
|
|
293
|
+
return `
|
|
294
|
+
import app from './app';
|
|
295
|
+
|
|
296
|
+
async function start() {
|
|
297
|
+
await app.run(); // default port: 4000
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
start();
|
|
238
301
|
`;
|
|
239
|
-
const formatServeFunc = await prettier.format(serveFunc, {
|
|
240
|
-
singleQuote: true,
|
|
241
|
-
singleAttributePerLine: true,
|
|
242
|
-
parser: "typescript",
|
|
243
|
-
});
|
|
244
|
-
await fs_extra_1.default.writeFile(path_1.default.join(appPaths.src, "serve.ts"), formatServeFunc);
|
|
245
|
-
// create package.json
|
|
246
|
-
const packageJson = `
|
|
247
|
-
{
|
|
248
|
-
"name": "${(0, lodash_1.snakeCase)(fname)}",
|
|
249
|
-
"version": "0.0.1",
|
|
250
|
-
"description": "Basic avleon application",
|
|
251
|
-
"main": "dist/serve.js",
|
|
252
|
-
"scripts": {
|
|
253
|
-
"start:dev": "nodemon ./src/serve.ts",
|
|
254
|
-
"start:debug": "nodemon --watch ./src --ext ts --exec node --inspect=0.0.0.0:9229 -r ts-node/register ./src/serve.ts",
|
|
255
|
-
"clean": "rimraf ./dist",
|
|
256
|
-
"build": "npm run clean & tsc --project ./tsconfig.build.json",
|
|
257
|
-
"start": "node ./dist/serve.js",
|
|
258
|
-
"test": "jest",
|
|
259
|
-
"test:watch": "jest --watch",
|
|
260
|
-
"test:cov": "jest --coverage",
|
|
261
|
-
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
262
|
-
"test:e2e": "jest --config ./test/e2e-spec.json"
|
|
263
|
-
},
|
|
264
|
-
"keywords": ["avleon","nodejs","typescript"],
|
|
265
|
-
"license": "ISC",
|
|
266
|
-
"dependencies": {
|
|
267
|
-
"@avleon/core": "^0.0.36",
|
|
268
|
-
"@avleon/cli": "^0.0.42",
|
|
269
|
-
"class-transformer": "^0.5.1",
|
|
270
|
-
"class-validator": "^0.14.1",
|
|
271
|
-
"rimraf": "^6.0.1",
|
|
272
|
-
"reflect-metadata": "^0.2.2"
|
|
273
|
-
},
|
|
274
|
-
"devDependencies": {
|
|
275
|
-
"@types/jest": "^29.5.14",
|
|
276
|
-
"@types/node": "^24.0.3",
|
|
277
|
-
"jest": "^29.7.0",
|
|
278
|
-
"ts-jest": "^29.2.6",
|
|
279
|
-
"nodemon": "^3.1.7",
|
|
280
|
-
"ts-node": "^10.9.2",
|
|
281
|
-
"typescript": "^5.7.2"
|
|
282
|
-
},
|
|
283
|
-
"jest": {
|
|
284
|
-
"preset": "ts-jest",
|
|
285
|
-
"rootDir": "src",
|
|
286
|
-
"testRegex": ".*.spec.ts$",
|
|
287
|
-
"collectCoverageFrom": [
|
|
288
|
-
"**/*.(t|j)s"
|
|
289
|
-
],
|
|
290
|
-
"coverageDirectory": "../coverage",
|
|
291
|
-
"testEnvironment": "node"
|
|
292
|
-
}
|
|
293
|
-
|
|
294
302
|
}
|
|
303
|
+
function packageJsonTemplate(fname, features) {
|
|
304
|
+
// ✅ Always resolve current installed versions at generation time
|
|
305
|
+
const { core, cli } = getAvleonVersions();
|
|
306
|
+
const deps = {
|
|
307
|
+
'@avleon/core': core,
|
|
308
|
+
'@avleon/cli': cli,
|
|
309
|
+
'class-transformer': resolveDepVersion('class-transformer', '^0.5.1'),
|
|
310
|
+
'class-validator': resolveDepVersion('class-validator', '^0.14.1'),
|
|
311
|
+
'rimraf': resolveDepVersion('rimraf', '^6.0.1'),
|
|
312
|
+
'reflect-metadata': resolveDepVersion('reflect-metadata', '^0.2.2'),
|
|
313
|
+
};
|
|
314
|
+
if (features.includes('cors')) {
|
|
315
|
+
deps['@fastify/cors'] = resolveDepVersion('@fastify/cors', '^10.0.0');
|
|
316
|
+
}
|
|
317
|
+
if (features.includes('typeorm')) {
|
|
318
|
+
deps['typeorm'] = resolveDepVersion('typeorm', '^0.3.20');
|
|
319
|
+
deps['pg'] = resolveDepVersion('pg', '^8.11.0');
|
|
320
|
+
}
|
|
321
|
+
if (features.includes('scalar')) {
|
|
322
|
+
deps['@scalar/fastify-api-reference'] = resolveDepVersion('@scalar/fastify-api-reference', '^1.0.0');
|
|
323
|
+
}
|
|
324
|
+
const devDeps = {
|
|
325
|
+
'@types/jest': resolveDepVersion('@types/jest', '^29.5.14'),
|
|
326
|
+
'@types/node': resolveDepVersion('@types/node', '^24.0.3'),
|
|
327
|
+
'jest': resolveDepVersion('jest', '^29.7.0'),
|
|
328
|
+
'ts-jest': resolveDepVersion('ts-jest', '^29.2.6'),
|
|
329
|
+
'typescript': resolveDepVersion('typescript', '^5.7.2'),
|
|
330
|
+
};
|
|
331
|
+
return JSON.stringify({
|
|
332
|
+
name: (0, lodash_1.kebabCase)(fname),
|
|
333
|
+
version: '0.0.1',
|
|
334
|
+
description: `${(0, lodash_1.capitalize)(fname)} API built with Avleon`,
|
|
335
|
+
main: 'dist/serve.js',
|
|
336
|
+
scripts: {
|
|
337
|
+
'start:dev': 'avleon serve',
|
|
338
|
+
'clean': 'rimraf ./dist',
|
|
339
|
+
'build': 'npm run clean && tsc --project ./tsconfig.build.json',
|
|
340
|
+
'start': 'node ./dist/serve.js',
|
|
341
|
+
'test': 'jest',
|
|
342
|
+
'test:watch': 'jest --watch',
|
|
343
|
+
'test:cov': 'jest --coverage',
|
|
344
|
+
'test:e2e': 'jest --config ./test/e2e-spec.json',
|
|
345
|
+
},
|
|
346
|
+
keywords: ['avleon', 'nodejs', 'typescript'],
|
|
347
|
+
license: 'ISC',
|
|
348
|
+
dependencies: deps,
|
|
349
|
+
devDependencies: devDeps,
|
|
350
|
+
jest: {
|
|
351
|
+
preset: 'ts-jest',
|
|
352
|
+
rootDir: 'src',
|
|
353
|
+
testRegex: '.*.spec.ts$',
|
|
354
|
+
collectCoverageFrom: ['**/*.(t|j)s'],
|
|
355
|
+
coverageDirectory: '../coverage',
|
|
356
|
+
testEnvironment: 'node',
|
|
357
|
+
},
|
|
358
|
+
}, null, 2);
|
|
359
|
+
}
|
|
360
|
+
function tsconfigTemplate() {
|
|
361
|
+
return JSON.stringify({
|
|
362
|
+
compilerOptions: {
|
|
363
|
+
target: 'ES2017',
|
|
364
|
+
module: 'CommonJS',
|
|
365
|
+
moduleResolution: 'node',
|
|
366
|
+
experimentalDecorators: true,
|
|
367
|
+
emitDecoratorMetadata: true,
|
|
368
|
+
outDir: './dist',
|
|
369
|
+
esModuleInterop: true,
|
|
370
|
+
allowSyntheticDefaultImports: true,
|
|
371
|
+
forceConsistentCasingInFileNames: true,
|
|
372
|
+
skipLibCheck: true,
|
|
373
|
+
strict: false,
|
|
374
|
+
},
|
|
375
|
+
include: ['src/**/*'],
|
|
376
|
+
exclude: ['node_modules', 'dist'],
|
|
377
|
+
}, null, 2);
|
|
378
|
+
}
|
|
379
|
+
function tsconfigBuildTemplate() {
|
|
380
|
+
return JSON.stringify({
|
|
381
|
+
extends: './tsconfig.json',
|
|
382
|
+
exclude: ['node_modules', 'coverage', 'test', 'dist', '**/*spec.ts'],
|
|
383
|
+
}, null, 2);
|
|
384
|
+
}
|
|
385
|
+
function e2eConfigTemplate() {
|
|
386
|
+
return JSON.stringify({
|
|
387
|
+
moduleFileExtensions: ['js', 'json', 'ts'],
|
|
388
|
+
rootDir: '.',
|
|
389
|
+
testEnvironment: 'node',
|
|
390
|
+
testRegex: '.e2e-spec.ts$',
|
|
391
|
+
transform: { '^.+\\.(t|j)s$': 'ts-jest' },
|
|
392
|
+
}, null, 2);
|
|
393
|
+
}
|
|
394
|
+
function gitignoreTemplate() {
|
|
395
|
+
return `node_modules/\ndist/\n.env\n.env.local\ncoverage/\n`;
|
|
396
|
+
}
|
|
397
|
+
function envTemplate() {
|
|
398
|
+
return `NODE_ENV=development\nPORT=4000\n`;
|
|
399
|
+
}
|
|
400
|
+
function readmeTemplate(fname, pm) {
|
|
401
|
+
const runCmd = pm === 'npm' ? 'npm run start:dev' : `${pm} start:dev`;
|
|
402
|
+
return `# ${(0, lodash_1.capitalize)(fname)}
|
|
403
|
+
|
|
404
|
+
Built with [Avleon](https://github.com/xtareq/avleon).
|
|
405
|
+
|
|
406
|
+
## Getting Started
|
|
407
|
+
|
|
408
|
+
\`\`\`bash
|
|
409
|
+
${runCmd}
|
|
410
|
+
\`\`\`
|
|
411
|
+
|
|
412
|
+
## Scripts
|
|
413
|
+
|
|
414
|
+
| Script | Description |
|
|
415
|
+
|--------|-------------|
|
|
416
|
+
| \`start:dev\` | Start development server with hot reload |
|
|
417
|
+
| \`build\` | Compile TypeScript to JavaScript |
|
|
418
|
+
| \`start\` | Start production server |
|
|
419
|
+
| \`test\` | Run unit tests |
|
|
420
|
+
|
|
421
|
+
## Project Structure
|
|
422
|
+
|
|
423
|
+
\`\`\`
|
|
424
|
+
src/
|
|
425
|
+
├── controllers/ # Route controllers
|
|
426
|
+
├── services/ # Business logic
|
|
427
|
+
├── config/ # App configuration
|
|
428
|
+
├── app.ts # App setup
|
|
429
|
+
└── serve.ts # Entry point
|
|
430
|
+
\`\`\`
|
|
295
431
|
`;
|
|
296
|
-
const formattedPackageJson = await prettier.format(packageJson, {
|
|
297
|
-
parser: "json",
|
|
298
|
-
});
|
|
299
|
-
await fs_extra_1.default.writeFile(path_1.default.join(appPaths.root, "package.json"), formattedPackageJson);
|
|
300
|
-
const tsconfigJson = await prettier.format(`{
|
|
301
|
-
"compilerOptions": {
|
|
302
|
-
"target": "ES2017",
|
|
303
|
-
"module": "CommonJS",
|
|
304
|
-
"moduleResolution": "node",
|
|
305
|
-
"experimentalDecorators": true,
|
|
306
|
-
"emitDecoratorMetadata": true,
|
|
307
|
-
"outDir": "./dist",
|
|
308
|
-
"esModuleInterop": true,
|
|
309
|
-
"allowSyntheticDefaultImports": true,
|
|
310
|
-
"forceConsistentCasingInFileNames": true,
|
|
311
|
-
"skipLibCheck": true
|
|
312
|
-
},
|
|
313
|
-
"include": ["src/**/*"],
|
|
314
|
-
"exclude": ["node_modules", "dist"]
|
|
315
|
-
}`, { parser: "json" });
|
|
316
|
-
await fs_extra_1.default.writeFile(path_1.default.join(appPaths.root, "tsconfig.json"), tsconfigJson);
|
|
317
|
-
const tsconfigBuildJson = await prettier.format(`{
|
|
318
|
-
"extends": "./tsconfig.json",
|
|
319
|
-
"exclude": ["node_modules","coverage", "test", "dist", "**/*spec.ts"]
|
|
320
432
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
433
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
434
|
+
async function createApplication(fname, flags = {}) {
|
|
435
|
+
const appPaths = (0, paths_1.appPaths)(fname);
|
|
436
|
+
banner(fname);
|
|
437
|
+
// ── Prompts ───────────────────────────────────────────────────────────────
|
|
438
|
+
const pm = await askForPackageManager();
|
|
439
|
+
const features = await askForFeatures();
|
|
440
|
+
const useScalar = features.includes('scalar');
|
|
441
|
+
// ── Directories ───────────────────────────────────────────────────────────
|
|
442
|
+
section('Scaffolding project structure');
|
|
443
|
+
const dirs = [
|
|
444
|
+
appPaths.root + '/src',
|
|
445
|
+
appPaths.root + '/src/controllers',
|
|
446
|
+
appPaths.root + '/src/config',
|
|
447
|
+
appPaths.root + '/src/services',
|
|
448
|
+
appPaths.root + '/test',
|
|
449
|
+
appPaths.root + '/public',
|
|
450
|
+
];
|
|
451
|
+
for (const dir of dirs) {
|
|
452
|
+
await fs_extra_1.default.ensureDir(dir);
|
|
453
|
+
step('Created directory', path_1.default.relative(process.cwd(), dir));
|
|
454
|
+
}
|
|
455
|
+
// ── Source files ──────────────────────────────────────────────────────────
|
|
456
|
+
section('Generating source files');
|
|
457
|
+
await writeFile(path_1.default.join(appPaths.services, 'home.service.ts'), await fmt(homeServiceTemplate()), 'HomeService');
|
|
458
|
+
await writeFile(path_1.default.join(appPaths.controllers, 'home.controller.ts'), await fmt(homeControllerTemplate()), 'HomeController');
|
|
459
|
+
await writeFile(path_1.default.join(appPaths.controllers, 'home.controller.spec.ts'), await fmt(homeControllerTestTemplate()), 'HomeController spec');
|
|
460
|
+
if (features.includes('openapi')) {
|
|
461
|
+
await writeFile(path_1.default.join(appPaths.configs, 'openapi.config.ts'), await fmt(openApiConfigTemplate(fname, useScalar)), 'OpenApiConfig');
|
|
462
|
+
}
|
|
463
|
+
await writeFile(path_1.default.join(appPaths.src, 'app.ts'), await fmt(appTemplate(features)), 'app.ts');
|
|
464
|
+
await writeFile(path_1.default.join(appPaths.src, 'serve.ts'), await fmt(serveTemplate()), 'serve.ts');
|
|
465
|
+
// ── Config files ──────────────────────────────────────────────────────────
|
|
466
|
+
section('Generating config files');
|
|
467
|
+
await writeFile(path_1.default.join(appPaths.root, 'package.json'), await fmt(packageJsonTemplate(fname, features), 'json'), 'package.json');
|
|
468
|
+
await writeFile(path_1.default.join(appPaths.root, 'tsconfig.json'), await fmt(tsconfigTemplate(), 'json'), 'tsconfig.json');
|
|
469
|
+
await writeFile(path_1.default.join(appPaths.root, 'tsconfig.build.json'), await fmt(tsconfigBuildTemplate(), 'json'), 'tsconfig.build.json');
|
|
470
|
+
await writeFile(path_1.default.join(appPaths.root, 'test/e2e-spec.json'), await fmt(e2eConfigTemplate(), 'json'), 'e2e-spec.json');
|
|
471
|
+
await writeFile(path_1.default.join(appPaths.root, '.gitignore'), gitignoreTemplate(), '.gitignore');
|
|
472
|
+
await writeFile(path_1.default.join(appPaths.root, '.env'), envTemplate(), '.env');
|
|
473
|
+
await writeFile(path_1.default.join(appPaths.root, 'README.md'), readmeTemplate(fname, pm), 'README.md');
|
|
474
|
+
// ── Install ───────────────────────────────────────────────────────────────
|
|
475
|
+
section('Installing dependencies');
|
|
339
476
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
await installDependencies(pm, fname);
|
|
477
|
+
await installDependencies(pm, appPaths.root);
|
|
478
|
+
success('Dependencies installed');
|
|
343
479
|
}
|
|
344
480
|
catch (error) {
|
|
345
|
-
|
|
481
|
+
fail(`Installation failed: ${error instanceof Error ? error.message : error}`);
|
|
482
|
+
console.log(`\n ${C.yellow}Install manually:${C.reset}`);
|
|
483
|
+
console.log(` ${C.dim}cd ${fname} && ${pm} install${C.reset}\n`);
|
|
484
|
+
}
|
|
485
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
486
|
+
const runCmd = pm === 'npm' ? 'npm run start:dev' : `${pm} start:dev`;
|
|
487
|
+
console.log();
|
|
488
|
+
console.log(`${C.bold}${C.green} ✔ Project created successfully!${C.reset}`);
|
|
489
|
+
console.log();
|
|
490
|
+
console.log(` ${C.dim}Next steps:${C.reset}`);
|
|
491
|
+
console.log(` ${C.cyan} cd ${fname}${C.reset}`);
|
|
492
|
+
console.log(` ${C.cyan} ${runCmd}${C.reset}`);
|
|
493
|
+
if (features.includes('openapi')) {
|
|
494
|
+
console.log();
|
|
495
|
+
console.log(` ${C.dim}API docs:${C.reset} ${C.white}http://localhost:4000/docs${C.reset}`);
|
|
346
496
|
}
|
|
347
|
-
console.
|
|
497
|
+
console.log();
|
|
348
498
|
}
|