@pitifulhawk/flash-up
Version:
Interactive project scaffolder for modern web applications
451 lines โข 18.1 kB
JavaScript
import { Command } from "commander";
import { Framework, PackageManager, ProjectLanguage, } from "./types/index.js";
import { promptForProjectConfig, confirmProjectConfig } from "./ui/prompts.js";
import { logger } from "./ui/logger.js";
import { ProjectScaffolder } from "./core/scaffolder.js";
import { detectPackageManager, validatePackageManager, } from "./core/package-manager.js";
import { validateProjectName, validateFramework, validatePackageManager as validatePMInput, } from "./utils/validation.js";
const VERSION = "1.0.0";
const PRESETS = {
react: {
framework: Framework.REACT,
language: ProjectLanguage.JAVASCRIPT,
description: "React with Vite (JavaScript)",
},
"react-ts": {
framework: Framework.REACT,
language: ProjectLanguage.TYPESCRIPT,
description: "React with Vite (TypeScript)",
},
next: {
framework: Framework.NEXTJS,
language: ProjectLanguage.JAVASCRIPT,
description: "Next.js (JavaScript)",
},
"next-ts": {
framework: Framework.NEXTJS,
language: ProjectLanguage.TYPESCRIPT,
description: "Next.js (TypeScript)",
},
express: {
framework: Framework.EXPRESS,
language: ProjectLanguage.JAVASCRIPT,
description: "Express.js (JavaScript)",
},
"express-ts": {
framework: Framework.EXPRESS,
language: ProjectLanguage.TYPESCRIPT,
description: "Express.js (TypeScript)",
},
};
class FlashUpCLI {
program;
options = {};
constructor() {
this.program = new Command();
this.setupCommands();
}
setupCommands() {
this.program
.name("flash-up")
.description("Interactive project scaffolder for modern web applications")
.version(VERSION)
.argument("[preset]", "preset template (react, react-ts, next, next-ts, express, express-ts)")
.option("-n, --name <name>", "project name")
.option("-f, --framework <framework>", "framework (react, nextjs, express)")
.option("-p, --package-manager <pm>", "package manager (npm, pnpm, yarn, bun)")
.option("--skip-prompts", "skip interactive prompts")
.option("-v, --verbose", "verbose output")
.action(async (preset, options) => {
this.options = options;
if (preset) {
await this.runWithPreset(preset);
}
else {
await this.run();
}
});
this.program
.command("help")
.description("display help information")
.action(() => {
this.displayHelp();
});
this.program
.command("version")
.description("display version information")
.action(() => {
console.log(VERSION);
});
}
async runWithPreset(presetName) {
try {
if (this.options.verbose) {
logger.setVerbose(true);
}
const preset = PRESETS[presetName];
if (!preset) {
console.error(`โ Unknown preset: ${presetName}`);
console.log(`Available presets: ${Object.keys(PRESETS).join(", ")}`);
process.exit(1);
}
this.displayBanner();
let projectName = this.options.name;
if (!projectName) {
projectName = `my-${presetName}-app`;
}
const nameValidation = validateProjectName(projectName);
if (!nameValidation.valid) {
console.error(`โ Invalid project name: ${nameValidation.error}`);
process.exit(1);
}
let packageManager = this.options.packageManager;
if (!packageManager) {
const detectedPM = await detectPackageManager();
packageManager = detectedPM || PackageManager.NPM;
}
else {
const pmValidation = validatePMInput(packageManager);
if (!pmValidation.valid) {
console.error(`โ Invalid package manager: ${pmValidation.error}`);
process.exit(1);
}
}
const config = {
name: projectName,
language: preset.language,
framework: preset.framework,
packageManager,
addOns: {
categories: [],
includeLinting: false,
includeAuthentication: false,
includeClientRouting: false,
includeDocker: false,
},
targetPath: projectName,
};
console.log();
logger.info(`๐ Creating ${preset.description} project: ${projectName}`);
console.log();
if (this.options.skipPrompts) {
await this.createProject(config);
}
else {
const confirmed = await confirmProjectConfig(config);
if (!confirmed) {
logger.info("Project creation cancelled.");
process.exit(0);
}
const enhancedConfig = await this.enhanceConfigWithPrompts(config);
await this.createProject(enhancedConfig);
}
}
catch (error) {
logger.error(`Failed to create project: ${error.message}`);
if (this.options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
async run() {
try {
if (this.options.verbose) {
logger.setVerbose(true);
}
this.displayBanner();
const config = await this.getProjectConfiguration();
if (!this.options.skipPrompts) {
const confirmed = await confirmProjectConfig(config);
if (!confirmed) {
logger.info("Project creation cancelled.");
process.exit(0);
}
}
const isValidPM = await validatePackageManager(config.packageManager);
if (!isValidPM) {
logger.error(`Package manager ${config.packageManager} is not available on this system.`);
logger.info("Please install the package manager or choose a different one.");
process.exit(1);
}
await this.createProject(config);
}
catch (error) {
logger.error(`Error: ${error.message}`);
if (this.options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
async enhanceConfigWithPrompts(config) {
const enhancedConfig = await promptForProjectConfig({
name: config.name,
language: config.language,
framework: config.framework,
packageManager: config.packageManager,
addOns: config.addOns,
});
return enhancedConfig;
}
displayHelp() {
console.log();
console.log("๐ flash-up - Interactive project scaffolder for modern web applications");
console.log();
console.log("QUICK START (Preset Commands):");
console.log(" npx flash-up react Create React + Vite project (JavaScript)");
console.log(" npx flash-up react-ts Create React + Vite project (TypeScript)");
console.log(" npx flash-up next Create Next.js project (JavaScript)");
console.log(" npx flash-up next-ts Create Next.js project (TypeScript)");
console.log(" npx flash-up express Create Express.js project (JavaScript)");
console.log(" npx flash-up express-ts Create Express.js project (TypeScript)");
console.log();
console.log("INTERACTIVE MODE:");
console.log(" npx flash-up Start interactive configuration");
console.log();
console.log("OPTIONS:");
console.log(" -n, --name <name> Project name");
console.log(" -p, --package-manager <pm> Package manager (npm, pnpm, yarn, bun)");
console.log(" --skip-prompts Skip interactive prompts");
console.log(" -v, --verbose Verbose output");
console.log(" -h, --help Display help information");
console.log(" -V, --version Display version number");
console.log();
console.log("EXAMPLES:");
console.log(" npx flash-up react-ts --name my-app");
console.log(" npx flash-up next --name my-blog --package-manager pnpm");
console.log(" npx flash-up express-ts --skip-prompts");
console.log();
}
displayBanner() {
if (!this.options.skipPrompts) {
logger.banner("โก flash-up - Project Scaffolder");
logger.info("Create modern web applications with ease!");
logger.newLine();
}
}
async getProjectConfiguration() {
const initialConfig = {};
if (this.options.name) {
const nameValidation = validateProjectName(this.options.name);
if (!nameValidation.valid) {
throw new Error(`Invalid project name: ${nameValidation.error}`);
}
initialConfig.name = this.options.name;
}
if (this.options.framework) {
const frameworkValidation = validateFramework(this.options.framework);
if (!frameworkValidation.valid) {
throw new Error(`Invalid framework: ${frameworkValidation.error}`);
}
initialConfig.framework = this.options.framework;
}
if (this.options.packageManager) {
const pmValidation = validatePMInput(this.options.packageManager);
if (!pmValidation.valid) {
throw new Error(`Invalid package manager: ${pmValidation.error}`);
}
initialConfig.packageManager = this.options
.packageManager;
}
else {
const detected = await detectPackageManager();
if (detected) {
initialConfig.packageManager = detected;
logger.debug(`Detected package manager: ${detected}`);
}
}
if (this.options.skipPrompts) {
return this.getDefaultConfiguration(initialConfig);
}
return await promptForProjectConfig(initialConfig);
}
getDefaultConfiguration(partial) {
const name = partial.name || "my-app";
const language = partial.language || ProjectLanguage.TYPESCRIPT;
const framework = partial.framework || Framework.REACT;
const packageManager = partial.packageManager || PackageManager.NPM;
const addOns = {
categories: [],
includeLinting: false,
includeAuthentication: false,
includeClientRouting: false,
includeDocker: false,
};
return {
name,
language,
framework,
packageManager,
addOns,
targetPath: name,
};
}
async createProject(config) {
logger.section("Creating Project");
logger.keyValue("Name", config.name);
logger.keyValue("Language", config.language);
logger.keyValue("Framework", config.framework);
logger.keyValue("Package Manager", config.packageManager);
logger.keyValue("Features", this.getAddOnsDisplayText(config.addOns));
logger.newLine();
const scaffolder = new ProjectScaffolder(config);
const success = await scaffolder.createProject();
if (success) {
logger.newLine();
logger.success("Project created successfully! ๐");
logger.newLine();
const nextSteps = scaffolder.getNextSteps();
logger.nextSteps(nextSteps);
this.displayTips(config);
}
else {
logger.error("Project creation failed. Please check the errors above.");
process.exit(1);
}
}
displayTips(config) {
logger.section("Tips");
switch (config.framework) {
case Framework.REACT:
logger.list([
"Edit src/App.tsx to start building your React application",
"Add components in the src/components directory",
"Use Vite's hot reload for fast development",
]);
break;
case Framework.NEXTJS:
logger.list([
"Create pages in the src/app directory",
"Use Next.js App Router for routing",
"Add API routes in src/app/api",
]);
break;
case Framework.EXPRESS:
logger.list([
"Add routes in src/routes directory",
"Use middleware for authentication and validation",
"Build with `npm run build` before deployment",
]);
break;
}
logger.newLine();
logger.info(`๐ฏ Your ${config.language} project is ready!`);
if (config.addOns.cssFramework) {
logger.newLine();
logger.info(`๐ก ${config.addOns.cssFramework} CSS is configured and ready to use!`);
if (config.addOns.uiLibrary) {
logger.info(`๐จ ${config.addOns.uiLibrary} UI components are available!`);
}
}
if (config.addOns.stateManagement) {
logger.newLine();
logger.info(`๐๏ธ ${config.addOns.stateManagement} state management is set up!`);
const storeFile = config.language === ProjectLanguage.TYPESCRIPT
? "store.ts"
: "store.js";
logger.list([
`Check src/store/${storeFile} for your global state configuration`,
]);
}
if (config.addOns.includeClientRouting) {
logger.newLine();
logger.info("๐ฃ๏ธ React Router is configured for client-side routing!");
logger.list([
"Check src/pages/ for your route components",
"Update src/App.tsx to add new routes",
]);
}
if (config.addOns.httpClient) {
logger.newLine();
logger.info(`๐ ${config.addOns.httpClient} HTTP client is configured!`);
const apiFile = config.language === ProjectLanguage.TYPESCRIPT ? "api.ts" : "api.js";
const apiPath = config.addOns.httpClient === "axios" ? "src/lib/" : "src/utils/";
logger.list([`Check ${apiPath}${apiFile} for API utilities`]);
}
if (config.addOns.testingFramework) {
logger.newLine();
logger.info(`๐งช ${config.addOns.testingFramework} testing environment is ready!`);
logger.list([
"Run 'npm test' to run your tests",
"Check src/__tests__/ for test examples",
]);
}
if (config.addOns.includeAuthentication) {
logger.newLine();
logger.info("๐ JWT Authentication is configured!");
logger.list([
"Check src/routes/auth.ts for authentication endpoints",
"Update .env with your JWT_SECRET",
]);
}
if (config.addOns.database && config.addOns.orm) {
logger.newLine();
logger.info(`๐๏ธ ${config.addOns.database} + ${config.addOns.orm} is configured!`);
logger.list([
"Update .env with your database connection string",
"Run database migrations if using Prisma/Sequelize",
]);
}
if (config.addOns.includeLinting) {
logger.newLine();
logger.info("๐ง ESLint and Prettier are configured for code quality.");
logger.list([
"Run 'npm run lint' to check for issues",
"Run 'npm run format' to format your code",
]);
}
if (config.addOns.includeDocker) {
logger.newLine();
logger.info("๐ณ Docker configuration is ready!");
logger.list([
"Run 'docker build -t my-app .' to build your image",
"Run 'docker-compose up' for full stack development",
]);
}
logger.newLine();
logger.info("Happy coding! ๐");
}
getAddOnsDisplayText(addOns) {
const features = [];
if (addOns.cssFramework && addOns.cssFramework !== "none") {
let cssText = addOns.cssFramework;
if (addOns.uiLibrary && addOns.uiLibrary !== "none") {
cssText += ` + ${addOns.uiLibrary}`;
}
features.push(cssText);
}
if (addOns.includeLinting) {
features.push("ESLint + Prettier");
}
if (addOns.httpClient) {
features.push(addOns.httpClient);
}
return features.length > 0 ? features.join(", ") : "None";
}
async parseAndRun(argv) {
await this.program.parseAsync(argv);
}
}
async function main() {
const cli = new FlashUpCLI();
await cli.parseAndRun(process.argv);
}
process.on("unhandledRejection", (reason, promise) => {
logger.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
process.exit(1);
});
process.on("uncaughtException", (error) => {
logger.error(`Uncaught Exception: ${error.message}`);
if (process.env["NODE_ENV"] === "development") {
console.error(error.stack);
}
process.exit(1);
});
main().catch((error) => {
logger.error(`Fatal error: ${error.message}`);
process.exit(1);
});
//# sourceMappingURL=index.js.map