budgie-react
Version:
π¦ Flexible React project initializer β TypeScript, Redux Toolkit, React Router v6, Error Boundaries, Axios and more.
283 lines (236 loc) β’ 11.1 kB
JavaScript
;
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const Console = require('./console');
const {
makePackageJson,
makeTsConfig,
makeWebpackConfig,
makeBabelRc,
makeIndexHtml,
makeIndexTsx,
makeAppTsx,
makeGlobalsCss,
makeGitIgnore,
makeEnvFiles,
makeEslintConfig,
makePrettierConfig,
makeJestConfig,
} = require('./base');
const { makeStore, makeRootReducer, makeCounterSlice, makeHooksTsx } = require('./redux');
const { makeRouterIndex, makeHomePage, makeNotFoundPage, makeAboutPage } = require('./routing');
const { makeErrorBoundary, makeErrorFallback } = require('./errorBoundary');
const { makeApiService, makeApiTypes } = require('./axios');
// βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function writeFile(filePath, content) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, 'utf8');
}
function step(msg) {
Console.log(` ${Console.FgCyan}β${Console.Reset} ${msg}`);
}
// βββ Main Generator βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function generateProject(targetDir, name, config) {
let running = true;
// Spinner while generating
Console.spinner(
['β ', 'β ', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ', 'β '],
'Generating project filesβ¦',
80,
() => running
);
try {
fs.mkdirSync(targetDir, { recursive: true });
// ββ Base files ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
writeFile(path.join(targetDir, 'package.json'), makePackageJson(name, config));
writeFile(path.join(targetDir, 'tsconfig.json'), makeTsConfig());
writeFile(path.join(targetDir, 'webpack.config.js'), makeWebpackConfig(config));
writeFile(path.join(targetDir, '.babelrc'), makeBabelRc());
writeFile(path.join(targetDir, '.gitignore'), makeGitIgnore());
writeFile(path.join(targetDir, 'public', 'index.html'), makeIndexHtml(name));
writeFile(path.join(targetDir, 'src', 'index.tsx'), makeIndexTsx(config));
writeFile(path.join(targetDir, 'src', 'App.tsx'), makeAppTsx(config));
writeFile(path.join(targetDir, 'src', 'styles', 'globals.css'), makeGlobalsCss());
// ββ Environment config ββββββββββββββββββββββββββββββββββββββββββββββββββ
if (config.envConfig) {
const envs = makeEnvFiles(name);
writeFile(path.join(targetDir, '.env.development'), envs.dev);
writeFile(path.join(targetDir, '.env.production'), envs.prod);
writeFile(path.join(targetDir, '.env.example'), envs.example);
}
// ββ ESLint + Prettier βββββββββββββββββββββββββββββββββββββββββββββββββββ
if (config.eslint) {
writeFile(path.join(targetDir, '.eslintrc.js'), makeEslintConfig());
writeFile(path.join(targetDir, '.prettierrc'), makePrettierConfig());
}
// ββ Jest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (config.jest) {
writeFile(path.join(targetDir, 'jest.config.js'), makeJestConfig());
writeFile(
path.join(targetDir, 'src', '__tests__', 'App.test.tsx'),
makeAppTest(config)
);
}
// ββ Redux βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (config.redux) {
writeFile(path.join(targetDir, 'src', 'store', 'index.ts'), makeStore());
writeFile(path.join(targetDir, 'src', 'store', 'rootReducer.ts'), makeRootReducer());
writeFile(
path.join(targetDir, 'src', 'features', 'counter', 'counterSlice.ts'),
makeCounterSlice()
);
writeFile(path.join(targetDir, 'src', 'hooks', 'index.ts'), makeHooksTsx());
}
// ββ Routing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (config.routing) {
writeFile(path.join(targetDir, 'src', 'routes', 'index.tsx'), makeRouterIndex(config));
writeFile(path.join(targetDir, 'src', 'pages', 'Home.tsx'), makeHomePage(config));
writeFile(path.join(targetDir, 'src', 'pages', 'About.tsx'), makeAboutPage());
writeFile(path.join(targetDir, 'src', 'pages', 'NotFound.tsx'), makeNotFoundPage());
}
// ββ Error Boundary ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (config.errorBoundary) {
writeFile(
path.join(targetDir, 'src', 'components', 'ErrorBoundary.tsx'),
makeErrorBoundary()
);
writeFile(
path.join(targetDir, 'src', 'components', 'ErrorFallback.tsx'),
makeErrorFallback()
);
}
// ββ Axios βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (config.axios) {
writeFile(path.join(targetDir, 'src', 'services', 'api.ts'), makeApiService(config));
writeFile(path.join(targetDir, 'src', 'services', 'types.ts'), makeApiTypes());
}
// ββ README ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
writeFile(path.join(targetDir, 'README.md'), makeReadme(name, config));
} finally {
running = false;
// Small pause to let spinner clear
await new Promise((r) => setTimeout(r, 200));
}
Console.log('');
// ββ File summary ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const generated = countFiles(targetDir);
Console.success(`Generated ${generated} files in ${targetDir}`);
Console.log('');
Console.info(`Run ${Console.Bright}cd ${config.projectName} && ${config.packageManager} install${Console.Reset} to get started.`);
}
function countFiles(dir) {
let count = 0;
const items = fs.readdirSync(dir);
for (const item of items) {
const full = path.join(dir, item);
if (item === 'node_modules') continue;
const stat = fs.statSync(full);
if (stat.isDirectory()) count += countFiles(full);
else count += 1;
}
return count;
}
// βββ App Test Template ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function makeAppTest(config) {
return `import React from 'react';
import { render, screen } from '@testing-library/react';
${config.redux ? "import { Provider } from 'react-redux';\nimport { store } from '../store';" : ''}
${config.routing ? "import { MemoryRouter } from 'react-router-dom';" : ''}
import App from '../App';
${config.redux && config.routing ? `
const renderApp = () =>
render(
<Provider store={store}>
<MemoryRouter>
<App />
</MemoryRouter>
</Provider>
);
` : config.redux ? `
const renderApp = () =>
render(
<Provider store={store}>
<App />
</Provider>
);
` : config.routing ? `
const renderApp = () =>
render(
<MemoryRouter>
<App />
</MemoryRouter>
);
` : `
const renderApp = () => render(<App />);
`}
describe('App', () => {
it('renders without crashing', () => {
renderApp();
expect(document.body).toBeTruthy();
});
});
`;
}
// βββ README βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function makeReadme(name, config) {
const features = [
'- β‘ **Webpack 5** + **Babel 7** with hot module replacement',
'- π· **TypeScript** (strict mode, path aliases)',
config.redux && '- ποΈ **Redux Toolkit** with typed hooks and example counter slice',
config.routing && '- π§ **React Router v6** with lazy-loaded pages',
config.errorBoundary && '- π‘οΈ **Error Boundary** with fallback UI',
config.axios && '- π **Axios** pre-configured with interceptors and base URL',
config.envConfig && '- π **Environment config** via dotenv-webpack',
config.eslint && '- π¨ **ESLint + Prettier** with Airbnb TypeScript rules',
config.jest && '- π§ͺ **Jest + Testing Library** ready for unit tests',
]
.filter(Boolean)
.join('\n');
return `# ${name}
> Scaffolded with [Budgie React](https://github.com/yashdatir/budgie-react) π¦
## Features
${features}
## Getting Started
\`\`\`bash
# Install dependencies
npm install
# Start dev server (http://localhost:3000)
npm run dev
# Production build
npm run build
${config.jest ? '\n# Run tests\nnpm test' : ''}
\`\`\`
## Project Structure
\`\`\`
src/
βββ index.tsx # App entry point
βββ App.tsx # Root component${config.redux ? '\nβββ store/ # Redux store & root reducer\nβββ features/ # Redux slices (e.g. counter)\nβββ hooks/ # Typed useAppDispatch / useAppSelector' : ''}${config.routing ? '\nβββ routes/ # Route definitions\nβββ pages/ # Page components (Home, About, 404)' : ''}${config.errorBoundary ? '\nβββ components/ # ErrorBoundary, ErrorFallback' : ''}${config.axios ? '\nβββ services/ # Axios instance + API types' : ''}
βββ styles/ # Global CSS
public/
βββ index.html
\`\`\`
${config.redux ? `
## Redux
Slices live under \`src/features/\`. Use the typed hooks from \`src/hooks/\`:
\`\`\`tsx
import { useAppSelector, useAppDispatch } from './hooks';
import { increment } from './features/counter/counterSlice';
const count = useAppSelector((s) => s.counter.value);
const dispatch = useAppDispatch();
dispatch(increment());
\`\`\`
` : ''}
${config.axios ? `
## HTTP Client
The Axios instance in \`src/services/api.ts\` reads \`REACT_APP_API_URL\` from your env file.
\`\`\`ts
import api from './services/api';
const data = await api.get('/users');
\`\`\`
` : ''}
## License
MIT
`;
}
module.exports = { generateProject };