feat: refactoring project

This commit is contained in:
Carlos
2024-11-23 14:56:07 -05:00
parent f0c2a50c18
commit 1c6db5818d
2351 changed files with 39323 additions and 60326 deletions

View File

@@ -0,0 +1 @@
// Placeholder for babel.config.js

View File

@@ -0,0 +1 @@
// Placeholder for webpack.config.js

26
src/setup/dependencies.js Normal file
View File

@@ -0,0 +1,26 @@
const { execSync } = require('child_process');
const ora = require('ora');
const { deps } = require('./deps');
function installDependencies(userInput, options) {
const spinner = ora('🔄 Installing dependencies...').start();
try {
if (userInput.useAntd) {
deps.push('antd');
}
if (userInput.useRedux) {
deps.push('@reduxjs/toolkit');
deps.push('react-redux');
deps.push('redux');
}
execSync(
`npm install ${deps.join(' ')} ${options.verbose ? '--verbose' : ''}`
);
spinner.succeed('✅ Dependencies installed.');
} catch (error) {
spinner.fail('❌ Failed to install dependencies.');
console.error(error);
}
}
module.exports = { installDependencies };

27
src/setup/deps.js Normal file
View File

@@ -0,0 +1,27 @@
const deps = [
'react',
'react-dom',
'@babel/core',
'@babel/preset-env',
'webpack-cli',
'webpack',
'web-vitals',
'typescript',
'style-loader',
'react-scripts',
'react-dom',
'react',
'jest',
'babel-loader',
'ajv',
'@types/react-dom',
'@types/react',
'@types/node',
'@types/jest',
'@testing-library/user-event',
'@testing-library/react',
'@testing-library/jest-dom',
'@babel/preset-react',
];
module.exports = { deps };

View File

@@ -0,0 +1,18 @@
const { execSync } = require('child_process');
const ora = require('ora');
const { devDeps } = require('./devDeps');
function installDevDependencies(options){
const spinner = ora('🔄 Installing additional dev dependencies...').start();
try {
execSync(
`npm install ${devDeps.join(' ')} ${options.verbose ? '--verbose' : ''}`
);
spinner.succeed('✅ Additional dev dependencies installed.');
} catch (error) {
spinner.fail('❌ Failed to install dependencies.');
console.error(error);
}
}
module.exports = { installDevDependencies };

16
src/setup/devDeps.js Normal file
View File

@@ -0,0 +1,16 @@
const devDeps = [
'pretty-quick',
'webpack-dev-server',
'url-loader',
'husky',
'dotenv-webpack',
'dotenv',
'@svgr/webpack',
'@commitlint/config-conventional',
'@commitlint/cli',
'prettier',
'ora',
'@babel/plugin-proposal-private-property-in-object',
];
module.exports = { devDeps };

17
src/setup/gitInit.js Normal file
View File

@@ -0,0 +1,17 @@
// Purpose: Initialize a Git repository and run Git commands.
const { execSync } = require('child_process');
const ora = require('ora');
async function setupGit(options) {
const spinner = ora('🔧 Running Git commands...').start();
try {
execSync(`git init ${options.verbose ? '--verbose' : ''}`);
spinner.succeed('✅ Git commands executed successfully.');
} catch (error) {
spinner.fail('❌ Failed to execute Git commands.');
console.error(error);
}
}
module.exports = { setupGit };

15
src/setup/husky.js Normal file
View File

@@ -0,0 +1,15 @@
const { execSync } = require('child_process');
const ora = require('ora');
function setupHusky(options) {
const spinner = ora('🐶 Setting up Husky...').start();
execSync(
`npx husky-init && npm install ${options.verbose ? '--verbose' : ''}`
);
execSync(
`npx husky add .husky/pre-commit "npm test" ${options.verbose ? '--verbose' : ''}`
);
spinner.succeed('🐶 Husky set up.');
}
module.exports = { setupHusky };

65
src/setup/init.js Normal file
View File

