3x-ui/frontend/README.md
MHSanaei 8be84e6e2c
docs(frontend): refresh README + simplify deprecated-scan config
README rewrite reflects the post-Zod-migration state:
- 3 Vite entries (index/login/subpage), not "one per panel route"
- New folders: schemas/, lib/xray/, generated/, test/, layouts/
- Scripts table covers test/gen:api/gen:zod alongside the existing
  dev/build/lint/typecheck
- New sections on the Zod schema tree, the three validation layers,
  the unified Form.useForm + antdRule pattern, and the golden
  fixture testing setup
- "Adding a new page" updated to reflect that most additions are
  just react-router entries in routes.tsx, not new Vite bundles
- Explicit note that `@deprecated` in the prose is a JSDoc tag, not
  a shell command — comes with the exact one-line npx invocation

eslint.deprecated.config.js trimmed: dropping the
recommendedTypeChecked spread + the ~28 rule overrides that came
with it. The config now wires the @typescript-eslint and
react-hooks plugins manually and enables exactly one rule
(`@typescript-eslint/no-deprecated`). 45 lines → 30, same output:
zero false-positives, zero noise, zero deprecations on the current
tree.
2026-05-27 01:51:50 +02:00

201 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 3x-ui frontend
React 19 + Ant Design 6 + TypeScript + Vite 8. Three SPA bundles —
`index.html` (admin panel SPA, all `/panel/*` routes), `login.html`
(login + 2FA), and `subpage.html` (public subscription viewer). All
three are built into `../web/dist/` and embedded into the Go binary
via `embed.FS`.
State is split between local `useState`, TanStack Query for server
state, and `useTheme` / `useWebSocket` contexts. Form validation,
API parsing, and the xray config model all run through a single
shared Zod schema tree (see [Schemas](#schemas)).
## Dev
```sh
npm install
npm run dev
```
Vite serves on `http://localhost:5173/`. API calls and `/panel/*`
routes proxy to the Go panel at `http://localhost:2053/`, so start
the Go panel first (`go run main.go`) and then Vite. The proxy
auto-rewrites `/panel`, `/panel/settings`, `/panel/inbounds`,
`/panel/xray` to the matching Vite-served HTML, so the sidebar's
production-style links work without round-tripping through Go.
## Scripts
| Command | What |
|---|---|
| `npm run dev` | Vite dev server with API + WS proxy to Go |
| `npm run build` | Regenerates OpenAPI + Zod, then builds into `../web/dist/` |
| `npm run preview` | Serve the built bundle locally |
| `npm run typecheck` | `tsc --noEmit` (strict, no emit) |
| `npm run lint` | ESLint flat config (`@typescript-eslint` + `react-hooks`) |
| `npm run test` | Vitest single run (schema fixtures, link parsers, …) |
| `npm run test:watch` | Vitest watch mode |
| `npm run gen:api` | Build `public/openapi.json` from `pages/api-docs/endpoints.ts` |
| `npm run gen:zod` | Run the Go-side openapigen tool → `src/generated/{zod,types}.ts` |
CI runs `typecheck`, `lint`, `test`, and `build` on every PR
(see `../.github/workflows/ci.yml`).
### One-off: scan for deprecated APIs
Run this command to sweep the codebase for usages of APIs marked
with the JSDoc `@deprecated` tag (AntD prop renames, Zod renames,
removed Web APIs, etc.):
```sh
npx eslint --config eslint.deprecated.config.js src
```
It's a type-aware ESLint run against `eslint.deprecated.config.js`
and is not wired into `npm run lint` because typed linting triples
the wall-clock time.
## Production build
```sh
npm run build
```
Outputs to `../web/dist/` (HTML at the root, hashed JS/CSS under
`assets/`). `manualChunks` splits AntD, icons, codemirror, and
react-query into separate vendor bundles to keep the per-page
initial JS small. The Go binary embeds this directory at compile
time and `web/controller/dist.go` serves the per-page HTML.
## Layout
```
frontend/
├── index.html, login.html, subpage.html # 3 Vite entries
├── tsconfig.json
├── eslint.config.js
├── eslint.deprecated.config.js # On-demand type-aware lint config that flags
│ # usages of APIs marked with JSDoc @deprecated
├── vitest.config.ts
├── vite.config.js
├── scripts/
│ └── build-openapi.mjs # endpoints.ts → openapi.json
└── src/
├── entries/ # Per-page bootstrap (createRoot + render)
├── main.tsx # Shared root for the admin SPA (index.html)
├── routes.tsx # react-router routes mounted under /panel/
├── pages/ # One folder per route, page component + helpers
│ ├── index/, login/, inbounds/, clients/, xray/, nodes/,
│ ├── settings/, api-docs/, sub/
├── layouts/ # AdminLayout (sidebar + header + outlet)
├── components/ # Cross-page React components
├── hooks/ # useClients, useTheme, useWebSocket, …
├── api/ # Axios + CSRF interceptor, TanStack Query bridge,
│ # WebSocket client + queryClient.ts
├── i18n/ # react-i18next init (locales in web/translation/)
├── lib/xray/ # Pure functions: link generation, defaults,
│ # form ⇄ wire adapters, protocol capabilities
├── schemas/ # Zod source-of-truth (see "Schemas" below)
├── generated/ # Code-generated zod + ts types from Go
│ # (DO NOT hand-edit — regenerated by gen:zod)
├── models/ # Thin legacy types still in transit
│ # (DBInbound, Status, AllSetting, reality-targets)
├── styles/ # Shared CSS modules
├── test/ # Vitest specs + golden fixtures
│ ├── *.test.ts
│ ├── __snapshots__/
│ └── golden/fixtures/ # Per-(protocol × network × security) JSON
└── utils/ # HttpUtil, ClipboardManager, SizeFormatter, …
```
## Schemas
`src/schemas/` is the single source of truth for the xray
configuration model. Every API response is parsed through it,
every form field is validated against it, and TypeScript types
are inferred via `z.infer<typeof X>` — never hand-written.
```
schemas/
├── primitives/ # Atomic reusable schemas (port, protocol, sniffing, …)
├── api/ # Backend response shapes (e.g. SlimInboundSchema)
├── forms/ # User-facing form shapes (narrower than api/)
├── protocols/
│ ├── inbound/ # Per-protocol settings (vmess, vless, trojan, …)
│ ├── outbound/
│ ├── stream/ # Network transports (tcp, ws, grpc, xhttp, kcp, …)
│ └── security/ # TLS, Reality, none
├── client.ts, dns.ts, routing.ts, setting.ts, status.ts, xray.ts
└── _envelope.ts # Generic `Msg<T>` envelope wrapper
```
Patterns:
- **Discriminated unions** for polymorphic data — inbound `settings`
is `z.discriminatedUnion('protocol', […])`, same for stream and
security.
- **Three validation layers**, non-overlapping:
- API boundary: `parseMsg(msg, schema, ctx)` inside TanStack
Query `queryFn` — warn-only in prod, throws in dev
- Form input: `antdRule(schema.shape.field)` on every `<Form.Item>`
blocks submit + per-field inline error
- Wire request: `Schema.parse(payload)` inside `mutationFn` — throws,
because a malformed payload here is always a developer bug
- **No `.loose()` or `[key: string]: any`** in production schemas.
`@typescript-eslint/no-explicit-any: error` is enforced.
## Form pattern (Pattern A)
All non-trivial modals use this single pattern:
```tsx
const [form] = Form.useForm<InboundFormValues>();
const onFinish = async () => {
const values = await form.validateFields();
await createInbound.mutateAsync(values);
};
<Form form={form} onFinish={onFinish}>
<Form.Item
name="port"
label="Port"
rules={[antdRule(InboundFormSchema.shape.port, t)]}
>
<InputNumber min={1} max={65535} />
</Form.Item>
</Form>
```
No `safeParse`-on-submit handlers, no `useRef<any>` for form
references, no inline `z.string().min(1)` in rules. Conditional
fields use `<Form.Item dependencies={...} shouldUpdate>` with the
nested protocol schema.
## Testing
Vitest runs everything under `src/test/`. Schemas have **golden
fixture suites** — one JSON per `(protocol × network × security)`
combination round-tripped through `schema.parse` → link generator
→ snapshot. Regenerate snapshots after intentional changes:
```sh
npx vitest run -u
```
Fixtures live in `src/test/golden/fixtures/` and are auto-discovered
via `import.meta.glob`.
## Adding a new page
Most new routes go inside the admin SPA (`index.html`) via
`routes.tsx` — no new HTML or Vite entry needed.
1. Add the page component under `src/pages/<page>/`.
2. Register it in `src/routes.tsx` under the `/panel/...` tree.
3. If you need a brand-new top-level bundle (login-style standalone
page), add the HTML at `frontend/<page>.html`, an entry at
`src/entries/<page>.tsx`, and register it in `rollupOptions.input`
in `vite.config.js`. Then add the Go controller call to
`serveDistPage(c, "<page>.html")`.