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) <noreply@anthropic.com>
This commit is contained in:
saltbo
2026-04-09 21:32:25 -04:00
parent 26f01687a0
commit 2f7ef9f7a1
8 changed files with 37 additions and 25 deletions
+1 -1
View File
@@ -1 +1 @@
pnpm lint-staged
pnpm typecheck && pnpm lint-staged
+22 -1
View File
@@ -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<AppType>('/')
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`.
+1 -1
View File
@@ -76,7 +76,7 @@ function makeStorage(overrides: Partial<Storage> = {}): Storage {
secretKey: 'SECRET',
filePath: '$UID/$RAW_NAME$RAW_EXT',
customHost: '',
status: 1,
status: 'active',
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
...overrides,
+7
View File
@@ -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',
+2 -2
View File
@@ -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
}
@@ -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),
@@ -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'
-16
View File
@@ -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
}