@@ -0,0 +1,65 @@
const path = require('path');
const { execSync } = require('child_process');
const ora = require('ora');
const fs = require('fs');
const { installDependencies } = require('./dependencies');
const { installDevDependencies } = require('./devDependencies');
const { setupHusky } = require('./husky');
const { setupRedux } = require('./redux');
const { setupStyles } = require('./styles');
const { setupGit } = require('./gitInit');
const { setupTesting } = require('./testing');
const { createAtomicStructure } = require('../templates/atomicStructure');
const { updatePackageJson } = require('../templates/packageJson');
const { askUserWhereToOpen } = require('../utils/logging');
const inquirer = require('inquirer');
async function initProject(projectDirectory, userInput, options) {
const root = path.resolve(projectDirectory);
const verboseFlag = options.verbose ? '--verbose' : '';
// Create the project directory
fs.mkdirSync(root, { recursive: true });
process.chdir(root);
console.log(`🚀 Creating a new React app in ${root}...`);
const spinner = ora('Installing base Create React App...').start();
try {
// Initialize CRA with or without TypeScript
const template = userInput.language === 'TypeScript' ? '--template typescript' : '';
execSync(`npx create-react-app . ${template} ${verboseFlag}`, { stdio: 'inherit' });
spinner.succeed('✅ Base React app created successfully.');
} catch (error) {
spinner.fail('❌ Failed to create base React app.');
console.error(error);
process.exit(1);
}
// Set up Git
setupGit(options);
// Install additional dependencies
installDependencies(userInput, options);
// Install additional dev dependencies
installDevDependencies(options);
// Set up additional features based on user input
if (userInput.useHusky) setupHusky(options);
if (userInput.useRedux) setupRedux(options);
setupStyles(userInput.styling);
setupTesting(userInput.testingFramework);
// Create atomic design structure
createAtomicStructure();
// Update package.json
updatePackageJson(userInput);
console.log('🎉 Project setup complete!');
// Ask user where to open the project
askUserWhereToOpen(root);
}
module.exports = { initProject };

29
src/setup/init.test.js Normal file
View File

@@ -0,0 +1,29 @@
const fs = require('fs');
const path = require('path');
const { initProject } = require('./init');
jest.mock('fs');
jest.mock('ora', () => () => ({
start: jest.fn(() => ({ succeed: jest.fn(), fail: jest.fn() })),
}));
jest.mock('./dependencies', () => ({ installDependencies: jest.fn() }));
jest.mock('../utils/prompts', () => ({ askUserWhereToOpen: jest.fn() }));
describe('initProject', () => {
const testDir = 'test-project';
beforeEach(() => {
fs.mkdirSync.mockClear();
fs.existsSync.mockReturnValue(false);
});
test('Creates project directory and initializes app', async () => {
initProject(testDir, { useHusky: false }, { verbose: false });
expect(fs.mkdirSync).toHaveBeenCalledWith(path.resolve(testDir), { recursive: true });
});
test('Fails if directory already exists and not overwritten', () => {
fs.existsSync.mockReturnValue(true);
expect(() => initProject(testDir, {}, {})).toThrowError();
});
});

60
src/setup/redux.js Normal file
View File

