react-atomic-structure
Version:
A command-line tool to set up a predefined folder structure in a React project using Vite.
272 lines (234 loc) • 9.26 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import inquirer from 'inquirer';
// Import JSON file using 'import' syntax in ES modules
import { readFileSync, writeFileSync } from 'fs';
const projectPath = process.cwd();
const packageJsonPath = path.join(projectPath, 'package.json');
const postcssConfigPath = path.join(projectPath, 'postcss.config.js');
const prettierConfigPath = path.join(projectPath, '.prettierrc');
const prettierIgnorePath = path.join(projectPath, '.prettierignore');
const gitIgnorePath = path.join(projectPath, '.gitignore');
const eslintConfigPath = path.join(projectPath, '.eslintrc.json');
// Function to ensure all dependencies are installed
async function installDependencies(packageManager) {
const dependencies = [
'eslint', 'postcss', 'autoprefixer', 'husky', 'prettier'
];
console.log(`Installing dependencies with ${packageManager}...`);
if (packageManager === 'yarn') {
execSync(`yarn add ${dependencies.join(' ')} --dev`, { stdio: 'inherit' });
} else {
execSync(`npm install ${dependencies.join(' ')} --save-dev`, { stdio: 'inherit' });
}
console.log('Dependencies installed!');
}
// Function to add missing configuration to package.json
async function addPackageJsonConfig() {
// Use fs.readFileSync to read the JSON file
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
// Add lint-staged config
if (!packageJson['lint-staged']) {
packageJson['lint-staged'] = {
"*.js": "eslint --fix",
"*.ts": "eslint --fix",
"*.tsx": "eslint --fix"
};
}
// Add husky configuration if it doesn't exist
if (!packageJson.scripts) {
packageJson.scripts = {};
}
if (!packageJson.scripts['prepare']) {
packageJson.scripts['prepare'] = "husky install";
}
// Write the updated package.json
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log('Updated package.json with lint-staged and husky configurations.');
}
// Function to choose package manager
async function choosePackageManager() {
const answers = await inquirer.prompt([{
type: 'list',
name: 'packageManager',
message: 'Which package manager are you using?',
choices: ['npm', 'yarn'],
default: 'npm',
}]);
return answers.packageManager;
}
// Function to choose Vite template
async function chooseTemplate() {
const answers = await inquirer.prompt([{
type: 'list',
name: 'template',
message: 'Please choose a Vite template:',
choices: ['react', 'react-ts', 'vanilla', 'vanilla-ts', 'vue', 'svelte'],
default: 'react',
}]);
return answers.template;
}
// Function to ask for install settings
async function askInstallSettings() {
const answers = await inquirer.prompt([{
type: 'confirm',
name: 'installSettings',
message: 'Do you want to install ESLint, PostCSS, Husky, Autoprefixer, and Prettier?',
default: true,
}]);
return answers.installSettings;
}
// Function to add "import React from 'react';" to a file if not already present
function addReactImportToFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
if (!/import\s+React\s+from\s+['"]react['"]/.test(content)) {
const newContent = `import React from 'react';\n${content}`;
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`Updated: ${filePath}`);
}
}
// Recursively traverse a directory and process .jsx and .tsx files
function traverseAndAddReactImport(dir) {
fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
traverseAndAddReactImport(fullPath);
} else if (entry.isFile() && (fullPath.endsWith('.jsx') || fullPath.endsWith('.tsx'))) {
addReactImportToFile(fullPath);
}
});
}
async function setupProject() {
const packageManager = await choosePackageManager();
if (!fs.existsSync(packageJsonPath)) {
console.log('No package.json found! Initializing a new Vite project...');
const template = await chooseTemplate();
execSync(`npm create vite@latest . -- --template ${template}`, { stdio: 'inherit' });
console.log(`Vite project setup complete with ${template} template.`);
}
const folders = [
'src', 'src/assets', 'src/assets/fonts', 'src/assets/icons', 'src/assets/images',
'src/components', 'src/components/atoms', 'src/components/molecules', 'src/components/organisms',
'src/hooks', 'src/models', 'src/pages', 'src/services', 'src/store', 'src/store/api',
'src/store/slices', 'src/templates', 'src/utils', 'src/styles', 'public', 'tests'
];
folders.forEach(folder => {
const folderPath = path.join(projectPath, folder);
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
console.log(`Created: ${folder}`);
}
});
console.log('Folder structure successfully set up!');
await installDependencies(packageManager);
const installSettings = await askInstallSettings();
if (installSettings) {
console.log('Installing ESLint, PostCSS, Husky, Autoprefixer, and Prettier...');
await installDependencies(packageManager);
console.log('Initializing ESLint...');
execSync(`npx eslint --init`, { stdio: 'inherit' });
// Update or create the ESLint configuration for React 19 compatibility
if (!fs.existsSync(eslintConfigPath)) {
const eslintConfig = {
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime"
],
"plugins": [
"react"
],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"react/react-in-jsx-scope": "off",
"react/jsx-no-target-blank": "warn"
}
};
fs.writeFileSync(eslintConfigPath, JSON.stringify(eslintConfig, null, 2));
console.log('Created .eslintrc.json with React 19 compatible settings.');
} else {
console.log("ESLint configuration (.eslintrc.json) already exists. Please ensure it includes React 19 settings.");
}
// Add lint-staged and husky hooks
addPackageJsonConfig();
// Initialize Husky and install hooks
if (!fs.existsSync(path.join(projectPath, '.git'))) {
console.log('Git is not initialized. Initializing now...');
execSync('git init', { stdio: 'inherit' });
}
execSync(`npx husky-init && ${packageManager} install`, { stdio: 'inherit' });
// Set up pre-commit hook to run lint-staged using the chosen package manager
const lintCommand = packageManager === 'yarn' ? 'yarn lint-staged' : 'npm run lint-staged';
execSync(`npx husky add .husky/pre-commit "${lintCommand}"`, { stdio: 'inherit' });
// Add a pre-push hook to run build using the chosen package manager
const buildCommand = packageManager === 'yarn' ? 'yarn build' : 'npm run build';
execSync(`npx husky add .husky/pre-push "${buildCommand}"`, { stdio: 'inherit' });
console.log('Husky hooks (pre-commit and pre-push) set up complete!');
// Setup PostCSS and Prettier
console.log('Setting up PostCSS and Autoprefixer...');
if (!fs.existsSync(postcssConfigPath)) {
fs.writeFileSync(postcssConfigPath, `export default {\n plugins: {\n autoprefixer: {}\n }\n};`);
console.log('Created postcss.config.js for Autoprefixer.');
}
console.log('Creating Prettier configuration...');
if (!fs.existsSync(prettierConfigPath)) {
fs.writeFileSync(prettierConfigPath, JSON.stringify({
singleQuote: true,
trailingComma: 'all',
tabWidth: 3,
semi: false,
}, null, 2));
console.log('Created .prettierrc for Prettier.');
}
console.log('Creating Prettier ignore configuration...');
if (!fs.existsSync(prettierIgnorePath)) {
fs.writeFileSync(prettierIgnorePath, `node_modules
dist
package-lock.json
yarn.lock
.idea`);
console.log('Created .prettierignore file.');
}
console.log('Creating .gitignore configuration...');
if (!fs.existsSync(gitIgnorePath)) {
fs.writeFileSync(gitIgnorePath, `# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Build directories
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?`);
console.log('Created .gitignore file.');
}
console.log('Configuration complete!');
// Finally, traverse the "src" folder and add React import where missing
const srcPath = path.join(projectPath, 'src');
if (fs.existsSync(srcPath)) {
console.log("Adding 'import React from \"react\";' to all .jsx and .tsx files in the src folder...");
traverseAndAddReactImport(srcPath);
console.log("React import added to all applicable files.");
}
}
}
setupProject();