@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.
222 lines • 10.9 kB
JavaScript
import { UnidentifiedProjectTypeError, UnidentifiedTemplateError, } from '../../exceptions/error.js';
import { __detectProjectTypeFromInput, __isContainHarassmentWords, __isLookLikePath, __isValidProjectName, __renewProjectName, __sanitizeProjectName, } from '../../utils/string.js';
import { __getUserRealName, BASE_PATH, CACHE_BASE_PATH } from '../../config.js';
import { DIRTY_WORDS, PROJECT_TYPES, TEMPLATES_META_MAP, } from '../../constants/global.js';
import { __pathNotFound } from '../../exceptions/trigger.js';
import { isNull, isUndefined } from '../../utils/guard.js';
import { errorBox, infoBox, warnBox } from '../../utils/info-box.js';
import chalk from 'chalk';
import fse from 'fs-extra';
import inquirer from 'inquirer';
import ora, {} from 'ora';
import path from 'path';
import { BackendGenerator } from '../generators/backend.js';
import { FrontendGenerator } from '../generators/frontend.js';
import { MicroGenerator } from '../generators/micro.js';
export class CreateCommand {
static #instance;
microGenerator;
constructor(microGenerator) {
this.microGenerator = microGenerator;
}
static get instance() {
if (!CreateCommand.#instance) {
CreateCommand.#instance = new CreateCommand(MicroGenerator.instance);
}
return CreateCommand.#instance;
}
async create(params) {
const spinner = ora({
spinner: 'dots8',
color: 'green',
interval: 100,
});
const start = performance.now();
try {
// VALIDATE PROJECT TYPE
if (!isUndefined(params.projectType) &&
!PROJECT_TYPES.includes(params.projectType)) {
throw new UnidentifiedProjectTypeError(`${params.projectType} is not found or exist. Choose between ${PROJECT_TYPES.join(', ')}`);
}
// PROMPT PROJECT NAME
const projectNameQuestion = await inquirer.prompt({
name: 'projectName',
type: 'input',
message: "What's the name of your project?",
default: 'allin-project',
when: () => isUndefined(params.options.name) && isUndefined(params.projectName),
validate: (input) => {
if (__isLookLikePath(input)) {
return 'Project name must not be a path or contain path separators.';
}
const sanitizedStr = __sanitizeProjectName(input);
if (!__isValidProjectName(sanitizedStr)) {
return 'Project name invalid. Use letters, digits, hyphen or underscore, start with a letter.';
}
if (__isContainHarassmentWords(sanitizedStr, DIRTY_WORDS)) {
return 'Please choose a different project name (contains disallowed words).';
}
return true;
},
});
const resolveProjectName = () => {
return (params.options.name ??
params.projectName ??
projectNameQuestion.projectName);
};
const userProjectName = __renewProjectName(resolveProjectName());
const isProjectTypeDetected = __detectProjectTypeFromInput(userProjectName);
// WARN IF PROJECT TYPE MANUALLY SET BUT AUTO DETECTED
if (!isNull(isProjectTypeDetected) && !isUndefined(params.projectType)) {
warnBox('Warning Information', 'The [type] argument will not be used, as the system detects the project type from the project name.');
}
// PROMPT PROJECT TYPE IF NOT PROVIDED
const projectTypeQuestion = await inquirer.prompt([
{
name: 'projectType',
type: 'list',
message: 'What type of project do you want create:',
choices: PROJECT_TYPES,
default: 'backend',
when: () => isNull(isProjectTypeDetected) &&
isUndefined(params.options.template) &&
isUndefined(params.options.type) &&
isUndefined(params.projectType),
},
]);
const resolveProjectType = () => {
if (isProjectTypeDetected)
return isProjectTypeDetected;
if (params.options.template) {
const selectedTemplate = TEMPLATES_META_MAP.get(params.options.template);
if (!selectedTemplate) {
throw new UnidentifiedTemplateError(`${chalk.bold('Unidentified template model')}: ${chalk.bold(params.options.template)} template model is not found.`);
}
return selectedTemplate.category;
}
return params.projectType ?? projectTypeQuestion.projectType;
};
const userProjectType = resolveProjectType();
// PROMPT PROJECT DIRECTORY
const projectDirQuestion = await inquirer.prompt([
{
name: 'projectDir',
type: 'input',
message: 'Where do you want to save your project?',
default: process.cwd(),
when: () => isUndefined(params.options.dir) && isUndefined(params.projectDir),
validate: (input) => {
if (!__isLookLikePath(input)) {
return 'Project name must be a path or contain path separators.';
}
return true;
},
},
]);
const resolveProjectDir = () => params.options.dir ??
params.projectDir ??
projectDirQuestion.projectDir;
const userProjectDir = resolveProjectDir();
__pathNotFound(userProjectDir);
// HANDLE CACHE & REUSE CONFIRMATION
const cachedForType = await this.microGenerator.__getListCachedProjects(CACHE_BASE_PATH, userProjectType);
const reuseChoicePrompt = await inquirer.prompt({
name: 'reuseProject',
type: 'confirm',
message: `You've already generated ${userProjectType} projects before. Do you want to reuse one of them?`,
default: false,
when: () => cachedForType.length > 0,
});
if (!reuseChoicePrompt.reuseProject &&
typeof reuseChoicePrompt.reuseProject !== 'undefined') {
infoBox('Project Information', `Allright ${chalk.bold((await __getUserRealName()).split(' ')[0])}, thanks for the confirmation.`);
}
// RUN GENERATION PROCESS
switch (userProjectType) {
case 'backend':
await this.__runBackendProjectGeneratingProcess(userProjectType, params.projectName, spinner, params.options, userProjectName, userProjectDir, reuseChoicePrompt.reuseProject, cachedForType);
break;
case 'frontend':
await this.__runFrontendProjectGeneratingProcess(userProjectType, params.projectName, spinner, params.options, userProjectName, userProjectDir, reuseChoicePrompt.reuseProject, cachedForType);
break;
}
// DONE
const end = performance.now();
const convertToSeconds = ((end - start) / 1000).toFixed(3);
spinner.succeed(`Your ${chalk.bold(userProjectName)} is already created. Executed for ${chalk.bold(convertToSeconds)} s`);
}
catch (error) {
spinner.fail('Failed to create project.\n');
let errorMessage = error instanceof Error
? error.message
: `${chalk.bold('Error')}: An unknown error occurred.`;
if (error.name === 'ExitPromptError') {
errorMessage = `${chalk.bold('Exit prompt error')}: User forced close the prompt.`;
}
if (error) {
errorMessage = error.message;
}
const tempPath = path.join(BASE_PATH, 'templates', 'temp');
this.__removeUnusedProject(tempPath);
errorBox(error);
}
finally {
spinner.clear();
}
}
// --------------------------------------------------------------------------
// HELPER METHODS
// --------------------------------------------------------------------------
async __removeUnusedProject(tempPath) {
const exists = await fse.pathExists(tempPath);
if (!exists)
await fse.ensureDir(tempPath);
const subFolders = await fse.readdir(tempPath, { withFileTypes: true });
for (const folder of subFolders) {
if (!folder.isDirectory())
continue;
const itemPath = path.join(tempPath, folder.name);
try {
await fse.remove(itemPath);
}
catch (error) {
errorBox(error);
}
}
}
async __runBackendProjectGeneratingProcess(projectType, projectNameArg, spinner, options, projectName, projectDir, isReuseProject, cachedForType) {
const templateDir = path.join(BASE_PATH, 'templates', projectType);
__pathNotFound(templateDir);
const templateFiles = fse.readdirSync(templateDir, { withFileTypes: true });
const backendGenerator = BackendGenerator.instance;
await backendGenerator.generate({
projectNameArg: projectNameArg,
spinner: spinner,
optionValues: options,
templatesFiles: templateFiles,
projectName: projectName,
projectType: projectType,
projectDir: projectDir,
isUsingCacheProject: isReuseProject,
cachedEntries: cachedForType,
});
}
async __runFrontendProjectGeneratingProcess(projectType, projectNameArg, spinner, options, projectName, projectDir, isReuseProject, cachedForType) {
const templateDir = path.join(BASE_PATH, 'templates', projectType);
__pathNotFound(templateDir);
const templateFiles = fse.readdirSync(templateDir, { withFileTypes: true });
const frontendGenerator = FrontendGenerator.instance;
await frontendGenerator.generate({
projectNameArg: projectNameArg,
spinner: spinner,
optionValues: options,
templatesFiles: templateFiles,
projectName: projectName,
projectType: projectType,
projectDir: projectDir,
isUsingCacheProject: isReuseProject,
cachedEntries: cachedForType,
});
}
}
//# sourceMappingURL=create.js.map