From 2f7ef9f7a12568fcf30f10d5b48453fc0e43ba8e Mon Sep 17 00:00:00 2001 From: saltbo Date: Thu, 9 Apr 2026 21:32:25 -0400 Subject: [PATCH] chore: strengthen infrastructure quality gates - Pre-commit: add typecheck before lint-staged (catches type errors locally) - Shared types: add StorageStatus constant, fix Storage.status type (string not number) - Delete duplicate web/src/types/storage.ts, import from @zpan/shared - Use StorageStatus.ACTIVE constant instead of magic string - CLAUDE.md: document Hono RPC requirement, shared types rule, updated hook description Co-Authored-By: Claude Opus 4.6 (1M context) --- .husky/pre-commit | 2 +- CLAUDE.md | 23 ++++++++++++++++++- packages/server/src/services/s3.test.ts | 2 +- packages/shared/src/constants.ts | 7 ++++++ packages/shared/src/types/index.ts | 4 ++-- .../components/admin/storage-form-dialog.tsx | 2 +- .../_authenticated/admin/storages/index.tsx | 6 ++--- packages/web/src/types/storage.ts | 16 ------------- 8 files changed, 37 insertions(+), 25 deletions(-) delete mode 100644 packages/web/src/types/storage.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index cb2c84d5..dcb4a435 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -pnpm lint-staged +pnpm typecheck && pnpm lint-staged diff --git a/CLAUDE.md b/CLAUDE.md index f2fee081..184afc0c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,4 +25,25 @@ Conventional Commits (`feat:`, `fix:`, `docs:`, etc.). PRs target `master`. ## Pre-commit Hooks -Husky + lint-staged run biome auto-fix on every `git commit`. **Never** bypass with `--no-verify`. **Never** run `pnpm install --ignore-scripts` — the `prepare` script must run so hooks are installed. If a hook fails, fix the underlying lint error and re-commit. +Husky runs `pnpm typecheck` + lint-staged (biome auto-fix) on every `git commit`. **Never** bypass with `--no-verify`. **Never** run `pnpm install --ignore-scripts` — the `prepare` script must run so hooks are installed. If a hook fails, fix the underlying issue and re-commit. + +## API Client (Hono RPC) + +The frontend **must** use Hono RPC client for all API calls. **Never** use raw `fetch()` with hardcoded URL strings. + +```typescript +// ✅ Correct — type-safe, compile-time path validation +import { hc } from 'hono/client' +import type { AppType } from '@zpan/server/app' +const client = hc('/') +const res = await client.api.admin.storages.$get() + +// ❌ Wrong — hardcoded path, no type safety +const res = await fetch('/api/admin/storages') +``` + +Exception: `uploadToS3()` calls external S3 presigned URLs, not our API — raw `fetch` is OK there. + +## Types + +All shared types live in `packages/shared`. **Never** create duplicate type definitions in `packages/web` or `packages/server`. Import from `@zpan/shared/types` and `@zpan/shared/constants`. diff --git a/packages/server/src/services/s3.test.ts b/packages/server/src/services/s3.test.ts index 6908030b..6143d550 100644 --- a/packages/server/src/services/s3.test.ts +++ b/packages/server/src/services/s3.test.ts @@ -76,7 +76,7 @@ function makeStorage(overrides: Partial = {}): Storage { secretKey: 'SECRET', filePath: '$UID/$RAW_NAME$RAW_EXT', customHost: '', - status: 1, + status: 'active', createdAt: '2026-01-01', updatedAt: '2026-01-01', ...overrides, diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index ce1a7800..12c99e88 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -20,6 +20,13 @@ export const DirType = { export type DirType = (typeof DirType)[keyof typeof DirType] +export const StorageStatus = { + ACTIVE: 'active', + INACTIVE: 'inactive', +} as const + +export type StorageStatus = (typeof StorageStatus)[keyof typeof StorageStatus] + export const ObjectStatus = { DRAFT: 'draft', ACTIVE: 'active', diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index fa6ecf0b..808152dc 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1,4 +1,4 @@ -import type { DirType, ObjectStatus, StorageMode } from '../constants' +import type { DirType, ObjectStatus, StorageMode, StorageStatus } from '../constants' export interface StorageObject { id: string @@ -28,7 +28,7 @@ export interface Storage { secretKey: string filePath: string customHost: string - status: number + status: StorageStatus createdAt: string updatedAt: string } diff --git a/packages/web/src/components/admin/storage-form-dialog.tsx b/packages/web/src/components/admin/storage-form-dialog.tsx index c72a235b..0a0357c8 100644 --- a/packages/web/src/components/admin/storage-form-dialog.tsx +++ b/packages/web/src/components/admin/storage-form-dialog.tsx @@ -1,5 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' +import type { Storage } from '@zpan/shared/types' import { Eye, EyeOff } from 'lucide-react' import type { ReactNode } from 'react' import { useEffect, useState } from 'react' @@ -11,7 +12,6 @@ import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import type { Storage } from '@/types/storage' const storageFormSchema = z.object({ title: z.string().min(1), diff --git a/packages/web/src/routes/_authenticated/admin/storages/index.tsx b/packages/web/src/routes/_authenticated/admin/storages/index.tsx index 030cfdc2..7d40992a 100644 --- a/packages/web/src/routes/_authenticated/admin/storages/index.tsx +++ b/packages/web/src/routes/_authenticated/admin/storages/index.tsx @@ -1,18 +1,18 @@ import { useQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' +import type { Storage } from '@zpan/shared/types' import { Database, Pencil, Plus, Trash2 } from 'lucide-react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { DeleteStorageDialog } from '@/components/admin/delete-storage-dialog' import { StorageFormDialog } from '@/components/admin/storage-form-dialog' import { Button } from '@/components/ui/button' -import type { Storage } from '@/types/storage' export const Route = createFileRoute('/_authenticated/admin/storages/')({ component: StoragesPage, }) -const STORAGE_STATUS_ACTIVE = 'active' +import { StorageStatus } from '@zpan/shared/constants' function StoragesPage() { const { t } = useTranslation() @@ -124,7 +124,7 @@ function StorageTableRow({ }) { const { t } = useTranslation() - const isActive = storage.status === STORAGE_STATUS_ACTIVE + const isActive = storage.status === StorageStatus.ACTIVE const modeBadge = storage.mode === 'public' ? 'bg-green-500/10 text-green-700 dark:text-green-400' : 'bg-primary/10 text-primary' diff --git a/packages/web/src/types/storage.ts b/packages/web/src/types/storage.ts deleted file mode 100644 index 2663d666..00000000 --- a/packages/web/src/types/storage.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface Storage { - id: string - uid: string - title: string - mode: 'private' | 'public' - bucket: string - endpoint: string - region: string - accessKey: string - secretKey: string - filePath: string - customHost: string - status: string - createdAt: string - updatedAt: string -}