#!/usr/bin/env node const { execSync } = require('child_process'); const chalk = require('chalk'); const fs = require('fs'); const path = require('path'); const commander = require('commander'); const ora = require('ora'); const packageJson = require('./package.json'); const fsExtra = require('fs-extra'); const inquirer = require('inquirer'); const { spawn } = require('child_process'); const os = require('os'); const program = new commander.Command(packageJson.name) .version(packageJson.version) .arguments('') .usage(`${chalk.green('')}`) .option('-v, --verbose', 'Run with verbose logging') // <-- Add this line .action((projectDirectory, options) => { // <-- Update the action to take options createApp(projectDirectory, options); }) .parse(process.argv); function createApp(projectDirectory, options) { console.clear(); // Clears the console before anything else const root = path.resolve(projectDirectory); const verboseFlag = options.verbose ? '--verbose' : ''; const stdioOption = verboseFlag === '--verbose' ? 'inherit' : 'ignore'; // Check if the directory already exists if (fs.existsSync(root)) { console.clear(); console.log(chalk.red.bold('❌ Error: Directory already exists!')); console.log( chalk.yellow(`\nThe directory ${chalk.blue(root)} already exists.`) ); console.log( chalk.cyan( 'Please choose a different project name or delete the existing directory.' ) ); return; // Exit the function if the directory exists } const appName = path.basename(root); console.log(`πŸš€ Creating a new React app in ${chalk.green(root)}...`); fs.mkdirSync(root); process.chdir(root); const spinner = ora( '⏳ Installing packages. This might take a couple of minutes.' ).start(); execSync(`npx create-react-app . --template typescript ${verboseFlag}`, { stdio: stdioOption, }); spinner.succeed('πŸ“¦ Packages installed successfully.'); installDependencies(verboseFlag, stdioOption); installDevDependencies(verboseFlag, stdioOption); setupAntd(); setupSass(); setupTesting(stdioOption); setupHusky(stdioOption); setupCommitlint(stdioOption); setupRedux(); createAtomicStructure(); updatePackageJson(); copyPreConfiguredFiles(root); // Copy pre-configured files like prettier-commit.js deletePreCommitHook(); // Delete the .husky/pre-commit hook runGitCommands(stdioOption); // Run git add . and git commit -m "feat: happy coding" runPrettierCommit(stdioOption); // Run npm run prettier:commit printCommandSummary(); // Print the command summary console.log(chalk.green('πŸŽ‰ All done! Happy coding.')); // Ask the user where they want to open the project askUserWhereToOpen(root); } function askUserWhereToOpen(directory) { inquirer .prompt([ { type: 'list', name: 'openIn', message: 'Where would you like to open the project?', choices: ['Terminal', 'VSCode', 'Neovim', 'None'], }, ]) .then((answers) => { switch (answers.openIn) { case 'Terminal': openInTerminal(directory); break; case 'VSCode': openInVSCode(directory); break; case 'Neovim': openInNeovim(directory); break; default: console.log( chalk.yellow( 'Project setup complete. You can manually open the project if needed.' ) ); } }); } function openInTerminal(directory) { const platform = os.platform(); if (platform === 'darwin') { // macOS spawn('open', ['-a', 'Terminal', directory]); } else if (platform === 'win32') { // Windows spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K', `cd /d ${directory}`], { shell: true, }); } else if (platform === 'linux') { // Linux spawn('gnome-terminal', ['--working-directory=' + directory]); } else { console.log( chalk.red( 'Unsupported platform. Please manually navigate to the directory.' ) ); } } function openInVSCode(directory, stdioOption) { spawn('code', [directory], { stdio: 'inherit' }); } function openInNeovim(directory) { spawn('nvim', [directory], { stdio: 'inherit' }); } function printCommandSummary() { console.log(chalk.yellow('\nπŸ”‘ Project Setup Summary:')); console.log(chalk.cyan('\nAvailable Commands:')); console.log(chalk.green('1. πŸš€ npm start')); console.log( chalk.white( ' Starts the development server with Webpack. The project is served using Webpack Dev Server with the configuration specified in webpack.config.js.' ) ); console.log(chalk.green('\n2. πŸ› οΈ npm run build')); console.log( chalk.white( ' Builds the project for production. Webpack compiles the project and outputs the optimized bundle in the /dist directory.' ) ); console.log(chalk.green('\n3. πŸ§ͺ npm test')); console.log( chalk.white( ' Placeholder for running tests. Currently, it does not run any tests but can be customized to run Jest or other test suites.' ) ); console.log(chalk.green('\n4. πŸ§ͺ npm run test:dev')); console.log( chalk.white( ' Runs tests in watch mode using React Scripts. Suitable for a test-driven development approach.' ) ); console.log(chalk.green('\n5. 🎨 npm run pretty-quick')); console.log( chalk.white( ' Formats all staged files using Prettier. Ensures that code is consistently formatted before committing.' ) ); console.log(chalk.green('\n6. πŸ” npm run lint:prettier')); console.log( chalk.white( ' Checks the format of the entire codebase using a custom script. It can be used to ensure that all files adhere to Prettier’s formatting rules.' ) ); console.log(chalk.green('\n7. ✨ npm run prettier')); console.log( chalk.white( ' Formats the entire codebase using Prettier based on the configuration in .prettierrc.' ) ); console.log(chalk.green('\n8. ✨ npm run prettier:commit')); console.log( chalk.white( ' Applies Prettier formatting to staged files before committing. Ensures that committed code is properly formatted.' ) ); console.log(chalk.green('\n9. 🚨 npm run eject')); console.log( chalk.white( ' Ejects the project from Create React App. This command exposes the underlying configuration files for full control but cannot be undone.' ) ); console.log(chalk.green('\n10. πŸ›‘οΈ npm run prepare')); console.log( chalk.white( ' Installs Husky hooks. This script is automatically run after dependencies are installed, setting up Git hooks for the project.' ) ); console.log( chalk.yellow( '\nπŸŽ‰ Your project is ready! Use the above commands to start working on your new React app.' ) ); } function runPrettierCommit(stdioOption) { const spinner = ora('🎨 Running prettier:commit script...').start(); try { execSync('npm run prettier:commit', { stdio: stdioOption }); spinner.succeed('βœ… Prettier commit script executed successfully.'); } catch (error) { spinner.fail('❌ Failed to run prettier:commit script.'); console.error(error); } } function runGitCommands(stdioOption) { const spinner = ora('πŸ”§ Running Git commands...').start(); try { execSync('git add .', { stdio: stdioOption }); execSync('git commit -m "feat: happy coding"', { stdio: stdioOption }); spinner.succeed('βœ… Git commands executed successfully.'); } catch (error) { spinner.fail('❌ Failed to execute Git commands.'); console.error(error); } } function deletePreCommitHook() { const spinner = ora('πŸ—‘οΈ Deleting .husky/pre-commit hook...').start(); const preCommitPath = path.resolve('.husky', 'pre-commit'); if (fs.existsSync(preCommitPath)) { fs.unlinkSync(preCommitPath); spinner.succeed('πŸ—‘οΈ .husky/pre-commit hook deleted.'); } else { spinner.warn('⚠️ .husky/pre-commit hook not found.'); } } function updatePackageJson() { const spinner = ora( 'πŸ“ Updating package.json with custom scripts...' ).start(); const packageJsonPath = path.resolve('package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); packageJson.scripts = { start: 'webpack serve --config webpack.config.js --mode development', build: 'webpack --config webpack.config.js --mode production', test: 'echo "Error: no test specified" && exit 0', 'test:dev': 'react-scripts test', 'pretty-quick': 'pretty-quick', 'lint:prettier': 'node check-format.js', prettier: 'prettier --write . --config .prettierrc', 'prettier:commit': 'node prettier-commit.js', eject: 'react-scripts eject', prepare: 'husky install', }; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); spinner.succeed('πŸ“ package.json updated with custom scripts.'); } function copyPreConfiguredFiles(destinationPath) { const spinner = ora('πŸ“ Copying pre-configured files...').start(); const filesToCopy = [ { src: path.resolve(__dirname, 'pre-files/check-format.js'), dest: path.join(destinationPath, 'check-format.js'), }, { src: path.resolve(__dirname, 'pre-files/commit-msg-linter.js'), dest: path.join(destinationPath, '.husky/commit-msg-linter.js'), }, { src: path.resolve(__dirname, 'pre-files/lint-check.js'), dest: path.join(destinationPath, '.husky/lint-check.js'), }, { src: path.resolve(__dirname, 'pre-files/prettier-commit.js'), dest: path.join(destinationPath, 'prettier-commit.js'), }, { src: path.resolve(__dirname, 'pre-files/webpack.config.js'), dest: path.join(destinationPath, 'webpack.config.js'), }, { src: path.resolve(__dirname, 'pre-files/.babelrc'), dest: path.join(destinationPath, '.babelrc'), }, { src: path.resolve(__dirname, 'pre-files/.eslintrc.js'), dest: path.join(destinationPath, '.eslintrc.js'), }, { src: path.resolve(__dirname, 'pre-files/.prettierrc'), dest: path.join(destinationPath, '.prettierrc'), }, // Add more files here if needed ]; filesToCopy.forEach((file) => { fs.copyFileSync(file.src, file.dest); }); // Recursively copy all files from pre-files/src/* to destinationPath/src/ const srcPath = path.resolve(__dirname, 'pre-files/src'); const destPath = path.join(destinationPath, 'src'); fsExtra.copySync(srcPath, destPath); spinner.succeed('πŸ“ Pre-configured files copied.'); } function installDependencies(verboseFlag, stdioOption) { const spinner = ora('πŸ”„ Installing additional dependencies...').start(); execSync( `npm install @babel/core @babel/preset-env @babel/preset-react @reduxjs/toolkit @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest @types/node @types/react @types/react-dom ajv antd babel-loader css-loader jest playwright react react-dom react-redux react-scripts redux sass sass-loader style-loader typescript web-vitals webpack webpack-cli ${verboseFlag}`, { stdio: stdioOption } ); spinner.succeed('βœ… Additional dependencies installed.'); } function installDevDependencies(verboseFlag, stdioOption) { const spinner = ora('πŸ”„ Installing additional dev dependencies...').start(); execSync( `npm install --save-dev @babel/plugin-proposal-private-property-in-object ora prettier @commitlint/cli @commitlint/config-conventional @svgr/webpack dotenv dotenv-webpack husky url-loader webpack-dev-server pretty-quick ${verboseFlag}`, { stdio: stdioOption } ); spinner.succeed('βœ… Additional dev dependencies installed.'); } function setupAntd() { const spinner = ora('🎨 Setting up Ant Design...').start(); const indexCssPath = path.resolve('src', 'index.css'); const indexCss = fs.readFileSync(indexCssPath, 'utf8'); fs.writeFileSync(indexCssPath, `@import '~antd/dist/antd.css';\n${indexCss}`); spinner.succeed('🎨 Ant Design set up.'); } function setupSass() { const spinner = ora('🎨 Setting up SASS...').start(); const appCssPath = path.resolve('src', 'App.css'); fs.renameSync(appCssPath, appCssPath.replace('.css', '.scss')); spinner.succeed('🎨 SASS set up.'); } function setupTesting(stdioOption) { const spinner = ora('πŸ§ͺ Setting up Playwright and Jest...').start(); try { execSync('npx playwright install', { stdio: stdioOption }); spinner.succeed('πŸ§ͺ Playwright and Jest set up.'); } catch (error) { spinner.fail('❌ Failed to set up Playwright and Jest.'); console.error(error); } } function setupHusky(stdioOption) { const spinner = ora('🐢 Setting up Husky...').start(); execSync('npx husky-init && npm install', { stdio: stdioOption, }); execSync('npx husky add .husky/pre-commit "npm test"', { stdio: stdioOption, }); spinner.succeed('🐢 Husky set up.'); } function setupCommitlint(stdioOption) { const spinner = ora('πŸ” Setting up Commitlint...').start(); const commitMsg = `#!/bin/sh . "$(dirname "$0")/_/husky.sh" node ./.husky/commit-msg-linter.js "$1"`; const commitLintMsgLinter = `module.exports = { extends: ['@commitlint/config-conventional'], };`; const prePush = `#!/bin/sh . "$(dirname "$0")/_/husky.sh" node .husky/lint-check.js`; execSync('npm install @commitlint/{config-conventional,cli} --save-dev', { stdio: stdioOption, }); execSync('touch .husky/commit-msg', { stdio: stdioOption }); execSync('touch .husky/pre-push', { stdio: stdioOption }); execSync('chmod +x .husky/commit-msg', { stdio: stdioOption }); execSync('chmod +x .husky/pre-push', { stdio: stdioOption }); fs.writeFileSync(path.resolve('.husky/commit-msg'), commitMsg); fs.writeFileSync(path.resolve('commitlint.config.js'), commitLintMsgLinter); fs.writeFileSync(path.resolve('.husky/pre-push'), prePush); spinner.succeed('πŸ” Commitlint set up.'); } function setupRedux() { const spinner = ora('πŸ› οΈ Setting up Redux...').start(); const reduxStructure = [ 'src/store', 'src/store/slices', 'src/store/middleware', 'src/store/selectors', ]; reduxStructure.forEach((dir) => { fs.mkdirSync(dir, { recursive: true }); }); const storeIndex = ` import { configureStore } from β€˜@reduxjs/toolkit’; const store = configureStore({ reducer: { // Add your reducers here }, middleware: (getDefaultMiddleware) => getDefaultMiddleware(), }); export default store; `; fs.writeFileSync(path.resolve('src/store/index.ts'), storeIndex); const appTsxPath = path.resolve('src/App.tsx'); let appTsx = fs.readFileSync(appTsxPath, 'utf8'); appTsx = ` import React from 'react'; import { Provider } from 'react-redux'; import store from './store'; const App: React.FC = () => { return ( ${appTsx} ); }; export default App; `; fs.writeFileSync(appTsxPath, appTsx); spinner.succeed('πŸ› οΈ Redux set up.'); } function createAtomicStructure() { const spinner = ora('πŸ—οΈ Creating atomic design structure…').start(); const atomicStructure = [ 'src/components/atoms', 'src/components/molecules', 'src/components/organisms', 'src/components/templates', 'src/components/pages', ]; atomicStructure.forEach((dir) => { fs.mkdirSync(dir, { recursive: true }); }); spinner.succeed('πŸ—οΈ Atomic design structure created.'); } if (!program.args.length) { program.help(); } module.exports = { printCommandSummary, installDependencies, // Export other functions as needed };