create-ignite-kit
Version:
A CLI tool for generating production-ready React applications.
441 lines (381 loc) ⢠14.7 kB
JavaScript
#!/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);
}