reactwizard/index.js
2024-08-13 21:25:15 -04:00

497 lines
15 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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('<project-directory>')
.usage(`${chalk.green('<project-directory>')}`)
.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 Prettiers 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 (
<Provider store={store}>
${appTsx}
</Provider>
);
};
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
};