mirror of
https://github.com/zuiidea/antd-admin.git
synced 2026-10-20 00:15:38 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ef2fe7f8 | ||
|
|
3d17e6d636 | ||
|
|
f28bae8f3e |
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.6
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
version: 10.33.2
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: E2E (core)
|
||||
working-directory: apps/with-lingui
|
||||
run: pnpm run test:e2e:core
|
||||
run: pnpm run test:e2e
|
||||
|
||||
- name: Build
|
||||
working-directory: apps/with-lingui
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
Antd Admin provides production-style admin templates with modern frontend tooling, mock-first development, and practical testing coverage.
|
||||
|
||||
Available templates:
|
||||
|
||||
- `basic`: English-only setup
|
||||
- `with-lingui`: Bilingual setup (`en` + `zh`) with Lingui
|
||||
|
||||
Scaffolding CLI:
|
||||
|
||||
- `init-antd-admin` (from `packages/create`)
|
||||
|
||||
## Key Features
|
||||
@@ -45,17 +47,19 @@ pnpm dlx init-antd-admin@latest my-app --example with-lingui --skip-install
|
||||
|
||||
### Common options
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `[project-directory]` | Target folder |
|
||||
| `-e, --example <name|url>` | Template source |
|
||||
| `--example-path <path>` | Subpath inside remote repository |
|
||||
| `-m, --package-manager <pm>` | `npm` / `pnpm` / `yarn` / `bun` |
|
||||
| `--skip-install` | Skip dependency install |
|
||||
| `--skip-transforms` | Skip rewrite transforms |
|
||||
| `--no-git` | Skip git initialization |
|
||||
|
||||
For detailed CLI behavior, see [`packages/create/README.md`](./packages/create/README.md).
|
||||
| Option | Description |
|
||||
| ---------------------------- | -------------------------------- |
|
||||
| `[project-directory]` | Target folder |
|
||||
| `-e, --example <name | url>` |
|
||||
| `--example-path <path>` | Subpath inside remote repository |
|
||||
| `-m, --package-manager <pm>` | `npm` / `pnpm` / `yarn` / `bun` |
|
||||
| `--skip-install` | Skip dependency install |
|
||||
| `--skip-transforms` | Skip rewrite transforms |
|
||||
| `--no-git` | Skip git initialization |
|
||||
|
||||
|
||||
For detailed CLI behavior, see `[packages/create/README.md](./packages/create/README.md)`.
|
||||
|
||||
Production docs: [https://antd-admin-doc.zuiidea.top](https://antd-admin-doc.zuiidea.top)
|
||||
|
||||
|
||||
+68
-54
@@ -56,12 +56,9 @@ English-only admin scaffold (no Lingui): MSW mocks, minimal RBAC, full CRUD, and
|
||||
|
||||
### Install & Run
|
||||
|
||||
From the monorepo root, or after `cd apps/basic`:
|
||||
|
||||
```bash
|
||||
cd apps/basic
|
||||
vp install
|
||||
vp dev
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173) — you'll be redirected to the login page.
|
||||
@@ -71,75 +68,92 @@ Open [http://localhost:5173](http://localhost:5173) — you'll be redirected to
|
||||
### Build
|
||||
|
||||
```bash
|
||||
vp build
|
||||
vp preview
|
||||
pnpm run build
|
||||
pnpm run preview
|
||||
```
|
||||
|
||||
### Project Setup
|
||||
|
||||
```bash
|
||||
pnpm run prepare
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
pnpm run fmt
|
||||
pnpm run lint
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```bash
|
||||
pnpm run test:unit
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
```bash
|
||||
cd apps/basic
|
||||
pnpm run test:e2e
|
||||
pnpm run test:e2e:core
|
||||
pnpm run test:e2e:ui # interactive UI mode
|
||||
```
|
||||
|
||||
`test:e2e:core` runs the scaffold's highest-value flows only: login and users CRUD.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
```text
|
||||
src/
|
||||
├── api/ # Zod schemas, endpoint constants, type exports
|
||||
│ ├── schemas.ts # Domain models (User, AuthTokens, MenuItem, etc.)
|
||||
│ ├── auth.ts # Auth endpoint constants
|
||||
│ └── user.ts # User CRUD endpoint constants
|
||||
├── components/
|
||||
│ ├── Aurora/ # Login background effect
|
||||
│ ├── Auth/ # Auth (permission gate): index.tsx
|
||||
│ ├── DataTable/ # Shared table shell, skeleton, empty state
|
||||
│ ├── FilterToolbar/ # Shared filter/action toolbar
|
||||
│ ├── FormModal/ # Reusable modal + form shell
|
||||
│ ├── Icon/ # Shared icons and theme toggle icon
|
||||
│ └── Layout/ # Admin shell (sidebar, header, main)
|
||||
│ ├── MainLayout/ # Main layout shell: index.tsx
|
||||
│ ├── AppFooter/ # Login footer: Powered by + GitHub → zuiidea/antd-admin
|
||||
│ ├── Sidebar/ # Dynamic menu sidebar: index.tsx
|
||||
│ ├── UserMenu/ # User dropdown in sidebar: index.tsx + index.css
|
||||
│ └── Header/ # Top bar: index.tsx
|
||||
├── hooks/
|
||||
│ ├── tokenBuilders.ts # Shared Ant Design token/config builders
|
||||
│ ├── useAppTheme.ts # Theme selection hook (ConfigProvider)
|
||||
│ ├── usePermission.ts # Permission check hook
|
||||
│ └── useResourceCRUD.ts # Shared CRUD query/mutation wiring
|
||||
├── utils/
|
||||
│ ├── constants.ts # API base URL, static assets
|
||||
│ └── http.ts # HTTP client with JWT injection & error handling
|
||||
├── mocks/
|
||||
│ ├── browser.ts # MSW worker setup
|
||||
│ ├── createHandler.ts # Shared MSW success/error/delay helpers
|
||||
│ ├── data.ts # Mock seed data (users, menus)
|
||||
│ ├── utils.ts # Mock-only helpers (filters, pagination, demo avatar URLs)
|
||||
│ └── handlers/ # Request handlers (auth, user CRUD)
|
||||
├── routes/ # TanStack Router file-based routes
|
||||
│ ├── __root.tsx # Root layout (QueryClient, ConfigProvider, en_US)
|
||||
│ ├── _auth.tsx # Auth guard layout (redirects to /login)
|
||||
├── api/ # Zod models + endpoint contracts
|
||||
├── components/ # Reusable UI primitives and layout shells
|
||||
│ ├── Layout/ # MainLayout, Header, Sidebar, UserMenu, AppFooter
|
||||
│ ├── DataTable/ # Shared table frame/skeleton/empty state
|
||||
│ ├── FormModal/ # Generic modal form wrapper
|
||||
│ ├── FilterToolbar/ # Reusable list page toolbar
|
||||
│ ├── Auth/ # Permission gate component
|
||||
│ ├── Aurora/ # Login page background effect
|
||||
│ ├── Icon/ # Shared icons (GitHub, Theme, etc.)
|
||||
│ ├── NotFound/ # Not-found visual component
|
||||
│ └── RouteError.tsx # Route-level error boundary UI
|
||||
├── hooks/ # Shared hooks (theme, CRUD, URL state, permissions)
|
||||
│ ├── useAppTheme.ts
|
||||
│ ├── usePermission.ts
|
||||
│ ├── useResourceCRUD.ts
|
||||
│ ├── useCrudToasts.ts
|
||||
│ ├── useUrlSearchState.ts
|
||||
│ ├── useTableFitHeight.ts
|
||||
│ └── tokenBuilders.ts
|
||||
├── mocks/ # MSW bootstrap, handlers, seed data, test helpers
|
||||
│ ├── browser.ts
|
||||
│ ├── createHandler.ts
|
||||
│ ├── createHandler.test.ts
|
||||
│ ├── data.ts
|
||||
│ ├── utils.ts
|
||||
│ └── handlers/
|
||||
├── routes/ # TanStack Router file routes
|
||||
│ ├── __root.tsx # QueryClient + ConfigProvider (en_US) wiring
|
||||
│ ├── _auth.tsx # Protected route layout
|
||||
│ ├── _auth/dashboard/index.tsx
|
||||
│ ├── _auth/users/index.tsx # Full CRUD with URL-synced search params
|
||||
│ ├── _auth/users/index.tsx
|
||||
│ ├── _auth/users/-Toolbar.tsx
|
||||
│ ├── _auth/users/-FormModal.tsx
|
||||
│ ├── _auth/403/index.tsx
|
||||
│ ├── login/index.tsx
|
||||
│ ├── register/index.tsx
|
||||
│ ├── 404/index.tsx
|
||||
│ └── index.tsx # Redirects / → /login
|
||||
├── stores/
|
||||
│ ├── auth.ts # Auth store (tokens, user, menus, permissions)
|
||||
│ ├── createPersistentStore.ts # Shared persisted-store factory
|
||||
│ └── settings.ts # Settings store (darkMode, sidebar)
|
||||
└── main.tsx # Entry point (MSW init → React render)
|
||||
│ └── index.tsx
|
||||
├── stores/ # Persisted auth/settings stores
|
||||
├── utils/ # HTTP client, constants, session/menu helpers
|
||||
├── main.tsx # App bootstrap (MSW init + React render)
|
||||
└── routeTree.gen.ts # Generated TanStack route tree
|
||||
|
||||
e2e/ # Playwright E2E tests
|
||||
e2e/ # Playwright E2E suites
|
||||
├── helpers.ts
|
||||
├── login.spec.ts
|
||||
└── users.spec.ts
|
||||
├── users.spec.ts
|
||||
├── auth-refresh.spec.ts
|
||||
├── rbac.spec.ts
|
||||
└── url-state.spec.ts
|
||||
```
|
||||
|
||||
## Language & i18n
|
||||
|
||||
+19
-31
@@ -11,44 +11,32 @@
|
||||
"fmt": "vp fmt",
|
||||
"lint": "vp lint",
|
||||
"check": "vp check --no-fmt",
|
||||
"test:unit": "vitest run",
|
||||
"test:unit": "vp test run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:core": "playwright test e2e/login.spec.ts e2e/users.spec.ts e2e/auth-refresh.spec.ts e2e/rbac.spec.ts e2e/url-state.spec.ts",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router": "^1.167.4",
|
||||
"antd": "^6.3.3",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
"@tanstack/react-query": "^5.104.3",
|
||||
"@tanstack/react-router": "^1.170.45",
|
||||
"antd": "^6.6.6",
|
||||
"lucide-react": "^1.59.0",
|
||||
"react": "^19.3.1",
|
||||
"react-dom": "^19.3.1",
|
||||
"zod": "^4.6.7",
|
||||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tanstack/router-plugin": "^1.166.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||
"msw": "^2.12.12",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
||||
"vite-plus": "latest",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
||||
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"msw"
|
||||
]
|
||||
"@playwright/test": "^1.64.1",
|
||||
"@tanstack/router-plugin": "^1.168.47",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react-swc": "^4.3.3",
|
||||
"msw": "^2.15.0",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.3",
|
||||
"vite-plus": "0.3.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.2",
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
"public"
|
||||
|
||||
Generated
+1878
-2507
File diff suppressed because it is too large
Load Diff
@@ -7,111 +7,114 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = "2.12.12";
|
||||
const INTEGRITY_CHECKSUM = "4db4a41e972cec1b64cc569c66952d82";
|
||||
const IS_MOCKED_RESPONSE = Symbol("isMockedResponse");
|
||||
const activeClientIds = new Set();
|
||||
const PACKAGE_VERSION = '2.15.0'
|
||||
const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
addEventListener("install", function () {
|
||||
self.skipWaiting();
|
||||
});
|
||||
addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
addEventListener("activate", function (event) {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
addEventListener("message", async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, "id");
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId);
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: "window",
|
||||
});
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case "KEEPALIVE_REQUEST": {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: "KEEPALIVE_RESPONSE",
|
||||
});
|
||||
break;
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "INTEGRITY_CHECK_REQUEST": {
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: "INTEGRITY_CHECK_RESPONSE",
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "MOCK_ACTIVATE": {
|
||||
activeClientIds.add(clientId);
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: "MOCKING_ENABLED",
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: {
|
||||
client: {
|
||||
id: client.id,
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "CLIENT_CLOSED": {
|
||||
activeClientIds.delete(clientId);
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId;
|
||||
});
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister();
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
addEventListener("fetch", function (event) {
|
||||
const requestInterceptedAt = Date.now();
|
||||
addEventListener('fetch', function (event) {
|
||||
const requestInterceptedAt = Date.now()
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (event.request.mode === "navigate") {
|
||||
return;
|
||||
if (event.request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
|
||||
return;
|
||||
if (
|
||||
event.request.cache === 'only-if-cached' &&
|
||||
event.request.mode !== 'same-origin'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been terminated (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID();
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt));
|
||||
});
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
@@ -119,23 +122,38 @@ addEventListener("fetch", function (event) {
|
||||
* @param {number} requestInterceptedAt
|
||||
*/
|
||||
async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
const client = await resolveMainClient(event);
|
||||
const requestCloneForEvents = event.request.clone();
|
||||
const response = await getResponse(event, client, requestId, requestInterceptedAt);
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(
|
||||
event,
|
||||
client,
|
||||
requestId,
|
||||
requestInterceptedAt,
|
||||
)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents);
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
|
||||
// Omit the body of server-sent event stream responses.
|
||||
// Cloning such responses would prevent client-side stream cancelations
|
||||
// from reaching the original stream (a teed stream only cancels its
|
||||
// source once both of its branches cancel) and would buffer the
|
||||
// entire stream into the unconsumed clone indefinitely.
|
||||
const isEventStreamResponse = response.headers
|
||||
.get('content-type')
|
||||
?.toLowerCase()
|
||||
.startsWith('text/event-stream')
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone();
|
||||
const responseClone = isEventStreamResponse ? null : response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: "RESPONSE",
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
request: {
|
||||
@@ -143,19 +161,21 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
type: response.type,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
body: responseClone ? responseClone.body : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
);
|
||||
responseClone && responseClone.body
|
||||
? [serializedRequest.body, responseClone.body]
|
||||
: [],
|
||||
)
|
||||
}
|
||||
|
||||
return response;
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,30 +187,30 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId);
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client;
|
||||
return client
|
||||
}
|
||||
|
||||
if (client?.frameType === "top-level") {
|
||||
return client;
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: "window",
|
||||
});
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === "visible";
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id);
|
||||
});
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,34 +223,36 @@ async function resolveMainClient(event) {
|
||||
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = event.request.clone();
|
||||
const requestClone = event.request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers);
|
||||
const headers = new Headers(requestClone.headers)
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get("accept");
|
||||
const acceptHeader = headers.get('accept')
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(",").map((value) => value.trim());
|
||||
const filteredValues = values.filter((value) => value !== "msw/passthrough");
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter(
|
||||
(value) => value !== 'msw/passthrough',
|
||||
)
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set("accept", filteredValues.join(", "));
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
} else {
|
||||
headers.delete("accept");
|
||||
headers.delete('accept')
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers });
|
||||
return fetch(requestClone, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough();
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
@@ -238,15 +260,15 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough();
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const serializedRequest = await serializeRequest(event.request);
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: "REQUEST",
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
interceptedAt: requestInterceptedAt,
|
||||
@@ -254,19 +276,19 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
},
|
||||
},
|
||||
[serializedRequest.body],
|
||||
);
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case "MOCK_RESPONSE": {
|
||||
return respondWithMock(clientMessage.data);
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case "PASSTHROUGH": {
|
||||
return passthrough();
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough();
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,18 +299,21 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel();
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error);
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data);
|
||||
};
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
|
||||
});
|
||||
client.postMessage(message, [
|
||||
channel.port2,
|
||||
...transferrables.filter(Boolean),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,17 +326,17 @@ function respondWithMock(response) {
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error();
|
||||
return Response.error()
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response);
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
});
|
||||
})
|
||||
|
||||
return mockedResponse;
|
||||
return mockedResponse
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,5 +357,5 @@ async function serializeRequest(request) {
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ function mapSkeletonColumn<RecordType extends object>(
|
||||
...c,
|
||||
sorter: false,
|
||||
sortOrder: undefined,
|
||||
render: (_: unknown, __: RecordType) => {
|
||||
render: () => {
|
||||
if (isActionColumn(c)) {
|
||||
const w = actionSkeletonDiameter(c);
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { applyOptimisticListDelete, applyOptimisticListUpdate } from "./useResourceCRUD";
|
||||
|
||||
type Row = { id: string; title: string; n: number };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
paginatedListSchema,
|
||||
|
||||
+154
-147
@@ -8,184 +8,191 @@
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as AuthRouteImport } from "./routes/_auth";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as RegisterIndexRouteImport } from "./routes/register/index";
|
||||
import { Route as LoginIndexRouteImport } from "./routes/login/index";
|
||||
import { Route as R404IndexRouteImport } from "./routes/404/index";
|
||||
import { Route as AuthUsersIndexRouteImport } from "./routes/_auth/users/index";
|
||||
import { Route as AuthDashboardIndexRouteImport } from "./routes/_auth/dashboard/index";
|
||||
import { Route as Auth403IndexRouteImport } from "./routes/_auth/403/index";
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AuthRouteImport } from './routes/_auth'
|
||||
import { Route as R404IndexRouteImport } from './routes/404/index'
|
||||
import { Route as LoginIndexRouteImport } from './routes/login/index'
|
||||
import { Route as RegisterIndexRouteImport } from './routes/register/index'
|
||||
import { Route as Auth403IndexRouteImport } from './routes/_auth/403/index'
|
||||
import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
|
||||
import { Route as AuthUsersIndexRouteImport } from './routes/_auth/users/index'
|
||||
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: "/_auth",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const RegisterIndexRoute = RegisterIndexRouteImport.update({
|
||||
id: "/register/",
|
||||
path: "/register/",
|
||||
} as any)
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: '/_auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const LoginIndexRoute = LoginIndexRouteImport.update({
|
||||
id: "/login/",
|
||||
path: "/login/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const R404IndexRoute = R404IndexRouteImport.update({
|
||||
id: "/404/",
|
||||
path: "/404/",
|
||||
id: '/404/',
|
||||
path: '/404/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AuthUsersIndexRoute = AuthUsersIndexRouteImport.update({
|
||||
id: "/users/",
|
||||
path: "/users/",
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any);
|
||||
const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
|
||||
id: "/dashboard/",
|
||||
path: "/dashboard/",
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const LoginIndexRoute = LoginIndexRouteImport.update({
|
||||
id: '/login/',
|
||||
path: '/login/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const RegisterIndexRoute = RegisterIndexRouteImport.update({
|
||||
id: '/register/',
|
||||
path: '/register/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const Auth403IndexRoute = Auth403IndexRouteImport.update({
|
||||
id: "/403/",
|
||||
path: "/403/",
|
||||
id: '/403/',
|
||||
path: '/403/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
|
||||
id: '/dashboard/',
|
||||
path: '/dashboard/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthUsersIndexRoute = AuthUsersIndexRouteImport.update({
|
||||
id: '/users/',
|
||||
path: '/users/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
"/404/": typeof R404IndexRoute;
|
||||
"/login/": typeof LoginIndexRoute;
|
||||
"/register/": typeof RegisterIndexRoute;
|
||||
"/403/": typeof Auth403IndexRoute;
|
||||
"/dashboard/": typeof AuthDashboardIndexRoute;
|
||||
"/users/": typeof AuthUsersIndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
'/404/': typeof R404IndexRoute
|
||||
'/login/': typeof LoginIndexRoute
|
||||
'/register/': typeof RegisterIndexRoute
|
||||
'/403/': typeof Auth403IndexRoute
|
||||
'/dashboard/': typeof AuthDashboardIndexRoute
|
||||
'/users/': typeof AuthUsersIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/404": typeof R404IndexRoute;
|
||||
"/login": typeof LoginIndexRoute;
|
||||
"/register": typeof RegisterIndexRoute;
|
||||
"/403": typeof Auth403IndexRoute;
|
||||
"/dashboard": typeof AuthDashboardIndexRoute;
|
||||
"/users": typeof AuthUsersIndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
'/404': typeof R404IndexRoute
|
||||
'/login': typeof LoginIndexRoute
|
||||
'/register': typeof RegisterIndexRoute
|
||||
'/403': typeof Auth403IndexRoute
|
||||
'/dashboard': typeof AuthDashboardIndexRoute
|
||||
'/users': typeof AuthUsersIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/_auth": typeof AuthRouteWithChildren;
|
||||
"/404/": typeof R404IndexRoute;
|
||||
"/login/": typeof LoginIndexRoute;
|
||||
"/register/": typeof RegisterIndexRoute;
|
||||
"/_auth/403/": typeof Auth403IndexRoute;
|
||||
"/_auth/dashboard/": typeof AuthDashboardIndexRoute;
|
||||
"/_auth/users/": typeof AuthUsersIndexRoute;
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/_auth': typeof AuthRouteWithChildren
|
||||
'/404/': typeof R404IndexRoute
|
||||
'/login/': typeof LoginIndexRoute
|
||||
'/register/': typeof RegisterIndexRoute
|
||||
'/_auth/403/': typeof Auth403IndexRoute
|
||||
'/_auth/dashboard/': typeof AuthDashboardIndexRoute
|
||||
'/_auth/users/': typeof AuthUsersIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: "/" | "/404/" | "/login/" | "/register/" | "/403/" | "/dashboard/" | "/users/";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to: "/" | "/404" | "/login" | "/register" | "/403" | "/dashboard" | "/users";
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/404/'
|
||||
| '/login/'
|
||||
| '/register/'
|
||||
| '/403/'
|
||||
| '/dashboard/'
|
||||
| '/users/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/404' | '/login' | '/register' | '/403' | '/dashboard' | '/users'
|
||||
id:
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/_auth"
|
||||
| "/404/"
|
||||
| "/login/"
|
||||
| "/register/"
|
||||
| "/_auth/403/"
|
||||
| "/_auth/dashboard/"
|
||||
| "/_auth/users/";
|
||||
fileRoutesById: FileRoutesById;
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/_auth'
|
||||
| '/404/'
|
||||
| '/login/'
|
||||
| '/register/'
|
||||
| '/_auth/403/'
|
||||
| '/_auth/dashboard/'
|
||||
| '/_auth/users/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
AuthRoute: typeof AuthRouteWithChildren;
|
||||
R404IndexRoute: typeof R404IndexRoute;
|
||||
LoginIndexRoute: typeof LoginIndexRoute;
|
||||
RegisterIndexRoute: typeof RegisterIndexRoute;
|
||||
IndexRoute: typeof IndexRoute
|
||||
AuthRoute: typeof AuthRouteWithChildren
|
||||
R404IndexRoute: typeof R404IndexRoute
|
||||
LoginIndexRoute: typeof LoginIndexRoute
|
||||
RegisterIndexRoute: typeof RegisterIndexRoute
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
"/_auth": {
|
||||
id: "/_auth";
|
||||
path: "";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof AuthRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/register/": {
|
||||
id: "/register/";
|
||||
path: "/register";
|
||||
fullPath: "/register/";
|
||||
preLoaderRoute: typeof RegisterIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/login/": {
|
||||
id: "/login/";
|
||||
path: "/login";
|
||||
fullPath: "/login/";
|
||||
preLoaderRoute: typeof LoginIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/404/": {
|
||||
id: "/404/";
|
||||
path: "/404";
|
||||
fullPath: "/404/";
|
||||
preLoaderRoute: typeof R404IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/_auth/users/": {
|
||||
id: "/_auth/users/";
|
||||
path: "/users";
|
||||
fullPath: "/users/";
|
||||
preLoaderRoute: typeof AuthUsersIndexRouteImport;
|
||||
parentRoute: typeof AuthRoute;
|
||||
};
|
||||
"/_auth/dashboard/": {
|
||||
id: "/_auth/dashboard/";
|
||||
path: "/dashboard";
|
||||
fullPath: "/dashboard/";
|
||||
preLoaderRoute: typeof AuthDashboardIndexRouteImport;
|
||||
parentRoute: typeof AuthRoute;
|
||||
};
|
||||
"/_auth/403/": {
|
||||
id: "/_auth/403/";
|
||||
path: "/403";
|
||||
fullPath: "/403/";
|
||||
preLoaderRoute: typeof Auth403IndexRouteImport;
|
||||
parentRoute: typeof AuthRoute;
|
||||
};
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth': {
|
||||
id: '/_auth'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/404/': {
|
||||
id: '/404/'
|
||||
path: '/404'
|
||||
fullPath: '/404/'
|
||||
preLoaderRoute: typeof R404IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/login/': {
|
||||
id: '/login/'
|
||||
path: '/login'
|
||||
fullPath: '/login/'
|
||||
preLoaderRoute: typeof LoginIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/register/': {
|
||||
id: '/register/'
|
||||
path: '/register'
|
||||
fullPath: '/register/'
|
||||
preLoaderRoute: typeof RegisterIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth/403/': {
|
||||
id: '/_auth/403/'
|
||||
path: '/403'
|
||||
fullPath: '/403/'
|
||||
preLoaderRoute: typeof Auth403IndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/dashboard/': {
|
||||
id: '/_auth/dashboard/'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard/'
|
||||
preLoaderRoute: typeof AuthDashboardIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/users/': {
|
||||
id: '/_auth/users/'
|
||||
path: '/users'
|
||||
fullPath: '/users/'
|
||||
preLoaderRoute: typeof AuthUsersIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthRouteChildren {
|
||||
Auth403IndexRoute: typeof Auth403IndexRoute;
|
||||
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute;
|
||||
AuthUsersIndexRoute: typeof AuthUsersIndexRoute;
|
||||
Auth403IndexRoute: typeof Auth403IndexRoute
|
||||
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
|
||||
AuthUsersIndexRoute: typeof AuthUsersIndexRoute
|
||||
}
|
||||
|
||||
const AuthRouteChildren: AuthRouteChildren = {
|
||||
Auth403IndexRoute: Auth403IndexRoute,
|
||||
AuthDashboardIndexRoute: AuthDashboardIndexRoute,
|
||||
AuthUsersIndexRoute: AuthUsersIndexRoute,
|
||||
};
|
||||
}
|
||||
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren);
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
@@ -193,7 +200,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
R404IndexRoute: R404IndexRoute,
|
||||
LoginIndexRoute: LoginIndexRoute,
|
||||
RegisterIndexRoute: RegisterIndexRoute,
|
||||
};
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
@@ -60,7 +60,7 @@ function DashboardSkeleton() {
|
||||
active
|
||||
style={{
|
||||
width: w,
|
||||
height: Math.round(token.fontSizeHeading5 * token.lineHeightHeading5),
|
||||
height: Math.round(Number(token.fontSizeHeading5) * Number(token.lineHeightHeading5)),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { defineConfig } from "vite-plus";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
|
||||
+86
-54
@@ -49,8 +49,8 @@ Balanced admin scaffold: i18n (Lingui), MSW mocks, minimal RBAC, full CRUD, and
|
||||
### Install & Run
|
||||
|
||||
```bash
|
||||
vp install
|
||||
vp dev
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173) — you'll be redirected to the login page.
|
||||
@@ -60,78 +60,110 @@ Open [http://localhost:5173](http://localhost:5173) — you'll be redirected to
|
||||
### Build
|
||||
|
||||
```bash
|
||||
vp build
|
||||
vp preview
|
||||
pnpm run build
|
||||
pnpm run preview
|
||||
```
|
||||
|
||||
### Project Setup
|
||||
|
||||
```bash
|
||||
pnpm run prepare
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
pnpm run fmt
|
||||
pnpm run lint
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```bash
|
||||
pnpm run test:unit
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
```bash
|
||||
vp run test:e2e
|
||||
vp run test:e2e:core
|
||||
vp run test:e2e:ui # interactive UI mode
|
||||
pnpm run test:e2e
|
||||
pnpm run test:e2e:core
|
||||
pnpm run test:e2e:ui # interactive UI mode
|
||||
```
|
||||
|
||||
`test:e2e:core` runs the scaffold's highest-value flows: login, users CRUD, auth refresh, RBAC, URL state (see `package.json` script list).
|
||||
### Lingui Scripts
|
||||
|
||||
```bash
|
||||
# extract messages from source to .po catalogs
|
||||
vp exec lingui extract -- --clean
|
||||
|
||||
# compile catalogs to runtime messages
|
||||
vp exec lingui compile
|
||||
|
||||
# CI-oriented check: extract + ensure no pending locale diff
|
||||
pnpm run i18n:extract && git diff --exit-code -- src/locales
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.github/instructions/ # Scoped AI recipes (mirrored in ../../.cursor/rules/with-lingui-*.mdc)
|
||||
.github/instructions/ # Scoped AI guidance (frontend/testing/api/refactor/add-resource)
|
||||
src/
|
||||
├── api/ # Zod schemas, endpoint constants, type exports
|
||||
│ ├── schemas.ts # Domain models (User, AuthTokens, MenuItem, etc.)
|
||||
│ ├── auth.ts # Auth endpoint constants
|
||||
│ └── user.ts # User CRUD endpoint constants
|
||||
├── components/
|
||||
│ ├── Aurora/ # Login background effect
|
||||
│ ├── Auth/ # Auth (permission gate): index.tsx
|
||||
│ ├── DataTable/ # Shared table shell, skeleton, empty state
|
||||
│ ├── FilterToolbar/ # Shared filter/action toolbar
|
||||
│ ├── FormModal/ # Reusable modal + form shell
|
||||
│ ├── Icon/ # Shared icons and theme toggle icon
|
||||
│ └── Layout/ # Admin shell (sidebar, header, main)
|
||||
│ ├── MainLayout/ # Main layout shell: index.tsx
|
||||
│ ├── AppFooter/ # Login footer: Powered by + GitHub → zuiidea/antd-admin
|
||||
│ ├── Sidebar/ # Dynamic menu sidebar: index.tsx
|
||||
│ ├── UserMenu/ # User dropdown in sidebar: index.tsx + index.css
|
||||
│ └── Header/ # Top bar: index.tsx
|
||||
├── hooks/
|
||||
│ ├── tokenBuilders.ts # Shared Ant Design token/config builders
|
||||
│ ├── useAppTheme.ts # Theme selection hook (ConfigProvider)
|
||||
│ ├── usePermission.ts # Permission check hook
|
||||
│ └── useResourceCRUD.ts # Shared CRUD query/mutation wiring
|
||||
├── utils/
|
||||
│ ├── constants.ts # API base URL, static assets
|
||||
│ └── http.ts # HTTP client with JWT injection & error handling
|
||||
├── locales/ # LinguiJS catalogs (.po + compiled .js)
|
||||
├── api/ # Zod models + endpoint contracts
|
||||
├── components/ # Reusable UI primitives and layout shells
|
||||
│ ├── Layout/ # MainLayout, Header, Sidebar, UserMenu, AppFooter
|
||||
│ ├── DataTable/ # Shared table frame/skeleton/empty state
|
||||
│ ├── FormModal/ # Generic modal form wrapper
|
||||
│ ├── FilterToolbar/ # Reusable list page toolbar
|
||||
│ ├── Auth/ # Permission gate component
|
||||
│ ├── Aurora/ # Login page background effect
|
||||
│ ├── Icon/ # Shared icons (GitHub, Theme, etc.)
|
||||
│ ├── NotFound/ # Not-found visual component
|
||||
│ └── RouteError.tsx # Route-level error boundary UI
|
||||
├── hooks/ # Shared hooks (theme, CRUD, URL state, permissions)
|
||||
│ ├── useAppTheme.ts
|
||||
│ ├── usePermission.ts
|
||||
│ ├── useResourceCRUD.ts
|
||||
│ ├── useCrudToasts.ts
|
||||
│ ├── useUrlSearchState.ts
|
||||
│ ├── useTableFitHeight.ts
|
||||
│ └── tokenBuilders.ts
|
||||
├── locales/ # Lingui catalogs + locale loader
|
||||
│ ├── en/messages.po
|
||||
│ └── zh/messages.po
|
||||
├── mocks/
|
||||
│ ├── browser.ts # MSW worker setup
|
||||
│ ├── createHandler.ts # Shared MSW success/error/delay helpers
|
||||
│ ├── data.ts # Mock seed data (users, menus)
|
||||
│ ├── utils.ts # Mock-only helpers (filters, pagination, demo avatar URLs)
|
||||
│ └── handlers/ # Request handlers (auth, users, …)
|
||||
├── routes/ # TanStack Router file-based routes
|
||||
│ ├── __root.tsx # Root layout (QueryClient, ConfigProvider, I18n)
|
||||
│ ├── _auth.tsx # Auth guard layout (redirects to /login)
|
||||
│ ├── zh/messages.po
|
||||
│ └── loadLocaleCatalog.ts
|
||||
├── mocks/ # MSW bootstrap, handlers, seed data, test helpers
|
||||
│ ├── browser.ts
|
||||
│ ├── createHandler.ts
|
||||
│ ├── createHandler.test.ts
|
||||
│ ├── data.ts
|
||||
│ ├── utils.ts
|
||||
│ └── handlers/
|
||||
├── routes/ # TanStack Router file routes
|
||||
│ ├── __root.tsx # QueryClient + ConfigProvider + I18n wiring
|
||||
│ ├── _auth.tsx # Protected route layout
|
||||
│ ├── _auth/dashboard/index.tsx
|
||||
│ ├── _auth/users/index.tsx # Full CRUD with URL-synced search params
|
||||
│ ├── _auth/users/index.tsx
|
||||
│ ├── _auth/users/-Toolbar.tsx
|
||||
│ ├── _auth/users/-FormModal.tsx
|
||||
│ ├── _auth/403/index.tsx
|
||||
│ ├── login/index.tsx
|
||||
│ ├── register/index.tsx
|
||||
│ ├── 404/index.tsx
|
||||
│ └── index.tsx # Redirects / → /login
|
||||
├── stores/
|
||||
│ ├── auth.ts # Auth store (tokens, user, menus, permissions)
|
||||
│ ├── createPersistentStore.ts # Shared persisted-store factory
|
||||
│ └── settings.ts # Settings store (darkMode, locale, sidebar)
|
||||
└── main.tsx # Entry point (MSW init → React render)
|
||||
│ └── index.tsx
|
||||
├── stores/ # Persisted auth/settings stores
|
||||
├── utils/ # HTTP client, constants, session/menu helpers
|
||||
├── main.tsx # App bootstrap (MSW init + React render)
|
||||
└── routeTree.gen.ts # Generated TanStack route tree
|
||||
|
||||
e2e/ # Playwright E2E tests
|
||||
e2e/ # Playwright E2E suites
|
||||
├── helpers.ts
|
||||
├── login.spec.ts
|
||||
└── users.spec.ts
|
||||
├── users.spec.ts
|
||||
├── auth-refresh.spec.ts
|
||||
├── rbac.spec.ts
|
||||
└── url-state.spec.ts
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -11,53 +11,41 @@
|
||||
"fmt": "vp fmt",
|
||||
"lint": "vp lint",
|
||||
"check": "vp check --no-fmt",
|
||||
"test:unit": "vitest run",
|
||||
"test:unit": "vp test run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:core": "playwright test e2e/login.spec.ts e2e/users.spec.ts e2e/auth-refresh.spec.ts e2e/rbac.spec.ts e2e/url-state.spec.ts",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"i18n:extract": "vp exec lingui extract -- --clean",
|
||||
"i18n:compile": "vp exec lingui compile",
|
||||
"i18n:check": "pnpm run i18n:extract && git diff --exit-code -- src/locales"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/core": "^6.0.0",
|
||||
"@lingui/react": "^6.0.0",
|
||||
"@tanstack/react-query": "^5.100.5",
|
||||
"@tanstack/react-router": "^1.168.25",
|
||||
"antd": "^6.3.7",
|
||||
"lucide-react": "^1.11.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
"@lingui/core": "^6.10.0",
|
||||
"@lingui/react": "^6.10.0",
|
||||
"@tanstack/react-query": "^5.104.3",
|
||||
"@tanstack/react-router": "^1.170.45",
|
||||
"antd": "^6.6.6",
|
||||
"lucide-react": "^1.59.0",
|
||||
"react": "^19.3.1",
|
||||
"react-dom": "^19.3.1",
|
||||
"zod": "^4.6.7",
|
||||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^6.0.0",
|
||||
"@lingui/format-po": "^6.0.0",
|
||||
"@lingui/swc-plugin": "^6.0.0",
|
||||
"@lingui/vite-plugin": "^6.0.0",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tanstack/router-plugin": "^1.167.28",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||
"msw": "^2.13.6",
|
||||
"@lingui/cli": "^6.10.0",
|
||||
"@lingui/format-po": "^6.10.0",
|
||||
"@lingui/swc-plugin": "^6.7.1",
|
||||
"@lingui/vite-plugin": "^6.10.0",
|
||||
"@playwright/test": "^1.64.1",
|
||||
"@tanstack/router-plugin": "^1.168.47",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react-swc": "^4.3.3",
|
||||
"msw": "^2.15.0",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
||||
"vite-plus": "latest",
|
||||
"vitest": "4.1.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
||||
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"msw"
|
||||
]
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.3",
|
||||
"vite-plus": "0.3.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.2",
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
"public"
|
||||
|
||||
Generated
+2362
-1951
File diff suppressed because it is too large
Load Diff
@@ -7,111 +7,114 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = "2.12.12";
|
||||
const INTEGRITY_CHECKSUM = "4db4a41e972cec1b64cc569c66952d82";
|
||||
const IS_MOCKED_RESPONSE = Symbol("isMockedResponse");
|
||||
const activeClientIds = new Set();
|
||||
const PACKAGE_VERSION = '2.15.0'
|
||||
const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
addEventListener("install", function () {
|
||||
self.skipWaiting();
|
||||
});
|
||||
addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
addEventListener("activate", function (event) {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
addEventListener("message", async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, "id");
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId);
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: "window",
|
||||
});
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case "KEEPALIVE_REQUEST": {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: "KEEPALIVE_RESPONSE",
|
||||
});
|
||||
break;
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "INTEGRITY_CHECK_REQUEST": {
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: "INTEGRITY_CHECK_RESPONSE",
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "MOCK_ACTIVATE": {
|
||||
activeClientIds.add(clientId);
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: "MOCKING_ENABLED",
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: {
|
||||
client: {
|
||||
id: client.id,
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "CLIENT_CLOSED": {
|
||||
activeClientIds.delete(clientId);
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId;
|
||||
});
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister();
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
addEventListener("fetch", function (event) {
|
||||
const requestInterceptedAt = Date.now();
|
||||
addEventListener('fetch', function (event) {
|
||||
const requestInterceptedAt = Date.now()
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (event.request.mode === "navigate") {
|
||||
return;
|
||||
if (event.request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
|
||||
return;
|
||||
if (
|
||||
event.request.cache === 'only-if-cached' &&
|
||||
event.request.mode !== 'same-origin'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been terminated (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID();
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt));
|
||||
});
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
@@ -119,23 +122,38 @@ addEventListener("fetch", function (event) {
|
||||
* @param {number} requestInterceptedAt
|
||||
*/
|
||||
async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
const client = await resolveMainClient(event);
|
||||
const requestCloneForEvents = event.request.clone();
|
||||
const response = await getResponse(event, client, requestId, requestInterceptedAt);
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(
|
||||
event,
|
||||
client,
|
||||
requestId,
|
||||
requestInterceptedAt,
|
||||
)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents);
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
|
||||
// Omit the body of server-sent event stream responses.
|
||||
// Cloning such responses would prevent client-side stream cancelations
|
||||
// from reaching the original stream (a teed stream only cancels its
|
||||
// source once both of its branches cancel) and would buffer the
|
||||
// entire stream into the unconsumed clone indefinitely.
|
||||
const isEventStreamResponse = response.headers
|
||||
.get('content-type')
|
||||
?.toLowerCase()
|
||||
.startsWith('text/event-stream')
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone();
|
||||
const responseClone = isEventStreamResponse ? null : response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: "RESPONSE",
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
request: {
|
||||
@@ -143,19 +161,21 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
type: response.type,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
body: responseClone ? responseClone.body : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
);
|
||||
responseClone && responseClone.body
|
||||
? [serializedRequest.body, responseClone.body]
|
||||
: [],
|
||||
)
|
||||
}
|
||||
|
||||
return response;
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,30 +187,30 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId);
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client;
|
||||
return client
|
||||
}
|
||||
|
||||
if (client?.frameType === "top-level") {
|
||||
return client;
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: "window",
|
||||
});
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === "visible";
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id);
|
||||
});
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,34 +223,36 @@ async function resolveMainClient(event) {
|
||||
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = event.request.clone();
|
||||
const requestClone = event.request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers);
|
||||
const headers = new Headers(requestClone.headers)
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get("accept");
|
||||
const acceptHeader = headers.get('accept')
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(",").map((value) => value.trim());
|
||||
const filteredValues = values.filter((value) => value !== "msw/passthrough");
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter(
|
||||
(value) => value !== 'msw/passthrough',
|
||||
)
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set("accept", filteredValues.join(", "));
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
} else {
|
||||
headers.delete("accept");
|
||||
headers.delete('accept')
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers });
|
||||
return fetch(requestClone, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough();
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
@@ -238,15 +260,15 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough();
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const serializedRequest = await serializeRequest(event.request);
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: "REQUEST",
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
interceptedAt: requestInterceptedAt,
|
||||
@@ -254,19 +276,19 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
},
|
||||
},
|
||||
[serializedRequest.body],
|
||||
);
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case "MOCK_RESPONSE": {
|
||||
return respondWithMock(clientMessage.data);
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case "PASSTHROUGH": {
|
||||
return passthrough();
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough();
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,18 +299,21 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel();
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error);
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data);
|
||||
};
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
|
||||
});
|
||||
client.postMessage(message, [
|
||||
channel.port2,
|
||||
...transferrables.filter(Boolean),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,17 +326,17 @@ function respondWithMock(response) {
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error();
|
||||
return Response.error()
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response);
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
});
|
||||
})
|
||||
|
||||
return mockedResponse;
|
||||
return mockedResponse
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,5 +357,5 @@ async function serializeRequest(request) {
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ function mapSkeletonColumn<RecordType extends object>(
|
||||
...c,
|
||||
sorter: false,
|
||||
sortOrder: undefined,
|
||||
render: (_: unknown, __: RecordType) => {
|
||||
render: () => {
|
||||
if (isActionColumn(c)) {
|
||||
const w = actionSkeletonDiameter(c);
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { applyOptimisticListDelete, applyOptimisticListUpdate } from "./useResourceCRUD";
|
||||
|
||||
type Row = { id: string; title: string; n: number };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
paginatedListSchema,
|
||||
|
||||
@@ -8,184 +8,191 @@
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as AuthRouteImport } from "./routes/_auth";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as RegisterIndexRouteImport } from "./routes/register/index";
|
||||
import { Route as LoginIndexRouteImport } from "./routes/login/index";
|
||||
import { Route as R404IndexRouteImport } from "./routes/404/index";
|
||||
import { Route as AuthUsersIndexRouteImport } from "./routes/_auth/users/index";
|
||||
import { Route as AuthDashboardIndexRouteImport } from "./routes/_auth/dashboard/index";
|
||||
import { Route as Auth403IndexRouteImport } from "./routes/_auth/403/index";
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AuthRouteImport } from './routes/_auth'
|
||||
import { Route as R404IndexRouteImport } from './routes/404/index'
|
||||
import { Route as LoginIndexRouteImport } from './routes/login/index'
|
||||
import { Route as RegisterIndexRouteImport } from './routes/register/index'
|
||||
import { Route as Auth403IndexRouteImport } from './routes/_auth/403/index'
|
||||
import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
|
||||
import { Route as AuthUsersIndexRouteImport } from './routes/_auth/users/index'
|
||||
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: "/_auth",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const RegisterIndexRoute = RegisterIndexRouteImport.update({
|
||||
id: "/register/",
|
||||
path: "/register/",
|
||||
} as any)
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: '/_auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const LoginIndexRoute = LoginIndexRouteImport.update({
|
||||
id: "/login/",
|
||||
path: "/login/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const R404IndexRoute = R404IndexRouteImport.update({
|
||||
id: "/404/",
|
||||
path: "/404/",
|
||||
id: '/404/',
|
||||
path: '/404/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AuthUsersIndexRoute = AuthUsersIndexRouteImport.update({
|
||||
id: "/users/",
|
||||
path: "/users/",
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any);
|
||||
const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
|
||||
id: "/dashboard/",
|
||||
path: "/dashboard/",
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const LoginIndexRoute = LoginIndexRouteImport.update({
|
||||
id: '/login/',
|
||||
path: '/login/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const RegisterIndexRoute = RegisterIndexRouteImport.update({
|
||||
id: '/register/',
|
||||
path: '/register/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const Auth403IndexRoute = Auth403IndexRouteImport.update({
|
||||
id: "/403/",
|
||||
path: "/403/",
|
||||
id: '/403/',
|
||||
path: '/403/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
|
||||
id: '/dashboard/',
|
||||
path: '/dashboard/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthUsersIndexRoute = AuthUsersIndexRouteImport.update({
|
||||
id: '/users/',
|
||||
path: '/users/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
"/404/": typeof R404IndexRoute;
|
||||
"/login/": typeof LoginIndexRoute;
|
||||
"/register/": typeof RegisterIndexRoute;
|
||||
"/403/": typeof Auth403IndexRoute;
|
||||
"/dashboard/": typeof AuthDashboardIndexRoute;
|
||||
"/users/": typeof AuthUsersIndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
'/404/': typeof R404IndexRoute
|
||||
'/login/': typeof LoginIndexRoute
|
||||
'/register/': typeof RegisterIndexRoute
|
||||
'/403/': typeof Auth403IndexRoute
|
||||
'/dashboard/': typeof AuthDashboardIndexRoute
|
||||
'/users/': typeof AuthUsersIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/404": typeof R404IndexRoute;
|
||||
"/login": typeof LoginIndexRoute;
|
||||
"/register": typeof RegisterIndexRoute;
|
||||
"/403": typeof Auth403IndexRoute;
|
||||
"/dashboard": typeof AuthDashboardIndexRoute;
|
||||
"/users": typeof AuthUsersIndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
'/404': typeof R404IndexRoute
|
||||
'/login': typeof LoginIndexRoute
|
||||
'/register': typeof RegisterIndexRoute
|
||||
'/403': typeof Auth403IndexRoute
|
||||
'/dashboard': typeof AuthDashboardIndexRoute
|
||||
'/users': typeof AuthUsersIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/_auth": typeof AuthRouteWithChildren;
|
||||
"/404/": typeof R404IndexRoute;
|
||||
"/login/": typeof LoginIndexRoute;
|
||||
"/register/": typeof RegisterIndexRoute;
|
||||
"/_auth/403/": typeof Auth403IndexRoute;
|
||||
"/_auth/dashboard/": typeof AuthDashboardIndexRoute;
|
||||
"/_auth/users/": typeof AuthUsersIndexRoute;
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/_auth': typeof AuthRouteWithChildren
|
||||
'/404/': typeof R404IndexRoute
|
||||
'/login/': typeof LoginIndexRoute
|
||||
'/register/': typeof RegisterIndexRoute
|
||||
'/_auth/403/': typeof Auth403IndexRoute
|
||||
'/_auth/dashboard/': typeof AuthDashboardIndexRoute
|
||||
'/_auth/users/': typeof AuthUsersIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: "/" | "/404/" | "/login/" | "/register/" | "/403/" | "/dashboard/" | "/users/";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to: "/" | "/404" | "/login" | "/register" | "/403" | "/dashboard" | "/users";
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/404/'
|
||||
| '/login/'
|
||||
| '/register/'
|
||||
| '/403/'
|
||||
| '/dashboard/'
|
||||
| '/users/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/404' | '/login' | '/register' | '/403' | '/dashboard' | '/users'
|
||||
id:
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/_auth"
|
||||
| "/404/"
|
||||
| "/login/"
|
||||
| "/register/"
|
||||
| "/_auth/403/"
|
||||
| "/_auth/dashboard/"
|
||||
| "/_auth/users/";
|
||||
fileRoutesById: FileRoutesById;
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/_auth'
|
||||
| '/404/'
|
||||
| '/login/'
|
||||
| '/register/'
|
||||
| '/_auth/403/'
|
||||
| '/_auth/dashboard/'
|
||||
| '/_auth/users/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
AuthRoute: typeof AuthRouteWithChildren;
|
||||
R404IndexRoute: typeof R404IndexRoute;
|
||||
LoginIndexRoute: typeof LoginIndexRoute;
|
||||
RegisterIndexRoute: typeof RegisterIndexRoute;
|
||||
IndexRoute: typeof IndexRoute
|
||||
AuthRoute: typeof AuthRouteWithChildren
|
||||
R404IndexRoute: typeof R404IndexRoute
|
||||
LoginIndexRoute: typeof LoginIndexRoute
|
||||
RegisterIndexRoute: typeof RegisterIndexRoute
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
"/_auth": {
|
||||
id: "/_auth";
|
||||
path: "";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof AuthRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/register/": {
|
||||
id: "/register/";
|
||||
path: "/register";
|
||||
fullPath: "/register/";
|
||||
preLoaderRoute: typeof RegisterIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/login/": {
|
||||
id: "/login/";
|
||||
path: "/login";
|
||||
fullPath: "/login/";
|
||||
preLoaderRoute: typeof LoginIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/404/": {
|
||||
id: "/404/";
|
||||
path: "/404";
|
||||
fullPath: "/404/";
|
||||
preLoaderRoute: typeof R404IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/_auth/users/": {
|
||||
id: "/_auth/users/";
|
||||
path: "/users";
|
||||
fullPath: "/users/";
|
||||
preLoaderRoute: typeof AuthUsersIndexRouteImport;
|
||||
parentRoute: typeof AuthRoute;
|
||||
};
|
||||
"/_auth/dashboard/": {
|
||||
id: "/_auth/dashboard/";
|
||||
path: "/dashboard";
|
||||
fullPath: "/dashboard/";
|
||||
preLoaderRoute: typeof AuthDashboardIndexRouteImport;
|
||||
parentRoute: typeof AuthRoute;
|
||||
};
|
||||
"/_auth/403/": {
|
||||
id: "/_auth/403/";
|
||||
path: "/403";
|
||||
fullPath: "/403/";
|
||||
preLoaderRoute: typeof Auth403IndexRouteImport;
|
||||
parentRoute: typeof AuthRoute;
|
||||
};
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth': {
|
||||
id: '/_auth'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/404/': {
|
||||
id: '/404/'
|
||||
path: '/404'
|
||||
fullPath: '/404/'
|
||||
preLoaderRoute: typeof R404IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/login/': {
|
||||
id: '/login/'
|
||||
path: '/login'
|
||||
fullPath: '/login/'
|
||||
preLoaderRoute: typeof LoginIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/register/': {
|
||||
id: '/register/'
|
||||
path: '/register'
|
||||
fullPath: '/register/'
|
||||
preLoaderRoute: typeof RegisterIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth/403/': {
|
||||
id: '/_auth/403/'
|
||||
path: '/403'
|
||||
fullPath: '/403/'
|
||||
preLoaderRoute: typeof Auth403IndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/dashboard/': {
|
||||
id: '/_auth/dashboard/'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard/'
|
||||
preLoaderRoute: typeof AuthDashboardIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/users/': {
|
||||
id: '/_auth/users/'
|
||||
path: '/users'
|
||||
fullPath: '/users/'
|
||||
preLoaderRoute: typeof AuthUsersIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthRouteChildren {
|
||||
Auth403IndexRoute: typeof Auth403IndexRoute;
|
||||
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute;
|
||||
AuthUsersIndexRoute: typeof AuthUsersIndexRoute;
|
||||
Auth403IndexRoute: typeof Auth403IndexRoute
|
||||
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
|
||||
AuthUsersIndexRoute: typeof AuthUsersIndexRoute
|
||||
}
|
||||
|
||||
const AuthRouteChildren: AuthRouteChildren = {
|
||||
Auth403IndexRoute: Auth403IndexRoute,
|
||||
AuthDashboardIndexRoute: AuthDashboardIndexRoute,
|
||||
AuthUsersIndexRoute: AuthUsersIndexRoute,
|
||||
};
|
||||
}
|
||||
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren);
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
@@ -193,7 +200,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
R404IndexRoute: R404IndexRoute,
|
||||
LoginIndexRoute: LoginIndexRoute,
|
||||
RegisterIndexRoute: RegisterIndexRoute,
|
||||
};
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
@@ -59,7 +59,7 @@ function DashboardSkeleton() {
|
||||
active
|
||||
style={{
|
||||
width: w,
|
||||
height: Math.round(token.fontSizeHeading5 * token.lineHeightHeading5),
|
||||
height: Math.round(Number(token.fontSizeHeading5) * Number(token.lineHeightHeading5)),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { defineConfig } from "vite-plus";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
|
||||
+26
-17
@@ -1,19 +1,28 @@
|
||||
{
|
||||
"name": "antd-admin",
|
||||
"version": "6.0.0",
|
||||
"description": "AI-friendly enterprise front-end best practices",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"setup:git-hooks": "git config core.hooksPath .githooks && git config core.hooksPath"
|
||||
"name": "antd-admin",
|
||||
"version": "6.0.0",
|
||||
"description": "AI-friendly enterprise front-end best practices",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"setup:git-hooks": "git config core.hooksPath .githooks && git config core.hooksPath"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"prettier": "^3.2.5",
|
||||
"turbo": "^2.9.6"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.2",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite@*": "npm:@voidzero-dev/vite-plus-core@0.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"prettier": "^3.2.5",
|
||||
"turbo": "^2.9.6"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.6"
|
||||
}
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"msw"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,15 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.10.1",
|
||||
"@inquirer/prompts": "^8.4.2",
|
||||
"commander": "^14.0.0",
|
||||
"giget": "^3.2.0",
|
||||
"picocolors": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript": "~6.0.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
Generated
+8681
-7515
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user