create-rvp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mostafa Khaled
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # crvp CLI
2
+
3
+ Interactive CLI for creating modern React + Vite projects with optional Tailwind CSS, Redux, React Query, and more.
4
+
5
+ [![Version](https://img.shields.io/badge/version-0.1.0-blue.svg)](https://www.npmjs.com/package/crvp)
6
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org/)
8
+
9
+ ## 🚀 Features
10
+
11
+ - **Interactive Prompts** - Guided project setup with beautiful CLI interface
12
+ - **Vite-powered** - Fast, modern build tool with HMR
13
+ - **TypeScript Support** - Optional TypeScript scaffolding
14
+ - **Modern Stack** - React 19, React Compiler, Tailwind CSS
15
+ - **State Management** - Optional Redux Toolkit integration
16
+ - **Data Fetching** - React Query (TanStack Query) + Axios
17
+ - **Form Handling** - React Hook Form with Zod validation
18
+ - **Routing** - React Router setup
19
+ - **Linting** - ESLint + Prettier configuration
20
+ - **Flexible Structure** - Simple or scalable folder architecture
21
+
22
+ ## 📦 Installation
23
+
24
+ ### Global Installation (Recommended)
25
+
26
+ ```bash
27
+ npm install -g create-rvp
28
+ # or
29
+ pnpm add -g create-rvp
30
+ # or
31
+ yarn global add create-rvp
32
+ ```
33
+
34
+ ### Using npx (No Installation)
35
+
36
+ ```bash
37
+ npx create-rvp [project-name]
38
+ ```
39
+
40
+ ## 🛠️ Usage
41
+
42
+ ### Create a new project
43
+
44
+ ```bash
45
+ # With interactive prompts
46
+ create-rvp
47
+
48
+ # With project name argument
49
+ create-rvp my-awesome-app
50
+ ```
51
+
52
+ ### Options
53
+
54
+ The CLI will guide you through selecting:
55
+
56
+ | Option | Description |
57
+ |--------|-------------|
58
+ | Package Manager | pnpm (default), npm, yarn, or bun |
59
+ | TypeScript | Enable TypeScript support |
60
+ | React Compiler | Enable React 19 compiler optimizations |
61
+ | Tailwind CSS | Add Tailwind CSS for styling |
62
+ | React Router | Include client-side routing |
63
+ | React Query + Axios | Add TanStack Query and Axios for data fetching |
64
+ | Redux Toolkit | Include Redux for global state management |
65
+ | React Hook Form + Zod | Form handling with schema validation |
66
+ | ESLint + Prettier | Code linting and formatting setup |
67
+ | Folder Structure | Simple (flat) or Scalable (feature-based) |
68
+
69
+ ## 📁 Project Structure
70
+
71
+ ### Simple Structure
72
+ ```
73
+ src/
74
+ ├── components/
75
+ ├── pages/
76
+ ├── hooks/
77
+ ├── utils/
78
+ ├── styles/
79
+ └── App.tsx
80
+ ```
81
+
82
+ ### Scalable Structure
83
+ ```
84
+ src/
85
+ ├── features/
86
+ │ └── feature-name/
87
+ │ ├── components/
88
+ │ ├── hooks/
89
+ │ └── api/
90
+ ├── shared/
91
+ │ ├── components/
92
+ │ ├── hooks/
93
+ │ └── utils/
94
+ └── App.tsx
95
+ ```
96
+
97
+ ## 🧰 Technologies
98
+
99
+ - [Vite](https://vitejs.dev/) - Next Generation Frontend Tooling
100
+ - [React](https://react.dev/) - A JavaScript library for building user interfaces
101
+ - [TypeScript](https://www.typescriptlang.org/) - Typed JavaScript
102
+ - [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
103
+ - [React Query](https://tanstack.com/query) - Powerful asynchronous state management
104
+ - [Redux Toolkit](https://redux-toolkit.js.org/) - State management made simple
105
+ - [React Hook Form](https://react-hook-form.com/) - Performant forms
106
+ - [Zod](https://zod.dev/) - TypeScript-first schema validation
107
+ - [React Router](https://reactrouter.com/) - Declarative routing
108
+
109
+ ## 🧑‍💻 Development
110
+
111
+ ### Prerequisites
112
+
113
+ - Node.js >= 18
114
+ - pnpm (recommended), npm, or yarn
115
+
116
+ ### Setup
117
+
118
+ ```bash
119
+ # Clone the repository
120
+ git clone https://github.com/M0stafa-Khaled/create-rvp.git
121
+ cd create-rvp
122
+
123
+ # Install dependencies
124
+ pnpm install
125
+
126
+ # Run in development mode
127
+ pnpm dev
128
+
129
+ # Build for production
130
+ pnpm build
131
+ ```
132
+
133
+ ### Scripts
134
+
135
+ | Command | Description |
136
+ |---------|-------------|
137
+ | `pnpm dev` | Run CLI in development mode with tsx |
138
+ | `pnpm start` | Run compiled CLI |
139
+ | `pnpm build` | Build for production with tsup |
140
+ | `pnpm lint` | Run ESLint |
141
+
142
+ ## 🤝 Contributing
143
+
144
+ Contributions are welcome! Please feel free to submit a Pull Request.
145
+
146
+ ## � License
147
+
148
+ MIT License - see the [LICENSE](LICENSE) file for details.
149
+
150
+ ## 👤 Author
151
+
152
+ **Mostafa Khaled**
153
+
154
+ - GitHub: [@M0stafa-Khaled](https://github.com/M0stafa-Khaled)
155
+
156
+ ---
157
+
158
+ <p align="center">Built with ❤️ for the React community</p>
package/bin/index.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js");
package/dist/cli.js ADDED
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env node
2
+ import*as ie from"@clack/prompts";import{program as Re}from"commander";import*as c from"@clack/prompts";import M from"picocolors";var _=async e=>{c.intro(M.bold("\u{1F680} Welcome to crvp CLI!")),c.note(`Interactive scaffolding for modern React + Vite projects
3
+ with Tailwind, Redux, React Query, etc.`,"crvp v0.1.0");let r,t=d=>/^(?:[a-z0-9]+(?:-[a-z0-9]+)*)$/.test(d),a=d=>d==="."||d==="..";if(e&&e.trim()!==""){let d=e.trim();if(!a(d)&&!t(d))return c.log.error("Invalid project name. Use lowercase letters, numbers, hyphens, or '.' / '..'"),null;r=d,c.log.info(`Using project name from argument: ${M.cyan(r)}`)}else{let d=await c.text({message:"Project name (folder name):",placeholder:"my-crvp-app",defaultValue:"my-crvp-app",validate:$=>{let w=$?.trim();if(w&&!a(w)&&!t(w))return"Use lowercase letters, numbers, hyphens, or '.' / '..'"}});if(c.isCancel(d))return c.cancel("Operation cancelled."),null;let k=d.trim();r=k===""?"my-crvp-app":k,c.log.info(`Using project name: ${M.cyan(r)}`)}let s=await c.select({message:"Preferred package manager:",options:[{value:"pnpm",label:"pnpm (recommended - fastest)"},{value:"npm",label:"npm"},{value:"yarn",label:"Yarn"},{value:"bun",label:"Bun"}],initialValue:"pnpm"});if(c.isCancel(s))return c.cancel("Operation cancelled."),null;let o=await c.confirm({message:"Use TypeScript?",initialValue:!0});if(c.isCancel(o))return c.cancel("Operation cancelled."),null;let n=await c.confirm({message:"Use React Compiler?",initialValue:!1});if(c.isCancel(n))return c.cancel("Operation cancelled."),null;let p=await c.confirm({message:"Use Tailwind CSS?",initialValue:!0});if(c.isCancel(p))return c.cancel("Operation cancelled."),null;let u=await c.confirm({message:"Use React Router?",initialValue:!0});if(c.isCancel(u))return c.cancel("Operation cancelled."),null;let l=await c.confirm({message:"Use React Query (TanStack) + Axios?",initialValue:!0});if(c.isCancel(l))return c.cancel("Operation cancelled."),null;let i=await c.confirm({message:"Use Redux Toolkit?",initialValue:!1});if(c.isCancel(i))return c.cancel("Operation cancelled."),null;let m=await c.confirm({message:"Use react-hook-form + zod?",initialValue:!0});if(c.isCancel(m))return c.cancel("Operation cancelled."),null;let h=await c.confirm({message:"Setup ESLint + Prettier?",initialValue:!0});if(c.isCancel(h))return c.cancel("Operation cancelled."),null;let C=await c.select({message:"Folder structure style:",options:[{value:"simple",label:"Simple (flat, good for small/medium projects)"},{value:"scalable",label:"Scalable (feature-based, for larger projects)"}],initialValue:"simple"});return c.isCancel(C)?(c.cancel("Operation cancelled."),null):(c.outro(M.green("All answers collected! Starting scaffolding... \u{1F389}")),{projectName:r,packageManager:s,useTypescript:o,useReactCompiler:n,useTailwind:p,useRouter:u,useReactQueryAxios:l,useRedux:i,useRhformZod:m,useEslintPrettier:h,folderStructure:C})};import.meta.url===`file://${process.argv[1]}`&&(async()=>{let e=await _();e&&console.log(`
4
+ Collected answers:`,e)})();import oe from"picocolors";import I from"fs/promises";import Ae from"path";import*as g from"@clack/prompts";import N from"picocolors";var ne=async e=>{let r=e.trim()||"my-crvp-app";for(;;){let t=Ae.resolve(process.cwd(),r);try{if(await I.access(t),(await I.readdir(t)).length===0)return t;g.note(`The directory ${N.cyan(r)} already exists and contains files.`,"Directory Conflict");let s=await g.select({message:"How do you want to proceed?",options:[{value:"overwrite",label:"Overwrite (delete existing content)"},{value:"new-name",label:"Choose a different name"},{value:"cancel",label:"Cancel"}],initialValue:"new-name"});if(g.isCancel(s)||s==="cancel")return g.cancel("Project creation cancelled."),null;if(s==="overwrite"){let n=await g.confirm({message:`Delete all files in ${N.cyan(r)}? This action cannot be undone.`,initialValue:!1});return g.isCancel(n)||!n?(g.cancel("Overwrite cancelled."),null):(g.log.warn("Deleting existing directory contents..."),await I.rm(t,{recursive:!0,force:!0}),await I.mkdir(t,{recursive:!0}),g.log.success(`Directory prepared: ${N.cyan(r)}`),t)}let o=await g.text({message:"Enter a new project name:",placeholder:"my-crvp-app-2",validate:n=>{let p=n?.trim();if(!p)return"Name is required";let u=i=>i==="."||i==="..",l=i=>/^(?:[a-z0-9]+(?:-[a-z0-9]+)*)$/.test(i);if(!u(p)&&!l(p))return"Use lowercase letters, numbers, hyphens, or '.' / '..'"}});if(g.isCancel(o))return g.cancel("Project creation cancelled."),null;r=o.trim()}catch(a){return a.code==="ENOENT"?(await I.mkdir(t,{recursive:!0}),g.log.success(`Created directory: ${N.cyan(r)}`),t):(g.log.error(`Error: ${a.message}`),null)}}};import{execa as Te}from"execa";import*as f from"@clack/prompts";import P from"fs/promises";import R from"path";import*as ce from"@clack/prompts";import je from"picocolors";var y=async(e,r,t,a="Failed")=>{let s=ce.spinner();s.start(e);try{let o=await t();return s.stop(r),o}catch(o){throw s.stop(`${je.red(a)}: ${o.message}`),o}};import{execa as Se}from"execa";import*as T from"@clack/prompts";import F from"fs/promises";import le from"path";var W=async(e,r)=>{if(!e.useTailwind)return!0;let t=e.packageManager,a=["tailwindcss","@tailwindcss/vite"],s=t,o=[t==="npm"?"install":"add","-D",...a];if(!await y(`Installing Tailwind CSS using ${t}...`,"Tailwind CSS installed successfully",async()=>(await Se(s,o,{cwd:r}),!0),"Failed to install Tailwind CSS"))return!1;let p=e.useTypescript?".ts":".js",u=le.join(r,`vite.config${p}`);try{await F.access(u);let i=await F.readFile(u,"utf-8");i.includes("tailwindcss from")||(i=`import tailwindcss from "@tailwindcss/vite";
5
+ `+i),i.includes("tailwindcss()")||(i=i.replace(/plugins:\s*\[/,"plugins: [tailwindcss(),")),await F.writeFile(u,i),T.log.success(`Updated vite.config${p} with Tailwind plugin.`)}catch(i){return T.log.error(`Failed to update vite.config${p}: ${i.message}`),!1}let l=le.join(r,"src/index.css");try{await F.access(l),(await F.readFile(l,"utf-8")).includes('@import "tailwindcss"')?T.log.info("Tailwind already imported in src/index.css."):(await F.writeFile(l,`@import "tailwindcss";
6
+ `),T.log.success("Added Tailwind import to src/index.css."))}catch(i){return T.log.error(`Failed to update src/index.css: ${i.message}`),!1}return!0};import{execa as ve}from"execa";import*as H from"@clack/prompts";import L from"fs/promises";import pe from"path";var G=async(e,r)=>{if(!e.useReactCompiler)return!0;let t=e.packageManager,a=["react-compiler-runtime@latest","babel-plugin-react-compiler@latest","eslint-plugin-react-compiler@latest"],s=t,o=["add","-D",...a];if(t==="yarn"&&(s="yarn"),t==="bun"&&(s="bun"),!await y(`Installing React Compiler using ${t}...`,"React Compiler packages installed",async()=>(await ve(s,o,{cwd:r}),!0),"Failed to install React Compiler packages"))return!1;let p=e.useTypescript?".ts":".js",u=pe.join(r,`vite.config${p}`);try{await L.access(u);let i=await L.readFile(u,"utf-8"),m=/react\s*\([^)]*\)\s*,?/g;i=i.replace(m,""),i=i.replace(/plugins:\s*\[/,`plugins: [
7
+ react({
8
+ babel: {
9
+ plugins: ["babel-plugin-react-compiler"],
10
+ },
11
+ }),`),await L.writeFile(u,i.trim()),H.log.success(`Updated vite.config${p} with React Compiler config.`)}catch(i){return H.log.error(`Failed to update vite.config${p}: ${i.message}`),!1}let l=pe.join(r,"eslint.config.js");try{try{await L.access(l)}catch{return!0}let i=await L.readFile(l,"utf-8");i.includes('from "eslint-plugin-react-compiler"')||(i=`import reactCompiler from "eslint-plugin-react-compiler";
12
+ `+i),!i.includes('"react-compiler"')&&!i.includes("'react-compiler'")&&(i=i.replace(/plugins:\s*\[([^\]]*)\]/,(h,C)=>{let d=C.trim();return`plugins: [${d?`${d}, "react-compiler"`:'"react-compiler"'}]`})),i.includes("reactCompiler.configs.recommended")||(i=i.replace(/extends:\s*\[([^\]]*)\]/,(h,C)=>{let d=C.trim();return`extends: [${d?`${d} reactCompiler.configs.recommended`:"reactCompiler.configs.recommended"}]`}))}catch(i){H.log.warn(`Failed to update ESLint config: ${i.message}. Skipping React Compiler ESLint rules. Add manually if needed.`)}return!0};import A from"fs/promises";import z from"path";import*as q from"@clack/prompts";var K=async(e,r)=>{let t=e.folderStructure;q.note(`Setting up ${t} folder structure...`,"Structure");let a=e.useTypescript?"ts":"js",s=e.useTypescript?"tsx":"jsx",o=z.join(r,"src");try{try{await A.stat(`${o}/App.css`),await A.unlink(`${o}/App.css`)}catch{}try{await A.stat(`${o}/App.${s}`),await A.unlink(`${o}/App.${s}`)}catch{}if(await A.access(o),t==="simple"){let n=["components","components/ui",...e.useRedux?["store"]:[],"pages","lib","hooks",...e.useRouter?["routes"]:[],...e.useRhformZod?["validations"]:[],...e.useTypescript?["types"]:[]];for(let u of n){let l=z.join(o,u);await A.mkdir(l,{recursive:!0})}let p=[...e.useTypescript?["types/index.d.ts"]:[]];for(let u of p){let l=z.join(o,u);await A.writeFile(l,"")}}else{let n=["app",...e.useRedux?["app/store"]:[],"features","features/auth/pages","features/auth/components",...e.useTypescript?["features/auth/types"]:[],"shared/components/ui","shared/lib","shared/hooks","shared/utils",...e.useTypescript?["shared/types"]:[]];for(let u of n){let l=z.join(o,u);await A.mkdir(l,{recursive:!0})}let p=[...e.useRouter?[`app/router.${s}`]:[],`features/auth/api.${a}`,`features/auth/layout.${s}`,...e.useTypescript?["features/auth/types.d.ts"]:[],...e.useRouter?[`features/auth/routes.${s}`]:[],...e.useRhformZod?[`features/auth/schema.${a}`]:[],...e.useTypescript?["shared/types/index.d.ts"]:[],`shared/api.${a}`,...e.useReactQueryAxios?[`shared/queriesAndMutations.${a}`]:[]];for(let u of p){let l=z.join(o,u);await A.writeFile(l,"")}}return q.log.success(`${t} folder structure setup complete.`),!0}catch(n){return q.log.error(`Failed to setup folder structure: ${n.message}`),!1}};import{execa as Pe}from"execa";import*as O from"@clack/prompts";import ue from"fs/promises";import Z from"path";var Y=async(e,r)=>{if(!e.useRedux)return!0;O.note("Setting up Redux Toolkit...","Redux");let t=e.packageManager,a=e.useTypescript?"ts":"js",s=Z.join(r,"src"),o=["@reduxjs/toolkit","react-redux"],n=t,p=[t==="npm"?"install":"add",...o];if(!await y(`Installing Redux Toolkit & react-redux using ${t}...`,"Redux packages installed",async()=>(await Pe(n,p,{cwd:r}),!0),"Failed to install Redux packages"))return!1;let l=e.folderStructure==="simple",i=l?Z.join(s,"store"):Z.join(s,"app/store"),m=Z.join(i,`index.${a}`);try{await ue.mkdir(i,{recursive:!0});let h=`import { configureStore } from "@reduxjs/toolkit";
13
+ ${e.useTypescript?`import {
14
+ type TypedUseSelectorHook,
15
+ useDispatch,
16
+ useSelector,
17
+ } from "react-redux";`:""}
18
+
19
+ export const store = configureStore({
20
+ reducer: {
21
+ // Add reducers here
22
+ },
23
+ });
24
+
25
+ ${e.useTypescript?`export type RootState = ReturnType<typeof store.getState>;
26
+ export type AppDispatch = typeof store.dispatch;
27
+
28
+ export const useAppDispatch: () => AppDispatch = useDispatch;
29
+ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;`:""}
30
+
31
+ `;await ue.writeFile(m,h),O.log.success(`Created ${l?"store":"app/store"}/index.${a} (basic store)`)}catch(h){return O.log.error(`Failed to create store: ${h.message}`),!1}return!0};import{execa as $e}from"execa";import*as j from"@clack/prompts";import Q from"fs/promises";import v from"path";var X=async(e,r)=>{if(!e.useReactQueryAxios)return!0;j.note("Setting up React Query + Axios...","React Query");let t=e.packageManager,a=e.useTypescript?"ts":"js",s=v.join(r,"src"),o=["@tanstack/react-query","axios"],n=t,p=["add",...o];if(t==="yarn"&&(n="yarn"),t==="bun"&&(n="bun"),!await y(`Installing React Query + Axios using ${t}...`,"React Query & Axios installed",async()=>(await $e(n,p,{cwd:r}),!0),"Failed to install React Query packages"))return!1;let l=e.folderStructure==="simple",i=l?v.join(s,"lib"):v.join(s,"shared/lib"),m=v.join(i,`api.${a}`);try{await Q.mkdir(i,{recursive:!0}),await Q.writeFile(m,`import axios from "axios";
32
+
33
+ export const api = axios.create({
34
+ baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000",
35
+ timeout: 10000,
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ },
39
+ });
40
+
41
+ // Request interceptor to add auth token
42
+ api.interceptors.request.use(
43
+ (config) => {
44
+ const token = localStorage.getItem("token");
45
+ if (token) {
46
+ config.headers.Authorization = \`Bearer \${token}\`;
47
+ }
48
+ return config;
49
+ },
50
+ (error) => {
51
+ return Promise.reject(error);
52
+ }
53
+ );
54
+
55
+ // Response interceptor to handle errors
56
+ api.interceptors.response.use(
57
+ (response) => response,
58
+ (error) => {
59
+ if (error.response?.status === 401) {
60
+ // Handle unauthorized - redirect to login or refresh token
61
+ localStorage.removeItem("token");
62
+ window.location.href = "/auth/login";
63
+ }
64
+ return Promise.reject(error);
65
+ }
66
+ );
67
+
68
+ export default api;
69
+ `),j.log.success(`Created ${l?"lib":"shared/lib"}/api.${a}`)}catch(d){return j.log.error(`Failed to create api instance: ${d.message}`),!1}let h=l?v.join(s,"lib"):v.join(s,"shared/lib"),C=v.join(h,`queryClient.${a}`);try{await Q.mkdir(h,{recursive:!0}),await Q.writeFile(C,`import { QueryClient } from "@tanstack/react-query";
70
+
71
+ export const queryClient = new QueryClient({
72
+ defaultOptions: {
73
+ queries: {
74
+ retry: 1,
75
+ staleTime: 1000 * 60, // 1 minute
76
+ gcTime: 1000 * 60 * 5, // 5 minutes
77
+ refetchOnWindowFocus: true,
78
+ refetchOnReconnect: true,
79
+ refetchOnMount: true,
80
+ },
81
+ },
82
+ });
83
+ `),j.log.success(`Created ${l?"lib":"shared/lib"}/queryClient.${a}`)}catch(d){return j.log.error(`Failed to create queryClient: ${d.message}`),!1}if(!l){let d=v.join(s,`shared/queriesAndMutations.${a}`);try{let k=`import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
84
+ import { api } from "./api";
85
+
86
+ // Example query hook
87
+ export const useExampleQuery = () => {
88
+ return useQuery({
89
+ queryKey: ["example"],
90
+ queryFn: async () => {
91
+ const { data } = await api.get("/example");
92
+ return data;
93
+ },
94
+ });
95
+ };
96
+
97
+ // Example mutation hook
98
+ export const useExampleMutation = () => {
99
+ const queryClient = useQueryClient();
100
+
101
+ return useMutation({
102
+ mutationFn: async (payload${e.useTypescript?": unknown":""}) => {
103
+ const { data } = await api.post("/example", payload);
104
+ return data;
105
+ },
106
+ onSuccess: () => {
107
+ // Invalidate and refetch
108
+ queryClient.invalidateQueries({ queryKey: ["example"] });
109
+ },
110
+ });
111
+ };
112
+ `;await Q.writeFile(d,k),j.log.success(`Created shared/queriesAndMutations.${a}`)}catch(k){return j.log.error(`Failed to create queriesAndMutations: ${k.message}`),!1}}return!0};import{execa as be}from"execa";import*as b from"@clack/prompts";import me from"fs/promises";import ee from"path";var te=async(e,r)=>{if(!e.useRhformZod)return!0;b.note("Setting up React Hook Form + Zod...","React Hook Form");let t=e.packageManager,a=e.useTypescript?"ts":"js",s=ee.join(r,"src"),o=["react-hook-form","zod","@hookform/resolvers"],n=t,p=["add",...o];if(t==="yarn"&&(n="yarn"),t==="bun"&&(n="bun"),!await y(`Installing React Hook Form & Zod using ${t}...`,"React Hook Form & Zod installed",async()=>(await be(n,p,{cwd:r}),!0),"Failed to install React Hook Form & Zod packages"))return!1;if(e.folderStructure==="simple"){let i=ee.join(s,`validations/index.${a}`);try{let m=e.useTypescript?`import { z } from "zod";
113
+
114
+ // Example validation schema
115
+ export const loginSchema = z
116
+ .object({
117
+ email: z.string().email("Invalid email address"),
118
+ password: z.string().min(6, "Password must be at least 6 characters"),
119
+ });
120
+
121
+ export type LoginFormData = z.infer<typeof loginSchema>;
122
+
123
+ // Add more validation schemas here
124
+ `:`import { z } from "zod";
125
+
126
+ // Example validation schema
127
+ export const loginSchema = z
128
+ .object({
129
+ email: z.string().email("Invalid email address"),
130
+ password: z.string().min(6, "Password must be at least 6 characters"),
131
+ });
132
+
133
+ // Add more validation schemas here
134
+ `;await me.writeFile(i,m),b.log.success(`Created validations/index.${a}`)}catch(m){return b.log.error(`Failed to create validations: ${m.message}`),!1}}else{let i=ee.join(s,`features/auth/schema.${a}`);try{let m=e.useTypescript?`import { z } from "zod";
135
+
136
+ // Auth feature validation schemas
137
+ export const loginSchema = z
138
+ .object({
139
+ email: z.string().email("Invalid email address"),
140
+ password: z.string().min(6, "Password must be at least 6 characters"),
141
+ });
142
+
143
+ export const registerSchema = z
144
+ .object({
145
+ email: z.string().email("Invalid email address"),
146
+ password: z.string().min(6, "Password must be at least 6 characters"),
147
+ confirmPassword: z.string(),
148
+ })
149
+ .refine((data) => data.password === data.confirmPassword, {
150
+ message: "Passwords don't match",
151
+ path: ["confirmPassword"],
152
+ });
153
+
154
+ export type LoginFormData = z.infer<typeof loginSchema>;
155
+ export type RegisterFormData = z.infer<typeof registerSchema>;
156
+
157
+ // Add more validation schemas here
158
+ `:`import { z } from "zod";
159
+
160
+ // Auth feature validation schemas
161
+ export const loginSchema = z
162
+ .object({
163
+ email: z.string().email("Invalid email address"),
164
+ password: z.string().min(6, "Password must be at least 6 characters"),
165
+ });
166
+
167
+ export const registerSchema = z
168
+ .object({
169
+ email: z.string().email("Invalid email address"),
170
+ password: z.string().min(6, "Password must be at least 6 characters"),
171
+ confirmPassword: z.string(),
172
+ })
173
+ .refine((data) => data.password === data.confirmPassword, {
174
+ message: "Passwords don't match",
175
+ path: ["confirmPassword"],
176
+ });
177
+
178
+ // Add more validation schemas here
179
+ `;await me.writeFile(i,m),b.log.success(`Created features/auth/schema.${a}`)}catch(m){return b.log.error(`Failed to create schema: ${m.message}`),!1}}return!0};import{execa as de}from"execa";import*as x from"@clack/prompts";import U from"fs/promises";import D from"path";var re=async(e,r)=>{if(!e.useEslintPrettier)return!0;let t=e.packageManager,a=e.useTypescript,s=["eslint","eslint-plugin-react","eslint-plugin-react-hooks","eslint-plugin-jsx-a11y","eslint-plugin-import","eslint-config-prettier","eslint-plugin-prettier","prettier",...e.useTailwind?["prettier-plugin-tailwindcss"]:[]];a&&s.push("@typescript-eslint/eslint-plugin","@typescript-eslint/parser","typescript-eslint");let o=t,n;if(t==="npm"?n=["install","-D",...s]:t==="yarn"?n=["add","-D",...s]:t==="bun"?n=["add","-d",...s]:n=["add","-D",...s],!await y(`Installing ESLint + Prettier using ${t}...`,"ESLint & Prettier installed successfully",async()=>(await de(o,n,{cwd:r}),!0),"Failed to install ESLint/Prettier packages"))return!1;let u=D.join(r,"eslint.config.js"),l=a?`import js from "@eslint/js";
180
+ import globals from "globals";
181
+ import reactHooks from "eslint-plugin-react-hooks";
182
+ import reactRefresh from "eslint-plugin-react-refresh";
183
+ import tseslint from "typescript-eslint";
184
+ import prettier from "eslint-plugin-prettier/recommended";
185
+ import react from "eslint-plugin-react";
186
+ import jsxA11y from "eslint-plugin-jsx-a11y";
187
+ import { defineConfig, globalIgnores } from "eslint/config";
188
+
189
+ export default defineConfig([
190
+ globalIgnores(["dist"]),
191
+ {
192
+ files: ["**/*.{ts,tsx}"],
193
+ extends: [
194
+ js.configs.recommended,
195
+ tseslint.configs.recommended,
196
+ reactHooks.configs.flat.recommended,
197
+ reactRefresh.configs.vite,
198
+ prettier,
199
+ ],
200
+ languageOptions: {
201
+ ecmaVersion: 2020,
202
+ globals: globals.browser,
203
+ parser: tseslint.parser,
204
+ parserOptions: {
205
+ project: "./tsconfig.app.json",
206
+ },
207
+ },
208
+ plugins: {
209
+ react,
210
+ "jsx-a11y": jsxA11y,
211
+ },
212
+ settings: {
213
+ react: {
214
+ version: "detect",
215
+ },
216
+ },
217
+ rules: {
218
+ "react-refresh/only-export-components": [
219
+ "warn",
220
+ { allowConstantExport: true },
221
+ ],
222
+ "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
223
+ "@typescript-eslint/explicit-function-return-type": "off",
224
+ "@typescript-eslint/no-explicit-any": "warn",
225
+ "jsx-a11y/alt-text": "warn",
226
+ "jsx-a11y/anchor-has-content": "warn",
227
+ "jsx-a11y/anchor-is-valid": "warn",
228
+ },
229
+ },
230
+ ]);
231
+ `:`import js from "@eslint/js";
232
+ import globals from "globals";
233
+ import reactHooks from "eslint-plugin-react-hooks";
234
+ import reactRefresh from "eslint-plugin-react-refresh";
235
+ import prettier from "eslint-plugin-prettier/recommended";
236
+ import react from "eslint-plugin-react";
237
+ import jsxA11y from "eslint-plugin-jsx-a11y";
238
+ import { defineConfig, globalIgnores } from "eslint/config";
239
+
240
+ export default defineConfig([
241
+ globalIgnores(["dist"]),
242
+ {
243
+ files: ["**/*.{js,jsx}"],
244
+ extends: [js.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, prettier],
245
+ languageOptions: {
246
+ ecmaVersion: 2020,
247
+ globals: globals.browser,
248
+ parserOptions: {
249
+ ecmaFeatures: {
250
+ jsx: true,
251
+ },
252
+ },
253
+ },
254
+ plugins: {
255
+ react,
256
+ "jsx-a11y": jsxA11y,
257
+ },
258
+ settings: {
259
+ react: {
260
+ version: "detect",
261
+ },
262
+ },
263
+ rules: {
264
+ "react-refresh/only-export-components": [
265
+ "warn",
266
+ { allowConstantExport: true },
267
+ ],
268
+ "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
269
+ "jsx-a11y/alt-text": "warn",
270
+ "jsx-a11y/anchor-has-content": "warn",
271
+ "jsx-a11y/anchor-is-valid": "warn",
272
+ },
273
+ },
274
+ ]);
275
+ `;try{await U.writeFile(u,l),x.log.success("Created eslint.config.js")}catch(w){return x.log.error(`Failed to create ESLint config: ${w.message}`),!1}let i=D.join(r,".prettierrc"),m={semi:!0,singleQuote:!1,tabWidth:2,trailingComma:"es5",...e.useTailwind&&{plugins:["prettier-plugin-tailwindcss"]}};try{await U.writeFile(i,JSON.stringify(m,null,2)),x.log.success("Created .prettierrc")}catch(w){return x.log.error(`Failed to create Prettier config: ${w.message}`),!1}let h=D.join(r,".prettierignore"),C=`# Dependencies
276
+ node_modules
277
+ .pnpm-store
278
+
279
+ # Build output
280
+ dist
281
+ build
282
+ *.min.js
283
+ *.min.css
284
+
285
+ # Logs
286
+ *.log
287
+
288
+ # Environment files
289
+ .env
290
+ .env.local
291
+ .env.*.local
292
+
293
+ # Cache
294
+ .cache
295
+ *.cache
296
+
297
+ # Public assets
298
+ public
299
+
300
+ # Lock files
301
+ package-lock.json
302
+ yarn.lock
303
+ pnpm-lock.yaml
304
+ bun.lockb
305
+ `;try{await U.writeFile(h,C),x.log.success("Created .prettierignore")}catch(w){return x.log.error(`Failed to create .prettierignore: ${w.message}`),!1}let d=D.join(r,"package.json");try{let w=await U.readFile(d,"utf-8"),B=JSON.parse(w);B.scripts={...B.scripts,lint:"eslint . --ext .ts,.tsx,.js,.jsx","lint:fix":"eslint . --ext .ts,.tsx,.js,.jsx --fix",format:"prettier --write .","format:check":"prettier --check ."},await U.writeFile(d,JSON.stringify(B,null,2)),x.log.success("Added lint and format scripts to package.json")}catch(w){return x.log.error(`Failed to update package.json: ${w.message}`),!1}let k=["globals"],$;t==="npm"?$=["install","-D",...k]:t==="yarn"?$=["add","-D",...k]:t==="bun"?$=["add","-d",...k]:$=["add","-D",...k];try{await de(o,$,{cwd:r}),x.log.success("Installed ESLint peer dependencies")}catch(w){x.log.warn(`Could not install peer dependencies: ${w.message}`)}return!0};import{execa as Fe}from"execa";import*as S from"@clack/prompts";import V from"fs/promises";import E from"path";var fe=e=>{let r=e.useTypescript,t=e.useRedux,a=e.useReactQueryAxios,s=e.useRouter,o=e.folderStructure==="simple",n=[];t&&(n.push('import { Provider } from "react-redux"'),n.push(`import { store } from "${o?"../store":"./store"}"`)),a&&(n.push('import { QueryClientProvider } from "@tanstack/react-query"'),n.push(`import { queryClient } from "${o?"../lib/queryClient":"../shared/lib/queryClient"}"`)),s&&(n.push('import { RouterProvider } from "react-router"'),n.push(`import { router } from "${o?"../routes":"./router"}"`)),!s&&r&&n.push('import type { ReactNode } from "react"');let p=i=>{let m=i;return a&&(m=`<QueryClientProvider client={queryClient}>
306
+ ${m}</QueryClientProvider>`),t&&(m=`<Provider store={store}>
307
+ ${m}</Provider>`),m},u=s?" <RouterProvider router={router} />":" {children}",l=s?"":r?"{ children }: { children: ReactNode }":"{ children }";return`${n.join(`
308
+ `)}
309
+
310
+ const Providers = (${l}) => {
311
+ return (
312
+ ${p(u)}
313
+ );
314
+ };
315
+
316
+ export default Providers;
317
+ `},ge=e=>{let r=e.folderStructure==="simple",t=e.useRouter;return`import Providers from ${r?'"./components/providers"':'"./app/providers"'};
318
+
319
+ const App = () => {
320
+ return ${t?"<Providers />":"<Providers><div>App</div></Providers>"};
321
+ };
322
+
323
+ export default App;
324
+ `},he=e=>`import { createBrowserRouter } from "react-router";
325
+
326
+ export const router = createBrowserRouter([
327
+ {
328
+ path: "/",
329
+ element: <div>Home</div>,
330
+ children: [
331
+ // Add your routes here
332
+ ],
333
+ },
334
+ ]);
335
+ `,ye=e=>`import AuthLayout from "./layout";
336
+
337
+ export const authRoutes = [
338
+ {
339
+ path: "auth",
340
+ element: <AuthLayout />,
341
+ children: [
342
+ {
343
+ path: "login",
344
+ element: <div>Login</div>,
345
+ },
346
+ {
347
+ path: "register",
348
+ element: <div>Register</div>,
349
+ },
350
+ ],
351
+ },
352
+ ];
353
+ `,we=e=>`import { createBrowserRouter } from "react-router";
354
+ import { authRoutes } from "../features/auth/routes";
355
+
356
+ export const router = createBrowserRouter([
357
+ {
358
+ path: "/",
359
+ element: <div>Home</div>,
360
+ },
361
+ ...authRoutes,
362
+ ]);
363
+ `,se=e=>`// Global type definitions
364
+
365
+ // Example: User type
366
+ export interface User {
367
+ id: string;
368
+ email: string;
369
+ name: string;
370
+ }
371
+
372
+ // Example: API response type
373
+ export interface ApiResponse<T> {
374
+ data: T;
375
+ message: string;
376
+ success: boolean;
377
+ }
378
+
379
+ // Add more global types here
380
+ `,ke=e=>`// Auth feature types
381
+
382
+ export interface AuthUser {
383
+ id: string;
384
+ email: string;
385
+ name: string;
386
+ }
387
+
388
+ export interface LoginCredentials {
389
+ email: string;
390
+ password: string;
391
+ }
392
+
393
+ export interface RegisterCredentials extends LoginCredentials {
394
+ confirmPassword: string;
395
+ }
396
+
397
+ // Add more auth-related types here
398
+ `,xe=e=>{let r=e.useTypescript;return e.useReactQueryAxios?`// Auth feature API calls using Axios
399
+ ${r?`import type { AuthUser, LoginCredentials, RegisterCredentials } from "./types";
400
+ import type { ApiResponse } from "../../shared/types";
401
+ import { api } from "../../shared/lib/api";`:'import { api } from "../shared/lib/api";'}
402
+
403
+ // Example API functions
404
+ export const login = async (credentials${r?": LoginCredentials":""})${r?": Promise<ApiResponse<AuthUser>>":""} => {
405
+ const { data } = await api.post("/auth/login", credentials);
406
+ return data;
407
+ };
408
+
409
+ export const register = async (credentials${r?": RegisterCredentials":""})${r?": Promise<ApiResponse<AuthUser>>":""} => {
410
+ const { data } = await api.post("/auth/register", credentials);
411
+ return data;
412
+ };
413
+
414
+ // Add more API functions here
415
+ `:`// Auth feature API calls using Fetch
416
+ ${r?`import type { AuthUser, LoginCredentials, RegisterCredentials } from "./types";
417
+ import type { ApiResponse } from "../shared/types";`:""}
418
+
419
+ // Example API functions
420
+ export const login = async (credentials${r?": LoginCredentials":""})${r?": Promise<ApiResponse<AuthUser>>":""} => {
421
+ // Implement login API call
422
+ const response = await fetch("/api/auth/login", {
423
+ method: "POST",
424
+ headers: { "Content-Type": "application/json" },
425
+ body: JSON.stringify(credentials),
426
+ });
427
+ return response.json();
428
+ };
429
+
430
+ export const register = async (credentials${r?": RegisterCredentials":""})${r?": Promise<ApiResponse<AuthUser>>":""} => {
431
+ // Implement register API call
432
+ const response = await fetch("/api/auth/register", {
433
+ method: "POST",
434
+ headers: { "Content-Type": "application/json" },
435
+ body: JSON.stringify(credentials),
436
+ });
437
+ return response.json();
438
+ };
439
+
440
+ // Add more API functions here
441
+ `},J=e=>`import { Outlet } from "react-router";
442
+
443
+ const AuthLayout = () => {
444
+ return (
445
+ <div>
446
+ Auth Layout
447
+ <Outlet />
448
+ </div>
449
+ );
450
+ };
451
+
452
+ export default AuthLayout;
453
+ `;var ae=async(e,r)=>{if(!e.useRouter)return!0;S.note("Setting up React Router...","React Router");let t=e.packageManager,a=e.useTypescript?"tsx":"jsx",s=E.join(r,"src"),o=["react-router"],n=t,p=["add",...o];if(t==="npm"?p=["install",...o]:t==="yarn"?p=["add",...o]:t==="bun"&&(p=["add",...o]),!await y(`Installing React Router using ${t}...`,"React Router installed",async()=>(await Fe(n,p,{cwd:r}),!0),"Failed to install React Router"))return!1;let l=e.folderStructure==="simple";try{if(l){let i=E.join(s,"routes");await V.mkdir(i,{recursive:!0});let m=E.join(i,`index.${a}`);await V.writeFile(m,he(e)),S.log.success(`Created routes/index.${a}`)}else{let i=E.join(s,`app/router.${a}`);await V.writeFile(i,we(e)),S.log.success(`Created app/router.${a}`);let m=E.join(s,`features/auth/routes.${a}`);await V.writeFile(m,ye(e)),S.log.success(`Created features/auth/routes.${a}`);let h=E.join(s,`features/auth/layout.${a}`);await V.writeFile(h,J(e)),S.log.success(`Created features/auth/layout.${a}`)}return S.log.success("React Router setup complete!"),!0}catch(i){return S.log.error(`Failed to create router files: ${i.message}`),!1}};var Ce=async(e,r)=>await Ee(e,r)?(f.note("Next steps (dependencies, Tailwind, etc.) will be added gradually."),await W(e,r)?await G(e,r)?await K(e,r)?await ae(e,r)?await Y(e,r)?await X(e,r)?await te(e,r)?await re(e,r)?await Ie(e,r)?!0:(f.log.error("Failed to generate application files."),!1):(f.log.error("Failed to setup ESLint & Prettier."),!1):(f.log.error("Failed to setup react hook form & zod."),!1):(f.log.error("Failed to setup react query & axios."),!1):(f.log.error("Failed to setup redux toolkit."),!1):(f.log.error("Failed to setup React Router."),!1):(f.log.error("Failed to setup folder structure."),!1):(f.log.error("Failed to setup React Compiler. Aborting."),!1):(f.log.error("Failed to setup Tailwind. Aborting."),!1)):(f.log.error("Failed to create Vite project. Aborting."),!1),Ee=async(e,r)=>{let t=e.useTypescript?"react-ts":"react",a=e.packageManager,s,o;return a==="npm"?(s="npm",o=["create","vite@latest",".","--","--template",t,"--no-interactive"]):a==="yarn"?(s="yarn",o=["create","vite",".","--template",t,"--no-interactive"]):a==="bun"?(s="bun",o=["create","vite",".","--template",t,"--no-interactive"]):(s="pnpm",o=["create","vite@latest",".","--template",t,"--no-interactive","--"]),await y(`Creating Vite project using ${a}...`,"Vite project created successfully! \u{1F389}",async()=>{let n=await Te(s,o,{cwd:r,stdio:"pipe"});return n.stderr&&f.log.warn(`Warnings/Logs: ${n.stderr}`),f.log.message(n.stdout||"Done."),!0},"Failed to create Vite project").catch(n=>(f.log.error(`Error details: ${n.message}`),!1))},Ie=async(e,r)=>{f.note("Generating application files...","Files");let t=e.useTypescript?"ts":"js",a=e.useTypescript?"tsx":"jsx",s=R.join(r,"src"),o=e.folderStructure==="simple";try{let n=R.join(s,`App.${a}`);await P.writeFile(n,ge(e)),f.log.success(`Created App.${a}`);let p=o?R.join(s,`components/providers.${a}`):R.join(s,`app/providers.${a}`);if(await P.writeFile(p,fe(e)),f.log.success(`Created ${o?"components":"app"}/providers.${a}`),e.useTypescript)if(o){let u=R.join(s,"types/index.d.ts");await P.writeFile(u,se(e)),f.log.success("Created types/index.d.ts")}else{let u=R.join(s,"shared/types/index.d.ts");await P.writeFile(u,se(e)),f.log.success("Created shared/types/index.d.ts");let l=R.join(s,"features/auth/types.d.ts");await P.writeFile(l,ke(e)),f.log.success("Created features/auth/types.d.ts")}if(!o){let u=R.join(s,`features/auth/api.${t}`);await P.writeFile(u,xe(e)),f.log.success(`Created features/auth/api.${t}`);let l=R.join(s,`features/auth/layout.${a}`);await P.writeFile(l,J(e)),f.log.success(`Created features/auth/layout.${a}`);let i=R.join(s,`shared/api.${t}`),m=e.useReactQueryAxios?`// Re-export API from lib location | OR can add shared api calls here
454
+ export { api } from "./lib/api";
455
+ `:`// Shared API configuration
456
+ export const api = {
457
+ // Add your shared API configuration here
458
+ };
459
+ `;await P.writeFile(i,m),f.log.success(`Created shared/api.${t}`)}return f.log.success("Application files generated successfully!"),!0}catch(n){return f.log.error(`Failed to generate application files: ${n.message}`),!1}};console.log(oe.bold("crvp CLI v0.1.0"));Re.name("crvp").description("Interactive CLI to create modern React + Vite projects").version("0.1.0").argument("[project-name]","Name of the project (folder)").action(async e=>{let t=await _(e);if(t){ie.log.step("Preparing project directory...");let a=await ne(t.projectName);if(!a){console.log(oe.yellow("Project creation cancelled."));return}a&&t&&(await Ce(t,a)||process.exit(1)),ie.log.success(`Ready to scaffold in: ${oe.cyan(a)}`)}});Re.parse();
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "create-rvp",
3
+ "version": "1.0.0",
4
+ "description": "Interactive CLI for creating modern React + Vite projects with shadcn, Tailwind, Redux, etc.",
5
+ "type": "module",
6
+ "main": "dist/cli.js",
7
+ "author": "Mostafa Khaled",
8
+ "keywords": [
9
+ "cli",
10
+ "vite",
11
+ "react",
12
+ "create",
13
+ "scaffold",
14
+ "tailwind"
15
+ ],
16
+ "bin": {
17
+ "create-rvp": "bin/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "bin"
22
+ ],
23
+ "scripts": {
24
+ "dev": "tsx src/cli.ts",
25
+ "start": "node dist/cli.js",
26
+ "build": "tsup src/cli.ts --format esm --clean --minify",
27
+ "lint": "eslint . --ext .ts"
28
+ },
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "@clack/prompts": "^1.0.1",
35
+ "commander": "^14.0.3",
36
+ "execa": "^9.6.1",
37
+ "fs-extra": "^11.3.3",
38
+ "picocolors": "^1.1.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.3.0",
42
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
43
+ "@typescript-eslint/parser": "^8.56.1",
44
+ "eslint": "^10.0.2",
45
+ "tsup": "^8.5.1",
46
+ "tsx": "^4.21.0",
47
+ "typescript": "^5.9.3"
48
+ }
49
+ }