@@ -0,0 +1,60 @@
const ora = require('ora');
const fs = require('fs');
const path = require('path');
function setupRedux(options) {
console.log('options...', options);
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 appFileName = options.language === 'TypeScript' ? 'App.tsx' : 'App.js';
const extension = options.language === 'TypeScript' ? '.ts' : '.js';
const reactFileType = options.language === 'JavaScript' ? '' : ': React.FC';
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${extension}`), storeIndex);
const appTsxJsPath = path.resolve(`src/${appFileName}`);
let appTsxJs = fs.readFileSync(appTsxPath, 'utf8');
appTsxJs = `
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
const App${reactFileType} = () => {
return (
<Provider store={store}>
${appTsx}
</Provider>
);
};
export default App;
`;
fs.writeFileSync(appTsxJsPath, appTsxJs);
spinner.succeed('🛠️ Redux set up.');
}
module.exports = { setupRedux };

52
src/setup/styles.js Normal file
View File

@@ -0,0 +1,52 @@
const fs = require('fs');
const path = require('path');
const ora = require('ora');
const { execSync } = require('child_process');
function setupStyles(styleChoice) {
const spinner = ora('🎨 Setting up styles...').start();
try {
const stylesMap = {
CSS: { fileName: 'index.css', dependency: '' },
SCSS: { fileName: 'index.scss', dependency: 'sass' },
SASS: { fileName: 'index.sass', dependency: 'sass' },
LESS: { fileName: 'index.less', dependency: 'less' },
};
// Ensure styleChoice is valid
if (!stylesMap[styleChoice]) {
spinner.fail(`❌ Unsupported style option: ${styleChoice}`);
return;
}
const { fileName, dependency } = stylesMap[styleChoice];
const stylePath = path.resolve('src', fileName);
// Create the style file with a placeholder content
fs.writeFileSync(stylePath, `/* Placeholder for ${styleChoice} styles */`);
// Update index.js or index.tsx to include the style
const indexPath = path.resolve('src', 'index.tsx');
let indexContent = fs.readFileSync(indexPath, 'utf8');
// Remove any existing style imports and add the new one
indexContent = indexContent.replace(/import\s+['"].*\.(css|scss|sass|less)['"];?/g, '');
indexContent = `import './${fileName}';\n${indexContent}`;
fs.writeFileSync(indexPath, indexContent);
// Install necessary dependency for the chosen style
if (dependency) {
spinner.text = `📦 Installing ${dependency}...`;
execSync(`npm install ${dependency}`, { stdio: 'inherit' });
}
spinner.succeed(`🎨 ${styleChoice} styles set up successfully.`);
} catch (error) {
spinner.fail('❌ Failed to set up styles.');
console.error(error);
}
}
module.exports = { setupStyles };

23
src/setup/styles.test.js Normal file
View File

@@ -0,0 +1,23 @@
const fs = require('fs');
const { setupStyles } = require('./styles');
const { execSync } = require('child_process');
jest.mock('fs');
jest.mock('child_process');
describe('setupStyles', () => {
beforeEach(() => {
fs.writeFileSync.mockClear();
execSync.mockClear();
});
test('Sets up SCSS styles', () => {
setupStyles('SCSS');
expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('index.scss'), expect.any(String));
expect(execSync).toHaveBeenCalledWith('npm install sass', { stdio: 'inherit' });
});
test('Fails for unsupported style options', () => {
expect(() => setupStyles('UnknownStyle')).not.toThrow();
});
});

69
src/setup/testing.js Normal file
View File

@@ -0,0 +1,69 @@
const fs = require('fs');
const path = require('path');
const ora = require('ora');
const { execSync } = require('child_process');
function setupTesting(testingFramework) {
const spinner = ora('🧪 Setting up testing framework...').start();
try {
const testingMap = {
Jest: {
dependency: 'jest @types/jest ts-jest',
configFile: 'jest.config.js',
},
Mocha: {
dependency: 'mocha chai @types/mocha',
configFile: 'mocha.opts',
},
};
// Ensure testingFramework is valid
if (!testingMap[testingFramework]) {
spinner.fail(`❌ Unsupported testing framework: ${testingFramework}`);
return;
}
const { dependency, configFile } = testingMap[testingFramework];
// Install necessary dependencies for the chosen testing framework
spinner.text = `📦 Installing ${testingFramework} and related packages...`;
execSync(`npm install --save-dev ${dependency}`, { stdio: 'inherit' });
// Create a configuration file for the testing framework
const configPath = path.resolve(configFile);
let configContent = '';
if (testingFramework === 'Jest') {
configContent = `module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};`;
} else if (testingFramework === 'Mocha') {
configContent = `--require ts-node/register
--recursive
`;
}
fs.writeFileSync(configPath, configContent);
spinner.succeed(
`🧪 ${testingFramework} set up successfully with configuration file: ${configFile}`
);
// Add test script to package.json
const packageJsonPath = path.resolve('package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.scripts = {
...packageJson.scripts,
test: testingFramework === 'Jest' ? 'jest' : 'mocha',
};
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
} catch (error) {
spinner.fail('❌ Failed to set up testing framework.');
console.error(error);
}
}
module.exports = { setupTesting };

View File

@@ -0,0 +1,17 @@
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.');
}
module.exports = { createAtomicStructure };

View File

@@ -0,0 +1,27 @@
// Placeholder for packageJson.js
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.');
}
module.exports = { updatePackageJson };

View File

@@ -0,0 +1 @@
// Placeholder for reduxTemplate.js

1
src/utils/fileUtils.js Normal file
View File

@@ -0,0 +1 @@
// Placeholder for fileUtils.js

35
src/utils/logging.js Normal file
View File

@@ -0,0 +1,35 @@
const chalk = require('chalk');
const inquirer = require('inquirer');
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.'
)
);
}
});
}
module.exports = { askUserWhereToOpen };

View File

@@ -0,0 +1,28 @@
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
async function overWriteFolder(root) {
// Check if directory exists
if (fs.existsSync(root)) {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: `⚠️ The directory ${root} already exists. Do you want to overwrite it?`,
default: false,
},
]);
if (!overwrite) {
console.error(`❌ Exiting setup as the directory ${root} already exists.`);
process.exit(1);
}
console.log(`🗑️ Removing existing directory: ${root}`);
fs.rmSync(root, { recursive: true, force: true });
}
return true;
}
module.exports = { overWriteFolder };

View File

@@ -0,0 +1,39 @@
const fs = require('fs');
const path = require('path');
const { overWrite } = require('./overWriteProject');
jest.mock('fs');
jest.mock('inquirer', () => ({
prompt: jest.fn(() => Promise.resolve({ overwrite: true })),
}));
describe('overWrite Function', () => {
const testDir = path.resolve('test-dir');
beforeEach(() => {
fs.existsSync.mockClear();
fs.rmSync.mockClear();
});
test('Removes directory if overwrite is confirmed', async () => {
fs.existsSync.mockReturnValue(true);
const result = await overWrite(testDir);
expect(fs.rmSync).toHaveBeenCalledWith(testDir, { recursive: true, force: true });
expect(result).toBe(true);
});
test('Exits if overwrite is declined', async () => {
require('inquirer').prompt.mockResolvedValueOnce({ overwrite: false });
fs.existsSync.mockReturnValue(true);
const result = await overWrite(testDir);
expect(fs.rmSync).not.toHaveBeenCalled();
expect(result).toBe(false);
});
test('Proceeds if directory does not exist', async () => {
fs.existsSync.mockReturnValue(false);
const result = await overWrite(testDir);
expect(fs.rmSync).not.toHaveBeenCalled();
expect(result).toBe(true);
});
});

17
src/utils/prompts.js Normal file
View File

@@ -0,0 +1,17 @@
const inquirer = require('inquirer');
async function askProjectDetails() {
return inquirer.prompt([
{ type: 'confirm', name: 'useHusky', message: 'Install Husky?' },
{ type: 'confirm', name: 'useAntd', message: 'Install Antd?' },
{ type: 'confirm', name: 'useRedux', message: 'Use Redux?' },
{ type: 'confirm', name: 'useModuleFederation', message: 'Use Module Federation Plugin?' },
{ type: 'list', name: 'language', message: 'Choose language:', choices: ['JavaScript', 'TypeScript'] },
{ type: 'list', name: 'testingFramework', message: 'Choose testing framework:', choices: ['Jest', 'Mocha'] },
{ type: 'list', name: 'styling', message: 'Choose styling option:', choices: ['CSS', 'SCSS', 'SASS', 'LESS'] },
{ type: 'list', name: 'modules', message: 'Use CSS Modules, styled-components, or emotion?', choices: ['Modules', 'styled-components', 'emotion'] },
]);
}
module.exports = { askProjectDetails };