UNPKG

create-ignite-kit

Version:

A CLI tool for generating production-ready React applications.

441 lines (381 loc) • 14.7 kB
#!/usr/bin/env node import { Command } from "commander"; import chalk from "chalk"; import inquirer from "inquirer"; import ora from "ora"; import fs from "fs-extra"; import { execa } from "execa"; import path from "path"; import { fileURLToPath } from "url"; // --- Helper to get the path of our CLI tool --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // --- Helper function for prompts --- const promptForConfiguration = async () => { const questions = [ { type: "list", name: "stateManagement", message: "Select a state management library:", choices: ["Redux Toolkit", "Zustand", "None"], default: "Redux Toolkit", }, { type: "list", name: "uiLibrary", message: "Select a UI library:", choices: ["Material UI", "Tailwind CSS", "None"], default: "Material UI", }, { type: "confirm", name: "useReactQuery", message: "Do you want to include TanStack Query (React Query)?", default: true, }, { type: "confirm", name: "useDocker", message: "Do you want to set up Docker?", default: true, }, { type: "confirm", name: "installDependencies", message: "Do you want to install npm dependencies automatically?", default: true, }, ]; const answers = await inquirer.prompt(questions); return answers; }; // --- Feature Function: Add Docker Support --- const addDockerSupport = async (projectPath) => { const dockerSpinner = ora("Adding Docker files...").start(); try { const dockerAssetPath = path.join(__dirname, "..", "assets", "docker"); await fs.copy( path.join(dockerAssetPath, "Dockerfile"), path.join(projectPath, "Dockerfile") ); await fs.copy( path.join(dockerAssetPath, ".dockerignore"), path.join(projectPath, ".dockerignore") ); dockerSpinner.succeed(chalk.green("Docker support added successfully.")); } catch (error) { dockerSpinner.fail(chalk.red("Failed to add Docker support.")); console.error(error); } }; // --- Feature Function: Add React Query Support --- const addReactQuerySupport = async (projectPath) => { const querySpinner = ora( "Integrating TanStack Query (React Query)..." ).start(); try { // 1. Add dependency to package.json const packageJsonPath = path.join(projectPath, "package.json"); const packageJson = await fs.readJson(packageJsonPath); packageJson.dependencies["@tanstack/react-query"] = "^5.51.1"; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); // 2. Modify src/main.tsx to wrap App with QueryClientProvider const mainTsxPath = path.join(projectPath, "src", "main.tsx"); let mainTsxContent = await fs.readFile(mainTsxPath, "utf-8"); // Add imports mainTsxContent = `import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\n${mainTsxContent}`; // Create a client instance mainTsxContent = mainTsxContent.replace( "import App from './App.tsx';", "import App from './App.tsx';\n\nconst queryClient = new QueryClient();" ); // Wrap the <App /> component mainTsxContent = mainTsxContent.replace( "<App />", "<QueryClientProvider client={queryClient}><App /></QueryClientProvider>" ); await fs.writeFile(mainTsxPath, mainTsxContent, "utf-8"); querySpinner.succeed( chalk.green("TanStack Query integrated successfully.") ); } catch (error) { querySpinner.fail(chalk.red("Failed to integrate TanStack Query.")); console.error(error); } }; // --- Feature Function: Add Material UI Support --- const addMuiSupport = async (projectPath) => { const muiSpinner = ora("Integrating Material UI...").start(); try { // Add dependencies to package.json const packageJsonPath = path.join(projectPath, "package.json"); const packageJson = await fs.readJson(packageJsonPath); packageJson.dependencies["@mui/material"] = "^6.1.9"; packageJson.dependencies["@emotion/react"] = "^11.13.5"; packageJson.dependencies["@emotion/styled"] = "^11.13.0"; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); // Modify App.tsx to include MUI Button demo const appTsxPath = path.join(projectPath, "src", "App.tsx"); let appTsxContent = await fs.readFile(appTsxPath, "utf-8"); // Add MUI import appTsxContent = `import { Button } from '@mui/material';\n${appTsxContent}`; // Replace the content to show MUI is working appTsxContent = appTsxContent.replace( '<div className="bg-amber-500">\n React Production Setup\n <Feature1Page />\n </div>', '<div className="bg-amber-500 p-4">\n <h1>React Production Setup</h1>\n <Button variant="contained" color="primary" sx={{ mt: 2, mb: 2 }}>\n Material UI Button\n </Button>\n <Feature1Page />\n </div>' ); await fs.writeFile(appTsxPath, appTsxContent, "utf-8"); muiSpinner.succeed(chalk.green("Material UI integrated successfully.")); } catch (error) { muiSpinner.fail(chalk.red("Failed to integrate Material UI.")); console.error(error); } }; // --- Feature Function: Add Redux Toolkit Support --- const addReduxSupport = async (projectPath) => { const reduxSpinner = ora("Integrating Redux Toolkit...").start(); try { // 1. Add dependencies to package.json const packageJsonPath = path.join(projectPath, "package.json"); const packageJson = await fs.readJson(packageJsonPath); packageJson.dependencies["@reduxjs/toolkit"] = "^2.3.0"; packageJson.dependencies["react-redux"] = "^9.2.0"; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); // 2. Create store directory and files const storePath = path.join(projectPath, "src", "store"); await fs.ensureDir(storePath); // Create store.ts const storeContent = `import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './features/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; `; await fs.writeFile(path.join(storePath, "store.ts"), storeContent, "utf-8"); // Create features directory and counter slice const featuresPath = path.join(storePath, "features"); await fs.ensureDir(featuresPath); const counterSliceContent = `import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface CounterState { value: number; } const initialState: CounterState = { value: 0, }; export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; `; await fs.writeFile( path.join(featuresPath, "counterSlice.ts"), counterSliceContent, "utf-8" ); // 3. Modify src/main.tsx to wrap App with Redux Provider const mainTsxPath = path.join(projectPath, "src", "main.tsx"); let mainTsxContent = await fs.readFile(mainTsxPath, "utf-8"); // Add Redux imports mainTsxContent = `import { Provider } from 'react-redux';\nimport { store } from './store/store';\n${mainTsxContent}`; // Wrap with Provider (handle nesting with QueryClientProvider if it exists) if (mainTsxContent.includes("QueryClientProvider")) { mainTsxContent = mainTsxContent.replace( "<QueryClientProvider client={queryClient}>", "<Provider store={store}><QueryClientProvider client={queryClient}>" ); mainTsxContent = mainTsxContent.replace( "</QueryClientProvider>", "</QueryClientProvider></Provider>" ); } else { mainTsxContent = mainTsxContent.replace( "<App />", "<Provider store={store}><App /></Provider>" ); } await fs.writeFile(mainTsxPath, mainTsxContent, "utf-8"); reduxSpinner.succeed(chalk.green("Redux Toolkit integrated successfully.")); } catch (error) { reduxSpinner.fail(chalk.red("Failed to integrate Redux Toolkit.")); console.error(error); } }; // --- Feature Function: Add Zustand Support --- const addZustandSupport = async (projectPath) => { const zustandSpinner = ora("Integrating Zustand...").start(); try { // 1. Add dependency to package.json const packageJsonPath = path.join(projectPath, "package.json"); const packageJson = await fs.readJson(packageJsonPath); packageJson.dependencies["zustand"] = "^5.0.2"; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); // 2. Create store directory and store file const storePath = path.join(projectPath, "src", "store"); await fs.ensureDir(storePath); const storeContent = `import { create } from 'zustand'; interface CounterState { count: number; increment: () => void; decrement: () => void; reset: () => void; } export const useCounterStore = create<CounterState>((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); `; await fs.writeFile( path.join(storePath, "useAppStore.ts"), storeContent, "utf-8" ); zustandSpinner.succeed(chalk.green("Zustand integrated successfully.")); } catch (error) { zustandSpinner.fail(chalk.red("Failed to integrate Zustand.")); console.error(error); } }; // --- Main creation function --- const createProject = async (projectName) => { const projectPath = path.resolve(process.cwd(), projectName); if (fs.existsSync(projectPath)) { console.error( chalk.red(`\nError: Directory '${projectName}' already exists.`) ); process.exit(1); } console.log( chalk.green.bold( `\nšŸ”„ Starting the setup for your new project: ${projectName}\n` ) ); const options = await promptForConfiguration(); console.log(chalk.blue.bold("\nYour selected configuration:")); console.log(chalk.blue(`- State Management: ${options.stateManagement}`)); console.log(chalk.blue(`- UI Library: ${options.uiLibrary}`)); console.log( chalk.blue(`- React Query: ${options.useReactQuery ? "Yes" : "No"}`) ); console.log( chalk.blue(`- Docker: ${options.useDocker ? "Yes" : "No"}`) ); console.log( chalk.blue(`- Install Dependencies: ${options.installDependencies ? "Yes" : "No"}\n`) ); const copySpinner = ora( `Copying base template files to '${projectName}'...` ).start(); try { const templatePath = path.join(__dirname, "..", "template"); await fs.copy(templatePath, projectPath); copySpinner.succeed(chalk.green("Base files copied successfully.")); // --- Apply Configurations --- if (options.useDocker) { await addDockerSupport(projectPath); } if (options.useReactQuery) { await addReactQuerySupport(projectPath); } if (options.stateManagement === "Redux Toolkit") { await addReduxSupport(projectPath); } if (options.stateManagement === "Zustand") { await addZustandSupport(projectPath); } if (options.uiLibrary === "Material UI") { await addMuiSupport(projectPath); } // Note: Tailwind CSS is already configured in the template // --- Install Dependencies --- if (options.installDependencies) { const installSpinner = ora( "Installing dependencies... This might take a few minutes." ).start(); process.chdir(projectPath); await execa("npm", ["install"]); installSpinner.succeed(chalk.green("Dependencies installed.")); } // --- Success Message --- console.log(chalk.green.bold("\nšŸš€ Success! Your project is ready.")); console.log(chalk.cyan(`\nTo get started, run the following commands:\n`)); console.log(chalk.white(` cd ${projectName}`)); if (!options.installDependencies) { console.log(chalk.white(` npm install`)); } console.log( chalk.white(` cp .env.example .env`) ); console.log(chalk.white(` npm run dev\n`)); } catch (error) { copySpinner.fail(chalk.red("An error occurred during project setup.")); console.error(chalk.red(error.message)); if (fs.existsSync(projectPath)) { fs.removeSync(projectPath); } process.exit(1); } }; // --- Main Program Definition --- const program = new Command(); program .name("create-ignite-kit") .description( chalk.cyan.bold( "A CLI tool for generating production-ready React applications." ) ) .version("1.0.0"); // Handle npm create ignite-kit pattern - when called without explicit command const args = process.argv.slice(2); if (args.length === 0 || (args.length === 1 && !args[0].startsWith("-"))) { // If no arguments or just a project name, prompt for project name and create const projectName = args[0] || (await inquirer .prompt([ { type: "input", name: "projectName", message: "What is your project name?", default: "my-ignite-app", validate: (input) => { if (!input.trim()) { return "Project name is required"; } if (!/^[a-zA-Z0-9-_]+$/.test(input)) { return "Project name can only contain letters, numbers, hyphens, and underscores"; } return true; }, }, ]) .then((answers) => answers.projectName)); await createProject(projectName); } else { // Handle explicit create command for backwards compatibility program .command("create <project-name>") .description("Create a new React project with a specified name") .action(createProject); // Parse the command-line arguments and execute the corresponding action program.parse(process.argv); }