Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18, 20, 22]

steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test
5 changes: 4 additions & 1 deletion bin/servergen.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ program
.option('-v --view <type>', 'Name of View Engine: Pug | Jade | EJS | HBS')
.option('--db', 'Install Mongoose & the Folder Directory for it')
.option('-p, --port <number>', 'Set default port for the app', '3000')
.option('--skip-install', 'Skip npm install step')
.option('--debug', 'Enable debug logging')
.parse(process.argv);

Expand All @@ -48,8 +49,9 @@ handleValidationErrors(validationResult);

const appName = fileName.cleanAppName(options.name);
const port = parseInt(options.port, 10) || 3000;
const skipInstall = options.skipInstall || false;

logger.debug('Parsed configuration', { appName, port, framework: options.framework });
logger.debug('Parsed configuration', { appName, port, framework: options.framework, skipInstall });

/**
* Main function to run the application generator.
Expand All @@ -62,6 +64,7 @@ const main = async () => {
view: options.view,
db: options.db,
port,
skipInstall,
config,
},
{
Expand Down
8 changes: 7 additions & 1 deletion lib/app_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class AppGenerator {
* @param {string|null} options.view - The view engine name.
* @param {boolean} options.db - Whether to include database configuration.
* @param {number} options.port - The port number for the app.
* @param {boolean} options.skipInstall - Whether to skip npm install.
* @param {Object} options.config - Configuration object from lib/config.
* @param {Object} dependencies - Injected dependencies.
* @param {Object} dependencies.fsHelper - File system helper module.
Expand All @@ -37,6 +38,7 @@ class AppGenerator {
this.view = options.view;
this.db = options.db;
this.port = options.port || 3000;
this.skipInstall = options.skipInstall || false;
this.config = options.config;

this.fsHelper = dependencies.fsHelper;
Expand All @@ -59,6 +61,7 @@ class AppGenerator {
appName: this.appName,
framework: this.framework,
port: this.port,
skipInstall: this.skipInstall,
});

this.createAppFolder();
Expand All @@ -67,7 +70,10 @@ class AppGenerator {
this.setupViews();
this.setupDatabase();
this.addSupportFiles();
await this.installDependencies();

if (!this.skipInstall) {
await this.installDependencies();
}

this.logger?.success(`Application ${this.appName} created successfully`);
}
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"type": "module",
"main": "./bin/servergen.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"repository": {
"type": "git",
Expand All @@ -32,5 +34,8 @@
"chalk": "^5.4.1",
"commander": "^14.0.2",
"fs-extra": "^11.3.3"
},
"devDependencies": {
"vitest": "^4.0.18"
}
}
136 changes: 136 additions & 0 deletions tests/integration/integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '..', '..');
const testOutput = path.join(__dirname, '.integration-output');

describe('CLI Integration', () => {
beforeEach(() => {
fs.ensureDirSync(testOutput);
});

afterEach(() => {
fs.removeSync(testOutput);
});

const runCLI = (args) => {
const cmd = `node ${path.join(projectRoot, 'bin', 'servergen.js')} ${args}`;
return execSync(cmd, {
cwd: testOutput,
encoding: 'utf-8',
timeout: 60000,
});
};

describe('help command', () => {
it('displays help information', () => {
const output = runCLI('--help');
expect(output).toContain('Usage:');
expect(output).toContain('-n, --name');
expect(output).toContain('-f, --framework');
});

it('displays version', () => {
const output = runCLI('--version');
expect(output).toMatch(/\d+\.\d+\.\d+/);
});
});

describe('Express app generation', () => {
it('generates Express app with correct structure', () => {
runCLI('-n testapp -f express --skip-install');

const appDir = path.join(testOutput, 'testapp');
expect(fs.existsSync(appDir)).toBe(true);
expect(fs.existsSync(path.join(appDir, 'index.js'))).toBe(true);
expect(fs.existsSync(path.join(appDir, 'package.json'))).toBe(true);
expect(fs.existsSync(path.join(appDir, 'routes'))).toBe(true);
expect(fs.existsSync(path.join(appDir, 'controllers'))).toBe(true);
expect(fs.existsSync(path.join(appDir, 'model'))).toBe(true);
});

it('generates package.json with express dependency', () => {
runCLI('-n expresstest -f express --skip-install');

const pkgPath = path.join(testOutput, 'expresstest', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

expect(pkg.name).toBe('expresstest');
expect(pkg.dependencies.express).toBeDefined();
expect(pkg.dependencies.nodemon).toBeDefined();
expect(pkg.dependencies.cors).toBeDefined();
});

it('includes view engine when specified', () => {
runCLI('-n viewtest -f express -v ejs --skip-install');

const pkgPath = path.join(testOutput, 'viewtest', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

expect(pkg.dependencies.ejs).toBeDefined();
});

it('includes mongoose when --db flag used', () => {
runCLI('-n dbtest -f express --db --skip-install');

const pkgPath = path.join(testOutput, 'dbtest', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

expect(pkg.dependencies.mongoose).toBeDefined();
expect(fs.existsSync(path.join(testOutput, 'dbtest', 'config'))).toBe(true);
});
});

describe('Node app generation', () => {
it('generates Node app with correct structure', () => {
runCLI('-n nodetest -f node --skip-install');

const appDir = path.join(testOutput, 'nodetest');
expect(fs.existsSync(appDir)).toBe(true);
expect(fs.existsSync(path.join(appDir, 'index.js'))).toBe(true);
expect(fs.existsSync(path.join(appDir, 'package.json'))).toBe(true);
});

it('generates package.json without express dependency', () => {
runCLI('-n purenode -f node --skip-install');

const pkgPath = path.join(testOutput, 'purenode', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

expect(pkg.dependencies.express).toBeUndefined();
expect(pkg.dependencies.nodemon).toBeDefined();
});
});

describe('support files', () => {
it('includes .gitignore', () => {
runCLI('-n gittest -f express --skip-install');
expect(fs.existsSync(path.join(testOutput, 'gittest', '.gitignore'))).toBe(true);
});

it('includes Dockerfile', () => {
runCLI('-n dockertest -f express --skip-install');
expect(fs.existsSync(path.join(testOutput, 'dockertest', 'Dockerfile'))).toBe(true);
});

it('includes .dockerignore', () => {
runCLI('-n dockertest2 -f express --skip-install');
expect(fs.existsSync(path.join(testOutput, 'dockertest2', '.dockerignore'))).toBe(true);
});
});

describe('custom port', () => {
it('configures custom port in index.js', () => {
runCLI('-n porttest -f express -p 8080 --skip-install');

const indexPath = path.join(testOutput, 'porttest', 'index.js');
const content = fs.readFileSync(indexPath, 'utf-8');

expect(content).toContain('8080');
});
});
});
68 changes: 68 additions & 0 deletions tests/unit/build_helper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import * as buildHelper from '../../lib/build_helper.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const testDir = path.join(__dirname, '.test-output');

describe('build_helper', () => {
beforeEach(() => {
fs.ensureDirSync(testDir);
});

afterEach(() => {
fs.removeSync(testDir);
});

describe('createDir', () => {
it('creates a directory inside app directory', () => {
buildHelper.createDir(testDir, 'controllers');
const dirPath = path.join(testDir, 'controllers');
expect(fs.existsSync(dirPath)).toBe(true);
});

it('creates nested directory structure', () => {
buildHelper.createDir(testDir, 'config');
buildHelper.createDir(testDir, 'routes');
expect(fs.existsSync(path.join(testDir, 'config'))).toBe(true);
expect(fs.existsSync(path.join(testDir, 'routes'))).toBe(true);
});
});

describe('buildFilewithContents', () => {
it('copies file content to destination', () => {
const sourceFile = path.join(testDir, 'source.txt');
fs.writeFileSync(sourceFile, 'test content');

buildHelper.buildFilewithContents(sourceFile, testDir, 'dest.txt');

const destContent = fs.readFileSync(path.join(testDir, 'dest.txt'), 'utf-8');
expect(destContent).toBe('test content');
});
});

describe('buildFolderforApp', () => {
it('creates a new folder', () => {
const newFolder = path.join(testDir, 'new-app');
buildHelper.buildFolderforApp(newFolder);
expect(fs.existsSync(newFolder)).toBe(true);
});

it('exits if folder already exists', () => {
const existingFolder = path.join(testDir, 'existing');
fs.mkdirSync(existingFolder);

const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});

expect(() => {
buildHelper.buildFolderforApp(existingFolder);
}).toThrow('process.exit called');

mockExit.mockRestore();
});
});
});
72 changes: 72 additions & 0 deletions tests/unit/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';
import { getConfig } from '../../lib/config.js';
import path from 'path';

describe('getConfig', () => {
const baseDir = '/test/base';
const cwd = '/test/cwd';

it('returns configuration object', () => {
const config = getConfig(baseDir, cwd);
expect(config).toBeDefined();
expect(config.paths).toBeDefined();
expect(config.validation).toBeDefined();
expect(config.defaults).toBeDefined();
});

describe('paths', () => {
it('sets express template path', () => {
const config = getConfig(baseDir, cwd);
expect(config.paths.templates.express).toBe(
path.join(baseDir, '..', 'templates', 'express')
);
});

it('sets node template path', () => {
const config = getConfig(baseDir, cwd);
expect(config.paths.templates.node).toBe(
path.join(baseDir, '..', 'templates', 'node')
);
});

it('sets views path', () => {
const config = getConfig(baseDir, cwd);
expect(config.paths.templates.views).toBe(
path.join(baseDir, '..', 'templates', 'express', 'views')
);
});

it('sets cwd correctly', () => {
const config = getConfig(baseDir, cwd);
expect(config.paths.cwd).toBe(cwd);
});
});

describe('validation rules', () => {
it('includes valid frameworks', () => {
const config = getConfig(baseDir, cwd);
expect(config.validation.frameworks).toContain('node');
expect(config.validation.frameworks).toContain('express');
});

it('includes valid views', () => {
const config = getConfig(baseDir, cwd);
expect(config.validation.views).toContain('ejs');
expect(config.validation.views).toContain('pug');
expect(config.validation.views).toContain('jade');
expect(config.validation.views).toContain('hbs');
});
});

describe('defaults', () => {
it('sets default framework to express', () => {
const config = getConfig(baseDir, cwd);
expect(config.defaults.framework).toBe('express');
});

it('sets default port to 3000', () => {
const config = getConfig(baseDir, cwd);
expect(config.defaults.port).toBe(3000);
});
});
});
Loading