mirror of
https://github.com/saltbo/zpan.git
synced 2026-10-29 18:05:37 +08:00
feat: migrate from CF Pages to Workers with one-click deploy button
- Replace Pages Functions with Workers entry (`workers/bootstrap.ts`) - Add Deploy to Cloudflare button in README - Integrate `@cloudflare/vite-plugin` for CF dev with HMR - Integrate `@hono/vite-dev-server` for Node dev with HMR - `npm run dev` now defaults to CF Workers mode - Add `run_worker_first = ["/api/*"]` so static assets stay free - Extract shared Node bootstrap (`server/bootstrap.ts`) for reuse - Update all docs from Pages to Workers references Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a082a51dca
commit
3b80eff413
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Local development environment variables for `wrangler pages dev`.
|
||||
# Local development environment variables for `wrangler dev`.
|
||||
# Copy this file to `.dev.vars` (gitignored) and fill in real values.
|
||||
#
|
||||
# For the Node entry (`npm run dev` / Docker), these same variables must be
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ Complete rewrite from Go to TypeScript.
|
||||
|
||||
### Changed
|
||||
- New tech stack: Hono + Drizzle + Better Auth
|
||||
- Dual deployment: Cloudflare Pages and Docker
|
||||
- Dual deployment: Cloudflare Workers and Docker
|
||||
- R2 as default storage backend
|
||||
- Better Auth replaces custom JWT implementation
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
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.
|
||||
ZPan v2 is an open-source, S3-native file hosting platform written in TypeScript. Cloudflare Workers 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.
|
||||
|
||||
## Key Context
|
||||
|
||||
- Single package: `server/` (Hono API), `src/` (React SPA), `shared/` (types/schemas)
|
||||
- CF Pages Functions: `functions/api/[[route]].ts` (CF Pages + D1) and `server/entry-node.ts` (Node + SQLite)
|
||||
- CF Workers: `workers/bootstrap.ts` (CF Workers + D1) and `server/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
|
||||
|
||||
|
||||
+11
-16
@@ -16,7 +16,7 @@ npm install
|
||||
|
||||
```sh
|
||||
npm run dev # Start server + web dev servers (reads .dev.vars)
|
||||
npm run dev:pages # Build & run as Cloudflare Pages locally
|
||||
npm run dev:cf # Build & run as Cloudflare Workers locally
|
||||
```
|
||||
|
||||
## Quality Gates
|
||||
@@ -43,9 +43,9 @@ npm run e2e # Playwright E2E tests
|
||||
|
||||
## Preview Verification
|
||||
|
||||
Every PR that touches UI or API behavior **must** be verified in the Cloudflare Pages preview environment before merging. The verification report **must** be posted as a PR comment — a PR without a verification comment cannot be merged.
|
||||
Every PR that touches UI or API behavior **must** be verified in the Cloudflare Workers preview environment before merging. The verification report **must** be posted as a PR comment — a PR without a verification comment cannot be merged.
|
||||
|
||||
Cloudflare Pages automatically deploys each PR to a preview URL (posted as a PR comment, e.g. `https://<hash>.zpan.pages.dev`).
|
||||
Cloudflare Workers automatically deploys each PR to a preview URL (posted as a PR comment).
|
||||
|
||||
Before merging, the reviewer **must** verify in the preview environment and post a PR comment with:
|
||||
- **Screenshots** proving the feature works (golden path + edge cases)
|
||||
@@ -68,33 +68,28 @@ Schema is defined in `server/db/schema.ts` and `server/db/auth-schema.ts`.
|
||||
```sh
|
||||
npm run db:generate # Generate migration SQL after schema changes
|
||||
npm run db:migrate # Apply migrations (Node/SQLite)
|
||||
wrangler d1 migrations apply zpan-db --local # Apply migrations (CF Pages local)
|
||||
wrangler d1 migrations apply zpan-db --remote # Apply migrations (CF Pages production)
|
||||
wrangler d1 migrations apply zpan-db --local # Apply migrations (D1 local)
|
||||
wrangler d1 migrations apply zpan-db --remote # Apply migrations (D1 production)
|
||||
```
|
||||
|
||||
To reset local databases with seed data (admin user + dev storage):
|
||||
|
||||
```sh
|
||||
npm run db:reset # Reset Node database (zpan.db)
|
||||
npm run db:reset:pages # Reset D1 local database (.wrangler)
|
||||
npm run db:reset:d1 # Reset D1 local database (.wrangler)
|
||||
```
|
||||
|
||||
Migration files live in `migrations/` at project root. Always commit them.
|
||||
|
||||
## Deployment
|
||||
|
||||
Primary target is Cloudflare Pages. Node.js (Docker) is the backup runtime.
|
||||
Primary target is Cloudflare Workers. Node.js (Docker) is the backup runtime.
|
||||
|
||||
```sh
|
||||
npm run build:pages # Build frontend to dist/
|
||||
npm run deploy # Build + deploy to Cloudflare Pages
|
||||
npm run build # Build frontend to dist/
|
||||
npm run deploy # Build + deploy to Cloudflare Workers
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
@@ -102,9 +97,9 @@ zpan/
|
||||
├── server/ # Hono API (routes, middleware, auth, platform abstraction)
|
||||
├── src/ # React frontend (TanStack Router, shadcn/ui)
|
||||
├── shared/ # Shared types, Zod schemas, constants
|
||||
├── functions/ # CF Pages Functions entry
|
||||
├── workers/ # Cloudflare Workers entry
|
||||
├── migrations/ # D1/SQLite migrations (drizzle-kit generated, wrangler managed)
|
||||
├── wrangler.toml # Cloudflare Pages + Workers config
|
||||
├── wrangler.toml # Cloudflare Workers config
|
||||
└── biome.json # Lint + format config
|
||||
```
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ ZPan is a lightweight file hosting platform built on top of S3-compatible storag
|
||||
|
||||
## Deploy
|
||||
|
||||
### Cloudflare Pages (Recommended)
|
||||
### Cloudflare Workers (Recommended)
|
||||
|
||||
One-click deploy with zero server management. Free tier covers personal use.
|
||||
|
||||
<!-- TODO: Deploy button -->
|
||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/saltbo/zpan)
|
||||
|
||||
### Docker
|
||||
|
||||
@@ -38,7 +38,7 @@ docker compose up -d
|
||||
## Why ZPan?
|
||||
|
||||
- **Zero bandwidth bottleneck** — files transfer directly between client and S3
|
||||
- **Zero ops option** — deploy to Cloudflare Pages for free, no server needed
|
||||
- **Zero ops option** — deploy to Cloudflare Workers for free, no server needed
|
||||
- **Self-host friendly** — Docker deployment with any S3-compatible storage
|
||||
- **Tool ecosystem** — works with PicGo, ShareX, Flameshot out of the box
|
||||
- **Open source** — free forever for self-hosted users
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ Each version ships 1–2 major features. Ship small, ship often.
|
||||
|
||||
## Deployment Options
|
||||
|
||||
- **Cloudflare Pages** — Zero-ops, free tier covers personal use, one-click deploy
|
||||
- **Cloudflare Workers** — Zero-ops, free tier covers personal use, one-click deploy
|
||||
- **Docker** — Self-hosted, bring your own S3, full control
|
||||
|
||||
Same codebase, same features, two runtimes.
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"shared/**/*.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"functions/**/*.ts",
|
||||
"workers/**/*.ts",
|
||||
"e2e/**/*.ts",
|
||||
"!src/routeTree.gen.ts"
|
||||
],
|
||||
|
||||
+12
-13
@@ -5,12 +5,11 @@ Technical reference for ZPan v2 development. For product roadmap, see [V2_ROADMA
|
||||
## Global Architecture
|
||||
|
||||
```
|
||||
CF Pages (wrangler) Docker (Node.js)
|
||||
CF Workers (wrangler) Docker (Node.js)
|
||||
┌─────────────────────┐
|
||||
│ [assets] → web/dist │
|
||||
│ run_worker_first = │ entry-node.ts
|
||||
│ ["/api/*"] │ (serves both static + API)
|
||||
│ main: entry-cf.ts │ │
|
||||
│ [assets] → dist/ │
|
||||
│ SPA fallback │ entry-node.ts
|
||||
│ main: workers/ │ (serves both static + API)
|
||||
│ exports { fetch } │ │
|
||||
└──────────┬──────────┘ │
|
||||
│ Single deployment │
|
||||
@@ -35,8 +34,8 @@ CF Pages (wrangler) Docker (Node.js)
|
||||
|
||||
| Layer | Choice | Alternatives Considered | Why This One |
|
||||
|-------|--------|------------------------|--------------|
|
||||
| Web Framework | **Hono** | Express, Fastify, ElysiaJS | Native CF Pages Functions + Node.js dual runtime. Lightweight, fast, web-standard Request/Response API |
|
||||
| ORM | **Drizzle** | Prisma, Kysely | First-class D1 support, type-safe, SQL-like syntax, lightweight bundle for CF Pages Functions |
|
||||
| Web Framework | **Hono** | Express, Fastify, ElysiaJS | Native CF Workers + Node.js dual runtime. Lightweight, fast, web-standard Request/Response API |
|
||||
| ORM | **Drizzle** | Prisma, Kysely | First-class D1 support, type-safe, SQL-like syntax, lightweight bundle for CF Workers |
|
||||
| Auth | **Better Auth** | Lucia, Auth.js, custom JWT | Drizzle adapter, D1 support, built-in social login / OIDC / organization plugin, active development |
|
||||
| S3 SDK | **@aws-sdk/client-s3** | minio-js, custom fetch | Industry standard, works with every S3-compatible provider including R2 |
|
||||
| Package Manager | **npm** | pnpm, yarn, bun | Standard, no extra tooling, Volta manages Node version |
|
||||
@@ -92,11 +91,11 @@ zpan/
|
||||
│ ├── lib/ # API client (Hono RPC), utils
|
||||
│ └── i18n/ # Translations (en.json, zh.json)
|
||||
├── shared/ # Shared types, Zod schemas, constants
|
||||
├── functions/ # CF Pages Functions entry
|
||||
│ └── api/[[route]].ts # CF entry point
|
||||
├── workers/ # CF Workers entry
|
||||
│ └── app.ts # CF entry point
|
||||
├── migrations/ # D1/SQLite migrations (drizzle-kit generated)
|
||||
├── e2e/ # Playwright E2E tests
|
||||
├── wrangler.toml # Cloudflare Pages config
|
||||
├── wrangler.toml # Cloudflare Workers config
|
||||
├── biome.json # Lint + format config
|
||||
└── package.json # Single package, npm
|
||||
```
|
||||
@@ -145,7 +144,7 @@ GitHub Actions:
|
||||
- `npm e2e` — Playwright tests
|
||||
|
||||
**On merge to master:**
|
||||
- Build + deploy to CF Pages (preview / production)
|
||||
- Build + deploy to CF Workers (preview / production)
|
||||
- Build Docker image + push to Docker Hub (planned)
|
||||
|
||||
## Platform Abstraction
|
||||
@@ -172,8 +171,8 @@ Note: `s3` and `cron` will be added in later versions.
|
||||
|
||||
### Entry Points
|
||||
|
||||
- `entry-cloudflare.ts` — exports `default { fetch }`, wrangler bundles it directly. Static frontend served by wrangler's `[assets]` config with SPA fallback. `run_worker_first = ["/api/*"]` routes only API requests to the worker.
|
||||
- `entry-node.ts` — starts Hono via `@hono/node-server`, serves both API and static frontend from `../../dist` on the same port.
|
||||
- `workers/bootstrap.ts` — exports `default { fetch }`, wrangler bundles it directly. Static frontend served by wrangler's `[assets]` config with SPA fallback.
|
||||
- `server/entry-node.ts` — starts Hono via `@hono/node-server`, serves both API and static frontend from `./dist` on the same port.
|
||||
|
||||
## Per-Version Technical Decisions
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ The minimum viable ZPan v2. Get the new stack running, ship it, validate it.
|
||||
- Per-user storage quotas set by admin
|
||||
- Site name and description configurable via admin panel
|
||||
- Environment variable based configuration
|
||||
- Cloudflare Pages one-click deploy
|
||||
- Cloudflare Workers one-click deploy
|
||||
- Docker image + docker-compose
|
||||
|
||||
### Web UI
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { createApp } from '../../server/app'
|
||||
import { createAuth } from '../../server/auth'
|
||||
import { createCloudflarePlatform } from '../../server/platform/cloudflare'
|
||||
|
||||
interface Env {
|
||||
DB: D1Database
|
||||
BETTER_AUTH_SECRET: string
|
||||
BETTER_AUTH_URL?: string
|
||||
TRUSTED_ORIGINS?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export const onRequest: PagesFunction<Env> = async (context) => {
|
||||
const { BETTER_AUTH_SECRET, BETTER_AUTH_URL, TRUSTED_ORIGINS } = context.env
|
||||
if (!BETTER_AUTH_SECRET) {
|
||||
throw new Error('BETTER_AUTH_SECRET is not configured for this deployment.')
|
||||
}
|
||||
const platform = createCloudflarePlatform(context.env)
|
||||
const trustedOrigins = TRUSTED_ORIGINS?.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean)
|
||||
const auth = createAuth(platform.db, BETTER_AUTH_SECRET, BETTER_AUTH_URL, trustedOrigins)
|
||||
return createApp(platform, auth).fetch(context.request)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.server.json"
|
||||
}
|
||||
Generated
+83
@@ -56,8 +56,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@cloudflare/vite-plugin": "^1.31.2",
|
||||
"@cloudflare/vitest-pool-workers": "^0.14.0",
|
||||
"@cloudflare/workers-types": "^4.20260401.1",
|
||||
"@hono/vite-dev-server": "^0.25.1",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/router-plugin": "^1.167.12",
|
||||
@@ -1596,6 +1598,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/vite-plugin": {
|
||||
"version": "1.31.2",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.31.2.tgz",
|
||||
"integrity": "sha512-6RyoPhqmpuHPB+Zudt7mOUdGzB1+DQtJtPdAxUajhlS2ZUU0+bCn9Cj4g6Z2EvajBrkBTw1yVLqtt4bsUnp1Ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cloudflare/unenv-preset": "2.16.0",
|
||||
"miniflare": "4.20260409.0",
|
||||
"unenv": "2.0.0-rc.24",
|
||||
"wrangler": "4.81.1",
|
||||
"ws": "8.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^6.1.0 || ^7.0.0 || ^8.0.0",
|
||||
"wrangler": "^4.81.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/vitest-pool-workers": {
|
||||
"version": "0.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.14.3.tgz",
|
||||
@@ -2730,6 +2750,36 @@
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/vite-dev-server": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@hono/vite-dev-server/-/vite-dev-server-0.25.1.tgz",
|
||||
"integrity": "sha512-H6EqoPtCV+uH1fu8nKyRy5Wh3LU012QxloEYMlUcyMjjEfVFiRXPjHx/0TQiiRi140Vu8San5A4XQWyElKFKaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.11",
|
||||
"minimatch": "^9.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "*",
|
||||
"miniflare": "*",
|
||||
"wrangler": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"hono": {
|
||||
"optional": false
|
||||
},
|
||||
"miniflare": {
|
||||
"optional": true
|
||||
},
|
||||
"wrangler": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/zod-validator": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.7.6.tgz",
|
||||
@@ -9641,6 +9691,13 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -9869,6 +9926,16 @@
|
||||
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
@@ -13413,6 +13480,22 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
|
||||
+6
-4
@@ -4,15 +4,15 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"tsx watch --env-file=.dev.vars server/entry-node.ts\" \"vite\"",
|
||||
"dev:pages": "vite build && wrangler pages dev dist",
|
||||
"dev": "vite dev",
|
||||
"dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node",
|
||||
"build": "vite build",
|
||||
"build:server": "tsup server/entry-node.ts --format esm --outDir dist-server",
|
||||
"deploy": "vite build && wrangler pages deploy dist",
|
||||
"deploy": "vite build && wrangler deploy",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:reset": "tsx --env-file=.dev.vars scripts/db-reset.ts",
|
||||
"db:reset:pages": "tsx --env-file=.dev.vars scripts/db-reset.ts --pages",
|
||||
"db:reset:d1": "tsx --env-file=.dev.vars scripts/db-reset.ts --d1",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.web.json",
|
||||
"test": "vitest run",
|
||||
"test:cf": "vitest run --config vitest.cloudflare.config.ts",
|
||||
@@ -75,8 +75,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@cloudflare/vite-plugin": "^1.31.2",
|
||||
"@cloudflare/vitest-pool-workers": "^0.14.0",
|
||||
"@cloudflare/workers-types": "^4.20260401.1",
|
||||
"@hono/vite-dev-server": "^0.25.1",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/router-plugin": "^1.167.12",
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ import * as authSchema from '../server/db/auth-schema'
|
||||
import * as schema from '../server/db/schema'
|
||||
import type { Platform } from '../server/platform/interface'
|
||||
|
||||
const isPages = process.argv.includes('--pages')
|
||||
const isD1 = process.argv.includes('--d1')
|
||||
|
||||
const NODE_DB_PATH = process.env.DATABASE_URL || './zpan.db'
|
||||
const D1_STATE_DIR = '.wrangler/state/v3/d1'
|
||||
@@ -34,7 +34,7 @@ const storageConfig = {
|
||||
}
|
||||
|
||||
// ── 1. reset database ──
|
||||
const platform = isPages ? resetPages() : resetNode()
|
||||
const platform = isD1 ? resetD1() : resetNode()
|
||||
|
||||
// ── 2. seed ──
|
||||
const secret = process.env.BETTER_AUTH_SECRET || 'dev-secret-for-seed'
|
||||
@@ -80,7 +80,7 @@ function resetNode(): Platform {
|
||||
return { db, getEnv: (key) => process.env[key] }
|
||||
}
|
||||
|
||||
function resetPages(): Platform {
|
||||
function resetD1(): Platform {
|
||||
// wipe D1 local state
|
||||
if (fs.existsSync(D1_STATE_DIR)) {
|
||||
fs.rmSync(D1_STATE_DIR, { recursive: true })
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApp } from './app'
|
||||
import { createAuth } from './auth'
|
||||
import { createNodePlatform } from './platform/node'
|
||||
|
||||
const platform = createNodePlatform()
|
||||
const secret = process.env.BETTER_AUTH_SECRET
|
||||
if (!secret) {
|
||||
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
|
||||
}
|
||||
const baseURL = process.env.BETTER_AUTH_URL || 'http://localhost:5173'
|
||||
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean) || ['http://localhost:5173']
|
||||
const auth = createAuth(platform.db, secret, baseURL, trustedOrigins)
|
||||
|
||||
export default createApp(platform, auth)
|
||||
+6
-21
@@ -1,28 +1,13 @@
|
||||
import { serve } from '@hono/node-server'
|
||||
import { serveStatic } from '@hono/node-server/serve-static'
|
||||
import { Hono } from 'hono'
|
||||
import { createApp } from './app'
|
||||
import { createAuth } from './auth'
|
||||
import { createNodePlatform } from './platform/node'
|
||||
import app from './bootstrap'
|
||||
|
||||
const platform = createNodePlatform()
|
||||
const secret = process.env.BETTER_AUTH_SECRET
|
||||
if (!secret) {
|
||||
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
|
||||
}
|
||||
const baseURL = process.env.BETTER_AUTH_URL || 'http://localhost:8222'
|
||||
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean) || ['http://localhost:5173']
|
||||
const auth = createAuth(platform.db, secret, baseURL, trustedOrigins)
|
||||
|
||||
const api = createApp(platform, auth)
|
||||
|
||||
const app = new Hono()
|
||||
app.route('/', api)
|
||||
app.use('/*', serveStatic({ root: './dist' }))
|
||||
app.get('/*', serveStatic({ root: './dist', path: 'index.html' }))
|
||||
const server = new Hono()
|
||||
server.route('/', app)
|
||||
server.use('/*', serveStatic({ root: './dist' }))
|
||||
server.get('/*', serveStatic({ root: './dist', path: 'index.html' }))
|
||||
|
||||
const port = Number(process.env.PORT) || 8222
|
||||
console.log(`ZPan server running on http://localhost:${port}`)
|
||||
serve({ fetch: app.fetch, port })
|
||||
serve({ fetch: server.fetch, port })
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"declaration": false,
|
||||
"declarationMap": false
|
||||
},
|
||||
"include": ["server", "shared", "functions"],
|
||||
"exclude": ["server/**/*.cf-test.ts", "server/test/apply-migrations.ts"]
|
||||
"include": ["server", "shared", "workers"],
|
||||
"exclude": ["server/**/*.cf-test.ts", "workers/**/*.cf-test.ts", "server/test/apply-migrations.ts"]
|
||||
}
|
||||
|
||||
+7
-5
@@ -1,10 +1,12 @@
|
||||
import path from 'node:path'
|
||||
import { cloudflare } from '@cloudflare/vite-plugin'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import devServer from '@hono/vite-dev-server'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => ({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
@@ -16,6 +18,9 @@ export default defineConfig({
|
||||
}),
|
||||
react(),
|
||||
tailwindcss(),
|
||||
...(mode === 'node'
|
||||
? [devServer({ entry: './server/bootstrap.ts', injectClientScript: false, exclude: [/^(?!\/api\/).*/] })]
|
||||
: [cloudflare()]),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -26,8 +31,5 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8222',
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
testTimeout: 15000,
|
||||
include: ['server/**/*.cf-test.ts'],
|
||||
include: ['server/**/*.cf-test.ts', 'workers/**/*.cf-test.ts'],
|
||||
setupFiles: ['./server/test/apply-migrations.ts'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { env } from 'cloudflare:workers'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import worker from './bootstrap'
|
||||
|
||||
describe('[CF] Worker fetch handler', () => {
|
||||
it('throws when BETTER_AUTH_SECRET is missing', async () => {
|
||||
const request = new Request('http://localhost/api/health')
|
||||
const envWithoutSecret = { ...env, BETTER_AUTH_SECRET: '' }
|
||||
await expect(worker.fetch(request, envWithoutSecret)).rejects.toThrow(
|
||||
'BETTER_AUTH_SECRET is not configured for this deployment.',
|
||||
)
|
||||
})
|
||||
|
||||
it('returns a response for a valid request', async () => {
|
||||
const request = new Request('http://localhost/api/health')
|
||||
const res = await worker.fetch(request, env)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({ status: 'ok' })
|
||||
})
|
||||
|
||||
it('splits and trims TRUSTED_ORIGINS when provided', async () => {
|
||||
const request = new Request('http://localhost/api/health')
|
||||
const envWithOrigins = { ...env, TRUSTED_ORIGINS: ' https://a.example.com , https://b.example.com ' }
|
||||
const res = await worker.fetch(request, envWithOrigins)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createApp } from '../server/app'
|
||||
import { createAuth } from '../server/auth'
|
||||
import { createCloudflarePlatform } from '../server/platform/cloudflare'
|
||||
|
||||
interface Env {
|
||||
DB: D1Database
|
||||
BETTER_AUTH_SECRET: string
|
||||
BETTER_AUTH_URL?: string
|
||||
TRUSTED_ORIGINS?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const { BETTER_AUTH_SECRET, BETTER_AUTH_URL, TRUSTED_ORIGINS } = env
|
||||
if (!BETTER_AUTH_SECRET) {
|
||||
throw new Error('BETTER_AUTH_SECRET is not configured for this deployment.')
|
||||
}
|
||||
const platform = createCloudflarePlatform(env)
|
||||
const trustedOrigins = TRUSTED_ORIGINS?.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean)
|
||||
const auth = createAuth(platform.db, BETTER_AUTH_SECRET, BETTER_AUTH_URL, trustedOrigins)
|
||||
return createApp(platform, auth).fetch(request)
|
||||
},
|
||||
}
|
||||
+12
-14
@@ -1,10 +1,13 @@
|
||||
name = "zpan"
|
||||
main = "./workers/bootstrap.ts"
|
||||
compatibility_date = "2024-12-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
pages_build_output_dir = "./dist"
|
||||
[assets]
|
||||
not_found_handling = "single-page-application"
|
||||
run_worker_first = ["/api/*"]
|
||||
|
||||
# Local development D1 binding (used by `wrangler pages dev`)
|
||||
# Local development D1 binding (used by `wrangler dev`)
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "zpan-db-local"
|
||||
@@ -14,18 +17,16 @@ migrations_dir = "./migrations"
|
||||
# ----------------------------------------------------------------------------
|
||||
# Environment-specific config.
|
||||
#
|
||||
# CF Pages has exactly two environments: production and preview. Top-level
|
||||
# bindings in wrangler.toml apply to BOTH of them unless an [env.<name>] block
|
||||
# overrides — so to isolate preview from production we must declare both.
|
||||
# Workers has two environments: production and preview. Top-level bindings
|
||||
# apply to BOTH unless an [env.<name>] block overrides.
|
||||
#
|
||||
# Secrets (BETTER_AUTH_SECRET etc.) cannot live in this file. Set them via:
|
||||
# wrangler pages secret put BETTER_AUTH_SECRET --project-name=zpan
|
||||
# For preview, the secret is managed directly via the Cloudflare API (there
|
||||
# is no per-env flag on `wrangler pages secret put` for Pages). See
|
||||
# scripts/setup-preview.sh (or the project runbook) for the PATCH command.
|
||||
# wrangler secret put BETTER_AUTH_SECRET
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# --- production ---
|
||||
# TODO: Update these URLs after creating the Workers project in CF Dashboard.
|
||||
# Workers subdomain will be https://zpan.<account>.workers.dev or your custom domain.
|
||||
[env.production.vars]
|
||||
BETTER_AUTH_URL = "https://zpan.pages.dev"
|
||||
TRUSTED_ORIGINS = "https://zpan.pages.dev"
|
||||
@@ -48,8 +49,5 @@ database_name = "zpan-db-preview"
|
||||
database_id = "a9197d7a-6524-42f0-b646-e714dcb30b3b"
|
||||
migrations_dir = "./migrations"
|
||||
|
||||
# Observability note: Pages Functions do not support [observability] in
|
||||
# wrangler.toml or via the deployment_configs API. Logs are real-time only:
|
||||
# npx wrangler pages deployment tail <deployment-id> --project-name=zpan
|
||||
# For persistent Workers Logs, the project would need to migrate from
|
||||
# Pages Functions to a standalone Worker.
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
Reference in New Issue
Block a user