feat: fix CF Pages deployment and update project docs

- Restructure for wrangler Workers mode (main + [assets] + run_worker_first)
- Move wrangler.toml to project root, add nodejs_compat flag
- Move migrations to root, managed by wrangler d1 migrations
- Web builds to root dist/, wrangler bundles worker entry directly
- Update CONTRIBUTING.md with full dev workflow and quality gates
- CLAUDE.md indexes docs instead of duplicating content

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saltbo
2026-04-01 11:09:18 -04:00
co-authored by Claude Opus 4.6
parent 729e64e60d
commit 175c52a548
13 changed files with 121 additions and 70 deletions
+12 -26
View File
@@ -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`.
+66 -13
View File
@@ -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
+3
View File
@@ -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",
+1 -1
View File
@@ -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',
+12 -15
View File
@@ -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<Env>().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<Response> {
const platform = createCloudflarePlatform(env)
const auth = createAuth(platform.db, env.BETTER_AUTH_SECRET, env.BETTER_AUTH_URL)
return createApp(platform, auth).fetch(request)
},
}
+2 -2
View File
@@ -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}`)
+2 -2
View File
@@ -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 },
},
-11
View File
@@ -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"
+4
View File
@@ -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: {
+19
View File
@@ -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"