mirror of
https://github.com/saltbo/zpan.git
synced 2026-10-29 18:05:37 +08:00
build: migrate project to pnpm
This commit is contained in:
+18
-24
@@ -17,19 +17,15 @@ jobs:
|
||||
if: github.repository == 'saltbo/zpan'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/cache@v4
|
||||
id: deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ hashFiles('package-lock.json') }}
|
||||
- if: steps.deps.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run typecheck
|
||||
- run: npx vitest run --project unit --coverage
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lint
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm exec vitest run --project unit --coverage
|
||||
- uses: codecov/codecov-action@v5
|
||||
if: always()
|
||||
with:
|
||||
@@ -37,7 +33,7 @@ jobs:
|
||||
flags: unit
|
||||
fail_ci_if_error: false
|
||||
handle_no_reports_found: true
|
||||
- run: npx vitest run --project integration --coverage
|
||||
- run: pnpm exec vitest run --project integration --coverage
|
||||
- uses: codecov/codecov-action@v5
|
||||
if: always()
|
||||
with:
|
||||
@@ -46,7 +42,7 @@ jobs:
|
||||
fail_ci_if_error: false
|
||||
handle_no_reports_found: true
|
||||
- run: mkdir -p dist
|
||||
- run: npm run test:cf
|
||||
- run: pnpm test:cf
|
||||
|
||||
e2e-node:
|
||||
name: E2E (Node)
|
||||
@@ -56,19 +52,18 @@ jobs:
|
||||
BETTER_AUTH_SECRET: ci-test-secret-that-is-at-least-32-chars
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ hashFiles('package-lock.json') }}
|
||||
- run: npx playwright install --with-deps chromium
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Install cloudflared
|
||||
run: |
|
||||
curl -L --fail --output cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
|
||||
chmod +x cloudflared
|
||||
- run: CLOUDFLARED_BIN=./cloudflared npm run e2e:cloud -- --runtime node
|
||||
- run: CLOUDFLARED_BIN=./cloudflared pnpm e2e:cloud -- --runtime node
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
@@ -82,19 +77,18 @@ jobs:
|
||||
needs: check
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ hashFiles('package-lock.json') }}
|
||||
- run: npx playwright install --with-deps chromium
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Install cloudflared
|
||||
run: |
|
||||
curl -L --fail --output cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
|
||||
chmod +x cloudflared
|
||||
- run: CLOUDFLARED_BIN=./cloudflared npm run e2e:cloud:cf
|
||||
- run: CLOUDFLARED_BIN=./cloudflared pnpm e2e:cloud:cf
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
|
||||
@@ -73,9 +73,11 @@ jobs:
|
||||
repository: saltbo/zpan
|
||||
ref: ${{ steps.release.outputs.version }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
@@ -87,7 +89,7 @@ jobs:
|
||||
- name: Setup SAM CLI
|
||||
uses: aws-actions/setup-sam@v2
|
||||
|
||||
- run: npm ci
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Ensure SAM artifact bucket exists
|
||||
id: bucket
|
||||
@@ -106,7 +108,7 @@ jobs:
|
||||
env:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: npx drizzle-kit migrate
|
||||
run: pnpm exec drizzle-kit migrate
|
||||
|
||||
- name: Resolve existing deployment state
|
||||
id: state
|
||||
@@ -139,10 +141,10 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npx vite build --mode node
|
||||
pnpm exec vite build --mode node
|
||||
|
||||
# Bundle Lambda entry; @libsql/client is external (native binding)
|
||||
npx tsup server/entry-lambda.ts --format cjs --outDir dist-lambda --external @libsql/client
|
||||
pnpm exec tsup server/entry-lambda.ts --format cjs --outDir dist-lambda --external @libsql/client
|
||||
|
||||
# Assemble minimal Lambda deployment package
|
||||
mkdir -p dist-lambda-pkg
|
||||
|
||||
@@ -89,12 +89,13 @@ jobs:
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3 — Node.js setup + install dependencies.
|
||||
# ------------------------------------------------------------------
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- run: npm ci
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4 — Log in to Azure using the service-principal credentials.
|
||||
@@ -202,7 +203,7 @@ jobs:
|
||||
# Step 7 — Build frontend + Azure Functions bundle.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Build
|
||||
run: npm run build:azure
|
||||
run: pnpm build:azure
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 8 — Run database migrations against Turso.
|
||||
@@ -211,7 +212,7 @@ jobs:
|
||||
env:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: npm run db:migrate
|
||||
run: pnpm db:migrate
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 9 — Install Azure Functions Core Tools and publish.
|
||||
@@ -219,7 +220,7 @@ jobs:
|
||||
# are already in place before this step runs.
|
||||
# ------------------------------------------------------------------
|
||||
- name: Install Azure Functions Core Tools
|
||||
run: npm install -g azure-functions-core-tools@4 --unsafe-perm true
|
||||
run: pnpm add --global azure-functions-core-tools@4
|
||||
|
||||
- name: Publish to Azure Functions
|
||||
working-directory: azure-functions
|
||||
|
||||
@@ -71,9 +71,11 @@ jobs:
|
||||
repository: saltbo/zpan
|
||||
ref: ${{ steps.release.outputs.version }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@v2
|
||||
@@ -90,8 +92,8 @@ jobs:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run db:migrate
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm db:migrate
|
||||
|
||||
- name: Store required secrets in Secret Manager
|
||||
env:
|
||||
|
||||
@@ -63,11 +63,13 @@ jobs:
|
||||
repository: saltbo/zpan
|
||||
ref: ${{ steps.release.outputs.version }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- run: npm ci
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Ensure D1 database exists
|
||||
id: d1
|
||||
@@ -75,10 +77,10 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: |
|
||||
DB_ID=$(npx wrangler d1 list --json | jq -r '.[] | select(.name == "zpan-db") | .uuid')
|
||||
DB_ID=$(pnpm exec wrangler d1 list --json | jq -r '.[] | select(.name == "zpan-db") | .uuid')
|
||||
if [ -z "$DB_ID" ]; then
|
||||
# wrangler d1 create does not support --json; parse the TOML snippet it prints.
|
||||
DB_ID=$(npx wrangler d1 create zpan-db | awk -F'"' '/database_id/{print $2; exit}')
|
||||
DB_ID=$(pnpm exec wrangler d1 create zpan-db | awk -F'"' '/database_id/{print $2; exit}')
|
||||
echo "Created D1 database: $DB_ID"
|
||||
else
|
||||
echo "Reusing D1 database: $DB_ID"
|
||||
@@ -142,10 +144,10 @@ jobs:
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: npx wrangler d1 migrations apply DB --remote
|
||||
run: pnpm exec wrangler d1 migrations apply DB --remote
|
||||
|
||||
- name: Build
|
||||
run: npx vite build
|
||||
run: pnpm exec vite build
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
@@ -156,7 +158,7 @@ jobs:
|
||||
# built Worker (dist/zpan/wrangler.json). Cd'ing into dist/zpan instead
|
||||
# causes wrangler 4.x to error on conflicting base paths between the
|
||||
# two configs.
|
||||
run: npx wrangler deploy
|
||||
run: pnpm exec wrangler deploy
|
||||
|
||||
- name: Set BETTER_AUTH_SECRET (first deploy only)
|
||||
env:
|
||||
@@ -164,12 +166,12 @@ jobs:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
USER_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
||||
run: |
|
||||
EXISTS=$(npx wrangler secret list --format json | jq -r '.[] | select(.name == "BETTER_AUTH_SECRET") | .name')
|
||||
EXISTS=$(pnpm exec wrangler secret list --format json | jq -r '.[] | select(.name == "BETTER_AUTH_SECRET") | .name')
|
||||
if [ -n "$USER_SECRET" ]; then
|
||||
echo "$USER_SECRET" | npx wrangler secret put BETTER_AUTH_SECRET
|
||||
echo "$USER_SECRET" | pnpm exec wrangler secret put BETTER_AUTH_SECRET
|
||||
echo "Set BETTER_AUTH_SECRET from GitHub secret"
|
||||
elif [ -z "$EXISTS" ]; then
|
||||
openssl rand -base64 32 | npx wrangler secret put BETTER_AUTH_SECRET
|
||||
openssl rand -base64 32 | pnpm exec wrangler secret put BETTER_AUTH_SECRET
|
||||
echo "Auto-generated BETTER_AUTH_SECRET"
|
||||
else
|
||||
echo "BETTER_AUTH_SECRET already set, skipping"
|
||||
@@ -180,5 +182,5 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: |
|
||||
echo "${{ steps.r2.outputs.url }}" | npx wrangler secret put PUBLIC_IMAGES_URL
|
||||
echo "${{ steps.r2.outputs.url }}" | pnpm exec wrangler secret put PUBLIC_IMAGES_URL
|
||||
echo "Set PUBLIC_IMAGES_URL = ${{ steps.r2.outputs.url }}"
|
||||
|
||||
@@ -66,28 +66,30 @@ jobs:
|
||||
repository: saltbo/zpan
|
||||
ref: ${{ steps.release.outputs.version }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- run: npm ci
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Apply Turso migrations
|
||||
env:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: npx drizzle-kit migrate
|
||||
run: pnpm exec drizzle-kit migrate
|
||||
|
||||
- name: Ensure BETTER_AUTH_SECRET is set (first deploy only)
|
||||
env:
|
||||
USER_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
||||
run: |
|
||||
EXISTING=$(npx netlify env:get BETTER_AUTH_SECRET 2>&1 || true)
|
||||
EXISTING=$(pnpm exec netlify env:get BETTER_AUTH_SECRET 2>&1 || true)
|
||||
if [ -n "$USER_SECRET" ]; then
|
||||
npx netlify env:set BETTER_AUTH_SECRET "$USER_SECRET" --context production
|
||||
pnpm exec netlify env:set BETTER_AUTH_SECRET "$USER_SECRET" --context production
|
||||
echo "Set BETTER_AUTH_SECRET from GitHub secret"
|
||||
elif [ -z "$EXISTING" ]; then
|
||||
npx netlify env:set BETTER_AUTH_SECRET "$(openssl rand -base64 32)" --context production
|
||||
pnpm exec netlify env:set BETTER_AUTH_SECRET "$(openssl rand -base64 32)" --context production
|
||||
echo "Auto-generated BETTER_AUTH_SECRET"
|
||||
else
|
||||
echo "BETTER_AUTH_SECRET already set, skipping"
|
||||
@@ -95,8 +97,8 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build
|
||||
npm run build:netlify
|
||||
pnpm build
|
||||
pnpm build:netlify
|
||||
|
||||
- name: Deploy
|
||||
id: deploy
|
||||
@@ -104,7 +106,7 @@ jobs:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: |
|
||||
OUTPUT=$(npx netlify deploy \
|
||||
OUTPUT=$(pnpm exec netlify deploy \
|
||||
--prod \
|
||||
--dir=dist \
|
||||
--functions=netlify/functions \
|
||||
|
||||
@@ -61,21 +61,23 @@ jobs:
|
||||
repository: saltbo/zpan
|
||||
ref: ${{ steps.release.outputs.version }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run migrations (Turso)
|
||||
env:
|
||||
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
|
||||
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
|
||||
run: npm run db:migrate
|
||||
run: pnpm db:migrate
|
||||
|
||||
- name: Build
|
||||
run: npm run build:vercel
|
||||
run: pnpm build:vercel
|
||||
|
||||
- name: Link or create Vercel project
|
||||
env:
|
||||
@@ -84,7 +86,7 @@ jobs:
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
cp deploy/vercel/vercel.json vercel.json
|
||||
npx vercel link --yes \
|
||||
pnpm exec vercel link --yes \
|
||||
--token "$VERCEL_TOKEN" \
|
||||
--scope "$VERCEL_ORG_ID"
|
||||
|
||||
@@ -93,15 +95,15 @@ jobs:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
USER_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
|
||||
run: |
|
||||
EXISTS=$(npx vercel env ls production --token "$VERCEL_TOKEN" 2>/dev/null | grep -c "^BETTER_AUTH_SECRET" || true)
|
||||
EXISTS=$(pnpm exec vercel env ls production --token "$VERCEL_TOKEN" 2>/dev/null | grep -c "^BETTER_AUTH_SECRET" || true)
|
||||
if [ -n "$USER_SECRET" ]; then
|
||||
# User supplied their own secret — persist it (upsert).
|
||||
echo "$USER_SECRET" | npx vercel env add BETTER_AUTH_SECRET production --token "$VERCEL_TOKEN" --force
|
||||
echo "$USER_SECRET" | pnpm exec vercel env add BETTER_AUTH_SECRET production --token "$VERCEL_TOKEN" --force
|
||||
echo "Set BETTER_AUTH_SECRET from GitHub secret."
|
||||
elif [ "$EXISTS" -eq 0 ]; then
|
||||
# First deploy and no user secret — auto-generate and persist.
|
||||
SECRET=$(openssl rand -base64 32)
|
||||
echo "$SECRET" | npx vercel env add BETTER_AUTH_SECRET production --token "$VERCEL_TOKEN"
|
||||
echo "$SECRET" | pnpm exec vercel env add BETTER_AUTH_SECRET production --token "$VERCEL_TOKEN"
|
||||
echo "auto_generated=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Auto-generated BETTER_AUTH_SECRET and stored in Vercel project env."
|
||||
else
|
||||
@@ -120,7 +122,7 @@ jobs:
|
||||
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
|
||||
TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }}
|
||||
run: |
|
||||
DEPLOY_URL=$(npx vercel deploy --prod --token "$VERCEL_TOKEN" \
|
||||
DEPLOY_URL=$(pnpm exec vercel deploy --prod --token "$VERCEL_TOKEN" \
|
||||
--env TURSO_DATABASE_URL="$TURSO_DATABASE_URL" \
|
||||
--env TURSO_AUTH_TOKEN="$TURSO_AUTH_TOKEN" \
|
||||
--env BETTER_AUTH_URL="$BETTER_AUTH_URL" \
|
||||
|
||||
@@ -13,16 +13,17 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run typecheck
|
||||
- run: npm test
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lint
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm test
|
||||
- run: mkdir -p dist # required by vitest cloudflare config
|
||||
- run: npm run test:cf
|
||||
- run: pnpm test:cf
|
||||
|
||||
docker:
|
||||
name: Docker Image
|
||||
|
||||
@@ -34,7 +34,7 @@ Conventional Commits (`feat:`, `fix:`, `docs:`, etc.). PRs target `master`.
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
Husky runs `npm run typecheck` + lint-staged (biome auto-fix) on every `git commit`. **Never** bypass with `--no-verify`. **Never** run `npm install --ignore-scripts` — the `prepare` script must run so hooks are installed. If a hook fails, fix the underlying issue 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)
|
||||
|
||||
@@ -59,7 +59,7 @@ All shared types live in `shared/`. **Never** create duplicate type definitions
|
||||
|
||||
## Migrations
|
||||
|
||||
**Always** generate migrations with `npm run db:generate` (drizzle-kit). **Never** hand-author the `.sql` file or the `migrations/meta/_journal.json` entry.
|
||||
**Always** generate migrations with `pnpm db:generate` (drizzle-kit). **Never** hand-author the `.sql` file or the `migrations/meta/_journal.json` entry.
|
||||
|
||||
Why: drizzle-kit migrator orders migrations by the `when` timestamp in `_journal.json`. Hand-written entries with guessed timestamps break the ordering silently — drizzle sees the new migration's `when` as older than the last applied one and skips it, leaving dev databases out of sync while CI (which starts from empty) passes cleanly. Past incident: v2.3.0 T1/T2 shipped journal entries with `when = 1745000000000` (April 2025) while the last applied migration was `when = 1776200000000` (March 2026); every developer already at 0009 silently missed 0010/0011.
|
||||
|
||||
|
||||
+18
-18
@@ -9,14 +9,14 @@ Prerequisites: Node.js 24+ (managed by [Volta](https://volta.sh/))
|
||||
```sh
|
||||
git clone git@github.com:saltbo/zpan.git
|
||||
cd zpan
|
||||
npm install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
npm run dev # CF Workers mode with HMR (default, uses staging D1)
|
||||
npm run dev:node # Node.js mode with HMR (SQLite, reads .dev.vars)
|
||||
pnpm dev # CF Workers mode with HMR (default, uses staging D1)
|
||||
pnpm dev:node # Node.js mode with HMR (SQLite, reads .dev.vars)
|
||||
```
|
||||
|
||||
## Quality Gates
|
||||
@@ -24,18 +24,18 @@ npm run dev:node # Node.js mode with HMR (SQLite, reads .dev.vars)
|
||||
Every commit and PR must pass these checks. Husky enforces them on pre-commit.
|
||||
|
||||
```sh
|
||||
npm run lint # Biome — lint + format check
|
||||
npm run typecheck # TypeScript strict mode
|
||||
npm test # Unit + integration tests (Node runtime, 90% coverage gate)
|
||||
npm run test:cf # Integration tests (Cloudflare Workers runtime)
|
||||
npm run e2e # Playwright E2E tests
|
||||
pnpm lint # Biome — lint + format check
|
||||
pnpm typecheck # TypeScript strict mode
|
||||
pnpm test # Unit + integration tests (Node runtime, 90% coverage gate)
|
||||
pnpm test:cf # Integration tests (Cloudflare Workers runtime)
|
||||
pnpm e2e # Playwright E2E tests
|
||||
```
|
||||
|
||||
## Adding a Feature
|
||||
|
||||
1. **Write code** in the relevant directory (`server/`, `src/`, `shared/`)
|
||||
2. **Write tests** — co-locate with source as `*.test.ts` (Node) or `*.cf-test.ts` (CF Workers)
|
||||
3. **Run checks** — `npm run lint && npm run typecheck && npm test && npm run test:cf`
|
||||
3. **Run checks** — `pnpm lint && pnpm typecheck && pnpm test && pnpm test:cf`
|
||||
4. **Coverage** — new code must maintain 90%+ line coverage on `server/`
|
||||
5. **Commit** — use [Conventional Commits](https://www.conventionalcommits.org) (`feat:`, `fix:`, `docs:`, etc.)
|
||||
6. **PR** — target the `master` branch
|
||||
@@ -78,17 +78,17 @@ For admin feature testing, read admin credentials from the local `.dev.vars` fil
|
||||
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)
|
||||
npm run db:migrate:d1 # Apply migrations (D1 local)
|
||||
pnpm db:generate # Generate migration SQL after schema changes
|
||||
pnpm db:migrate # Apply migrations (Node/SQLite)
|
||||
pnpm db:migrate:d1 # 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:d1 # Reset D1 local database (.wrangler)
|
||||
pnpm db:reset # Reset Node database (zpan.db)
|
||||
pnpm db:reset:d1 # Reset D1 local database (.wrangler)
|
||||
```
|
||||
|
||||
Migration files live in `migrations/` at project root. Always commit them.
|
||||
@@ -100,13 +100,13 @@ When deploying the Node/Docker image against a Turso (libSQL) database, set `TUR
|
||||
```sh
|
||||
TURSO_DATABASE_URL=libsql://your-db.turso.io \
|
||||
TURSO_AUTH_TOKEN=your-token \
|
||||
npm run db:migrate
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
For local libSQL files the token can be omitted:
|
||||
|
||||
```sh
|
||||
TURSO_DATABASE_URL=file:./zpan.db npm run db:migrate
|
||||
TURSO_DATABASE_URL=file:./zpan.db pnpm db:migrate
|
||||
```
|
||||
|
||||
Migrations run automatically at Docker container startup when `TURSO_DATABASE_URL` is set. See [docs/deploy/docker.md](docs/deploy/docker.md) for the full Docker + Turso setup.
|
||||
@@ -116,8 +116,8 @@ Migrations run automatically at Docker container startup when `TURSO_DATABASE_UR
|
||||
Primary target is Cloudflare Workers. Node.js (Docker) is the backup runtime.
|
||||
|
||||
```sh
|
||||
npm run build # Build frontend to dist/
|
||||
npm run deploy # Build + deploy to Cloudflare Workers
|
||||
pnpm build # Build frontend to dist/
|
||||
pnpm deploy # Build + deploy to Cloudflare Workers
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
+6
-4
@@ -6,12 +6,14 @@ RUN apt-get update \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
corepack enable \
|
||||
&& pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN npm run build:node \
|
||||
&& npm prune --omit=dev
|
||||
RUN pnpm build:node \
|
||||
&& pnpm prune --prod
|
||||
|
||||
FROM node:24-slim
|
||||
WORKDIR /app
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[build]
|
||||
# Build the React SPA then compile the Netlify Function.
|
||||
command = "npm run build && npm run build:netlify"
|
||||
command = "pnpm build && pnpm build:netlify"
|
||||
publish = "dist"
|
||||
|
||||
[functions]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"buildCommand": "npm run build:vercel",
|
||||
"buildCommand": "pnpm build:vercel",
|
||||
"outputDirectory": "dist",
|
||||
"functions": {
|
||||
"api/entry-vercel.js": {
|
||||
|
||||
@@ -38,7 +38,7 @@ CF Workers (wrangler) Docker (Node.js)
|
||||
| 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 |
|
||||
| Package Manager | **pnpm** | npm, yarn, bun | Fast, strict dependency installs with a committed lockfile; Volta pins the version |
|
||||
| Frontend Framework | **React 19 + Vite** | Vue, Solid, Svelte | Largest ecosystem, Capacitor-ready for future native apps if needed |
|
||||
| UI Components | **shadcn/ui + Tailwind CSS 4** | Ant Design, MUI | Zero runtime overhead (source code, not npm dep), best responsive/mobile support, modern aesthetic preferred by international users |
|
||||
| Admin Scaffold | **[shadcn-admin](https://github.com/satnaing/shadcn-admin)** (11.6k stars) | Custom build | Fork as starting point for the **admin backend panel**. Includes sidebar, navigation, search, dark mode, responsive layout, settings pages. Same stack: Vite + TanStack Router + shadcn/ui. The user-facing frontend is custom-built |
|
||||
@@ -97,7 +97,7 @@ zpan/
|
||||
├── e2e/ # Playwright E2E tests
|
||||
├── wrangler.toml # Cloudflare Workers config
|
||||
├── biome.json # Lint + format config
|
||||
└── package.json # Single package, npm
|
||||
└── package.json # Single package, pnpm
|
||||
```
|
||||
|
||||
## API Communication
|
||||
@@ -138,10 +138,10 @@ Coverage gate: 90% on server. Playwright tests live in `e2e/` at the repo root,
|
||||
GitHub Actions:
|
||||
|
||||
**On PR:**
|
||||
- `npm lint` — Biome lint + format check
|
||||
- `npm typecheck` — TypeScript compilation
|
||||
- `npm test` — Vitest unit + API tests
|
||||
- `npm e2e` — Playwright tests
|
||||
- `pnpm lint` — Biome lint + format check
|
||||
- `pnpm typecheck` — TypeScript compilation
|
||||
- `pnpm test` — Vitest unit + API tests
|
||||
- `pnpm e2e` — Playwright tests
|
||||
|
||||
**On merge to master:**
|
||||
- Build + deploy to CF Workers (preview / production)
|
||||
|
||||
@@ -100,8 +100,8 @@ The workflow triggers automatically on every push to `master` **and** can be tri
|
||||
- Storage Account (required by the Functions runtime)
|
||||
- Consumption plan (Y1 / Dynamic SKU)
|
||||
- Function App (Node 22, runtime v4)
|
||||
6. **Build** — `npm run build:azure` produces the `azure-functions/` publish directory.
|
||||
7. **Migrate** — `npm run db:migrate` applies Drizzle migrations to Turso.
|
||||
6. **Build** — `pnpm build:azure` produces the `azure-functions/` publish directory.
|
||||
7. **Migrate** — `pnpm db:migrate` applies Drizzle migrations to Turso.
|
||||
8. **Publish** — `func azure functionapp publish <name>` uploads the bundle.
|
||||
9. **Set `BETTER_AUTH_SECRET`** — checks whether the setting already exists; generates and sets it if missing.
|
||||
10. **Update `APP_URL`** — patches the real function-app URL into its own app settings.
|
||||
@@ -130,13 +130,13 @@ curl https://<your-func-app>.azurewebsites.net/api/health
|
||||
TURSO_DATABASE_URL=libsql://your-db.turso.io \
|
||||
TURSO_AUTH_TOKEN=your-token \
|
||||
BETTER_AUTH_SECRET=$(openssl rand -base64 32) \
|
||||
npm run dev:node
|
||||
pnpm dev:node
|
||||
```
|
||||
|
||||
### Local Azure Functions emulation
|
||||
|
||||
```sh
|
||||
npm run build:azure
|
||||
pnpm build:azure
|
||||
cd azure-functions
|
||||
func start
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ The workflow follows the standard 8-step deployment contract:
|
||||
3. **Resolve release tag** — defaults to the latest `saltbo/zpan` release; override via `workflow_dispatch` input.
|
||||
4. **Checkout** upstream code at the resolved tag.
|
||||
5. **Authenticate** to Google Cloud using the service account key.
|
||||
6. **Apply Turso migrations** (`npm run db:migrate`).
|
||||
6. **Apply Turso migrations** (`pnpm db:migrate`).
|
||||
7. **Store required secrets** in Secret Manager (`turso-database-url`, `better-auth-secret`).
|
||||
8. **Build + deploy** — Cloud Build builds the image; `gcloud run services replace deploy/cloud-run/service.yaml` creates or updates the service; a post-deploy step wires `BETTER_AUTH_URL` (from the real service URL) and `TURSO_AUTH_TOKEN` (if provided) into the running service via `gcloud run services update --update-secrets`.
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ When `TURSO_DATABASE_URL` is present:
|
||||
```sh
|
||||
TURSO_DATABASE_URL=libsql://your-db.turso.io \
|
||||
TURSO_AUTH_TOKEN=your-token \
|
||||
npm run db:migrate
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
`drizzle.config.ts` automatically switches to the `turso` dialect when `TURSO_DATABASE_URL` is set, so `npm run db:generate` and `npm run db:migrate` work against Turso without any extra flags.
|
||||
`drizzle.config.ts` automatically switches to the `turso` dialect when `TURSO_DATABASE_URL` is set, so `pnpm db:generate` and `pnpm db:migrate` work against Turso without any extra flags.
|
||||
|
||||
### Obtaining a Turso auth token
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ ZPan runs on Netlify via [Netlify Functions v2](https://docs.netlify.com/functio
|
||||
Or via CLI:
|
||||
|
||||
```sh
|
||||
npm install -g netlify-cli
|
||||
pnpm add --global netlify-cli
|
||||
netlify login
|
||||
netlify sites:create --name my-zpan
|
||||
```
|
||||
@@ -111,7 +111,7 @@ In the Netlify dashboard → **Domain management → Add a domain**. No changes
|
||||
```sh
|
||||
TURSO_DATABASE_URL=libsql://your-db.turso.io \
|
||||
TURSO_AUTH_TOKEN=your-token \
|
||||
npm run db:migrate
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
### Free-tier limits
|
||||
|
||||
@@ -46,7 +46,7 @@ Set these in your fork's GitHub repository under **Settings → Secrets and vari
|
||||
3. **Link your Vercel project** locally (first time only):
|
||||
```sh
|
||||
cp deploy/vercel/vercel.json vercel.json
|
||||
npx vercel link
|
||||
pnpm exec vercel link
|
||||
# Note the VERCEL_ORG_ID and VERCEL_PROJECT_ID from .vercel/project.json
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ Set these in your fork's GitHub repository under **Settings → Secrets and vari
|
||||
Install the Vercel CLI and run:
|
||||
|
||||
```sh
|
||||
npm install -g vercel
|
||||
pnpm add --global vercel
|
||||
cp deploy/vercel/vercel.json vercel.json
|
||||
|
||||
# Using a local libSQL file (no Turso token needed)
|
||||
@@ -72,7 +72,7 @@ The app will be available at `http://localhost:3000`.
|
||||
|
||||
## Build Output
|
||||
|
||||
`npm run build:vercel` produces:
|
||||
`pnpm build:vercel` produces:
|
||||
|
||||
| Path | Contents |
|
||||
|------|---------|
|
||||
|
||||
+3
-3
@@ -2,8 +2,8 @@ pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
lint:
|
||||
run: npm run lint
|
||||
run: pnpm lint
|
||||
typecheck:
|
||||
run: npm run typecheck
|
||||
run: pnpm typecheck
|
||||
test:
|
||||
run: npm test
|
||||
run: pnpm test
|
||||
|
||||
Generated
-18157
File diff suppressed because it is too large
Load Diff
+30
-2
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.30.2",
|
||||
"scripts": {
|
||||
"dev": "CLOUDFLARE_ENV=staging vite dev",
|
||||
"dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node",
|
||||
@@ -13,7 +14,7 @@
|
||||
"build:vercel": "vite build --mode node && tsup server/entry-vercel.ts --format esm --outDir api --external @libsql/client",
|
||||
"build:netlify": "tsup server/entry-netlify.ts --format esm --outDir netlify/functions --external @libsql/client",
|
||||
"build:azure": "vite build && tsup server/entry-azure.ts --format esm --outDir azure-functions --external @azure/functions --external @libsql/client && cp server/azure-host.json azure-functions/host.json && cp -r dist azure-functions/dist",
|
||||
"deploy": "npm run db:migrate:d1:prod && wrangler deploy",
|
||||
"deploy": "pnpm db:migrate:d1:prod && wrangler deploy",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:migrate:d1": "wrangler d1 migrations apply zpan-db-staging --local --env staging",
|
||||
@@ -64,6 +65,7 @@
|
||||
"@tanstack/react-router": "^1.93.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@uiw/react-md-editor": "^4.1.0",
|
||||
"@uiw/react-markdown-preview": "5.2.0",
|
||||
"@vidstack/react": "^1.12.13",
|
||||
"better-auth": "^1.6.2",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
@@ -142,8 +144,34 @@
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx,json,jsonc}": "biome check --write --no-errors-on-unmatched"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@better-auth/core": "1.6.2",
|
||||
"@better-auth/drizzle-adapter": "1.6.2",
|
||||
"@better-auth/kysely-adapter": "1.6.2",
|
||||
"@better-auth/memory-adapter": "1.6.2",
|
||||
"@better-auth/mongo-adapter": "1.6.2",
|
||||
"@better-auth/prisma-adapter": "1.6.2",
|
||||
"@better-auth/telemetry": "1.6.2",
|
||||
"@vitest/expect": "4.1.4",
|
||||
"@vitest/mocker": "4.1.4",
|
||||
"@vitest/pretty-format": "4.1.4",
|
||||
"@vitest/runner": "4.1.4",
|
||||
"@vitest/snapshot": "4.1.4",
|
||||
"@vitest/spy": "4.1.4",
|
||||
"@vitest/utils": "4.1.4",
|
||||
"undici": "7.24.8"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@swc/core",
|
||||
"better-sqlite3",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"workerd"
|
||||
]
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.14.1",
|
||||
"npm": "11.11.0"
|
||||
"pnpm": "10.30.2"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+10987
File diff suppressed because it is too large
Load Diff
@@ -47,11 +47,11 @@ const e2eEnv = {
|
||||
if (runtime === 'cf') {
|
||||
if (local) rmSync('.wrangler/state/v3/d1', { recursive: true, force: true })
|
||||
writeDevVars(e2eEnv)
|
||||
await run('npx', ['wrangler', 'd1', 'migrations', 'apply', 'DB', '--local'], e2eEnv)
|
||||
await run('pnpm', ['exec', 'wrangler', 'd1', 'migrations', 'apply', 'DB', '--local'], e2eEnv)
|
||||
}
|
||||
|
||||
try {
|
||||
await run('npx', ['playwright', 'test', spec, `--project=${project}`], e2eEnv)
|
||||
await run('pnpm', ['exec', 'playwright', 'test', spec, `--project=${project}`], e2eEnv)
|
||||
} finally {
|
||||
if (tunnel) {
|
||||
try {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* the poll-pending stage against the live API, then use locally-signed PASETO
|
||||
* certs to test the full feature-gate chain end-to-end.
|
||||
*
|
||||
* Run with: npx vitest run server/licensing/e2e-cloud-integration.test.ts
|
||||
* Run with: pnpm exec vitest run server/licensing/e2e-cloud-integration.test.ts
|
||||
*/
|
||||
|
||||
import { generateKeys, sign } from 'paseto-ts/v4'
|
||||
|
||||
@@ -1306,7 +1306,7 @@ describe('Quota Store API', () => {
|
||||
body: JSON.stringify({
|
||||
amount: 4096,
|
||||
currency: 'usd',
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
count: 2,
|
||||
}),
|
||||
})
|
||||
@@ -1335,7 +1335,7 @@ describe('Quota Store API', () => {
|
||||
expect(JSON.parse(generateInit.body as string)).toEqual({
|
||||
amount: 4096,
|
||||
currency: 'usd',
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
count: 2,
|
||||
})
|
||||
expect(String(listUrl)).toBe(`${ZPAN_CLOUD_URL_DEFAULT}${INSTANCE_STORE_PATH}/gift-cards?status=active`)
|
||||
@@ -2090,7 +2090,7 @@ describe('Quota Store API', () => {
|
||||
trafficBytes: 0,
|
||||
source: 'stripe',
|
||||
packageName: 'Storage Pack',
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
})
|
||||
|
||||
const res = await postWebhook(app, payload)
|
||||
@@ -2115,7 +2115,7 @@ describe('Quota Store API', () => {
|
||||
trafficBytes: 0,
|
||||
source: 'stripe',
|
||||
packageName: 'Storage Pack',
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
})
|
||||
|
||||
const res = await app.request('/api/store/webhook', {
|
||||
@@ -2149,7 +2149,7 @@ describe('Quota Store API', () => {
|
||||
trafficBytes: 0,
|
||||
source: 'stripe',
|
||||
packageName: 'Storage Pack',
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
})
|
||||
|
||||
const first = await postWebhook(app, payload)
|
||||
@@ -2175,7 +2175,7 @@ describe('Quota Store API', () => {
|
||||
WHERE org_id = ${orgId} AND resource_type = 'storage'
|
||||
`)
|
||||
expect(entitlement).toEqual([
|
||||
{ bytes: 4096, status: 'active', sourceId: 'order-1', expiresAt: Date.parse('2026-06-01T00:00:00.000Z') },
|
||||
{ bytes: 4096, status: 'active', sourceId: 'order-1', expiresAt: Date.parse('2099-06-01T00:00:00.000Z') },
|
||||
])
|
||||
const audit = await db.all<{ action: string; metadata: string }>(
|
||||
sql`SELECT action, metadata FROM activity_events WHERE org_id = ${orgId} ORDER BY created_at DESC LIMIT 1`,
|
||||
@@ -2210,7 +2210,7 @@ describe('Quota Store API', () => {
|
||||
packageId: 'pkg-monthly-storage-traffic',
|
||||
packageName: 'Team Plan',
|
||||
occurredAt: '2026-05-01T00:00:00.000Z',
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
customerId: 'buyer-1',
|
||||
customerEmail: 'buyer@example.com',
|
||||
})
|
||||
@@ -2245,14 +2245,14 @@ describe('Quota Store API', () => {
|
||||
{ resourceType: 'traffic', bytes: 2048 },
|
||||
])
|
||||
expect(new Date(entitlements[0].startsAt).toISOString()).toBe('2026-05-01T00:00:00.000Z')
|
||||
expect(new Date(entitlements[0].expiresAt).toISOString()).toBe('2026-06-01T00:00:00.000Z')
|
||||
expect(new Date(entitlements[0].expiresAt).toISOString()).toBe('2099-06-01T00:00:00.000Z')
|
||||
expect(JSON.parse(entitlements[0].metadata)).toMatchObject({
|
||||
eventId: 'evt-subscription-initial-entitlement',
|
||||
source: 'stripe_subscription',
|
||||
packageId: 'pkg-monthly-storage-traffic',
|
||||
packageName: 'Team Plan',
|
||||
trafficOveragePriceCents: 25,
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
customerId: 'buyer-1',
|
||||
customerEmail: 'buyer@example.com',
|
||||
})
|
||||
@@ -2303,7 +2303,7 @@ describe('Quota Store API', () => {
|
||||
source: 'stripe_subscription',
|
||||
packageName: 'Team Plan',
|
||||
occurredAt: '2026-05-01T00:00:00.000Z',
|
||||
expiresAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2099-06-01T00:00:00.000Z',
|
||||
}),
|
||||
)
|
||||
const renewal = await postWebhook(
|
||||
@@ -2319,7 +2319,7 @@ describe('Quota Store API', () => {
|
||||
source: 'stripe_subscription',
|
||||
packageName: 'Team Plan Plus',
|
||||
occurredAt: '2026-06-01T00:00:00.000Z',
|
||||
expiresAt: '2026-07-01T00:00:00.000Z',
|
||||
expiresAt: '2099-07-01T00:00:00.000Z',
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -2332,8 +2332,8 @@ describe('Quota Store API', () => {
|
||||
ORDER BY resource_type
|
||||
`)
|
||||
expect(entitlements).toEqual([
|
||||
{ resourceType: 'storage', bytes: 8192, expiresAt: Date.parse('2026-07-01T00:00:00.000Z') },
|
||||
{ resourceType: 'traffic', bytes: 4096, expiresAt: Date.parse('2026-07-01T00:00:00.000Z') },
|
||||
{ resourceType: 'storage', bytes: 8192, expiresAt: Date.parse('2099-07-01T00:00:00.000Z') },
|
||||
{ resourceType: 'traffic', bytes: 4096, expiresAt: Date.parse('2099-07-01T00:00:00.000Z') },
|
||||
])
|
||||
|
||||
const quotaRes = await app.request('/api/quotas/me', { headers })
|
||||
|
||||
Reference in New Issue
Block a user