UNPKG

@avleon/cli

Version:

CLI for scaffolding and running Avleon applications

499 lines (480 loc) 21.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createApplication = createApplication; const fs_extra_1 = __importDefault(require("fs-extra")); const paths_1 = require("./paths"); const prettier = __importStar(require("prettier")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const inquirer_1 = __importDefault(require("inquirer")); const child_process_1 = require("child_process"); const lodash_1 = require("lodash"); // ─── Colors ─────────────────────────────────────────────────────────────────── const C = { reset: '\x1b[0m', dim: '\x1b[90m', bold: '\x1b[1m', cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', white: '\x1b[37m', }; const icon = { success: `${C.green}${C.reset}`, error: `${C.red}${C.reset}`, warn: `${C.yellow}${C.reset}`, arrow: `${C.dim}${C.reset}`, }; function step(label, detail) { const pad = label.padEnd(30, ' '); console.log(` ${icon.arrow} ${C.white}${pad}${C.reset}${detail ? C.dim + detail + C.reset : ''}`); } function success(msg) { console.log(` ${icon.success} ${C.green}${msg}${C.reset}`); } function fail(msg) { console.log(` ${icon.error} ${C.red}${msg}${C.reset}`); } function section(title) { console.log(`\n${C.bold}${C.cyan} ${title}${C.reset}`); console.log(` ${'─'.repeat(40)}`); } // ─── Version resolver ───────────────────────────────────────────────────────── function resolveDepVersion(pkgName, fallback = 'latest') { try { const pkgJsonPath = require.resolve(`${pkgName}/package.json`); const pkg = JSON.parse(fs_extra_1.default.readFileSync(pkgJsonPath, 'utf8')); return `^${pkg.version}`; } catch { return fallback; } } function getAvleonVersions() { try { // Read CLI's own package.json (this file lives inside @avleon/cli) const cliPkgPath = path_1.default.resolve(__dirname, '../../package.json'); const cliPkg = JSON.parse(fs_extra_1.default.readFileSync(cliPkgPath, 'utf8')); // Core version — resolve from installed node_modules const corePkgPath = require.resolve('@avleon/core/package.json'); const corePkg = JSON.parse(fs_extra_1.default.readFileSync(corePkgPath, 'utf8')); return { core: `^${corePkg.version}`, cli: `^${cliPkg.version}`, }; } catch { return { core: 'latest', cli: 'latest' }; } } // ─── Banner ─────────────────────────────────────────────────────────────────── function banner(fname) { const { core } = getAvleonVersions(); console.clear(); console.log(); console.log(`${C.bold}${C.cyan} ╔═══════════════════════════════════════╗${C.reset}`); console.log(`${C.bold}${C.cyan} ║ AVLEON APPLICATION CLI ║${C.reset}`); console.log(`${C.bold}${C.cyan} ╚═══════════════════════════════════════╝${C.reset}`); console.log(); console.log(` ${C.dim}Creating project :${C.reset} ${C.bold}${C.white}${fname}${C.reset}`); console.log(` ${C.dim}Avleon version :${C.reset} ${C.bold}${C.cyan}${core}${C.reset}`); console.log(); } // ─── Shell ──────────────────────────────────────────────────────────────────── const getShell = () => os_1.default.platform() === 'win32' ? process.env.ComSpec || 'cmd.exe' : process.env.SHELL || '/bin/sh'; // ─── Prompts ────────────────────────────────────────────────────────────────── async function askForPackageManager() { const { packageManager } = await inquirer_1.default.prompt([ { type: 'list', name: 'packageManager', message: 'Package manager:', choices: [ { name: 'npm', value: 'npm' }, { name: 'yarn', value: 'yarn' }, { name: 'pnpm (recommended)', value: 'pnpm' }, ], default: 'npm', }, ]); return packageManager; } async function askForFeatures() { const { features } = await inquirer_1.default.prompt([ { type: 'checkbox', name: 'features', message: 'Select features to include:', choices: [ { name: 'OpenAPI / Swagger docs', value: 'openapi', checked: true }, { name: 'CORS support', value: 'cors', checked: false }, { name: 'TypeORM database', value: 'typeorm', checked: false }, { name: 'Scalar UI (replaces Swagger UI)', value: 'scalar', checked: false }, ], }, ]); return features; } // ─── Installer ──────────────────────────────────────────────────────────────── async function installDependencies(pm, cwd) { return new Promise((resolve, reject) => { const child = (0, child_process_1.spawn)(pm, ['install'], { cwd, shell: getShell(), stdio: 'inherit', }); child.on('exit', (code) => { code === 0 ? resolve() : reject(new Error(`Install failed with exit code ${code}`)); }); child.on('error', reject); }); } // ─── Format helper ──────────────────────────────────────────────────────────── async function fmt(source, parser = 'typescript') { return prettier.format(source, { singleQuote: true, singleAttributePerLine: true, parser, }); } // ─── File writer ────────────────────────────────────────────────────────────── async function writeFile(filePath, content, label) { await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath)); await fs_extra_1.default.writeFile(filePath, content); if (label) step(label, path_1.default.relative(process.cwd(), filePath)); } // ─── Templates ──────────────────────────────────────────────────────────────── function homeServiceTemplate() { return ` import { AppService } from '@avleon/core'; @AppService export class HomeService { sayHello() { return { message: 'Hello World!' }; } } `; } function homeControllerTemplate() { return ` import { ApiController, Get } from '@avleon/core'; import { HomeService } from '../services/home.service'; @ApiController('/') export class HomeController { constructor(private readonly homeService: HomeService) {} @Get() sayHello() { return this.homeService.sayHello(); } } `; } function homeControllerTestTemplate() { return ` import { AvleonTest } from '@avleon/core'; import { HomeController } from './home.controller'; describe('HomeController', () => { let controller!: HomeController; beforeAll(() => { controller = AvleonTest.getController(HomeController); }); it('should be defined', () => { expect(controller).toBeDefined(); }); it('should have sayHello method', () => { expect(typeof controller.sayHello).toBe('function'); }); it('should return hello message', () => { expect(controller.sayHello()).toEqual({ message: 'Hello World!' }); }); }); `; } function openApiConfigTemplate(fname, useScalar) { return ` import { AppConfig, Environment, IConfig } from '@avleon/core'; @AppConfig export class OpenApiConfig implements IConfig { config(env: Environment) { return { info: { title: '${(0, lodash_1.capitalize)(fname)} API', version: '0.0.1', description: 'API documentation for ${(0, lodash_1.capitalize)(fname)}', }, ${useScalar ? "provider: 'scalar'," : ''} routePrefix: '/docs', }; } } `; } function appTemplate(features) { const hasOpenApi = features.includes('openapi'); const hasCors = features.includes('cors'); const hasTypeorm = features.includes('typeorm'); const imports = [`import { Avleon } from '@avleon/core';`]; imports.push(`import { HomeController } from './controllers/home.controller';`); if (hasOpenApi) { imports.push(`import { OpenApiConfig } from './config/openapi.config';`); } const body = [`const app = Avleon.createApplication();`, '']; if (hasCors) { body.push(`app.useCors({ origin: '*' });`); } if (hasOpenApi) { body.push(`if (app.isDevelopment()) {`); body.push(` app.useOpenApi(OpenApiConfig);`); body.push(`}`); body.push(''); } if (hasTypeorm) { body.push(`// TODO: configure datasource in src/config/database.config.ts`); body.push(`// app.useDatasource(AppDataSource);`); body.push(''); } body.push(`app.useControllers([HomeController]);`); body.push(''); body.push(`export default app;`); return `${imports.join('\n')}\n\n${body.join('\n')}\n`; } function serveTemplate() { return ` import app from './app'; async function start() { await app.run(); // default port: 4000 } start(); `; } function packageJsonTemplate(fname, features) { // ✅ Always resolve current installed versions at generation time const { core, cli } = getAvleonVersions(); const deps = { '@avleon/core': core, '@avleon/cli': cli, 'class-transformer': resolveDepVersion('class-transformer', '^0.5.1'), 'class-validator': resolveDepVersion('class-validator', '^0.14.1'), 'rimraf': resolveDepVersion('rimraf', '^6.0.1'), 'reflect-metadata': resolveDepVersion('reflect-metadata', '^0.2.2'), }; if (features.includes('cors')) { deps['@fastify/cors'] = resolveDepVersion('@fastify/cors', '^10.0.0'); } if (features.includes('typeorm')) { deps['typeorm'] = resolveDepVersion('typeorm', '^0.3.20'); deps['pg'] = resolveDepVersion('pg', '^8.11.0'); } if (features.includes('scalar')) { deps['@scalar/fastify-api-reference'] = resolveDepVersion('@scalar/fastify-api-reference', '^1.0.0'); } const devDeps = { '@types/jest': resolveDepVersion('@types/jest', '^29.5.14'), '@types/node': resolveDepVersion('@types/node', '^24.0.3'), 'jest': resolveDepVersion('jest', '^29.7.0'), 'ts-jest': resolveDepVersion('ts-jest', '^29.2.6'), 'typescript': resolveDepVersion('typescript', '^5.7.2'), }; return JSON.stringify({ name: (0, lodash_1.kebabCase)(fname), version: '0.0.1', description: `${(0, lodash_1.capitalize)(fname)} API built with Avleon`, main: 'dist/serve.js', scripts: { 'start:dev': 'avleon serve', 'clean': 'rimraf ./dist', 'build': 'npm run clean && tsc --project ./tsconfig.build.json', 'start': 'node ./dist/serve.js', 'test': 'jest', 'test:watch': 'jest --watch', 'test:cov': 'jest --coverage', 'test:e2e': 'jest --config ./test/e2e-spec.json', }, keywords: ['avleon', 'nodejs', 'typescript'], license: 'ISC', dependencies: deps, devDependencies: devDeps, jest: { preset: 'ts-jest', rootDir: 'src', testRegex: '.*.spec.ts$', collectCoverageFrom: ['**/*.(t|j)s'], coverageDirectory: '../coverage', testEnvironment: 'node', }, }, null, 2); } function tsconfigTemplate() { return JSON.stringify({ compilerOptions: { target: 'ES2017', module: 'CommonJS', moduleResolution: 'node', experimentalDecorators: true, emitDecoratorMetadata: true, outDir: './dist', esModuleInterop: true, allowSyntheticDefaultImports: true, forceConsistentCasingInFileNames: true, skipLibCheck: true, strict: false, }, include: ['src/**/*'], exclude: ['node_modules', 'dist'], }, null, 2); } function tsconfigBuildTemplate() { return JSON.stringify({ extends: './tsconfig.json', exclude: ['node_modules', 'coverage', 'test', 'dist', '**/*spec.ts'], }, null, 2); } function e2eConfigTemplate() { return JSON.stringify({ moduleFileExtensions: ['js', 'json', 'ts'], rootDir: '.', testEnvironment: 'node', testRegex: '.e2e-spec.ts$', transform: { '^.+\\.(t|j)s$': 'ts-jest' }, }, null, 2); } function gitignoreTemplate() { return `node_modules/\ndist/\n.env\n.env.local\ncoverage/\n`; } function envTemplate() { return `NODE_ENV=development\nPORT=4000\n`; } function readmeTemplate(fname, pm) { const runCmd = pm === 'npm' ? 'npm run start:dev' : `${pm} start:dev`; return `# ${(0, lodash_1.capitalize)(fname)} Built with [Avleon](https://github.com/xtareq/avleon). ## Getting Started \`\`\`bash ${runCmd} \`\`\` ## Scripts | Script | Description | |--------|-------------| | \`start:dev\` | Start development server with hot reload | | \`build\` | Compile TypeScript to JavaScript | | \`start\` | Start production server | | \`test\` | Run unit tests | ## Project Structure \`\`\` src/ ├── controllers/ # Route controllers ├── services/ # Business logic ├── config/ # App configuration ├── app.ts # App setup └── serve.ts # Entry point \`\`\` `; } // ─── Main ───────────────────────────────────────────────────────────────────── async function createApplication(fname, flags = {}) { const appPaths = (0, paths_1.appPaths)(fname); banner(fname); // ── Prompts ─────────────────────────────────────────────────────────────── const pm = await askForPackageManager(); const features = await askForFeatures(); const useScalar = features.includes('scalar'); // ── Directories ─────────────────────────────────────────────────────────── section('Scaffolding project structure'); const dirs = [ appPaths.root + '/src', appPaths.root + '/src/controllers', appPaths.root + '/src/config', appPaths.root + '/src/services', appPaths.root + '/test', appPaths.root + '/public', ]; for (const dir of dirs) { await fs_extra_1.default.ensureDir(dir); step('Created directory', path_1.default.relative(process.cwd(), dir)); } // ── Source files ────────────────────────────────────────────────────────── section('Generating source files'); await writeFile(path_1.default.join(appPaths.services, 'home.service.ts'), await fmt(homeServiceTemplate()), 'HomeService'); await writeFile(path_1.default.join(appPaths.controllers, 'home.controller.ts'), await fmt(homeControllerTemplate()), 'HomeController'); await writeFile(path_1.default.join(appPaths.controllers, 'home.controller.spec.ts'), await fmt(homeControllerTestTemplate()), 'HomeController spec'); if (features.includes('openapi')) { await writeFile(path_1.default.join(appPaths.configs, 'openapi.config.ts'), await fmt(openApiConfigTemplate(fname, useScalar)), 'OpenApiConfig'); } await writeFile(path_1.default.join(appPaths.src, 'app.ts'), await fmt(appTemplate(features)), 'app.ts'); await writeFile(path_1.default.join(appPaths.src, 'serve.ts'), await fmt(serveTemplate()), 'serve.ts'); // ── Config files ────────────────────────────────────────────────────────── section('Generating config files'); await writeFile(path_1.default.join(appPaths.root, 'package.json'), await fmt(packageJsonTemplate(fname, features), 'json'), 'package.json'); await writeFile(path_1.default.join(appPaths.root, 'tsconfig.json'), await fmt(tsconfigTemplate(), 'json'), 'tsconfig.json'); await writeFile(path_1.default.join(appPaths.root, 'tsconfig.build.json'), await fmt(tsconfigBuildTemplate(), 'json'), 'tsconfig.build.json'); await writeFile(path_1.default.join(appPaths.root, 'test/e2e-spec.json'), await fmt(e2eConfigTemplate(), 'json'), 'e2e-spec.json'); await writeFile(path_1.default.join(appPaths.root, '.gitignore'), gitignoreTemplate(), '.gitignore'); await writeFile(path_1.default.join(appPaths.root, '.env'), envTemplate(), '.env'); await writeFile(path_1.default.join(appPaths.root, 'README.md'), readmeTemplate(fname, pm), 'README.md'); // ── Install ─────────────────────────────────────────────────────────────── section('Installing dependencies'); try { await installDependencies(pm, appPaths.root); success('Dependencies installed'); } catch (error) { fail(`Installation failed: ${error instanceof Error ? error.message : error}`); console.log(`\n ${C.yellow}Install manually:${C.reset}`); console.log(` ${C.dim}cd ${fname} && ${pm} install${C.reset}\n`); } // ── Done ────────────────────────────────────────────────────────────────── const runCmd = pm === 'npm' ? 'npm run start:dev' : `${pm} start:dev`; console.log(); console.log(`${C.bold}${C.green} ✔ Project created successfully!${C.reset}`); console.log(); console.log(` ${C.dim}Next steps:${C.reset}`); console.log(` ${C.cyan} cd ${fname}${C.reset}`); console.log(` ${C.cyan} ${runCmd}${C.reset}`); if (features.includes('openapi')) { console.log(); console.log(` ${C.dim}API docs:${C.reset} ${C.white}http://localhost:4000/docs${C.reset}`); } console.log(); }