@faisalrmdhn08/allin-cli
Version:
A modern full-stack CLI tool based on Typescript designed to accelerate your app development process — setup your entire stack in one seamless command.
599 lines • 27.9 kB
JavaScript
import { BASE_PATH, CACHE_BASE_PATH, CACHE_TTL_MS, INSTALL_TIMEOUT_MS, } from '../../config.js';
import { TYPESCRIPT_DEFAULT_DEPENDENCIES } from '../../constants/default.js';
import { BACKEND_FRAMEWORKS, FRONTEND_FRAMEWORKS, LICENSES, } from '../../constants/global.js';
import { UnidentifiedTemplateError } from '../../exceptions/error.js';
import { __pathNotFound } from '../../exceptions/trigger.js';
import { hasValue, isBackend, isEmptyString, isUndefined, } from '../../utils/guard.js';
import { errorBox, warnBox } from '../../utils/info-box.js';
import chalk from 'chalk';
import { execa } from 'execa';
import fse from 'fs-extra';
import inquirer from 'inquirer';
import path from 'path';
export class MicroGenerator {
static #instance;
#templateCache = new Map();
constructor() { }
static get instance() {
if (!MicroGenerator.#instance) {
MicroGenerator.#instance = new MicroGenerator();
}
return MicroGenerator.#instance;
}
async setupProject(params) {
await this.__checkCacheReady(CACHE_BASE_PATH, CACHE_TTL_MS);
const isPathExist = fse.existsSync(params.desPath);
if (params.optionValues.force && isPathExist) {
const forceOverwriteProjectConfirmation = await inquirer.prompt({
name: 'forceOverwrite',
type: 'confirm',
message: `Are you sure to overwrite ${params.desPath} project?`,
default: false,
when: () => isPathExist,
});
if (forceOverwriteProjectConfirmation.forceOverwrite) {
await fse.remove(params.desPath);
}
else {
process.exit(0);
}
}
const tempDir = path.join(BASE_PATH, 'templates', 'temp', params.projectName);
await fse.ensureDir(tempDir);
await fse.copy(params.sourcePath, tempDir);
return async () => {
await fse.copy(tempDir, params.desPath);
await fse.remove(tempDir);
try {
await this.__storeCachedProject(CACHE_BASE_PATH, CACHE_TTL_MS, params.projectType, params.desPath, params.projectName);
}
catch (error) {
errorBox(error);
}
};
}
async setupDocker(params) {
if (!params.isAddingDocker)
return;
if (!params.isAddingBake)
await this.__addDocker({
spinner: params.spinner,
desPath: params.desPath,
selectedPackageManager: params.selectedPackageManager,
});
else
await this.__addDockerBake({
spinner: params.spinner,
desPath: params.desPath,
selectedPackageManager: params.selectedPackageManager,
});
}
async setupOthers(params) {
if (params.optionValues.git) {
await this.__addGit(params.spinner, params.desPath);
}
if (params.optionValues.packageManager &&
!isEmptyString(params.optionValues.packageManager)) {
await this.__switchPackageManager({
spinner: params.spinner,
selectedPackageManager: params.optionValues.packageManager,
projectName: params.projectName,
desPath: params.desPath,
});
}
if (params.optionValues.typescript) {
await this.__useTypescript({
spinner: params.spinner,
projectType: params.projectType,
projectName: params.projectName,
selectedframework: params.selectedFramework,
selectedPackageManager: params.optionValues.packageManager,
desPath: params.desPath,
});
}
if (hasValue(params.optionValues.author) ||
hasValue(params.optionValues.description) ||
hasValue(params.optionValues.version)) {
await this.__updatePackageMetadata({
spinner: params.spinner,
optionValues: params.optionValues,
projectName: params.projectName,
desPath: params.desPath,
});
}
await this.__addLicense({
spinner: params.spinner,
optionValues: params.optionValues,
projectName: params.projectName,
desPath: params.desPath,
});
if (params.optionValues.readme) {
await this.__addReadme({
spinner: params.spinner,
optionValues: params.optionValues,
projectName: params.projectName,
projectType: params.projectType,
desPath: params.desPath,
});
}
if (params.optionValues.env) {
await this.__addEnv({
spinner: params.spinner,
optionValues: params.optionValues,
projectName: params.projectName,
projectType: params.projectType,
desPath: params.desPath,
});
}
}
async setupInstallation(params) {
if (params.selectedDependencies.length < 1) {
return;
}
await this.__installDependencies({
spinner: params.spinner,
selectedDependencies: params.selectedDependencies,
selectedPackageManager: params.selectedPackageManager,
desPath: params.desPath,
});
await this.__updateDependencies({
spinner: params.spinner,
selectedPackageManager: params.selectedPackageManager,
projectName: params.projectName,
desPath: params.desPath,
});
}
// --------------------------------------------------------------------------
// CACHE / LISTING
// --------------------------------------------------------------------------
async __getListCachedProjects(cacheBasePath, projectType) {
const isPathExist = await fse.pathExists(cacheBasePath);
if (!isPathExist) {
await fse.ensureDir(cacheBasePath);
}
if (!projectType)
return [];
const typeDir = path.join(cacheBasePath, projectType);
await fse.ensureDir(typeDir);
const names = await fse.readdir(typeDir);
const result = await Promise.all(names.map(async (name) => {
if (!name)
return null;
const projectPath = path.join(typeDir, name);
try {
const stat = await fse.stat(projectPath);
return {
name: name,
path: projectPath,
createdMs: stat.birthtimeMs,
};
}
catch (error) {
errorBox(error);
}
}));
return result.filter((x) => x !== null);
}
async __loadCachedProject(cacheBasePath, cacheName, projectType, desPath) {
const entryPath = path.join(cacheBasePath, projectType, cacheName);
await fse.ensureDir(entryPath).catch((error) => {
errorBox(error);
process.exit(0);
});
await fse.copy(entryPath, desPath);
}
// --------------------------------------------------------------------------
// DOCKER / BAKE
// --------------------------------------------------------------------------
async __addDocker(params) {
const dockerComposePaths = this.__getDockerPaths('compose.yml', params.desPath);
await this.__copyWithSpinner(params.spinner, 'docker compose file', dockerComposePaths.sourcePath, dockerComposePaths.desPath);
const dockerFileBasedOnPm = params.selectedPackageManager === 'npm'
? 'npm.Dockerfile'
: params.selectedPackageManager === 'pnpm'
? 'pnpm.Dockerfile'
: 'bun.Dockerfile';
const dockerFilePaths = this.__getDockerPaths(dockerFileBasedOnPm, params.desPath);
await this.__copyWithSpinner(params.spinner, 'dockerfile', dockerFilePaths.sourcePath, dockerFilePaths.desPath);
}
async __addDockerBake(params) {
const composePaths = this.__getDockerPaths('compose.yml', params.desPath);
await this.__copyWithSpinner(params.spinner, 'docker compose file', composePaths.sourcePath, composePaths.desPath);
const dockerFileBasedOnPm = params.selectedPackageManager === 'npm'
? 'npm.Dockerfile'
: params.selectedPackageManager === 'pnpm'
? 'pnpm.Dockerfile'
: 'bun.Dockerfile';
const dockerFilePaths = this.__getDockerPaths(dockerFileBasedOnPm, params.desPath);
await this.__copyWithSpinner(params.spinner, 'dockerfile', dockerFilePaths.sourcePath, dockerFilePaths.desPath);
const dockerBakePaths = this.__getDockerPaths('docker-bake.hcl', params.desPath);
await this.__copyWithSpinner(params.spinner, 'docker bake file', dockerBakePaths.sourcePath, dockerBakePaths.desPath);
}
// --------------------------------------------------------------------------
// GIT / LICENSE / README / ENV
// --------------------------------------------------------------------------
async __addGit(spinner, desPath) {
const initializeGitQuestion = await inquirer.prompt({
name: 'addGit',
type: 'confirm',
message: `Do you want us to run git init?`,
default: false,
});
if (!initializeGitQuestion.addGit) {
warnBox('Warning Information', `You can run ${chalk.bold('git init')} later.`);
}
spinner.start(`Initializing Git repository, please wait for a moment.`);
await execa('git', ['init'], {
cwd: desPath,
});
spinner.succeed(`Git repository successfully initialized.`);
}
async __addLicense(params) {
const licenseSelection = await inquirer.prompt({
name: 'license',
type: 'list',
message: 'Which license do you want to use:',
choices: LICENSES.licenses
.sort((i, e) => i.name.toLowerCase().localeCompare(e.name.toLowerCase(), 'en-US'))
.map((l) => l.actualName),
default: 'MIT License',
loop: false,
when: () => isUndefined(params.optionValues.license),
});
const licenseFile = isUndefined(params.optionValues.license)
? LICENSES.licenses.find((l) => l.actualName === licenseSelection.license)
: LICENSES.licenses.find((l) => l.name === params.optionValues.license);
if (!licenseFile) {
throw new UnidentifiedTemplateError(`${chalk.bold('Unidentified template')}: ${chalk.bold(licenseSelection.license)} file template is not defined.`);
}
params.spinner.start(`Start adding ${chalk.bold(licenseFile.actualName)} file into ${chalk.bold(params.projectName)}.`);
const licenseSrcPath = path.join(BASE_PATH, licenseFile.path);
await fse.copy(licenseSrcPath, params.desPath);
params.spinner.succeed(`Adding ${chalk.bold(licenseFile.actualName)} file on ${chalk.bold(params.projectName)} succeed.`);
}
async __addReadme(params) {
const backendReadme = this.__getReadmePaths('BACKEND_README.md', params.desPath);
const frontendReadme = this.__getReadmePaths('FRONTEND_README.md', params.desPath);
const readmeSourcePath = params.projectType !== 'backend'
? frontendReadme.sourcePath
: backendReadme.sourcePath;
const readmeTargetPath = path.join(params.desPath, 'README.md');
await this.__copyWithSpinner(params.spinner, 'readme', readmeSourcePath, readmeTargetPath);
}
async __addEnv(params) {
const backendEnvPaths = this.__getEnvPaths('.env.backend', params.desPath);
const frontendEnvPaths = this.__getEnvPaths('.env.frontend', params.desPath);
const envSourcePath = params.projectType !== 'backend'
? frontendEnvPaths.sourcePath
: backendEnvPaths.sourcePath;
const envTargetPath = path.join(params.desPath, '.env');
await this.__copyWithSpinner(params.spinner, '.env', envSourcePath, envTargetPath);
}
// --------------------------------------------------------------------------
// PACKAGE MANAGER / TYPESCRIPT
// --------------------------------------------------------------------------
async __switchPackageManager(params) {
const __lockFiles = [
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'bun.lock',
];
for (const file of __lockFiles) {
const lockFilePath = path.join(params.desPath, file);
if (await fse.exists(lockFilePath)) {
await fse.remove(lockFilePath);
}
}
const nodeModulePath = path.join(params.desPath, 'node_modules');
if (await fse.exists(nodeModulePath)) {
await fse.remove(nodeModulePath);
}
const executeChangingPmCommand = this.__choosePackageManagerCommand(params.selectedPackageManager, true);
await execa(executeChangingPmCommand, ['install'], {
cwd: params.desPath,
timeout: INSTALL_TIMEOUT_MS,
killSignal: 'SIGTERM',
stdio: 'pipe',
});
}
async __useTypescript(params) {
const frameworks = params.projectType !== 'backend'
? FRONTEND_FRAMEWORKS.frameworks
: BACKEND_FRAMEWORKS.frameworks;
const frameworkFile = frameworks.find((f) => f.name === params.selectedframework);
if (!frameworkFile) {
throw new UnidentifiedTemplateError(`${chalk.bold('Unidentified template')}: ${chalk.bold(params.selectedframework)} file template is not defined.`);
}
const frameworkPath = path.join(BASE_PATH, frameworkFile.path);
const frameworkFiles = fse.readdirSync(frameworkPath, {
withFileTypes: true,
});
const tsConfigFile = frameworkFiles.find((f) => f.name === 'tsconfig.json');
if (!isUndefined(tsConfigFile)) {
warnBox('Warning Information', `${chalk.bold('tsconfig.json')} is exist on ${chalk.bold(params.projectName)}, means that ${chalk.bold('Typescript')} already installed.`);
}
await this.__installTypescript({
spinner: params.spinner,
projectType: params.projectType,
selectedFramework: params.selectedframework,
selectedPackageManager: params.selectedPackageManager,
desPath: params.desPath,
});
const executeConditioningPmCommand = this.__choosePackageManagerCommand(params.selectedPackageManager, false);
const initializeTsQuestion = await inquirer.prompt({
name: 'addTsConfig',
type: 'confirm',
message: `Do you want us to execute
${executeConditioningPmCommand} tsc --init in your project? (optional)`,
default: false,
});
if (!initializeTsQuestion.addTsConfig) {
warnBox('Warning Information', `You can initialize ${chalk.bold('Typescript')} later.`);
}
params.spinner.start(`Initializing ${chalk.bold('Typescript')} into ${chalk.bold(params.projectName)}, please wait for a moment.`);
await execa(executeConditioningPmCommand, ['tsc', '--init'], {
cwd: params.desPath,
});
params.spinner.succeed(`Initializing ${chalk.bold('Typescript')} succeed.`);
params.spinner.start(`Start renaming .js files to .ts`);
const renamePairs = isBackend(params.projectType)
? [
[
path.join(params.desPath, 'index.js'),
path.join(params.desPath, 'index.ts'),
],
]
: [
[
path.join(params.desPath, 'src', 'main.js'),
path.join(params.desPath, 'src', 'main.ts'),
],
[
path.join(params.desPath, 'src', 'counter.js'),
path.join(params.desPath, 'src', 'counter.ts'),
],
];
for (const [__sourcePath, __desPath] of renamePairs) {
if (fse.existsSync(__sourcePath)) {
params.spinner.start(`Renaming ${chalk.bold(__sourcePath)} to ${chalk.bold(__desPath)}.`);
fse.renameSync(__sourcePath, __desPath);
params.spinner.succeed(`Renamed ${chalk.bold(__sourcePath)} → ${chalk.bold(__desPath)}.`);
}
}
params.spinner.succeed(`All file renames complete for ${chalk.bold(params.projectName)}.`);
}
// --------------------------------------------------------------------------
// INSTALLATIONS / CONFIGS
// --------------------------------------------------------------------------
async __installTypescript(params) {
const executeInstallBasedOnPm = this.__choosePackageManagerCommand(params.selectedPackageManager, true);
const deps = TYPESCRIPT_DEFAULT_DEPENDENCIES[params.projectType][params.selectedFramework] ?? [];
for (const p of deps) {
params.spinner.start(`Start installing ${chalk.bold(p)} package.`);
if (params.selectedPackageManager === 'npm') {
await execa(executeInstallBasedOnPm, ['install', '-D', p], {
cwd: params.desPath,
timeout: INSTALL_TIMEOUT_MS,
killSignal: 'SIGTERM',
stdio: 'pipe',
});
}
else {
await execa(executeInstallBasedOnPm, ['add', '-D', p], {
cwd: params.desPath,
timeout: INSTALL_TIMEOUT_MS,
killSignal: 'SIGTERM',
stdio: 'pipe',
});
}
params.spinner.succeed(`Installing ${chalk.bold(p)} package succeed.`);
}
}
async __installDependencies(params) {
const executeInstallBasedOnPm = this.__choosePackageManagerCommand(params.selectedPackageManager, true);
params.spinner.start(`Installing ${chalk.bold(params.selectedDependencies.join(', '))}, please wait for a moment.`);
await execa(executeInstallBasedOnPm, ['install', '--save', ...params.selectedDependencies], {
cwd: params.desPath,
timeout: INSTALL_TIMEOUT_MS,
killSignal: 'SIGTERM',
stdio: 'pipe',
});
const isPrettierSelected = params.selectedDependencies.includes('prettier');
const isEsLintSelected = params.selectedDependencies.includes('eslint');
const isWinstonSelected = params.selectedDependencies.includes('winston');
if (isPrettierSelected) {
await this.__installPrettier(params);
}
if (isEsLintSelected) {
await this.__installEsLint(params);
}
if (isWinstonSelected) {
await this.__installWinston(params);
}
params.spinner.succeed(`Installing all dependencies succeed.`);
}
async __installPrettier(params) {
const prettierTemplatesPath = path.join(BASE_PATH, 'templates/addons/config');
const { sourcePath } = this.__findTemplate(prettierTemplatesPath, '.prettierrc');
const prettierFileDesPath = path.join(params.desPath, '.prettierrc');
params.spinner.start(`Initializing ${chalk.bold('.prettierrc')} file.`);
await fse.copy(sourcePath, prettierFileDesPath);
params.spinner.succeed(`Adding ${chalk.bold('.prettierrc')} configuration completed.`);
}
async __installEsLint(params) {
const executeInstallBasedOnPm = this.__choosePackageManagerCommand(params.selectedPackageManager, false);
const initializeEsLintQuestion = await inquirer.prompt({
name: 'addESLintConfig',
type: 'confirm',
message: `Do you want us to execute ${`${executeInstallBasedOnPm}`} eslint --init in your project? (optional)`,
default: false,
});
if (!initializeEsLintQuestion.addESLintConfig) {
warnBox('Warning Information', `You can execute ${chalk.bold(`${executeInstallBasedOnPm} eslint --init`)} later.`);
}
await execa(`${executeInstallBasedOnPm}`, ['@eslint/create-config'], {
cwd: params.desPath,
stdio: 'inherit',
});
}
async __installWinston(params) {
params.spinner.start('Configuring Winston logger...');
const isUsingTypeScript = fse.existsSync(path.join(params.desPath, 'tsconfig.json'));
const snipperSrcPath = path.join(BASE_PATH, 'templates', 'addons', 'config', 'winston', isUsingTypeScript ? 'logger.ts' : 'logger.js');
const projectSrcDir = path.join(params.desPath, 'src');
const loggerTargetFile = path.join(projectSrcDir, isUsingTypeScript ? 'logger.ts' : 'logger.js');
fse.ensureDirSync(projectSrcDir);
// READ SNIPPET.
let fileContent = fse.readFileSync(snipperSrcPath, 'utf-8');
// REMOVE LEADING "//" EACH LINE
const uncommented = fileContent
.split('\n')
.map((line) => line.replace(/^\/\/\s?/, ''))
.join('\n');
fse.writeFileSync(loggerTargetFile, uncommented, 'utf-8');
params.spinner.succeed('Winston successfully configured!');
}
async __updateDependencies(params) {
const updateDependenciesQuestion = await inquirer.prompt({
name: 'updatePackages',
type: 'confirm',
message: `Do you want us to run ${params.selectedPackageManager} update? (optional)`,
default: false,
});
if (!updateDependenciesQuestion.updatePackages) {
warnBox('Warning Information', 'You can update the dependencies later.');
}
params.spinner.start(`Updating ${chalk.bold(params.projectName)} dependencies, please wait for a moment.`);
await execa(`${params.selectedPackageManager}`, ['update'], {
cwd: params.desPath,
});
params.spinner.succeed(`Updating ${chalk.bold(params.projectName)} dependencies succeed.`);
}
async __updatePackageMetadata(params) {
params.spinner.start(`Updating ${chalk.bold(params.projectName)} package metadata, please wait for a moment.`);
const packageJsonFilePath = path.join(params.desPath, 'package.json');
const packageJsonFile = await fse.readJSON(packageJsonFilePath);
packageJsonFile.author = params.optionValues.author;
packageJsonFile.description = params.optionValues.description;
packageJsonFile.version = params.optionValues.version;
await fse.writeJSON(packageJsonFilePath, packageJsonFile, { spaces: 2 });
params.spinner.succeed(`Updating ${chalk.bold(params.projectName)} package metadata succeed.`);
}
// ------------------------------------------------------------------------
// CACHE HELPERS
// ------------------------------------------------------------------------
async __checkCacheReady(cacheBasePath, cacheTtlMs) {
await fse.ensureDir(cacheBasePath);
await this.__clearCache(cacheBasePath, cacheTtlMs);
}
async __clearCache(cacheBasePath, cacheTtlMs) {
const isPathExist = await fse.pathExists(cacheBasePath);
if (!isPathExist) {
await fse.ensureDir(cacheBasePath);
}
const types = await fse.readdir(cacheBasePath);
const now = Date.now();
for (const type of types) {
const typeDir = path.join(cacheBasePath, type);
const statType = await fse.stat(typeDir).catch(() => null);
if (!statType || !statType.isDirectory())
continue;
const entries = await fse.readdir(typeDir);
for (const name of entries) {
const entryPath = path.join(typeDir, name);
try {
const statEntry = await fse.stat(entryPath);
const age = now - statEntry.birthtimeMs;
if (age > cacheTtlMs) {
await fse.remove(entryPath);
}
}
catch (error) {
continue;
}
}
}
}
async __storeCachedProject(cacheBasePath, cacheTtlMs, projectType, srcPath, nameBase) {
await this.__checkCacheReady(cacheBasePath, cacheTtlMs);
const typeDir = path.join(cacheBasePath, projectType);
await fse.ensureDir(typeDir);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const safeBase = nameBase.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
const cacheFolderName = `${safeBase}-${timestamp}`;
const desPath = path.join(typeDir, cacheFolderName);
await fse.copy(srcPath, desPath);
return desPath;
}
__findTemplate(basePath, filename) {
__pathNotFound(basePath);
const names = this.readDirCached(basePath);
const found = names.find((name) => name === filename);
if (!found) {
throw new UnidentifiedTemplateError(`${chalk.bold('Unidentified template')}: ${chalk.bold(filename)} file template is not defined.`);
}
return {
sourcePath: path.join(basePath, found),
name: found,
};
}
async __copyWithSpinner(spinner, label, sourcePath, targetPath) {
spinner.start(`Copying ${chalk.bold(label)} into ${chalk.bold(targetPath)}...`);
await fse.copy(sourcePath, targetPath);
spinner.succeed(`Copying ${chalk.bold(label)} succeed.`);
}
__choosePackageManagerCommand(packageManager, forInstall = true) {
if (forInstall) {
return packageManager === 'npm'
? 'npm'
: packageManager === 'pnpm'
? 'pnpm'
: 'bun';
}
return packageManager === 'npm'
? 'npx'
: packageManager === 'pnpm'
? 'pnpx'
: 'bunx';
}
// ------------------------------------------------------------------------
// TEMPLATE PATH GETTERS
// ------------------------------------------------------------------------
__getDockerPaths(filename, desPath) {
const dockerTemplatesPath = path.join(BASE_PATH, 'templates/addons/docker');
const { sourcePath } = this.__findTemplate(dockerTemplatesPath, filename);
return {
sourcePath,
desPath: path.join(desPath, filename),
};
}
__getReadmePaths(filename, desPath) {
const readmeTemplatesPath = path.join(BASE_PATH, 'templates/addons/others');
const { sourcePath } = this.__findTemplate(readmeTemplatesPath, filename);
return {
sourcePath,
desPath: path.join(desPath, filename),
};
}
__getEnvPaths(filename, desPath) {
const envTemplatesPath = path.join(BASE_PATH, 'templates/addons/config');
const { sourcePath } = this.__findTemplate(envTemplatesPath, filename);
return {
sourcePath,
desPath: path.join(desPath, filename),
};
}
// ------------------------------------------------------------------------
// MICRO HELPERS
// ------------------------------------------------------------------------
readDirCached(dir) {
const cached = this.#templateCache.get(dir);
if (cached)
return cached;
const entries = fse.readdirSync(dir);
this.#templateCache.set(dir, entries);
return entries;
}
}
//# sourceMappingURL=micro.js.map