nodejs-fs
Version:
CLI tool to scaffold production-ready Express + Mongoose backend with security best practices
188 lines • 6.88 kB
JavaScript
import path from 'path';
import fs from 'fs-extra';
import ora from 'ora';
import logger from './utils/logger.js';
import { copyTemplate, replacePlaceholders } from './utils/copyTemplate.js';
import { runCommand, commandExists } from './utils/runCommand.js';
const validateProjectName = (projectName) => {
const validNameRegex = /^[a-z0-9-_]+$/i;
if (!validNameRegex.test(projectName)) {
throw new Error('Project name can only contain letters, numbers, hyphens, and underscores');
}
const reservedNames = ['node_modules', 'src', 'test', 'tests'];
if (reservedNames.includes(projectName.toLowerCase())) {
throw new Error(`"${projectName}" is a reserved name and cannot be used`);
}
if (projectName.length < 1 || projectName.length > 214) {
throw new Error('Project name must be between 1 and 214 characters');
}
};
const isDirEmpty = async (dirPath) => {
try {
const files = await fs.readdir(dirPath);
return files.length === 0;
}
catch {
return true;
}
};
const createProjectDirectory = async (projectName, options) => {
const { verbose } = options;
const projectPath = path.resolve(process.cwd(), projectName);
if (await fs.pathExists(projectPath)) {
const isEmpty = await isDirEmpty(projectPath);
if (!isEmpty) {
throw new Error(`Directory "${projectName}" already exists and is not empty. ` +
'Please choose a different name or remove the existing directory.');
}
logger.debug(`Using existing empty directory: ${projectPath}`, verbose);
}
else {
await fs.ensureDir(projectPath);
logger.debug(`Created directory: ${projectPath}`, verbose);
}
return projectPath;
};
const copyTemplateFiles = async (projectPath, options) => {
const { template = 'full', verbose } = options;
const spinner = ora('Copying template files...').start();
try {
const templateMap = {
basic: 'base-backend',
secure: 'base-backend',
full: 'base-backend',
};
const templateName = templateMap[template] || 'base-backend';
await copyTemplate(templateName, projectPath, { verbose });
spinner.succeed('Template files copied');
}
catch (error) {
spinner.fail('Failed to copy template files');
throw error;
}
};
const updateProjectConfig = async (projectPath, projectName, options) => {
const { verbose } = options;
const spinner = ora('Configuring project...').start();
try {
const packageJsonPath = path.join(projectPath, 'package.json');
const readmePath = path.join(projectPath, 'README.md');
const packageJson = await fs.readJson(packageJsonPath);
packageJson.name = projectName;
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
logger.debug('Updated package.json', verbose);
if (await fs.pathExists(readmePath)) {
await replacePlaceholders(readmePath, { PROJECT_NAME: projectName });
logger.debug('Updated README.md', verbose);
}
spinner.succeed('Project configured');
}
catch (error) {
spinner.fail('Failed to configure project');
throw error;
}
};
const installDependencies = async (projectPath, options) => {
const { install = true, verbose } = options;
if (!install) {
logger.warn('Skipping dependency installation (--no-install flag)');
return;
}
const hasNpm = await commandExists('npm');
if (!hasNpm) {
logger.warn('npm not found. Skipping dependency installation.');
logger.info('Please install dependencies manually with: npm install');
return;
}
const spinner = ora('Installing dependencies (this may take a few minutes)...').start();
try {
await runCommand('npm', ['install'], {
cwd: projectPath,
verbose,
});
spinner.succeed('Dependencies installed');
}
catch (error) {
spinner.fail('Failed to install dependencies');
logger.warn('You can install dependencies manually by running: npm install');
if (verbose) {
throw error;
}
}
};
const initializeGit = async (projectPath, options) => {
const { git = false, verbose } = options;
if (!git) {
return;
}
const hasGit = await commandExists('git');
if (!hasGit) {
logger.warn('git not found. Skipping git initialization.');
return;
}
const spinner = ora('Initializing git repository...').start();
try {
await runCommand('git', ['init'], {
cwd: projectPath,
verbose,
});
await runCommand('git', ['add', '.'], {
cwd: projectPath,
verbose,
});
await runCommand('git', ['commit', '-m', 'Initial commit from nodejs-fs'], {
cwd: projectPath,
verbose,
});
spinner.succeed('Git repository initialized');
}
catch (error) {
spinner.fail('Failed to initialize git repository');
if (verbose) {
throw error;
}
}
};
const createProject = async (projectName, options = {}) => {
try {
logger.step('Validating project name...');
validateProjectName(projectName);
logger.success(`Project name "${projectName}" is valid`);
logger.blank();
logger.step('Creating project directory...');
const projectPath = await createProjectDirectory(projectName, options);
logger.success(`Project directory created: ${projectName}`);
logger.blank();
logger.step('Setting up project structure...');
await copyTemplateFiles(projectPath, options);
logger.blank();
logger.step('Updating project configuration...');
await updateProjectConfig(projectPath, projectName, options);
logger.blank();
if (options.install !== false) {
logger.step('Installing dependencies...');
await installDependencies(projectPath, options);
logger.blank();
}
if (options.git) {
logger.step('Initializing git repository...');
await initializeGit(projectPath, options);
logger.blank();
}
return projectPath;
}
catch (error) {
if (error instanceof Error) {
const nodeError = error;
if (nodeError.code === 'EACCES') {
throw new Error('Permission denied. Try running with elevated privileges.');
}
if (nodeError.code === 'ENOSPC') {
throw new Error('Not enough disk space available.');
}
}
throw error;
}
};
export default createProject;
//# sourceMappingURL=createProject.js.map