diff --git a/index.js b/index.js index ec5d59f..8ed5a8b 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,8 @@ import { run, createFolder, deleteFile } from './lib/utils.js'; import { initializePWA } from './lib/pwa.js'; import { setupCSSFramework } from './lib/css-frameworks.js'; import { createAxiosSetup, createAppComponent, setupRouterMain, createPWAReadme } from './lib/templates.js'; +import { setupTestingFramework } from './lib/testing.js'; +import { setupDevTools } from './lib/dev-tools.js'; (async () => { // 1. Collect user inputs @@ -26,6 +28,17 @@ import { createAxiosSetup, createAppComponent, setupRouterMain, createPWAReadme message: "Do you want to make this a Progressive Web App (PWA)?", default: false }, + { + type: "list", + name: "testingFramework", + message: "Choose a testing framework:", + choices: [ + { name: "None", value: "none" }, + { name: "Vitest + React Testing Library", value: "vitest" }, + { name: "Jest + React Testing Library", value: "jest" }, + { name: "Cypress (E2E)", value: "cypress" } + ] + }, { type: "checkbox", name: "packages", @@ -36,12 +49,26 @@ import { createAxiosSetup, createAppComponent, setupRouterMain, createPWAReadme { name: "React Hook Form", value: "react-hook-form" }, { name: "Yup", value: "yup" }, { name: "Formik", value: "formik" }, - { name: "Moment.js", value: "moment" } + { name: "Moment.js", value: "moment" }, + { name: "Zustand (State Management)", value: "zustand" }, + { name: "TanStack Query", value: "@tanstack/react-query" }, + { name: "Framer Motion", value: "framer-motion" }, + { name: "React Helmet (SEO)", value: "react-helmet-async" } + ] + }, + { + type: "checkbox", + name: "devTools", + message: "Select development tools:", + choices: [ + { name: "ESLint + Prettier", value: "eslint-prettier" }, + { name: "Husky (Git Hooks)", value: "husky" }, + { name: "Commitizen (Conventional Commits)", value: "commitizen" } ] } ]); - const { projectName, cssFramework, isPWA, packages } = answers; + const { projectName, cssFramework, isPWA, testingFramework, packages, devTools } = answers; const projectPath = path.join(process.cwd(), projectName); console.log(`\n๐Ÿš€ Creating ${projectName}${isPWA ? ' with PWA capabilities' : ''}...`); @@ -49,52 +76,103 @@ import { createAxiosSetup, createAppComponent, setupRouterMain, createPWAReadme // 2. Create Vite project run(`npm create vite@latest ${projectName} -- --template react`); - // 3. Create all necessary folder structure first - const folders = ["components", "pages", "hooks", "store", "utils", "assets"]; - folders.forEach((folder) => { - createFolder(path.join(projectPath, "src", folder)); - }); + // 3. Setup CSS framework + setupCSSFramework(cssFramework, projectPath); + + // 4. Setup testing framework + if (testingFramework !== "none") { + setupTestingFramework(testingFramework, projectPath); + } - // 4. Install packages + // 5. Install PWA functionality + if (isPWA) { + initializePWA(projectPath, projectName); + } + + // 6. Install packages with legacy peer deps for compatibility const defaultPackages = ["react-router-dom"]; const allPackages = [...defaultPackages, ...packages]; if (allPackages.length > 0) { - run(`npm install ${allPackages.join(" ")}`, projectPath); + run(`npm install ${allPackages.join(" ")} --legacy-peer-deps`, projectPath); } - // 5. Setup PWA if selected (after folder structure is created) - if (isPWA) { - initializePWA(projectPath, projectName); + // 7. Setup development tools + if (devTools.length > 0) { + setupDevTools(devTools, projectPath, testingFramework); } - // 6. Setup CSS framework - setupCSSFramework(cssFramework, projectPath); + // 8. Create folder structure + const folders = ["components", "pages", "hooks", "store", "utils", "assets"]; + folders.forEach((folder) => { + createFolder(path.join(projectPath, "src", folder)); + }); - // 7. Setup Axios if selected + // 9. Setup Axios if selected if (packages.includes("axios")) { createAxiosSetup(projectPath); } - // 8. Clean up default boilerplate files + // 10. Clean up default boilerplate files deleteFile(path.join(projectPath, "src", "App.css")); if (cssFramework !== "Tailwind") { deleteFile(path.join(projectPath, "src", "index.css")); } - // 9. Generate clean templates + // 11. Generate clean templates createAppComponent(projectPath, projectName, isPWA); setupRouterMain(projectPath, cssFramework); - // 10. Create comprehensive README + // 12. Create comprehensive README createPWAReadme(projectPath, projectName, cssFramework, packages, isPWA); - // 11. Success message + // 13. Enhanced success message console.log("\nโœ… Setup complete!"); + console.log(`\n๐ŸŽ‰ Your ${projectName} project is ready!`); + console.log(`\n๐Ÿ“ Project includes:`); + + if (testingFramework !== "none") { + const testingName = testingFramework === "vitest" ? "Vitest" : + testingFramework === "jest" ? "Jest" : "Cypress"; + console.log(` โ€ข ${testingName} testing setup`); + } + + if (devTools.includes("eslint-prettier")) { + console.log(` โ€ข ESLint + Prettier configuration`); + } + + if (devTools.includes("husky")) { + console.log(` โ€ข Husky git hooks`); + } + + if (devTools.includes("commitizen")) { + console.log(` โ€ข Commitizen for conventional commits`); + } + + if (packages.length > 0) { + console.log(` โ€ข Additional packages: ${packages.join(", ")}`); + } + if (isPWA) { - console.log("๐Ÿ“ฑ PWA features enabled - your app can be installed on mobile devices!"); - console.log("โš ๏ธ Important: Replace placeholder SVG icons with proper PNG icons for production"); + console.log(" โ€ข PWA features enabled - your app can be installed on mobile devices!"); + console.log(" โš ๏ธ Important: Replace placeholder SVG icons with proper PNG icons for production"); + } + + console.log(`\n๐Ÿš€ Next steps:`); + console.log(` cd ${projectName}`); + console.log(` npm install`); + console.log(` npm run dev`); + + if (testingFramework === "vitest") { + console.log(` npm test (run tests)`); + } else if (testingFramework === "jest") { + console.log(` npm test (run tests)`); + } else if (testingFramework === "cypress") { + console.log(` npm run test:e2e (run E2E tests)`); + } + + if (devTools.includes("eslint-prettier")) { + console.log(` npm run lint (check code quality)`); } - console.log(`\nNext steps:\n cd ${projectName}\n npm install\n npm run dev`); if (isPWA) { console.log(`\n๐Ÿ“ฑ To test PWA:\n npm run build\n npm run preview\n Open http://localhost:4173 and test install/offline features`); diff --git a/lib/dev-tools.js b/lib/dev-tools.js new file mode 100644 index 0000000..d26fb1d --- /dev/null +++ b/lib/dev-tools.js @@ -0,0 +1,143 @@ +import { run, writeFile, readFile } from './utils.js'; +import path from 'path'; +import fs from 'fs'; + +export const setupESLintPrettier = (projectPath) => { + run(`npm install -D eslint @eslint/js eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-refresh prettier eslint-config-prettier eslint-plugin-prettier`, projectPath); + + // Create ESLint config + const eslintConfig = `import js from '@eslint/js' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + settings: { react: { version: '18.3' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +]`; + writeFile(path.join(projectPath, "eslint.config.js"), eslintConfig); + + // Create Prettier config + const prettierConfig = `{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false +}`; + writeFile(path.join(projectPath, ".prettierrc"), prettierConfig); + + // Create .prettierignore + const prettierIgnore = `dist +node_modules +*.log +.DS_Store`; + writeFile(path.join(projectPath, ".prettierignore"), prettierIgnore); +}; + +export const setupHusky = (projectPath) => { + run(`npm install -D husky lint-staged`, projectPath); + run(`npx husky install`, projectPath); + run(`npx husky add .husky/pre-commit "npx lint-staged"`, projectPath); + + // Create lint-staged config in package.json + const packageJsonPath = path.join(projectPath, "package.json"); + let packageJson = JSON.parse(readFile(packageJsonPath)); + packageJson["lint-staged"] = { + "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], + "*.{css,scss,md}": ["prettier --write"] + }; + writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); +}; + +export const setupCommitizen = (projectPath) => { + run(`npm install -D commitizen cz-conventional-changelog`, projectPath); + + const packageJsonPath = path.join(projectPath, "package.json"); + let packageJson = JSON.parse(readFile(packageJsonPath)); + packageJson.config = { + commitizen: { + path: "cz-conventional-changelog" + } + }; + writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); +}; + +export const updatePackageScripts = (projectPath, testingFramework, devTools) => { + const packageJsonPath = path.join(projectPath, "package.json"); + let packageJson = JSON.parse(readFile(packageJsonPath)); + + // Add testing scripts based on framework chosen + if (testingFramework === "vitest") { + packageJson.scripts.test = "vitest"; + packageJson.scripts["test:ui"] = "vitest --ui"; + packageJson.scripts["test:coverage"] = "vitest --coverage"; + } else if (testingFramework === "jest") { + packageJson.scripts.test = "jest"; + packageJson.scripts["test:watch"] = "jest --watch"; + packageJson.scripts["test:coverage"] = "jest --coverage"; + } else if (testingFramework === "cypress") { + packageJson.scripts["test:e2e"] = "cypress open"; + packageJson.scripts["test:e2e:headless"] = "cypress run"; + } + + // Add linting scripts if ESLint is chosen + if (devTools.includes("eslint-prettier")) { + packageJson.scripts.lint = "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0"; + packageJson.scripts["lint:fix"] = "eslint . --ext js,jsx --fix"; + packageJson.scripts.format = 'prettier --write "src/**/*.{js,jsx,css,md}"'; + } + + // Add commit script if commitizen is chosen + if (devTools.includes("commitizen")) { + packageJson.scripts.commit = "cz"; + } + + writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); +}; + +export const setupDevTools = (devTools, projectPath, testingFramework) => { + if (devTools.includes("eslint-prettier")) { + setupESLintPrettier(projectPath); + } + + if (devTools.includes("husky")) { + setupHusky(projectPath); + } + + if (devTools.includes("commitizen")) { + setupCommitizen(projectPath); + } + + // Update package.json scripts + updatePackageScripts(projectPath, testingFramework, devTools); +}; diff --git a/lib/testing.js b/lib/testing.js new file mode 100644 index 0000000..3cbdb68 --- /dev/null +++ b/lib/testing.js @@ -0,0 +1,95 @@ +import { run, writeFile, createFolder } from './utils.js'; +import path from 'path'; +import fs from 'fs'; + +export const setupVitest = (projectPath) => { + run(`npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event`, projectPath); + + // Update vite.config.js for Vitest + const viteConfigPath = path.join(projectPath, "vite.config.js"); + let viteConfig = fs.readFileSync(viteConfigPath, "utf-8"); + + // Add test configuration + const testConfig = ` +/// +`; + viteConfig = testConfig + viteConfig; + viteConfig = viteConfig.replace( + /export default defineConfig\(\{/, + `export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + },` + ); + writeFile(viteConfigPath, viteConfig); + + // Create test setup file + createFolder(path.join(projectPath, "src", "test")); + const setupContent = `import '@testing-library/jest-dom';`; + writeFile(path.join(projectPath, "src", "test", "setup.ts"), setupContent); + + // Create sample test + const testContent = `import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import App from '../App'; + +describe('App', () => { + it('renders welcome message', () => { + render(); + expect(screen.getByText(/Welcome to/i)).toBeInTheDocument(); + }); +});`; + writeFile(path.join(projectPath, "src", "App.test.jsx"), testContent); +}; + +export const setupJest = (projectPath) => { + run(`npm install -D jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom`, projectPath); + + // Create Jest config + const jestConfig = `export default { + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/src/test/setup.js'], + moduleNameMapping: { + '\\\\.(css|less|scss)$': 'identity-obj-proxy', + }, +};`; + writeFile(path.join(projectPath, "jest.config.js"), jestConfig); + + // Create test setup file + createFolder(path.join(projectPath, "src", "test")); + const setupContent = `import '@testing-library/jest-dom';`; + writeFile(path.join(projectPath, "src", "test", "setup.js"), setupContent); +}; + +export const setupCypress = (projectPath) => { + run(`npm install -D cypress`, projectPath); + run(`npx cypress install`, projectPath); + + // Create cypress config + const cypressConfig = `import { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:5173', + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +})`; + writeFile(path.join(projectPath, "cypress.config.js"), cypressConfig); +}; + +export const setupTestingFramework = (testingFramework, projectPath) => { + const testingMap = { + "vitest": () => setupVitest(projectPath), + "jest": () => setupJest(projectPath), + "cypress": () => setupCypress(projectPath) + }; + + const setupFunction = testingMap[testingFramework]; + if (setupFunction) { + setupFunction(); + } +}; diff --git a/package.json b/package.json index a9d6996..0d1c724 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quickstart-react", - "version": "1.1.2", + "version": "1.1.3", "description": "A CLI tool to quickly scaffold a React + Vite project with optional CSS frameworks and useful packages, ready to use out of the box.", "main": "index.js", "type": "module", @@ -23,7 +23,12 @@ "mui", "javascript", "frontend", - "react-app" + "react-app", + "testing", + "vitest", + "eslint", + "prettier", + "development-tools" ], "author": "harshgupta20", "license": "MIT", diff --git a/readme.md b/readme.md index 4fb9f77..e0a2199 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,9 @@ ## โœจ Features - **Interactive Setup** โ€” prompts you for project name, CSS framework, and optional packages - **CSS Framework Support** โ€” Tailwind CSS, Bootstrap, or MUI (Material UI) -- **Optional Packages** โ€” easily add Axios, React Icons, React Hook Form, Yup, Formik, and Moment.js +- **Testing Framework Integration** โ€” Vitest, Jest, or Cypress support with pre-configured setups +- **Development Tools** โ€” ESLint + Prettier, Husky git hooks, Commitizen for conventional commits +- **Optional Packages** โ€” easily add Axios, React Icons, React Hook Form, Yup, Formik, Moment.js, Zustand, TanStack Query, Framer Motion, and React Helmet - **Automatic Folder Structure** โ€” creates `components`, `pages`, `hooks`, `store`, `utils`, `assets` folders - **Boilerplate Ready** โ€” replaces default Vite boilerplate with a clean welcome page - **Axios Setup** โ€” pre-configured Axios instance if selected @@ -21,7 +23,9 @@ npx quickstart-react When you run `npx quickstart-react`, you will be prompted to: 1. **Enter Project Name** โ€” e.g., `my-app` 2. **Choose CSS Framework** โ€” Tailwind, Bootstrap, or MUI -3. **Select Optional Packages** โ€” choose from a list of commonly used React libraries +3. **Select Testing Framework** โ€” Vitest, Jest, Cypress, or None +4. **Choose Optional Packages** โ€” choose from a list of commonly used React libraries +5. **Select Development Tools** โ€” ESLint + Prettier, Husky, Commitizen Example run: ```bash @@ -32,13 +36,18 @@ npx quickstart-react ``` ? Enter project name: my-portfolio ? Choose a CSS framework: Tailwind -? Select optional packages: Axios, React Icons +? Choose a testing framework: Vitest + React Testing Library +? Select optional packages: Axios, React Icons, Zustand +? Select development tools: ESLint + Prettier, Husky ``` This will: - Create a new Vite + React project in `my-portfolio/` - Install Tailwind CSS and configure it with Vite -- Install Axios and React Icons +- Set up Vitest with React Testing Library for testing +- Install Axios, React Icons, and Zustand +- Configure ESLint + Prettier for code quality +- Set up Husky for git hooks - Create standard project folders - Add a clean welcome screen - Set up an Axios instance at `src/utils/axiosInstance.js` @@ -87,6 +96,23 @@ You can add these during setup: - **Yup** โ€” schema validation - **Formik** โ€” form management - **Moment.js** โ€” date/time utilities +- **Zustand** โ€” lightweight state management +- **TanStack Query** โ€” data fetching and caching +- **Framer Motion** โ€” animation library +- **React Helmet** โ€” document head management for SEO + +## ๐Ÿงช Testing Framework Support +Choose from these testing options: +- **Vitest + React Testing Library** โ€” fast unit testing with Vite integration +- **Jest + React Testing Library** โ€” traditional React testing setup +- **Cypress** โ€” end-to-end testing framework +- **None** โ€” skip testing setup + +## ๐Ÿ› ๏ธ Development Tools +Enhance your development workflow with: +- **ESLint + Prettier** โ€” code linting and formatting +- **Husky** โ€” git hooks for pre-commit checks +- **Commitizen** โ€” conventional commit messages ## ๐Ÿš€ Quick Start ```bash