diff --git a/CLAUDE.md b/CLAUDE.md index 9516268f..93410bdd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,37 +2,23 @@ ## Project Overview -ZPan v2 is an open-source, S3-native file hosting platform written in TypeScript. It supports deployment to Cloudflare Pages (zero-ops) and Docker (self-hosted). +ZPan v2 is an open-source, S3-native file hosting platform written in TypeScript. Cloudflare Pages is the primary deployment target, Node.js (Docker) is backup. Core architecture: clients upload directly to S3-compatible storage via presigned URLs, bypassing server bandwidth. -## Tech Stack +## Key Context -- **Runtime**: Hono (runs on CF Workers + Node.js) -- **Database**: Drizzle ORM (D1 / SQLite / PostgreSQL) -- **Auth**: Better Auth (email/password, social login, OIDC) -- **Storage**: @aws-sdk/client-s3 (R2, AWS S3, MinIO, any S3-compatible) -- **Frontend**: TBD +- Monorepo: `packages/server` (Hono API), `packages/web` (React SPA), `packages/shared` (types/schemas) +- Dual runtime: `entry-cloudflare.ts` (CF Workers + D1) and `entry-node.ts` (Node + SQLite) +- Tests are co-located: `*.test.ts` (Node), `*.cf-test.ts` (CF Workers) +- Migrations: drizzle-kit generates SQL → wrangler manages D1 state -## Project Structure - -``` -src/ -├── app/ # Business logic (routes, middleware, services) -├── platform/ # Platform abstraction (CF vs Node.js) -├── entry-cloudflare.ts # CF Pages Functions entry -└── entry-node.ts # Docker / Node.js entry -``` - -## Development - -```bash -pnpm install # Install dependencies -pnpm dev # Start local dev server -pnpm test # Run tests -pnpm build # Build for production -``` +## Docs Index +- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, commands, quality gates, migration workflow, deployment +- [docs/architecture.md](docs/architecture.md) — system architecture, tech decisions, platform abstraction +- [V2_ROADMAP.md](V2_ROADMAP.md) — product positioning, release plan (v2.0–v2.9) +- [docs/roadmap/](docs/roadmap/) — per-version technical specs (v2.0.md–v2.9.md) ## Commit Convention -Use Conventional Commits (feat:, fix:, docs:, etc.). PRs target the master branch. +Conventional Commits (`feat:`, `fix:`, `docs:`, etc.). PRs target `master`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7d7f18a..88802740 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,12 +4,7 @@ By participating in this project, you agree to abide by our [Code of Conduct](CO ## Setup -ZPan v2 is written in TypeScript. - -Prerequisites: - -- Node.js 20+ -- pnpm +Prerequisites: Node.js 20+, pnpm 10+ ```sh git clone git@github.com:saltbo/zpan.git @@ -20,18 +15,76 @@ pnpm install ## Development ```sh -pnpm dev # Start local dev server -pnpm test # Run tests -pnpm build # Build for production +pnpm dev # Start server + web dev servers +pnpm pages:dev # Build & run as Cloudflare Pages locally ``` -## Commits +## Quality Gates -We use [Conventional Commits](https://www.conventionalcommits.org). Please format your commit messages accordingly. +Every commit and PR must pass these checks. Lefthook enforces them on pre-commit. -## Pull Requests +```sh +pnpm lint # Biome — lint + format check +pnpm typecheck # TypeScript strict mode, all packages +pnpm test # Unit + integration tests (Node runtime, 90% coverage gate) +pnpm test:cf # Integration tests (Cloudflare Workers runtime) +pnpm e2e # Playwright E2E tests +``` -Push your branch to your fork and open a pull request against the `master` branch. +## Adding a Feature + +1. **Write code** in the relevant package (`packages/server`, `packages/web`, `packages/shared`) +2. **Write tests** — co-locate with source as `*.test.ts` (Node) or `*.cf-test.ts` (CF Workers) +3. **Run checks** — `pnpm lint && pnpm typecheck && pnpm test && pnpm test:cf` +4. **Coverage** — new code must maintain 90%+ line coverage on `packages/server` +5. **Commit** — use [Conventional Commits](https://www.conventionalcommits.org) (`feat:`, `fix:`, `docs:`, etc.) +6. **PR** — target the `master` branch + +## Database Migrations + +Schema is defined in `packages/server/src/db/schema.ts` and `auth-schema.ts`. + +```sh +# After modifying schema, generate migration SQL +pnpm --filter @zpan/server exec drizzle-kit generate + +# Apply locally +wrangler d1 migrations apply zpan-db --local + +# Apply to production +wrangler d1 migrations apply zpan-db --remote +``` + +Migration files live in `migrations/` at project root. Always commit them. + +## Deployment + +Primary target is Cloudflare Pages. Node.js (Docker) is the backup runtime. + +```sh +pnpm build:pages # Build frontend to dist/ +pnpm deploy # Build + deploy to Cloudflare Pages +``` + +The wrangler config (`wrangler.toml`) uses: +- `main` → worker entry point (bundled by wrangler) +- `[assets]` → static frontend with SPA fallback +- `run_worker_first = ["/api/*"]` → only API routes hit the worker + +## Project Structure + +``` +zpan/ +├── packages/ +│ ├── server/ # Hono API (routes, middleware, auth, platform abstraction) +│ ├── web/ # React frontend (TanStack Router, shadcn/ui) +│ └── shared/ # Shared types, Zod schemas, constants +├── migrations/ # D1/SQLite migrations (drizzle-kit generated, wrangler managed) +├── e2e/ # Playwright E2E tests +├── wrangler.toml # Cloudflare Pages + Workers config +├── biome.json # Lint + format config +└── lefthook.yml # Pre-commit hooks +``` ## Financial Contributions diff --git a/packages/server/drizzle/0000_perfect_cyclops.sql b/migrations/0000_perfect_cyclops.sql similarity index 100% rename from packages/server/drizzle/0000_perfect_cyclops.sql rename to migrations/0000_perfect_cyclops.sql diff --git a/packages/server/drizzle/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json similarity index 100% rename from packages/server/drizzle/meta/0000_snapshot.json rename to migrations/meta/0000_snapshot.json diff --git a/packages/server/drizzle/meta/_journal.json b/migrations/meta/_journal.json similarity index 100% rename from packages/server/drizzle/meta/_journal.json rename to migrations/meta/_journal.json diff --git a/package.json b/package.json index b30c5936..2b1c0a02 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "scripts": { "dev": "pnpm --filter @zpan/server --filter @zpan/web --parallel dev", "build": "pnpm --filter @zpan/shared build && pnpm --filter @zpan/server build && pnpm --filter @zpan/web build", + "build:pages": "pnpm --filter @zpan/web exec vite build", + "pages:dev": "pnpm build:pages && wrangler dev", + "deploy": "pnpm build:pages && wrangler deploy", "typecheck": "pnpm -r typecheck", "test": "pnpm -r --parallel test", "test:cf": "pnpm --filter @zpan/server test:cf", diff --git a/packages/server/drizzle.config.ts b/packages/server/drizzle.config.ts index 6fded97c..b8cf4b55 100644 --- a/packages/server/drizzle.config.ts +++ b/packages/server/drizzle.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'drizzle-kit' export default defineConfig({ schema: ['./src/db/schema.ts', './src/db/auth-schema.ts'], - out: './drizzle', + out: '../../migrations', dialect: 'sqlite', dbCredentials: { url: process.env.DATABASE_URL || 'zpan.db', diff --git a/packages/server/src/entry-cloudflare.ts b/packages/server/src/entry-cloudflare.ts index e3e30787..234ebb64 100644 --- a/packages/server/src/entry-cloudflare.ts +++ b/packages/server/src/entry-cloudflare.ts @@ -1,21 +1,18 @@ -import { Hono } from 'hono' -import { handle } from 'hono/cloudflare-pages' import { createApp } from './app' import { createAuth } from './auth' import { createCloudflarePlatform } from './platform/cloudflare' -type Env = { - Bindings: { - DB: D1Database - BETTER_AUTH_SECRET: string - BETTER_AUTH_URL?: string - } +interface Env { + DB: D1Database + BETTER_AUTH_SECRET: string + BETTER_AUTH_URL?: string + [key: string]: unknown } -const app = new Hono().all('/*', (c) => { - const platform = createCloudflarePlatform(c.env) - const auth = createAuth(platform.db, c.env.BETTER_AUTH_SECRET, c.env.BETTER_AUTH_URL) - return createApp(platform, auth).fetch(c.req.raw) -}) - -export const onRequest = handle(app) +export default { + async fetch(request: Request, env: Env): Promise { + const platform = createCloudflarePlatform(env) + const auth = createAuth(platform.db, env.BETTER_AUTH_SECRET, env.BETTER_AUTH_URL) + return createApp(platform, auth).fetch(request) + }, +} diff --git a/packages/server/src/entry-node.ts b/packages/server/src/entry-node.ts index 9386a0a8..71e64757 100644 --- a/packages/server/src/entry-node.ts +++ b/packages/server/src/entry-node.ts @@ -15,8 +15,8 @@ const api = createApp(platform, auth) const app = new Hono() app.route('/', api) -app.use('/*', serveStatic({ root: '../web/dist' })) -app.get('/*', serveStatic({ root: '../web/dist', path: 'index.html' })) +app.use('/*', serveStatic({ root: '../../dist' })) +app.get('/*', serveStatic({ root: '../../dist', path: 'index.html' })) const port = Number(process.env.PORT) || 8222 console.log(`ZPan server running on http://localhost:${port}`) diff --git a/packages/server/vitest.cloudflare.config.ts b/packages/server/vitest.cloudflare.config.ts index fd54717b..8189e041 100644 --- a/packages/server/vitest.cloudflare.config.ts +++ b/packages/server/vitest.cloudflare.config.ts @@ -5,11 +5,11 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ plugins: [ cloudflareTest(async () => { - const migrationsPath = path.join(__dirname, 'drizzle') + const migrationsPath = path.join(__dirname, '../../migrations') const migrations = await readD1Migrations(migrationsPath) return { - wrangler: { configPath: './wrangler.toml' }, + wrangler: { configPath: '../../wrangler.toml' }, miniflare: { bindings: { TEST_MIGRATIONS: migrations }, }, diff --git a/packages/server/wrangler.toml b/packages/server/wrangler.toml deleted file mode 100644 index beedaa6e..00000000 --- a/packages/server/wrangler.toml +++ /dev/null @@ -1,11 +0,0 @@ -name = "zpan" -compatibility_date = "2024-12-01" -pages_build_output_dir = "../../dist" - -[vars] -BETTER_AUTH_SECRET = "zpan-test-secret" - -[[d1_databases]] -binding = "DB" -database_name = "zpan-db" -database_id = "local" diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index b20cc4b4..265e4a95 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -5,6 +5,10 @@ import react from '@vitejs/plugin-react-swc' import { defineConfig } from 'vite' export default defineConfig({ + build: { + outDir: '../../dist', + emptyOutDir: true, + }, plugins: [TanStackRouterVite({}), react(), tailwindcss()], resolve: { alias: { diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 00000000..7f454682 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,19 @@ +name = "zpan" +compatibility_date = "2024-12-01" +compatibility_flags = ["nodejs_compat"] +main = "./packages/server/src/entry-cloudflare.ts" + +[assets] +directory = "./dist" +not_found_handling = "single-page-application" +binding = "ASSETS" +run_worker_first = ["/api/*"] + +[vars] +BETTER_AUTH_SECRET = "zpan-test-secret" + +[[d1_databases]] +binding = "DB" +database_name = "zpan-db" +database_id = "local" +migrations_dir = "./migrations"