chore: add root .githooks dispatcher and sync with-lingui

Wire monorepo pre-commit by staged paths; document setup; update Lingui template.

Made-with: Cursor
This commit is contained in:
zuiidea
2026-04-28 12:04:29 +08:00
parent 1cb0819011
commit f12539812e
15 changed files with 144 additions and 51 deletions
+26
View File
@@ -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
```
+34
View File
@@ -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"
+10
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
pnpm lint && pnpm fmt
vp staged
@@ -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;
@@ -52,10 +52,10 @@ export type CrudResult<TListData, TCreateValues, TUpdateValues> = ResourceCRUDRe
type RollbackCtx<TListData> = { snapshot: TListData | undefined };
export function applyOptimisticListUpdate<TItem extends { id: string }, TUpdate extends { id: string }>(
data: ResourceListData<TItem>,
values: TUpdate,
): { list: TItem[]; total: number } {
export function applyOptimisticListUpdate<
TItem extends { id: string },
TUpdate extends { id: string },
>(data: ResourceListData<TItem>, values: TUpdate): { list: TItem[]; total: number } {
const { id, ...patch } = values as TUpdate & Record<string, unknown>;
return {
list: data.list.map((row) =>
@@ -147,7 +147,10 @@ export function useResourceCRUD<
if (applied) {
queryClient.setQueryData<TListData>(
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);
@@ -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<number | undefined>(() =>
@@ -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,
+1 -4
View File
@@ -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);
@@ -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);
});
+1 -5
View File
@@ -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,
@@ -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],
);
+6 -3
View File
@@ -163,7 +163,10 @@ async function request<T>(
export const httpClient = {
get: <T>(path: string, options?: RequestOptions) => request<T>("GET", path, undefined, options),
post: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>("POST", path, body, options),
put: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>("PUT", path, body, options),
delete: <T>(path: string, options?: RequestOptions) => request<T>("DELETE", path, undefined, options),
post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
request<T>("POST", path, body, options),
put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
request<T>("PUT", path, body, options),
delete: <T>(path: string, options?: RequestOptions) =>
request<T>("DELETE", path, undefined, options),
};
+7 -7
View File
@@ -1,8 +1,8 @@
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
+2 -4
View File
@@ -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/"),
},
];
+2 -1
View File
@@ -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",