
Research
/Security News
GlassWASM: WebAssembly Malware Found in Trojanized Open VSX Extensions
The trojanized extensions use TinyGo-compiled WebAssembly and Solana transaction memos to resolve command-and-control infrastructure.
Scaffold a production-ready Vite + React project with permissions, routing, Redux, and shadcn/ui
A CLI that scaffolds production-ready Vite + React projects with a built-in permission engine, protected routing, centralized API layer, Redux state management, module generator, and shadcn/ui components — all wired together and ready to go.
npx rvx-cli my-app
cd my-app
npm run dev
Most React scaffolding tools give you a blank canvas. rvx-cli gives you an opinionated, enterprise-ready architecture out of the box:
can(), canAny(), canAll() with super admin bypass, baked into routes, sidebar, and buttonsnpm run hcorp:add product creates page, add page, service, slice, API endpoints, permissions, route, and sidebar entry in one command# Create a new project
npx rvx-cli my-app
# Or pass the name directly
npx rvx-cli my-app
The CLI will:
npm install automaticallycd my-app
npm run dev # Start dev server (default: http://localhost:5173)
npm run build # Production build
npm run preview # Preview production build
During scaffolding, you choose which features to include:
? Select features:
◉ Tailwind CSS
◉ shadcn/ui
◉ Redux Toolkit
◉ React Router
| Feature | Default | What happens when disabled |
|---|---|---|
| Tailwind CSS | On | Removes Tailwind packages, generates plain CSS reset, strips className attributes |
| shadcn/ui | On | Removes Radix/lucide packages, replaces <Button> with plain <button>, removes components/ui/, lib/, hooks/alert/. Auto-enables Tailwind if shadcn is selected |
| Redux Toolkit | On | Removes Redux packages, deletes src/features/ directory, module generator skips slice creation |
| React Router | On | Removes router package, deletes src/layout/, generates state-based App.jsx with inline navigation |
The module generator (hcorp:add) also reads this config and adapts generated code accordingly.
src/
├── components/ui/ # shadcn/ui components (Button, AlertDialog, Switch)
├── constants/
│ ├── api/
│ │ └── api.js # Centralized API endpoints with crud() helper
│ ├── config/
│ │ ├── permissions.js # Permission constants per module
│ │ ├── colors.js # Color palette for JS usage (charts, inline styles)
│ │ └── text.js # UI text/label constants
│ └── data/
│ └── menu.data.js # Sidebar menu structure with permission filtering
├── context/
│ ├── auth/auth.context.jsx # Auth state + permission engine (can, canAny, canAll)
│ ├── theme/theme.context.jsx # Light/dark theme toggle
│ └── mobile/mobile.context.jsx # Mobile detection + sidebar state
├── features/
│ ├── store.js # Redux store configuration
│ └── master/
│ └── <module>/
│ └── <module>.slice.js # Redux slice with CRUD reducers
├── hooks/
│ ├── alert/use-alert.jsx # Alert dialog state management hook
│ └── api/use-api.jsx # React Query hooks (useApiQuery, useApiMutation)
├── layout/
│ ├── layout.jsx # Main layout (sidebar + top-nav + Outlet)
│ ├── top-nav.jsx # Header with hamburger, user info, logout
│ ├── sidebar.jsx # Permission-filtered navigation sidebar
│ ├── custom-routes.jsx # Route definitions with ProtectedRoute wrapper
│ └── menu.item.jsx # NavLink menu item with active state
├── lib/
│ └── utils.js # cn() utility for Tailwind class merging
├── pages/views/
│ ├── dashboard.jsx # Dashboard landing page
│ ├── auth/
│ │ ├── auth.jsx # Login page with form
│ │ └── auth.service.js # Auth API hooks (login, logout, me)
│ ├── admin/
│ │ └── permissions.jsx # Permission toggle admin page (live testing)
│ ├── master/
│ │ └── <module>/
│ │ ├── <module>.jsx # Module list page
│ │ ├── <module>.service.js # Module API hooks (React Query)
│ │ └── add/
│ │ └── add.<module>.jsx # Module add/create page
│ ├── dev-guide/
│ │ ├── dev-guide.jsx # Interactive developer documentation
│ │ └── sections.jsx # All documentation sections
│ └── errors/
│ ├── not-found.jsx # 404 page
│ ├── server-error.jsx # 500 page
│ ├── unauthorized.jsx # 403 page
│ └── countdown-redirect.jsx # Auto-redirect with countdown timer
├── main.jsx # Entry point with all providers stacked
└── index.css # Tailwind v4 theme variables + base styles
The most powerful feature — generate a complete CRUD module with a single command:
npm run hcorp:add product
| File | Path |
|---|---|
| List page | src/pages/views/master/product/product.jsx |
| Add page | src/pages/views/master/product/add/add.product.jsx |
| Service file | src/pages/views/master/product/product.service.js |
| Redux slice | src/features/master/product/product.slice.js |
| File | Change |
|---|---|
constants/api/api.js | Adds PRODUCT: crud("product") — generates LIST, CREATE, UPDATE, DELETE, SEARCH endpoints |
constants/config/permissions.js | Adds PRODUCT: { VIEW, ADD, EDIT, DELETE } permission block |
features/store.js | Imports and registers productReducer |
constants/data/menu.data.js | Adds "Product" to the Master menu group with product.view permission |
layout/custom-routes.jsx | Adds protected routes for /master/product and /master/product/add |
context/auth/auth.context.jsx | Adds product.view, product.button.add, product.button.edit, product.button.delete to default permissions |
The generator reads hcorp.config.json and adapts:
useNavigate<button> instead of <Button>className attributesEvery module follows a strict 4-permission pattern:
<module>.view → View the module page
<module>.button.add → Show add button / access add page
<module>.button.edit → Show edit button
<module>.button.delete → Show delete button
// src/constants/config/permissions.js
export const PERMISSIONS = {
CUSTOMER: {
VIEW: "customer.view",
ADD: "customer.button.add",
EDIT: "customer.button.edit",
DELETE: "customer.button.delete",
},
};
import { useAuth } from "@/context/auth/auth.context";
const { can, canAny, canAll } = useAuth();
can("customer.view") // single check
canAny(["customer.button.add", "customer.button.edit"]) // has ANY of these
canAll(["customer.view", "customer.button.delete"]) // has ALL of these
Roles super-admin, Super Admin, or superadmin (case-insensitive) automatically pass all permission checks.
| Layer | How |
|---|---|
| Buttons | {can(PERMISSIONS.CUSTOMER.ADD) && <Button>Add</Button>} |
| Menu items | permission: "customer.view" in menu.data.js — sidebar auto-filters |
| Routes | <ProtectedRoute permission="customer.view"> wrapper |
| Pages | can() / canAny() / canAll() in component logic |
Navigate to /admin/permissions to toggle permissions in real-time with switch toggles. Shows all registered permissions with a live JSON view — useful for testing permission-based UI behavior during development.
// src/constants/api/api.js
const BASE_URL = import.meta.env.VITE_API_BASE_URL || "";
function crud(module) {
return {
LIST: `${BASE_URL}/api/${module}/list`,
CREATE: `${BASE_URL}/api/${module}/create`,
UPDATE: `${BASE_URL}/api/${module}/update`,
DELETE: `${BASE_URL}/api/${module}/delete`,
SEARCH: `${BASE_URL}/api/${module}/search`,
};
}
export const API = {
AUTH: {
LOGIN: `${BASE_URL}/api/auth/login`,
LOGOUT: `${BASE_URL}/api/auth/logout`,
ME: `${BASE_URL}/api/auth/me`,
},
CUSTOMER: crud("customer"),
};
export const API = {
// ... existing
REPORT: {
SALES: `${BASE_URL}/api/report/sales`,
INVENTORY: `${BASE_URL}/api/report/inventory`,
},
};
The crud() helper generates 5 standard endpoints per module. The module generator auto-adds MODULE: crud("module") when you run hcorp:add.
Each module has a service file that exports React Query hooks. The hooks connect API endpoints to the useApi hook.
import { useApiQuery } from "@/hooks/api/use-api";
import { API } from "@/constants/api/api";
const { data, isLoading, error } = useApiQuery(
["customer", "list"], // cache key
API.CUSTOMER.LIST, // endpoint URL
{ page: 1, limit: 10 } // POST body (optional)
);
| Param | Type | Description |
|---|---|---|
key | string | string[] | React Query cache key |
endpoint | string | Full API URL from api.js |
body | object | POST body (auto JSON.stringify) |
options | object | React Query options (enabled, staleTime, etc.) |
import { useApiMutation } from "@/hooks/api/use-api";
import { API } from "@/constants/api/api";
const { mutate, isPending } = useApiMutation(
API.CUSTOMER.CREATE,
{
invalidateKeys: [["customer", "list"]], // auto-refresh list after success
onSuccess: (data) => { /* handle */ },
onError: (error) => { /* handle */ },
}
);
mutate({ name: "John", email: "john@example.com" });
| Param | Type | Description |
|---|---|---|
endpoint | string | Full API URL from api.js |
options.invalidateKeys | string[][] | Cache keys to invalidate on success |
options.onSuccess | function | Success callback |
options.onError | function | Error callback |
localStorage.getItem("token")JSON.stringify of request body// src/pages/views/master/customer/customer.service.js
import { useApiQuery, useApiMutation } from "@/hooks/api/use-api";
import { API } from "@/constants/api/api";
export function useCustomerList(params) {
return useApiQuery(["customer", "list", params], API.CUSTOMER.LIST, params);
}
export function useCustomerCreate(options = {}) {
return useApiMutation(API.CUSTOMER.CREATE, {
invalidateKeys: [["customer", "list"]],
...options,
});
}
export function useCustomerUpdate(options = {}) {
return useApiMutation(API.CUSTOMER.UPDATE, {
invalidateKeys: [["customer", "list"]],
...options,
});
}
export function useCustomerDelete(options = {}) {
return useApiMutation(API.CUSTOMER.DELETE, {
invalidateKeys: [["customer", "list"]],
...options,
});
}
Redux Toolkit is used for client-side global state. Server/API state is handled by React Query.
// src/features/store.js
import { configureStore } from "@reduxjs/toolkit";
import customerReducer from "./master/customer/customer.slice";
export const store = configureStore({
reducer: {
customer: customerReducer,
},
});
// src/features/master/customer/customer.slice.js
import { createSlice } from "@reduxjs/toolkit";
const customerSlice = createSlice({
name: "customer",
initialState: { list: [], loading: false, error: null },
reducers: {
setCustomers: (state, action) => { state.list = action.payload; },
addCustomer: (state, action) => { state.list.push(action.payload); },
updateCustomer: (state, action) => {
const index = state.list.findIndex(item => item.id === action.payload.id);
if (index !== -1) state.list[index] = action.payload;
},
removeCustomer: (state, action) => {
state.list = state.list.filter(item => item.id !== action.payload);
},
setLoading: (state, action) => { state.loading = action.payload; },
setError: (state, action) => { state.error = action.payload; },
},
});
export const { setCustomers, addCustomer, updateCustomer, removeCustomer, setLoading, setError } = customerSlice.actions;
export default customerSlice.reducer;
| Use Case | Technology |
|---|---|
| API data (fetch, create, update, delete) | React Query (useApiQuery / useApiMutation) |
| Client-only global state (UI state, selections) | Redux Toolkit |
| Auth / Theme / Mobile state | Context API |
// src/context/auth/auth.context.jsx
import { useAuth } from "@/context/auth/auth.context";
const {
user, // { name, email, role, permissions } | null
isAuthenticated, // boolean
can(permission), // check single permission
canAny([permissions]), // check if user has ANY
canAll([permissions]), // check if user has ALL
isSuperAdmin(), // check super admin role
login(userData), // set user + save token to localStorage
logout(), // clear user + token
updatePermissions([]), // update permission array
updateRole(role), // update user role
} = useAuth();
const { login } = useAuth();
login({
name: "John",
email: "john@example.com",
role: "admin",
permissions: ["customer.view", "customer.button.add"],
token: "jwt-token-from-api",
});
// Token is stored in localStorage automatically
The template ships with a default super-admin user for development with all permissions pre-enabled. Replace this with your actual auth API integration.
/auth → Login page (public, redirects if authenticated)
/ → Dashboard (authenticated)
/master/customer → Customer list (permission: customer.view)
/master/customer/add → Add customer (permission: customer.button.add)
/admin/permissions → Permission admin (authenticated)
/dev-guide → Developer guide (authenticated)
/error/unauthorized → 403 page
/error/server-error → 500 page
* → 404 page
function ProtectedRoute({ children, permission }) {
const { isAuthenticated, can } = useAuth();
if (!isAuthenticated) return <Navigate to="/auth" replace />;
if (permission && !can(permission)) return <Navigate to="/error/unauthorized" replace />;
return children;
}
// In custom-routes.jsx
import Product from "@/pages/views/master/product/product";
<Route
path="master/product"
element={
<ProtectedRoute permission="product.view">
<Product />
</ProtectedRoute>
}
/>
Or use npm run hcorp:add product to do this automatically.
┌────────────────────────────────────────────────┐
│ TopNav │
│ [☰] App Name [User] [Out] │
├──────────┬─────────────────────────────────────┤
│ │ │
│ Sidebar │ <Outlet /> │
│ (w-64) │ (page content) │
│ │ │
│ Menu │ │
│ Items │ │
│ │ │
└──────────┴─────────────────────────────────────┘
| File | Purpose |
|---|---|
layout/layout.jsx | Main wrapper — renders Sidebar + TopNav + <Outlet /> for child routes |
layout/top-nav.jsx | Header with hamburger toggle, user display, and logout button |
layout/sidebar.jsx | Navigation sidebar with permission-filtered menu items, grouped sections |
layout/menu.item.jsx | Individual NavLink item with active state styling |
The sidebar width is w-64 (256px). On desktop it pushes content via ml-64. On mobile it overlays with a backdrop.
// src/constants/data/menu.data.js
import { LayoutDashboard, Users, Shield } from "lucide-react";
export const MENU_ITEMS = [
{
title: "Dashboard",
items: [
{ label: "Dashboard", path: "/", icon: LayoutDashboard, permission: null },
],
},
{
title: "Master",
items: [
{ label: "Customer", path: "/master/customer", icon: Users, permission: "customer.view" },
],
},
{
title: "Admin",
items: [
{ label: "Permissions", path: "/admin/permissions", icon: Shield, permission: null },
],
},
];
permission: null to make an item always visible to authenticated userspermission: "module.view" to filter by permission — the sidebar auto-hides items the user can't accessAll theme colors are defined as CSS variables in src/index.css:
@theme {
--color-background: #ffffff;
--color-foreground: #0a0a0a;
--color-primary: #171717;
--color-primary-foreground: #fafafa;
--color-secondary: #f5f5f5;
--color-muted: #f5f5f5;
--color-muted-foreground: #737373;
--color-accent: #f5f5f5;
--color-destructive: #ef4444;
--color-border: #e5e5e5;
--color-sidebar-background: #fafafa;
--color-sidebar-foreground: #404040;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}
<div className="bg-primary text-primary-foreground">Primary</div>
<div className="bg-destructive">Error</div>
<div className="text-muted-foreground">Subtle text</div>
<div className="bg-sidebar-background">Sidebar</div>
import { useTheme } from "@/context/theme/theme.context";
const { theme, toggleTheme } = useTheme();
// theme is "light" or "dark"
// For charts, inline styles, etc.
import { COLORS } from "@/constants/config/colors";
// COLORS.primary, COLORS.danger, COLORS.success, etc.
To change the entire app's color scheme, edit the CSS variables in index.css — all components reference these variables.
import { useMobile } from "@/context/mobile/mobile.context";
const { isMobile, sidebarOpen, setSidebarOpen, toggleSidebar } = useMobile();
| Property | Type | Description |
|---|---|---|
isMobile | boolean | true when viewport < 768px |
sidebarOpen | boolean | Current sidebar state |
setSidebarOpen | function | Set sidebar state directly |
toggleSidebar | function | Toggle sidebar open/close |
Behavior:
Use the useAlert hook instead of window.alert() or window.confirm():
import { useAlert } from "@/hooks/alert/use-alert";
import {
AlertDialog, AlertDialogContent, AlertDialogHeader,
AlertDialogTitle, AlertDialogDescription,
AlertDialogFooter, AlertDialogAction, AlertDialogCancel,
} from "@/components/ui/alert-dialog";
function MyComponent() {
const { alertState, showAlert, handleConfirm, handleCancel } = useAlert();
const handleDelete = () => {
showAlert({
title: "Delete Customer",
description: "Are you sure? This cannot be undone.",
onConfirm: () => { /* delete logic */ },
});
};
return (
<>
<Button onClick={handleDelete}>Delete</Button>
<AlertDialog open={alertState.open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{alertState.title}</AlertDialogTitle>
<AlertDialogDescription>{alertState.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>Confirm</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
| Method | Description |
|---|---|
showAlert({ title, description, onConfirm, onCancel }) | Open the dialog |
handleConfirm() | Execute onConfirm and close |
handleCancel() | Execute onCancel and close |
closeAlert() | Close without running callbacks |
alertState.open | Boolean — is dialog currently open |
Pre-installed components in src/components/ui/:
| Component | File | Radix Primitive |
|---|---|---|
| Button | button.jsx | @radix-ui/react-slot |
| AlertDialog | alert-dialog.jsx | @radix-ui/react-alert-dialog |
| Switch | switch.jsx | @radix-ui/react-switch |
import { Button } from "@/components/ui/button";
<Button>Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>
Since this is JavaScript (not TypeScript), npx shadcn@latest add won't work directly. Instead:
src/components/ui/npm install @radix-ui/react-*All env vars must be prefixed with VITE_ to be exposed to client code.
| File | When Loaded | Git Tracked |
|---|---|---|
.env | Always (base defaults) | Yes |
.env.local | Always, overrides .env | No |
.env.staging | When --mode staging | Yes |
.env.production | When building for prod | Yes |
VITE_API_BASE_URL=http://localhost:3000
VITE_APP_NAME=HCorp App
VITE_PORT=5173
const baseUrl = import.meta.env.VITE_API_BASE_URL;
const appName = import.meta.env.VITE_APP_NAME;
VITE_API_BASE_URL is used inside constants/api/api.js to construct all endpoint URLs — you never need to reference it directly elsewhere.
npx vite build --mode staging
After scaffolding, hcorp.config.json is created in the project root:
{
"tailwind": true,
"shadcn": true,
"redux": true,
"router": true
}
This file tells the module generator which features are available. Do not manually change it after project creation — it reflects what was installed during scaffolding.
| Type | Pattern | Example |
|---|---|---|
| Page | <module>.jsx | customer.jsx |
| Add Page | add.<module>.jsx | add.customer.jsx |
| Service | <module>.service.js | customer.service.js |
| Slice | <module>.slice.js | customer.slice.js |
| Context | <name>.context.jsx | auth.context.jsx |
| Hook | use-<name>.jsx | use-api.jsx |
| Data | <name>.data.js | menu.data.js |
use<Module>List → useCustomerList
use<Module>Create → useCustomerCreate
use<Module>Update → useCustomerUpdate
use<Module>Delete → useCustomerDelete
set<Module>s → setCustomers
add<Module> → addCustomer
update<Module> → updateCustomer
remove<Module> → removeCustomer
<module>.view → customer.view
<module>.button.add → customer.button.add
<module>.button.edit → customer.button.edit
<module>.button.delete → customer.button.delete
src/
├── constants/api/api.js → API.CUSTOMER: crud("customer")
├── constants/config/permissions.js → PERMISSIONS.CUSTOMER
├── features/master/customer/ → customer.slice.js
└── pages/views/master/customer/
├── customer.jsx → List page
├── customer.service.js → React Query hooks
└── add/
└── add.customer.jsx → Add page
Every scaffolded project includes an interactive developer guide at /dev-guide. It covers:
hcorp:add) with generated file examplesThe guide is built as a React page with sidebar navigation — it's a living reference that stays in sync with the template.
| Page | Route | Description |
|---|---|---|
| Not Found | /error/not-found or * | 404 page with countdown redirect to home |
| Server Error | /error/server-error | 500 page with countdown redirect |
| Unauthorized | /error/unauthorized | 403 page with countdown redirect |
All error pages use the CountdownRedirect component that displays a countdown timer and auto-redirects to the home page.
| Technology | Version | Purpose |
|---|---|---|
| Vite | 6.0 | Build tool & dev server |
| React | 18.3 | UI library (JavaScript only, no TypeScript) |
| Tailwind CSS | 4.1 | Utility-first styling via Vite plugin |
| shadcn/ui | — | Accessible UI components (Radix + Tailwind) |
| Redux Toolkit | 2.5 | Client-side global state management |
| React Router | 7.1 | Client-side routing with protected routes |
| TanStack React Query | 5.62 | Server state, caching, API call management |
| Lucide React | 0.468 | Icon library |
| Radix UI | — | Accessible primitives (AlertDialog, Switch, Slot) |
MIT
FAQs
Scaffold a production-ready Vite + React project with permissions, routing, Redux, and shadcn/ui
We found that rvx-cli demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
The trojanized extensions use TinyGo-compiled WebAssembly and Solana transaction memos to resolve command-and-control infrastructure.

Security News
Anthropic says the directive cited national security concerns over a narrow jailbreak, but offered no specific technical details.

Security News
A network of 152 Chrome live wallpaper extensions hid ad tracking and made extension-driven traffic look like Google search clicks.