From 8be84e6e2cdd2bf9903fbd7d184e617f38fed4ce Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 27 May 2026 01:51:50 +0200 Subject: [PATCH] docs(frontend): refresh README + simplify deprecated-scan config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/README.md | 220 ++++++++++++++++++++------- frontend/eslint.deprecated.config.js | 37 ++--- 2 files changed, 179 insertions(+), 78 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index d94f6ee3..c6462da8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,8 +1,15 @@ # 3x-ui frontend -React 19 + Ant Design 6 + TypeScript + Vite 8. Multi-page app — one HTML -entry per panel route — built into `../web/dist/` and embedded into the -Go binary via `embed.FS`. +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 @@ -11,15 +18,44 @@ 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 in dev mode (see -`MIGRATED_ROUTES` in `vite.config.js`), so the sidebar's +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 @@ -27,57 +63,139 @@ npm run build ``` Outputs to `../web/dist/` (HTML at the root, hashed JS/CSS under -`assets/`). The Go binary embeds this directory at compile time and -`web/controller/dist.go` serves the per-page HTML. - -## Type check and lint - -```sh -npm run typecheck -npm run lint -``` - -`tsc --noEmit` against `tsconfig.json` (strict mode, `jsx: "react-jsx"`, -`@/*` → `src/*` alias). ESLint 10 with `eslint.config.js` (flat config) -— `@eslint/js` recommended plus `typescript-eslint` and -`eslint-plugin-react-hooks` rules. +`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/ -├── *.html # Vite entry HTML, one per panel route +├── 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) - ├── pages/ # One folder per route, each with the page - │ ├── index/ # component + helpers + sub-components - │ ├── login/ - │ ├── inbounds/ - │ ├── clients/ - │ ├── xray/ - │ ├── nodes/ - │ ├── settings/ - │ ├── api-docs/ - │ └── sub/ - ├── components/ # Cross-page React components - ├── hooks/ # Reusable hooks (useTheme, useWebSocket, …) - ├── api/ # Axios setup, CSRF interceptor, WebSocket - ├── i18n/ # react-i18next init (locales live in web/translation/) - ├── models/ # Inbound, Outbound, Status, … domain classes - ├── styles/ # Shared CSS modules (page-cards, …) - └── utils/ # HttpUtil, ObjectUtil, LanguageManager, … + ├── 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` — 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` 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 `` — + 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(); + +const onFinish = async () => { + const values = await form.validateFields(); + await createInbound.mutateAsync(values); +}; + +
+ + + +
+``` + +No `safeParse`-on-submit handlers, no `useRef` for form +references, no inline `z.string().min(1)` in rules. Conditional +fields use `` 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 -1. Add `frontend/.html` referencing `/src/entries/.tsx`. -2. Add `src/entries/.tsx` that imports the page component and - mounts it with `createRoot(...).render(...)`. -3. Add the page component under `src/pages//`. -4. Register the entry in `rollupOptions.input` in `vite.config.js`. -5. If the page is reachable from the sidebar at `/panel/`, add - it to `MIGRATED_ROUTES` so the dev proxy serves the Vite HTML. -6. Wire the Go controller to `serveDistPage(c, ".html")`. +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//`. +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/.html`, an entry at + `src/entries/.tsx`, and register it in `rollupOptions.input` + in `vite.config.js`. Then add the Go controller call to + `serveDistPage(c, ".html")`. diff --git a/frontend/eslint.deprecated.config.js b/frontend/eslint.deprecated.config.js index 5ac95dc5..e1784055 100644 --- a/frontend/eslint.deprecated.config.js +++ b/frontend/eslint.deprecated.config.js @@ -1,43 +1,26 @@ import tseslint from 'typescript-eslint'; +import reactHooks from 'eslint-plugin-react-hooks'; export default [ { ignores: ['node_modules/**', '../web/dist/**', 'src/generated/**'] }, - ...tseslint.configs.recommendedTypeChecked.map((config) => ({ - ...config, + { files: ['**/*.{ts,tsx}'], + plugins: { + '@typescript-eslint': tseslint.plugin, + 'react-hooks': reactHooks, + }, languageOptions: { - ...config.languageOptions, + parser: tseslint.parser, parserOptions: { - ...config.languageOptions?.parserOptions, projectService: true, tsconfigRootDir: import.meta.dirname, }, }, - })), - { - files: ['**/*.{ts,tsx}'], rules: { '@typescript-eslint/no-deprecated': 'warn', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-misused-promises': 'off', - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-base-to-string': 'off', - '@typescript-eslint/no-redundant-type-constituents': 'off', - '@typescript-eslint/unbound-method': 'off', - '@typescript-eslint/require-await': 'off', - '@typescript-eslint/await-thenable': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/prefer-promise-reject-errors': 'off', - '@typescript-eslint/only-throw-error': 'off', - '@typescript-eslint/no-unnecessary-type-assertion': 'off', - 'react-hooks/exhaustive-deps': 'off', + }, + linterOptions: { + reportUnusedDisableDirectives: 'off', }, }, ];