From f12539812e663c9141af2e0786960c34ceaf89e8 Mon Sep 17 00:00:00 2001 From: zuiidea Date: Tue, 28 Apr 2026 12:04:29 +0800 Subject: [PATCH] chore: add root .githooks dispatcher and sync with-lingui Wire monorepo pre-commit by staged paths; document setup; update Lingui template. Made-with: Cursor --- .githooks/README.md | 26 ++++++++++++++ .githooks/pre-commit | 34 +++++++++++++++++++ README.md | 10 ++++++ apps/with-lingui/.vite-hooks/pre-commit | 2 +- .../src/components/Layout/UserMenu/index.tsx | 7 +++- apps/with-lingui/src/hooks/useResourceCRUD.ts | 13 ++++--- .../src/hooks/useTableFitHeight.ts | 28 ++++++++++++--- apps/with-lingui/src/main.tsx | 5 +-- .../src/mocks/createHandler.test.ts | 2 +- apps/with-lingui/src/mocks/createHandler.ts | 6 +--- .../src/routes/_auth/users/index.tsx | 30 ++++++++-------- apps/with-lingui/src/utils/http.ts | 9 +++-- apps/with-lingui/vercel.json | 14 ++++---- apps/with-lingui/vite.config.ts | 6 ++-- package.json | 3 +- 15 files changed, 144 insertions(+), 51 deletions(-) create mode 100644 .githooks/README.md create mode 100755 .githooks/pre-commit diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 0000000..7425e1f --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,26 @@ +# Git hooks(仓库根) + +本目录为 **monorepo 根级** Git 钩子。提交时根据暂存文件路径,在对应子项目目录执行检查(例如 `apps/with-lingui` 下执行 `vp staged`,以便正确读取该目录的 `vite.config.ts`)。 + +模板应用目录内的 `.vite-hooks/`(如 `apps/with-lingui/.vite-hooks/`)留给 **模板单独拷贝出去使用**;在本仓库开发时请使用根级钩子,避免在仓库根执行 `vp staged` 找不到 `vite.config.ts`。 + +## 启用方式(一次性) + +在仓库根目录执行: + +```bash +git config core.hooksPath .githooks +``` + +或使用: + +```bash +pnpm run setup:git-hooks +``` + +验证: + +```bash +git config core.hooksPath +# 应输出:.githooks +``` diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..1664da5 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,34 @@ +#!/usr/bin/env sh +# Monorepo pre-commit: run Vite+ / checks in the app that owns staged files. +# Template copies under apps/*/ keep their own .vite-hooks for standalone use; this repo uses root hooksPath instead. +set -e + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +STAGED=$(git diff --cached --name-only --diff-filter=ACM) +FAILED=0 + +run_in_dir() { + dir="$1" + shift + (cd "$ROOT/$dir" && "$@") || FAILED=1 +} + +if echo "$STAGED" | grep -q '^apps/with-lingui/'; then + run_in_dir apps/with-lingui vp staged +fi + +if echo "$STAGED" | grep -q '^apps/basic/'; then + run_in_dir apps/basic vp staged +fi + +if echo "$STAGED" | grep -q '^packages/create/'; then + run_in_dir . pnpm --dir packages/create run check-types +fi + +if echo "$STAGED" | grep -q '^apps/docs/'; then + run_in_dir apps/docs pnpm run lint +fi + +exit "$FAILED" diff --git a/README.md b/README.md index d5bf219..943e4d6 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,16 @@ Core Playwright flows (from the app you are working in): cd apps/basic && pnpm run test:e2e:core ``` +### Git hooks + +提交前检查由仓库根目录 **`.githooks/pre-commit`** 按暂存路径分发到各子项目(在对应目录执行 `vp staged`,避免在仓库根找不到 `vite.config.ts`)。一次性启用: + +```bash +pnpm run setup:git-hooks +``` + +说明见 [.githooks/README.md](./.githooks/README.md)。模板目录内的 `.vite-hooks` 仍保留给模板单独拷贝使用;在本仓库开发请使用上述根级钩子。 + ## Getting Started ### Prerequisites diff --git a/apps/with-lingui/.vite-hooks/pre-commit b/apps/with-lingui/.vite-hooks/pre-commit index a54ea02..b64a1f1 100755 --- a/apps/with-lingui/.vite-hooks/pre-commit +++ b/apps/with-lingui/.vite-hooks/pre-commit @@ -1 +1 @@ -pnpm lint && pnpm fmt \ No newline at end of file +vp staged \ No newline at end of file diff --git a/apps/with-lingui/src/components/Layout/UserMenu/index.tsx b/apps/with-lingui/src/components/Layout/UserMenu/index.tsx index f066de7..ba485a7 100644 --- a/apps/with-lingui/src/components/Layout/UserMenu/index.tsx +++ b/apps/with-lingui/src/components/Layout/UserMenu/index.tsx @@ -13,7 +13,12 @@ interface UserMenuProps { accountMenuTriggerAriaLabel: string; } -export function UserMenu({ collapsed, user, userMenuItems, accountMenuTriggerAriaLabel }: UserMenuProps) { +export function UserMenu({ + collapsed, + user, + userMenuItems, + accountMenuTriggerAriaLabel, +}: UserMenuProps) { const { token } = theme.useToken(); const avatarSrc = (user?.avatar ?? "").trim() || undefined; diff --git a/apps/with-lingui/src/hooks/useResourceCRUD.ts b/apps/with-lingui/src/hooks/useResourceCRUD.ts index 2078e9c..e19ee16 100644 --- a/apps/with-lingui/src/hooks/useResourceCRUD.ts +++ b/apps/with-lingui/src/hooks/useResourceCRUD.ts @@ -52,10 +52,10 @@ export type CrudResult = ResourceCRUDRe type RollbackCtx = { snapshot: TListData | undefined }; -export function applyOptimisticListUpdate( - data: ResourceListData, - values: TUpdate, -): { list: TItem[]; total: number } { +export function applyOptimisticListUpdate< + TItem extends { id: string }, + TUpdate extends { id: string }, +>(data: ResourceListData, values: TUpdate): { list: TItem[]; total: number } { const { id, ...patch } = values as TUpdate & Record; return { list: data.list.map((row) => @@ -147,7 +147,10 @@ export function useResourceCRUD< if (applied) { queryClient.setQueryData( queryKey, - applyOptimisticListDelete(previous as ResourceListData<{ id: string }>, id) as unknown as TListData, + applyOptimisticListDelete( + previous as ResourceListData<{ id: string }>, + id, + ) as unknown as TListData, ); } options.deleteLifecycle?.onMutate?.(id); diff --git a/apps/with-lingui/src/hooks/useTableFitHeight.ts b/apps/with-lingui/src/hooks/useTableFitHeight.ts index 08aa764..ea7384c 100644 --- a/apps/with-lingui/src/hooks/useTableFitHeight.ts +++ b/apps/with-lingui/src/hooks/useTableFitHeight.ts @@ -12,8 +12,16 @@ export type UseTableFitHeightArgs = { }; export function useTableFitHeight(args: UseTableFitHeightArgs) { - const { pageShellRef, toolbarRef, middleRef, tableFrameRef, marginLG, rowCount, isLoading, showPagination } = - args; + const { + pageShellRef, + toolbarRef, + middleRef, + tableFrameRef, + marginLG, + rowCount, + isLoading, + showPagination, + } = args; const tableAvailableRef = useRef(0); const [tableAreaMaxHeight, setTableAreaMaxHeight] = useState(() => @@ -48,7 +56,10 @@ export function useTableFitHeight(args: UseTableFitHeightArgs) { ? mainRect.bottom : Math.min(shell.getBoundingClientRect().bottom, viewportBottom); - const maxMiddleFromShell = Math.max(0, Math.floor(clipBottom - toolbarRect.bottom - marginLG)); + const maxMiddleFromShell = Math.max( + 0, + Math.floor(clipBottom - toolbarRect.bottom - marginLG), + ); const midHRaw = mid.clientHeight; const midH = midHRaw > 0 ? Math.min(midHRaw, maxMiddleFromShell) : maxMiddleFromShell; @@ -103,7 +114,16 @@ export function useTableFitHeight(args: UseTableFitHeightArgs) { ro.observe(shell); ro.observe(mid); return () => ro.disconnect(); - }, [showPagination, rowCount, isLoading, marginLG, pageShellRef, toolbarRef, middleRef, tableFrameRef]); + }, [ + showPagination, + rowCount, + isLoading, + marginLG, + pageShellRef, + toolbarRef, + middleRef, + tableFrameRef, + ]); return { tableAreaMaxHeight, diff --git a/apps/with-lingui/src/main.tsx b/apps/with-lingui/src/main.tsx index 63b9a42..34467d8 100644 --- a/apps/with-lingui/src/main.tsx +++ b/apps/with-lingui/src/main.tsx @@ -26,10 +26,7 @@ async function enableMocking() { enableMocking() .then(async () => { - await Promise.all([ - useSettingsStore.persist.rehydrate(), - useAuthStore.persist.rehydrate(), - ]); + await Promise.all([useSettingsStore.persist.rehydrate(), useAuthStore.persist.rehydrate()]); const initialLocale = useSettingsStore.getState().locale; const messages = await loadLocaleCatalog(initialLocale); diff --git a/apps/with-lingui/src/mocks/createHandler.test.ts b/apps/with-lingui/src/mocks/createHandler.test.ts index 3d8232c..e6a64ac 100644 --- a/apps/with-lingui/src/mocks/createHandler.test.ts +++ b/apps/with-lingui/src/mocks/createHandler.test.ts @@ -55,7 +55,7 @@ describe("paginatedListSchema / paginatedWithSchema", () => { permissions: [] as string[], }; const res = paginatedWithSchema(UserSchema, { list: [row], total: 1 }); - const body = (await res.json()) as { data: { list: typeof row[]; total: number } }; + const body = (await res.json()) as { data: { list: (typeof row)[]; total: number } }; expect(body.data.total).toBe(1); expect(body.data.list).toHaveLength(1); }); diff --git a/apps/with-lingui/src/mocks/createHandler.ts b/apps/with-lingui/src/mocks/createHandler.ts index b75187a..be97492 100644 --- a/apps/with-lingui/src/mocks/createHandler.ts +++ b/apps/with-lingui/src/mocks/createHandler.ts @@ -51,11 +51,7 @@ export function successWithNullBody(message: string = "ok") { return successWithSchema(NullBodySchema, null, message); } -export function errorResponse( - code: number, - message: string, - data: unknown = null, -) { +export function errorResponse(code: number, message: string, data: unknown = null) { return HttpResponse.json({ code, data, diff --git a/apps/with-lingui/src/routes/_auth/users/index.tsx b/apps/with-lingui/src/routes/_auth/users/index.tsx index 54244a2..1b01a8b 100644 --- a/apps/with-lingui/src/routes/_auth/users/index.tsx +++ b/apps/with-lingui/src/routes/_auth/users/index.tsx @@ -256,21 +256,21 @@ function UsersPage() { () => showPagination ? { - total: data?.total ?? 0, - current: currentPage, - pageSize: search.limit, - showSizeChanger: true, - showTotal: (total) => t`${total} rows`, - onChange: (page, pageSize) => { - void navigate({ - search: { - ...search, - limit: pageSize ?? search.limit, - offset: (page - 1) * (pageSize ?? search.limit), - }, - }); - }, - } + total: data?.total ?? 0, + current: currentPage, + pageSize: search.limit, + showSizeChanger: true, + showTotal: (total) => t`${total} rows`, + onChange: (page, pageSize) => { + void navigate({ + search: { + ...search, + limit: pageSize ?? search.limit, + offset: (page - 1) * (pageSize ?? search.limit), + }, + }); + }, + } : false, [showPagination, data?.total, currentPage, search, navigate, t], ); diff --git a/apps/with-lingui/src/utils/http.ts b/apps/with-lingui/src/utils/http.ts index d049f0c..4ab53dc 100644 --- a/apps/with-lingui/src/utils/http.ts +++ b/apps/with-lingui/src/utils/http.ts @@ -163,7 +163,10 @@ async function request( export const httpClient = { get: (path: string, options?: RequestOptions) => request("GET", path, undefined, options), - post: (path: string, body?: unknown, options?: RequestOptions) => request("POST", path, body, options), - put: (path: string, body?: unknown, options?: RequestOptions) => request("PUT", path, body, options), - delete: (path: string, options?: RequestOptions) => request("DELETE", path, undefined, options), + post: (path: string, body?: unknown, options?: RequestOptions) => + request("POST", path, body, options), + put: (path: string, body?: unknown, options?: RequestOptions) => + request("PUT", path, body, options), + delete: (path: string, options?: RequestOptions) => + request("DELETE", path, undefined, options), }; diff --git a/apps/with-lingui/vercel.json b/apps/with-lingui/vercel.json index 70bff2f..1323cda 100644 --- a/apps/with-lingui/vercel.json +++ b/apps/with-lingui/vercel.json @@ -1,8 +1,8 @@ { - "rewrites": [ - { - "source": "/(.*)", - "destination": "/index.html" - } - ] -} \ No newline at end of file + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} diff --git a/apps/with-lingui/vite.config.ts b/apps/with-lingui/vite.config.ts index d0bde7a..60f53b2 100644 --- a/apps/with-lingui/vite.config.ts +++ b/apps/with-lingui/vite.config.ts @@ -6,8 +6,7 @@ import { lingui } from "@lingui/vite-plugin"; const VENDOR_CHUNKS: Array<{ chunk: string; test: (id: string) => boolean }> = [ { chunk: "vendor-antd", - test: (id) => - id.includes("node_modules/antd") || id.includes("node_modules/rc-"), + test: (id) => id.includes("node_modules/antd") || id.includes("node_modules/rc-"), }, { chunk: "vendor-tanstack", @@ -19,8 +18,7 @@ const VENDOR_CHUNKS: Array<{ chunk: string; test: (id: string) => boolean }> = [ }, { chunk: "vendor-i18n", - test: (id) => - id.includes("node_modules/@lingui/") || id.includes("/@lingui/"), + test: (id) => id.includes("node_modules/@lingui/") || id.includes("/@lingui/"), }, ]; diff --git a/package.json b/package.json index ea62600..6966a6b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "turbo run build", "dev": "turbo run dev", "lint": "turbo run lint", - "format": "prettier --write \"**/*.{ts,tsx,md}\"" + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "setup:git-hooks": "git config core.hooksPath .githooks && git config core.hooksPath" }, "devDependencies": { "eslint": "^8.57.0",