Move to infisical. Create local dev environment. Add ci gates. Modernize repo
This commit is contained in:
+4
-3
@@ -18,9 +18,10 @@ out
|
|||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
*.log
|
*.log
|
||||||
#.env
|
.env
|
||||||
#.env.*
|
.env.*
|
||||||
!.env.example
|
.local
|
||||||
|
**/.local
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
|
|
||||||
# Keep this file up-to-date when you add new variables to \`.env\`.
|
|
||||||
# This file will be committed to version control, so make sure not to have any secrets in it.
|
|
||||||
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
|
|
||||||
|
|
||||||
## Next.js ##
|
|
||||||
NODE_ENV=
|
|
||||||
SENTRY_AUTH_TOKEN=
|
|
||||||
PAYLOAD_SECRET= # openssl rand -hex 32 | wl-copy
|
|
||||||
PAYLOAD_DB_URL= # postgresql://user:password@host:5432/db_name
|
|
||||||
NEXT_PUBLIC_SITE_URL=https://example.com
|
|
||||||
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com # convex-backend:3210
|
|
||||||
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
|
||||||
NEXT_PUBLIC_SENTRY_URL=https://sentry.example.com
|
|
||||||
NEXT_PUBLIC_SENTRY_ORG=sentry
|
|
||||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME=example
|
|
||||||
|
|
||||||
## Convex ##
|
|
||||||
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com # convex-backend:3210
|
|
||||||
CONVEX_SELF_HOSTED_ADMIN_KEY= # Generate after hosted on docker
|
|
||||||
# Convex Auth
|
|
||||||
CONVEX_SITE_URL=http://localhost:3000 # Always localhost:3000 for local dev; update in Convex Dashboard for production
|
|
||||||
USESEND_API_KEY=
|
|
||||||
USESEND_URL=https://usesend.example.com
|
|
||||||
USESEND_FROM_EMAIL=Convex Admin <admin@convexmonorepo.gbrown.org>
|
|
||||||
AUTH_AUTHENTIK_ID=
|
|
||||||
AUTH_AUTHENTIK_SECRET=
|
|
||||||
AUTH_AUTHENTIK_ISSUER=
|
|
||||||
|
|
||||||
## Docker Compose Variables for Next App ##
|
|
||||||
NETWORK=nginx-bridge
|
|
||||||
NEXT_CONTAINER_NAME=convexmonorepo
|
|
||||||
NEXT_DOMAIN=convexmonorepo.gbrown.org
|
|
||||||
#NEXT_PORT=
|
|
||||||
|
|
||||||
## Docker Compose Variables for Self hosted Convex ##
|
|
||||||
BACKEND_TAG=latest
|
|
||||||
DASHBOARD_TAG=latest
|
|
||||||
BACKEND_CONTAINER_NAME=convex-backend
|
|
||||||
DASHBOARD_CONTAINER_NAME=convex-dashboard
|
|
||||||
BACKEND_DOMAIN=convex.convexmonorepo.gbrown.org
|
|
||||||
DASHBOARD_DOMAIN=dashboard.convexmonorepo.gbrown.org
|
|
||||||
INSTANCE_NAME=convex
|
|
||||||
#INSTANCE_SECRET=
|
|
||||||
CONVEX_CLOUD_ORIGIN=https://api.convexmonorepo.gbrown.org
|
|
||||||
CONVEX_SITE_ORIGIN=https://convex.convexmonorepo.gbrown.org
|
|
||||||
NEXT_PUBLIC_DEPLOYMENT_URL=https://api.convexmonorepo.gbrown.org
|
|
||||||
DISABLE_BEACON=true
|
|
||||||
REDACT_LOGS_TO_CLIENT=true
|
|
||||||
DO_NOT_REQUIRE_SSL=true
|
|
||||||
POSTGRES_URL= #postgresql://user:password@host:5432/db_name
|
|
||||||
#BACKEND_PORT=
|
|
||||||
#DASHBOARD_PORT
|
|
||||||
#SITE_PROXY_PORT=
|
|
||||||
#ACTIONS_USER_TIMEOUT_SECS=
|
|
||||||
#RUST_LOG=
|
|
||||||
#RUST_BACKTRACE=
|
|
||||||
|
|
||||||
## Docker Compose Variables for Postgres ##
|
|
||||||
POSTGRES_CONTAINER_NAME=convexmonorepo-postgres
|
|
||||||
POSTGRES_USER=
|
|
||||||
POSTGRES_PASSWORD=
|
|
||||||
POSTGRES_DB=convexmonorepo_payload
|
|
||||||
@@ -2,49 +2,55 @@ name: Build and Push Next App
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- 'apps/next/**'
|
- 'apps/**'
|
||||||
- 'packages/ui/**'
|
- 'packages/**'
|
||||||
- 'scripts/build-next-app'
|
|
||||||
- 'tools/**'
|
- 'tools/**'
|
||||||
|
- 'scripts/**'
|
||||||
- 'docker/**'
|
- 'docker/**'
|
||||||
- '.gitea/workflows/build-next.yml'
|
- '.gitea/workflows/build-next.yml'
|
||||||
|
- '.infisical.json'
|
||||||
|
- 'package.json'
|
||||||
|
- 'bun.lock'
|
||||||
|
- 'turbo.json'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-next:
|
quality:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
- name: Create .env file
|
bun-version: 1.3.10
|
||||||
|
- run: bun install --frozen-lockfile
|
||||||
|
- name: Lint, typecheck, and test
|
||||||
|
env:
|
||||||
|
DOTENV_PROD: ${{ secrets.DOTENV_PROD }}
|
||||||
run: |
|
run: |
|
||||||
cat > .env <<'EOF'
|
env_file="$(mktemp)"
|
||||||
${{ secrets.DOTENV_PROD }}
|
trap 'rm -f "$env_file"' EXIT
|
||||||
EOF
|
printf '%s\n' "$DOTENV_PROD" > "$env_file"
|
||||||
|
bunx dotenv -e "$env_file" -- env NODE_ENV=test SKIP_E2E=1 bun run ci:check
|
||||||
|
|
||||||
|
build-next:
|
||||||
|
needs: [quality]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- name: Log in to container registry
|
- name: Log in to container registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.gbrown.org -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
- name: Build image
|
||||||
|
env:
|
||||||
|
DOTENV_PROD: ${{ secrets.DOTENV_PROD }}
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.gbrown.org -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
env_file="$(mktemp)"
|
||||||
|
trap 'rm -f "$env_file"' EXIT
|
||||||
- name: Build app using project script
|
printf '%s\n' "$DOTENV_PROD" > "$env_file"
|
||||||
run: |
|
CI_ENV_FILE="$env_file" ./scripts/build-next-app staging
|
||||||
chmod +x ./scripts/build-next-app
|
- name: Tag and push image
|
||||||
./scripts/build-next-app
|
|
||||||
|
|
||||||
- name: Show built images
|
|
||||||
run: |
|
|
||||||
docker images
|
|
||||||
|
|
||||||
- name: Tag built image
|
|
||||||
run: |
|
run: |
|
||||||
docker tag convexmonorepo-next:latest git.gbrown.org/gib/convexmonorepo-next:${{ gitea.sha }}
|
docker tag convexmonorepo-next:latest git.gbrown.org/gib/convexmonorepo-next:${{ gitea.sha }}
|
||||||
docker tag convexmonorepo-next:latest git.gbrown.org/gib/convexmonorepo-next:latest
|
docker tag convexmonorepo-next:latest git.gbrown.org/gib/convexmonorepo-next:latest
|
||||||
|
|
||||||
- name: Push image tags
|
|
||||||
run: |
|
|
||||||
docker push git.gbrown.org/gib/convexmonorepo-next:${{ gitea.sha }}
|
docker push git.gbrown.org/gib/convexmonorepo-next:${{ gitea.sha }}
|
||||||
docker push git.gbrown.org/gib/convexmonorepo-next:latest
|
docker push git.gbrown.org/gib/convexmonorepo-next:latest
|
||||||
|
|||||||
+13
-1
@@ -10,6 +10,10 @@ node_modules
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
coverage
|
coverage
|
||||||
|
**/.cache/
|
||||||
|
|
||||||
|
# generated Convex client/server bindings (tracked, but not linted/formatted)
|
||||||
|
packages/backend/convex/_generated/
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
.next/
|
.next/
|
||||||
@@ -48,9 +52,17 @@ yarn-error.log*
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env
|
.env
|
||||||
|
.env.staging
|
||||||
|
.local/
|
||||||
|
|
||||||
# turbo
|
# turbo
|
||||||
.turbo
|
.turbo/
|
||||||
|
**/.turbo/
|
||||||
|
|
||||||
|
# playwright e2e
|
||||||
|
apps/next/tests/e2e/.auth/
|
||||||
|
apps/next/test-results/
|
||||||
|
apps/next/playwright-report/
|
||||||
|
|
||||||
# tanstack
|
# tanstack
|
||||||
.tanstack
|
.tanstack
|
||||||
|
|||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
bunx lint-staged --concurrent 1
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
bun run ci:check
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"workspaceId": "b2760150-c581-4e0d-acb5-b4da51cee58d",
|
||||||
|
"defaultEnvironment": "",
|
||||||
|
"gitBranchToEnvironmentMapping": null
|
||||||
|
}
|
||||||
@@ -1,495 +1,92 @@
|
|||||||
# Convex Turbo Monorepo
|
# Convex Turbo Monorepo
|
||||||
|
|
||||||
A production-ready Turborepo starter with Next.js, Expo, self-hosted Convex, and
|
A reusable Bun/Turborepo template with Next.js 16, Expo, self-hosted Convex,
|
||||||
embedded Payload CMS for live-editable marketing content. Built with TypeScript,
|
Payload CMS, shared UI/config packages, Vitest, and Docker deployment.
|
||||||
Tailwind CSS, and modern tooling.
|
|
||||||
|
|
||||||
---
|
## Local setup
|
||||||
|
|
||||||
## What's Inside?
|
Requirements: Bun 1.3.10, Docker or Podman, Node 22, and the Infisical CLI.
|
||||||
|
|
||||||
### Apps & Packages
|
```sh
|
||||||
|
bun install --frozen-lockfile
|
||||||
- **`apps/next`** - Next.js 16 web application with App Router
|
infisical login
|
||||||
- **`apps/expo`** - Expo 54 mobile application _(in progress)_
|
infisical init
|
||||||
- **`@gib/backend`** - Self-hosted Convex backend with authentication
|
bun db:up
|
||||||
- **`@gib/ui`** - Shared shadcn/ui component library
|
bun dev:next
|
||||||
- **`@gib/eslint-config`** - ESLint configuration
|
|
||||||
- **`@gib/prettier-config`** - Prettier configuration with import sorting
|
|
||||||
- **`@gib/tailwind-config`** - Tailwind CSS v4 configuration
|
|
||||||
- **`@gib/tsconfig`** - Shared TypeScript configurations
|
|
||||||
|
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
- **Framework:** Next.js 16 (App Router) + Expo 54
|
|
||||||
- **Backend:** Convex (self-hosted)
|
|
||||||
- **Auth:** @convex-dev/auth with Authentik OAuth & Password providers
|
|
||||||
- **CMS:** Payload CMS embedded inside the Next.js app
|
|
||||||
- **Styling:** Tailwind CSS v4 + shadcn/ui
|
|
||||||
- **Language:** TypeScript (strict mode)
|
|
||||||
- **Package Manager:** Bun
|
|
||||||
- **Monorepo:** Turborepo
|
|
||||||
- **Deployment:** Docker (standalone)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
This is a self-hosted template. The full setup requires a server (home server or VPS)
|
|
||||||
to host the Convex backend and dashboard, and a reverse proxy (nginx-proxy-manager is
|
|
||||||
recommended) to expose them over HTTPS. The Next.js app can run locally in dev mode
|
|
||||||
once the Convex containers are reachable.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- [Bun](https://bun.sh) (v1.2+)
|
|
||||||
- [Docker](https://www.docker.com/) & Docker Compose (for self-hosted Convex)
|
|
||||||
- Node.js 22+ (for compatibility)
|
|
||||||
- A running nginx-proxy-manager instance (or similar reverse proxy) to expose Convex over HTTPS
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 1 — Clone & Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.gbrown.org/gib/convex-monorepo
|
|
||||||
cd convex-monorepo
|
|
||||||
bun install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If you're using this as a template for a new project, remove the existing remote and
|
The committed `.infisical.json` links this repository to its own Infisical
|
||||||
add your own:
|
project. Local commands read `dev` by default and never fall back to `.env`
|
||||||
|
files. Select staging with `INFISICAL_ENV=staging bun dev:next`.
|
||||||
|
|
||||||
```bash
|
Local services:
|
||||||
git remote remove origin
|
|
||||||
git remote add origin https://your-git-host.com/your/new-repo.git
|
- Next.js: `http://localhost:3000`
|
||||||
|
- Convex API: `http://localhost:3210`
|
||||||
|
- Convex dashboard: `http://localhost:6791`
|
||||||
|
- Payload Postgres: `localhost:5432`
|
||||||
|
|
||||||
|
Next and Expo run on the host. Payload uses the local Postgres database. Convex
|
||||||
|
uses a separate self-hosted data volume and does not use Postgres by default.
|
||||||
|
The commented `POSTGRES_URL` in Compose is an opt-in example for cloned projects.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun db:down # stop; preserve Payload and Convex data
|
||||||
|
bun db:down:wipe # remove both volumes, generated admin key, and seed marker
|
||||||
|
bun db:sync:payload # refresh/apply the local Payload seed snapshot from staging
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
`db:sync:payload` reads the staging `PAYLOAD_DB_URL`, saves a private PostgreSQL
|
||||||
|
snapshot under `.local/`, and replaces only the localhost Payload database. It
|
||||||
|
never writes to staging or Convex. Normal `bun db:up` never contacts staging;
|
||||||
|
it only restores `.local/payload-staging.dump` when the local database marker is
|
||||||
|
missing or stale. The marker is `.local/payload-seed-state.env`, not a database
|
||||||
|
table, so Payload schema push will not try to drop it. Stop the Next development
|
||||||
|
server first so it does not reconnect while the database is being replaced. The
|
||||||
|
snapshot includes Payload users and password hashes, allowing the same admin
|
||||||
|
credentials locally; protect it as production-derived data and delete it when no
|
||||||
|
longer needed.
|
||||||
|
|
||||||
### Step 2 — Configure the Single Root Environment File
|
Physical devices cannot resolve their own `localhost`; override the public
|
||||||
|
Convex URL with the development host's LAN address when testing Expo on-device.
|
||||||
|
|
||||||
```bash
|
## Environment model
|
||||||
cp .env.example .env
|
|
||||||
|
- Local `dev` and `staging`: Infisical.
|
||||||
|
- Generated local state: `.local/<environment>.generated.env`.
|
||||||
|
- CI/CD: Gitea `DOTENV_PROD`, materialized only as a temporary runner file.
|
||||||
|
- Docker compilation: explicit Compose build args; `.env*` stays outside the
|
||||||
|
image context.
|
||||||
|
|
||||||
|
Run `sh scripts/with-env dev -- <command>` for an environment-aware command or
|
||||||
|
`sh scripts/export-env dev` to materialize a temporary merged dotenv stream.
|
||||||
|
Do not commit or maintain root `.env` files.
|
||||||
|
|
||||||
|
## Development and quality
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun dev:next
|
||||||
|
bun dev:expo
|
||||||
|
bun lint:ws
|
||||||
|
bun format
|
||||||
|
bun lint
|
||||||
|
bun typecheck
|
||||||
|
bun test:unit
|
||||||
|
bun test:integration
|
||||||
|
bun test:component
|
||||||
|
SKIP_E2E=1 bun run ci:check
|
||||||
```
|
```
|
||||||
|
|
||||||
The root `/.env` is the single required env file for this repo. It is used for:
|
`bun test:e2e` starts the isolated local stack and currently performs generic
|
||||||
|
stack smoke checks. It skips in CI and when `SKIP_E2E=1` is set.
|
||||||
|
|
||||||
- app/package runtime env vars
|
Shared dependency versions belong in root catalogs. Edit the root catalog, run
|
||||||
- Next.js build-time env vars
|
`bun install`, then `bun lint:ws`; do not run `bun update` inside a workspace.
|
||||||
- Payload env vars
|
|
||||||
- Docker Compose interpolation
|
|
||||||
- helper scripts in `scripts/`
|
|
||||||
|
|
||||||
Fill it in with your values. The Docker-related variables in the same file control the
|
|
||||||
Convex containers and Next.js container — you'll need to choose:
|
|
||||||
|
|
||||||
- `INSTANCE_NAME` — a unique name for your Convex instance
|
|
||||||
- `INSTANCE_SECRET` — a secret string (generate something random)
|
|
||||||
- `CONVEX_CLOUD_ORIGIN` — the public HTTPS URL for your Convex backend API (e.g. `https://api.convex.example.com`)
|
|
||||||
- `CONVEX_SITE_ORIGIN` — the public HTTPS URL for Convex Auth HTTP routes (e.g. `https://api.convex.example.com`)
|
|
||||||
- `NEXT_PUBLIC_DEPLOYMENT_URL` — the URL for the Convex dashboard (e.g. `https://dashboard.convex.example.com`)
|
|
||||||
- `NEXT_CONTAINER_NAME`, `BACKEND_CONTAINER_NAME`, and `DASHBOARD_CONTAINER_NAME` — the Docker service container names
|
|
||||||
|
|
||||||
Do not create or rely on a separate `docker/.env`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3 — Start the Convex Containers
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/docker-compose up -d backend dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
Wait a moment for `convex-backend` to pass its health check, then verify both
|
|
||||||
containers are running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
Reverse-proxy the two Convex services through nginx-proxy-manager (or your preferred
|
|
||||||
proxy) to the URLs you chose in Step 2. Both must be reachable over HTTPS before you
|
|
||||||
can proceed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 4 — Generate the Convex Admin Key
|
|
||||||
|
|
||||||
With the backend container running, generate the admin key:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/generate-convex-admin-key
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the printed key — you'll need it as `CONVEX_SELF_HOSTED_ADMIN_KEY` in the root
|
|
||||||
`.env` file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 5 — Finish Configuring Root Environment Variables
|
|
||||||
|
|
||||||
Fill out all values in root `/.env`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Next.js
|
|
||||||
NODE_ENV=development
|
|
||||||
SENTRY_AUTH_TOKEN= # From your self-hosted Sentry
|
|
||||||
PAYLOAD_SECRET= # openssl rand -hex 32
|
|
||||||
PAYLOAD_DB_URL=postgresql://user:password@host:5432/db_name
|
|
||||||
NEXT_PUBLIC_SITE_URL=https://example.com
|
|
||||||
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
|
|
||||||
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
|
||||||
NEXT_PUBLIC_SENTRY_URL=https://sentry.example.com
|
|
||||||
NEXT_PUBLIC_SENTRY_ORG=sentry
|
|
||||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME=my-project
|
|
||||||
|
|
||||||
# Convex
|
|
||||||
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
|
|
||||||
CONVEX_SELF_HOSTED_ADMIN_KEY= # From Step 4
|
|
||||||
CONVEX_SITE_URL=http://localhost:3000 # Always localhost:3000 for local dev
|
|
||||||
|
|
||||||
# Auth (synced to Convex in Step 6)
|
|
||||||
USESEND_API_KEY=
|
|
||||||
USESEND_URL=https://usesend.example.com
|
|
||||||
USESEND_FROM_EMAIL=My App <noreply@example.com>
|
|
||||||
AUTH_AUTHENTIK_ID=
|
|
||||||
AUTH_AUTHENTIK_SECRET=
|
|
||||||
AUTH_AUTHENTIK_ISSUER=https://auth.example.com/application/o/my-app/
|
|
||||||
|
|
||||||
# Docker Compose
|
|
||||||
NETWORK=nginx-bridge
|
|
||||||
NEXT_CONTAINER_NAME=next-app
|
|
||||||
BACKEND_CONTAINER_NAME=convex-backend
|
|
||||||
DASHBOARD_CONTAINER_NAME=convex-dashboard
|
|
||||||
INSTANCE_NAME=convex
|
|
||||||
INSTANCE_SECRET=
|
|
||||||
CONVEX_CLOUD_ORIGIN=https://api.convex.example.com
|
|
||||||
CONVEX_SITE_ORIGIN=https://convex.convex.example.com
|
|
||||||
NEXT_PUBLIC_DEPLOYMENT_URL=https://dashboard.convex.example.com
|
|
||||||
POSTGRES_URL=postgresql://user:password@host:5432/convex
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 6 — Generate JWT Keys & Sync Environment Variables to Convex
|
|
||||||
|
|
||||||
Generate the RS256 JWT keypair needed for Convex Auth:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd packages/backend
|
|
||||||
bun run scripts/generateKeys.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
This prints `JWT_PRIVATE_KEY` and `JWKS` values. Sync them to your Convex deployment
|
|
||||||
along with all other backend environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From packages/backend/
|
|
||||||
bun with-env npx convex env set JWT_PRIVATE_KEY "your-private-key"
|
|
||||||
bun with-env npx convex env set JWKS "your-jwks"
|
|
||||||
bun with-env npx convex env set AUTH_AUTHENTIK_ID "your-client-id"
|
|
||||||
bun with-env npx convex env set AUTH_AUTHENTIK_SECRET "your-client-secret"
|
|
||||||
bun with-env npx convex env set AUTH_AUTHENTIK_ISSUER "your-issuer-url"
|
|
||||||
bun with-env npx convex env set USESEND_API_KEY "your-api-key"
|
|
||||||
bun with-env npx convex env set USESEND_URL "https://usesend.example.com"
|
|
||||||
bun with-env npx convex env set USESEND_FROM_EMAIL "My App <noreply@example.com>"
|
|
||||||
```
|
|
||||||
|
|
||||||
**For production auth to work**, you must also update `CONVEX_SITE_URL` in the Convex
|
|
||||||
Dashboard to your production Next.js URL. Go to
|
|
||||||
`https://dashboard.convex.example.com` → Settings → Environment Variables and set:
|
|
||||||
|
|
||||||
```
|
|
||||||
CONVEX_SITE_URL = https://example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
The root `.env` value of `http://localhost:3000` is correct for local dev and should
|
|
||||||
not be changed — only update it in the Dashboard for production.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 7 — Start the Development Server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From project root
|
|
||||||
bun dev:next # Next.js app + Convex backend (most common)
|
|
||||||
# or
|
|
||||||
bun dev # All apps (Next.js + Expo + Backend)
|
|
||||||
```
|
|
||||||
|
|
||||||
**App URLs:**
|
|
||||||
|
|
||||||
- **Next.js:** http://localhost:3000
|
|
||||||
- **Convex Dashboard:** https://dashboard.convex.example.com
|
|
||||||
|
|
||||||
### Docker Helper Scripts
|
|
||||||
|
|
||||||
This repo includes helper scripts so you do not have to keep passing `--env-file` and
|
|
||||||
`-f` manually to Docker Compose.
|
|
||||||
|
|
||||||
Useful commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/docker-compose ps
|
|
||||||
./scripts/docker-compose up -d backend dashboard
|
|
||||||
./scripts/docker-compose up -d next
|
|
||||||
./scripts/build-next-app
|
|
||||||
./scripts/update-next-app
|
|
||||||
./scripts/update-convex
|
|
||||||
./scripts/generate-convex-admin-key
|
|
||||||
```
|
|
||||||
|
|
||||||
`./scripts/docker-compose` also accepts short service aliases like `next`, `backend`,
|
|
||||||
and `dashboard` and maps them to the container names from root `/.env`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
bun dev # Run all apps
|
|
||||||
bun dev:next # Next.js + Convex backend
|
|
||||||
bun dev:expo # Expo + Convex backend
|
|
||||||
bun dev:backend # Convex backend only
|
|
||||||
|
|
||||||
# Quality Checks
|
|
||||||
bun typecheck # Type checking (recommended for testing)
|
|
||||||
bun lint # Lint all packages
|
|
||||||
bun lint:fix # Lint and auto-fix
|
|
||||||
bun format # Check formatting
|
|
||||||
bun format:fix # Fix formatting
|
|
||||||
|
|
||||||
# Build
|
|
||||||
bun build # Build all packages (production)
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
bun ui-add # Add shadcn/ui components
|
|
||||||
bun clean # Clean node_modules
|
|
||||||
bun clean:ws # Clean workspace caches
|
|
||||||
```
|
|
||||||
|
|
||||||
### Single Package Commands
|
|
||||||
|
|
||||||
Use Turborepo filters to target specific packages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun turbo run dev -F @gib/next # Run Next.js only
|
|
||||||
bun turbo run typecheck -F @gib/backend # Typecheck backend
|
|
||||||
bun turbo run lint -F @gib/ui # Lint UI package
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
convex-monorepo/
|
|
||||||
├── apps/
|
|
||||||
│ ├── next/ # Next.js web app
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ │ ├── app/ # App Router pages
|
|
||||||
│ │ │ ├── components/ # React components
|
|
||||||
│ │ │ └── lib/ # Utilities
|
|
||||||
│ │ └── package.json
|
|
||||||
│ └── expo/ # Expo mobile app (WIP)
|
|
||||||
│
|
|
||||||
├── packages/
|
|
||||||
│ ├── backend/ # Convex backend
|
|
||||||
│ │ ├── convex/ # Convex functions (synced to deployment)
|
|
||||||
│ │ ├── scripts/ # Utilities (generateKeys.mjs)
|
|
||||||
│ │ └── types/ # Shared types
|
|
||||||
│ └── ui/ # shadcn/ui components
|
|
||||||
│ └── src/ # Component source files
|
|
||||||
│
|
|
||||||
├── tools/ # Shared configurations
|
|
||||||
│ ├── eslint/
|
|
||||||
│ ├── prettier/
|
|
||||||
│ ├── tailwind/
|
|
||||||
│ └── typescript/
|
|
||||||
│
|
|
||||||
├── docker/ # Self-hosted deployment
|
|
||||||
│ ├── compose.yml
|
|
||||||
│ ├── Dockerfile
|
|
||||||
│ └── data/ # Convex data volume (gitignored in real use)
|
|
||||||
│
|
|
||||||
├── scripts/ # Docker/deployment helper scripts using root .env
|
|
||||||
│ ├── docker-compose
|
|
||||||
│ ├── build-next-app
|
|
||||||
│ ├── update-next-app
|
|
||||||
│ └── update-convex
|
|
||||||
│
|
|
||||||
├── turbo.json # Turborepo configuration
|
|
||||||
└── package.json # Root workspace & catalogs
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
- **OAuth:** Authentik SSO integration
|
|
||||||
- **Password:** Custom password auth with email verification
|
|
||||||
- **OTP:** Email verification via self-hosted UseSend
|
|
||||||
- **Session Management:** Secure cookie-based sessions (30-day max age)
|
|
||||||
|
|
||||||
### Next.js App
|
|
||||||
|
|
||||||
- **App Router:** Next.js 16 with React Server Components
|
|
||||||
- **Editable Marketing Content:** Payload-backed landing page with live preview
|
|
||||||
- **Data Preloading:** SSR data fetching with `preloadQuery` + `usePreloadedQuery`
|
|
||||||
- **Middleware:** Route protection & IP-based security (`src/proxy.ts`)
|
|
||||||
- **Styling:** Tailwind CSS v4 with dark mode (OKLCH-based theme)
|
|
||||||
- **Analytics:** Plausible (privacy-focused, proxied through Next.js)
|
|
||||||
- **Monitoring:** Sentry error tracking & performance
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
- **Real-time:** Convex reactive queries
|
|
||||||
- **File Storage:** Built-in file upload/storage
|
|
||||||
- **Auth:** Multi-provider authentication
|
|
||||||
- **Type-safe:** Full TypeScript with generated types
|
|
||||||
- **Self-hosted:** Complete control over your data
|
|
||||||
|
|
||||||
### Developer Experience
|
|
||||||
|
|
||||||
- **Monorepo:** Turborepo for efficient builds and caching
|
|
||||||
- **Type Safety:** Strict TypeScript throughout
|
|
||||||
- **Code Quality:** ESLint + Prettier with auto-fix
|
|
||||||
- **Hot Reload:** Fast refresh for all packages
|
|
||||||
- **Catalog Deps:** Centralized dependency version management
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Production Deployment (Docker)
|
Production Compose retains the self-hosted Convex backend/dashboard and accepts
|
||||||
|
an external Payload `PAYLOAD_DB_URL`. Its commented Postgres service remains an
|
||||||
Once the Convex containers are running (they only need to be started once), deploying
|
optional Payload database. Gitea runs the quality gate first, builds the Next
|
||||||
a new version of the Next.js app is a one-command workflow:
|
image from a temporary Gitea-secret env file, then pushes SHA and `latest` tags.
|
||||||
|
CI never installs or invokes Infisical.
|
||||||
```bash
|
|
||||||
./scripts/build-next-app
|
|
||||||
```
|
|
||||||
|
|
||||||
To start all services from scratch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/docker-compose up -d backend dashboard
|
|
||||||
# Wait for backend health check to pass, then:
|
|
||||||
./scripts/docker-compose up -d next
|
|
||||||
```
|
|
||||||
|
|
||||||
**Services:**
|
|
||||||
|
|
||||||
- `next-app` — Next.js standalone build
|
|
||||||
- `convex-backend` — Convex backend (port 3210)
|
|
||||||
- `convex-dashboard` — Admin dashboard (port 6791)
|
|
||||||
|
|
||||||
**Network:** Uses `nginx-bridge` Docker network (reverse proxy via nginx-proxy-manager).
|
|
||||||
|
|
||||||
### Production Checklist
|
|
||||||
|
|
||||||
- [ ] Fill out root `/.env` with all app, Payload, and Docker Compose values
|
|
||||||
- [ ] Start `convex-backend` and `convex-dashboard` containers
|
|
||||||
- [ ] Generate and set `CONVEX_SELF_HOSTED_ADMIN_KEY` via `./scripts/generate-convex-admin-key`
|
|
||||||
- [ ] Reverse-proxy both Convex services via nginx-proxy-manager with SSL
|
|
||||||
- [ ] Generate JWT keys and sync all env vars to Convex (`bun with-env npx convex env set ...`)
|
|
||||||
- [ ] Update `CONVEX_SITE_URL` in the Convex Dashboard to your production Next.js URL
|
|
||||||
- [ ] Build and start the `next-app` container
|
|
||||||
- [ ] Back up `docker/data/` regularly (contains all Convex database data)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- **[AGENTS.md](./AGENTS.md)** — Comprehensive guide for AI agents & developers
|
|
||||||
- **[docs/payload-cms.md](./docs/payload-cms.md)** — How Payload is wired up and how to extend it in this template
|
|
||||||
- **[Convex Docs](https://docs.convex.dev)** — Official Convex documentation
|
|
||||||
- **[Turborepo Docs](https://turbo.build/repo/docs)** — Turborepo documentation
|
|
||||||
- **[Next.js Docs](https://nextjs.org/docs)** — Next.js documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Backend typecheck shows TypeScript help message
|
|
||||||
|
|
||||||
This is expected behavior. The backend package follows Convex's structure with only
|
|
||||||
`convex/tsconfig.json` (no root tsconfig). Running `bun typecheck` from the repo root
|
|
||||||
will show TypeScript's help text for `@gib/backend` — this is not an error.
|
|
||||||
|
|
||||||
### Imports from Convex require `.js` extension
|
|
||||||
|
|
||||||
The project uses ESM (`"type": "module"`), which requires explicit file extensions:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ Correct
|
|
||||||
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
|
|
||||||
// ❌ Wrong — will fail at runtime
|
|
||||||
import { api } from '@gib/backend/convex/_generated/api';
|
|
||||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker containers won't start
|
|
||||||
|
|
||||||
1. Check Docker logs: `./scripts/docker-compose logs`
|
|
||||||
2. Verify environment variables in root `/.env`
|
|
||||||
3. Ensure the `nginx-bridge` network exists: `sudo docker network create nginx-bridge`
|
|
||||||
4. Check that the required ports (3210, 6791) are not already in use
|
|
||||||
|
|
||||||
### Only one `.env` file matters
|
|
||||||
|
|
||||||
Use the root `/.env` for everything in this repo, including Docker helper scripts and
|
|
||||||
Docker Compose interpolation. Do not create or rely on a separate `docker/.env`.
|
|
||||||
|
|
||||||
### Auth doesn't work in production
|
|
||||||
|
|
||||||
Make sure `CONVEX_SITE_URL` is set to your production Next.js URL in the **Convex
|
|
||||||
Dashboard** (not just in the root `.env` file). The root `.env` should always contain
|
|
||||||
`http://localhost:3000`; the Dashboard must have your production URL.
|
|
||||||
|
|
||||||
### Catalog updates break workspace
|
|
||||||
|
|
||||||
After updating dependencies, if you see `sherif` errors on `bun install`:
|
|
||||||
|
|
||||||
1. Never use `bun update` inside individual package directories
|
|
||||||
2. Edit the version in root `package.json` catalog section instead
|
|
||||||
3. Run `bun install` from the root
|
|
||||||
4. Verify with `bun lint:ws`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
This is a personal monorepo template. Feel free to fork and adapt for your needs.
|
|
||||||
|
|
||||||
### Code Style
|
|
||||||
|
|
||||||
- Single quotes, trailing commas
|
|
||||||
- 80 character line width
|
|
||||||
- ESLint + Prettier enforced
|
|
||||||
- `const fn = () => {}` over `function fn()` (strong preference)
|
|
||||||
- Import order: Types → React → Next → Third-party → @gib → Local
|
|
||||||
|
|
||||||
Run `bun lint:fix` and `bun format:fix` before committing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acknowledgments
|
|
||||||
|
|
||||||
Built with inspiration from:
|
|
||||||
|
|
||||||
- [T3 Turbo](https://github.com/t3-oss/create-t3-turbo)
|
|
||||||
- [Convex](https://convex.dev)
|
|
||||||
- [shadcn/ui](https://ui.shadcn.com)
|
|
||||||
- [Turborepo](https://turbo.build)
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
[{"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts":"1","/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts":"2","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx":"3","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx":"4","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx":"5","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts":"6","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/convex.ts":"7","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts":"8"},{"size":28,"mtime":1768155639000,"results":"9","hashOfConfig":"10"},{"size":246,"mtime":1766222924000,"results":"11","hashOfConfig":"10"},{"size":836,"mtime":1774546669443,"results":"12","hashOfConfig":"10"},{"size":1935,"mtime":1774546669443,"results":"13","hashOfConfig":"10"},{"size":678,"mtime":1774546669443,"results":"14","hashOfConfig":"10"},{"size":880,"mtime":1768155639000,"results":"15","hashOfConfig":"10"},{"size":909,"mtime":1774546669443,"results":"16","hashOfConfig":"10"},{"size":272,"mtime":1768155639000,"results":"17","hashOfConfig":"10"},{"filePath":"18","messages":"19","suppressedMessages":"20","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"x7pzu2",{"filePath":"21","messages":"22","suppressedMessages":"23","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","suppressedMessages":"26","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"27","messages":"28","suppressedMessages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","suppressedMessages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"33","messages":"34","suppressedMessages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","suppressedMessages":"38","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"39","messages":"40","suppressedMessages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/convex.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",[],[]]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22"],{"key":"23","value":"24"},{"key":"25","value":"26"},{"key":"27","value":"28"},{"key":"29","value":"30"},{"key":"31","value":"32"},{"key":"33","value":"34"},{"key":"35","value":"36"},{"key":"37","value":"38"},{"key":"39","value":"40"},{"key":"41","value":"42"},{"key":"43","value":"44"},{"key":"45","value":"46"},{"key":"47","value":"48"},{"key":"49","value":"50"},{"key":"51","value":"52"},{"key":"53","value":"54"},{"key":"55","value":"56"},{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",{"size":1935,"mtime":1774546669443,"hash":"67","data":"68"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1768155639000,"hash":"69","data":"70"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"71"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":65,"mtime":1774546669443,"hash":"72","data":"73"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":678,"mtime":1774546669443,"hash":"74","data":"75"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/convex.ts",{"size":909,"mtime":1774546669443,"data":"76"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":566,"mtime":1774546669443,"hash":"77","data":"78"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":836,"mtime":1774546669443,"hash":"79","data":"80"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":89,"mtime":1774546669443,"hash":"81","data":"82"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.cache/.prettiercache",{"size":4789,"mtime":1774718093704},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.cache/.eslintcache",{"size":3079,"mtime":1774717490445},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"83"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/package.json",{"size":2228,"mtime":1774588990619,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":171,"mtime":1774031879321,"hash":"86","data":"87"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":274,"mtime":1774546669443,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639000,"hash":"92","data":"93"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"94","data":"95"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639000,"hash":"96","data":"97"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"98","data":"99"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768155639000,"hash":"100","data":"101"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1768155639000,"hash":"102","data":"103"},"73c235a66242df70b69394cce29d1ed3",{"hashOfOptions":"104"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"105"},"1e8ac0d261e95efb19d290ffcf70ce36","b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"106"},"ead19d73283f9d8e08b55c896c9fd570",{"hashOfOptions":"107"},{"hashOfOptions":"108"},"a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"109"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"110"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"111"},"863da15dbd856008b7c24077ca746d91","d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"112"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"113"},"1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"114"},"6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"115"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"116"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"117"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"118"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"119"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"120"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"121"},"3000879843","3103968608","384110377","141502567","1235541372","1050155876","2025343866","4147067111","4228440757","3451484829","4039211292","3318113268","2585374463","45764855","1418614640","2754339483","1950506033","3468872477"]
|
|
||||||
@@ -4,15 +4,16 @@
|
|||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "git clean -xdf .cache .expo .turbo android ios node_modules",
|
"clean": "git clean -xdf .cache .expo .turbo android ios node_modules",
|
||||||
"dev": "expo start",
|
"dev": "bun with-env expo start",
|
||||||
"dev:tunnel": "expo start --tunnel",
|
"dev:tunnel": "bun with-env expo start --tunnel",
|
||||||
"dev:android": "expo start --android",
|
"dev:android": "bun with-env expo start --android",
|
||||||
"dev:ios": "expo start --ios",
|
"dev:ios": "bun with-env expo start --ios",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore",
|
||||||
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "catalog:convex",
|
"@convex-dev/auth": "catalog:convex",
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module '*.css';
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ import { reactConfig } from '@gib/eslint-config/react';
|
|||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(
|
||||||
{
|
{
|
||||||
ignores: ['.next/**'],
|
ignores: ['.next/**', 'payload-types.ts'],
|
||||||
},
|
},
|
||||||
baseConfig,
|
baseConfig,
|
||||||
reactConfig,
|
reactConfig,
|
||||||
|
|||||||
+10
-3
@@ -5,7 +5,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun with-env next build",
|
"build": "bun with-env next build",
|
||||||
"build:env": "bun with-env next build",
|
"build:docker": "next build --webpack",
|
||||||
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
||||||
"dev": "bun with-env next dev --turbo",
|
"dev": "bun with-env next dev --turbo",
|
||||||
"dev:tunnel": "bun with-env next dev --turbo",
|
"dev:tunnel": "bun with-env next dev --turbo",
|
||||||
@@ -14,7 +14,10 @@
|
|||||||
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
||||||
"start": "bun with-env next start",
|
"start": "bun with-env next start",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"with-env": "dotenv -e ../../.env --"
|
"test:unit": "NODE_ENV=test vitest run --project unit",
|
||||||
|
"test:integration": "NODE_ENV=test vitest run --project integration",
|
||||||
|
"test:component": "NODE_ENV=test vitest run --project component",
|
||||||
|
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "catalog:convex",
|
"@convex-dev/auth": "catalog:convex",
|
||||||
@@ -41,15 +44,19 @@
|
|||||||
"@gib/prettier-config": "workspace:*",
|
"@gib/prettier-config": "workspace:*",
|
||||||
"@gib/tailwind-config": "workspace:*",
|
"@gib/tailwind-config": "workspace:*",
|
||||||
"@gib/tsconfig": "workspace:*",
|
"@gib/tsconfig": "workspace:*",
|
||||||
|
"@gib/vitest-config": "workspace:*",
|
||||||
|
"@testing-library/react": "catalog:test",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/react": "catalog:react19",
|
"@types/react": "catalog:react19",
|
||||||
"@types/react-dom": "catalog:react19",
|
"@types/react-dom": "catalog:react19",
|
||||||
"baseline-browser-mapping": "^2.10.11",
|
"baseline-browser-mapping": "^2.10.11",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
|
"jsdom": "catalog:test",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:",
|
||||||
|
"vitest": "catalog:test"
|
||||||
},
|
},
|
||||||
"prettier": "@gib/prettier-config"
|
"prettier": "@gib/prettier-config"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
* This file was automatically generated by Payload.
|
* This file was automatically generated by Payload.
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export const generateMetadata = (): Metadata => {
|
|||||||
const ProfileLayout = async ({
|
const ProfileLayout = async ({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) => {
|
}: Readonly<{ children: React.ReactNode }>) => {
|
||||||
if (!(await isAuthenticatedNextjs()))
|
if (!(await isAuthenticatedNextjs())) redirect('/sign-in');
|
||||||
redirect('/sign-in');
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
export default ProfileLayout;
|
export default ProfileLayout;
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module '@payloadcms/next/css';
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const Greeting = ({ name }: { name: string }) => <p>Hello {name}</p>;
|
||||||
|
|
||||||
|
describe('component test harness', () => {
|
||||||
|
it('renders through jsdom and Testing Library', () => {
|
||||||
|
render(<Greeting name='world' />);
|
||||||
|
expect(screen.getByText('Hello world')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('integration test harness', () => {
|
||||||
|
it('provides modern Node globals', () => {
|
||||||
|
expect(typeof globalThis.structuredClone).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('unit test harness', () => {
|
||||||
|
it('executes isolated assertions', () => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
import { jsdomProject, nodeProject } from '@gib/vitest-config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
projects: [
|
||||||
|
nodeProject('unit', ['tests/unit/**/*.test.{ts,tsx}']),
|
||||||
|
nodeProject('integration', ['tests/integration/**/*.test.{ts,tsx}']),
|
||||||
|
jsdomProject('component', ['tests/component/**/*.test.{ts,tsx}']),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
+29
-4
@@ -1,24 +1,49 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM oven/bun:alpine AS base
|
FROM docker.io/oven/bun:1.3.10-alpine AS base
|
||||||
|
|
||||||
# Builder stage
|
# Builder stage
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG SENTRY_AUTH_TOKEN
|
||||||
|
ARG SENTRY_DISABLE_AUTO_UPLOAD=false
|
||||||
|
ARG NEXT_PUBLIC_SITE_URL
|
||||||
|
ARG NEXT_PUBLIC_CONVEX_URL
|
||||||
|
ARG NEXT_PUBLIC_PLAUSIBLE_URL
|
||||||
|
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||||
|
ARG NEXT_PUBLIC_SENTRY_URL
|
||||||
|
ARG NEXT_PUBLIC_SENTRY_ORG
|
||||||
|
ARG NEXT_PUBLIC_SENTRY_PROJECT_NAME
|
||||||
|
ARG PAYLOAD_SECRET
|
||||||
|
ARG PAYLOAD_DB_URL
|
||||||
|
|
||||||
|
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
|
||||||
|
ENV SENTRY_DISABLE_AUTO_UPLOAD=$SENTRY_DISABLE_AUTO_UPLOAD
|
||||||
|
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
||||||
|
ENV NEXT_PUBLIC_CONVEX_URL=$NEXT_PUBLIC_CONVEX_URL
|
||||||
|
ENV NEXT_PUBLIC_PLAUSIBLE_URL=$NEXT_PUBLIC_PLAUSIBLE_URL
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_URL=$NEXT_PUBLIC_SENTRY_URL
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_ORG=$NEXT_PUBLIC_SENTRY_ORG
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_PROJECT_NAME=$NEXT_PUBLIC_SENTRY_PROJECT_NAME
|
||||||
|
ENV PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||||
|
ENV PAYLOAD_DB_URL=$PAYLOAD_DB_URL
|
||||||
|
|
||||||
# Copy source code (node_modules excluded via .dockerignore)
|
# Copy source code (node_modules excluded via .dockerignore)
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Install all dependencies
|
# Install all dependencies
|
||||||
RUN bun install
|
ENV HUSKY=0
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
# Build with proper environment
|
# Build with proper environment
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN bun run build --filter=@gib/next
|
RUN cd apps/next && bun run build:docker
|
||||||
|
|
||||||
# Runner stage
|
# Runner stage
|
||||||
FROM node:22-alpine AS runner
|
FROM docker.io/library/node:22-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: convexmonorepo-local
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: convexmonorepo-local-postgres
|
||||||
|
ports: ['${POSTGRES_PORT:-5432}:5432']
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-localdev}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB:-convexmonorepo_payload}
|
||||||
|
volumes: [payload-postgres-data:/var/lib/postgresql/data]
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}']
|
||||||
|
start_period: 10s
|
||||||
|
interval: 5s
|
||||||
|
retries: 10
|
||||||
|
timeout: 5s
|
||||||
|
|
||||||
|
convex-backend:
|
||||||
|
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||||
|
container_name: convexmonorepo-local-convex
|
||||||
|
ports:
|
||||||
|
- '${BACKEND_PORT:-3210}:3210'
|
||||||
|
- '${SITE_PROXY_PORT:-3211}:3211'
|
||||||
|
environment:
|
||||||
|
- INSTANCE_NAME=${LOCAL_INSTANCE_NAME:-convexmonorepo_local}
|
||||||
|
- INSTANCE_SECRET=${LOCAL_INSTANCE_SECRET:-0000000000000000000000000000000000000000000000000000000000000000}
|
||||||
|
- CONVEX_CLOUD_ORIGIN=http://localhost:${BACKEND_PORT:-3210}
|
||||||
|
- CONVEX_SITE_ORIGIN=http://localhost:${SITE_PROXY_PORT:-3211}
|
||||||
|
- DISABLE_BEACON=true
|
||||||
|
- REDACT_LOGS_TO_CLIENT=false
|
||||||
|
- DO_NOT_REQUIRE_SSL=true
|
||||||
|
# Convex uses its own volume by default. A cloned project may opt into
|
||||||
|
# Convex-on-Postgres by configuring a separate database URL here:
|
||||||
|
# - POSTGRES_URL=postgres://user:password@postgres:5432/convex?sslmode=disable
|
||||||
|
volumes: [convex-data:/convex/data]
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'curl', '-f', 'http://localhost:3210/version']
|
||||||
|
start_period: 10s
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
timeout: 5s
|
||||||
|
stop_grace_period: 10s
|
||||||
|
stop_signal: SIGINT
|
||||||
|
|
||||||
|
convex-dashboard:
|
||||||
|
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||||
|
container_name: convexmonorepo-local-convex-dashboard
|
||||||
|
ports: ['${DASHBOARD_PORT:-6791}:6791']
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_DEPLOYMENT_URL=http://localhost:${BACKEND_PORT:-3210}
|
||||||
|
depends_on:
|
||||||
|
convex-backend:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
payload-postgres-data:
|
||||||
|
convex-data:
|
||||||
+17
-2
@@ -7,7 +7,19 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: ./docker/Dockerfile
|
dockerfile: ./docker/Dockerfile
|
||||||
image: ${NEXT_CONTAINER_NAME}:latest
|
args:
|
||||||
|
SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN}
|
||||||
|
SENTRY_DISABLE_AUTO_UPLOAD: ${SENTRY_DISABLE_AUTO_UPLOAD:-false}
|
||||||
|
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL}
|
||||||
|
NEXT_PUBLIC_CONVEX_URL: ${NEXT_PUBLIC_CONVEX_URL}
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_URL: ${NEXT_PUBLIC_PLAUSIBLE_URL}
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN}
|
||||||
|
NEXT_PUBLIC_SENTRY_URL: ${NEXT_PUBLIC_SENTRY_URL}
|
||||||
|
NEXT_PUBLIC_SENTRY_ORG: ${NEXT_PUBLIC_SENTRY_ORG}
|
||||||
|
NEXT_PUBLIC_SENTRY_PROJECT_NAME: ${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
|
||||||
|
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
|
||||||
|
PAYLOAD_DB_URL: ${PAYLOAD_DB_URL}
|
||||||
|
image: convexmonorepo-next:latest
|
||||||
#image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
|
#image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
|
||||||
container_name: ${NEXT_CONTAINER_NAME}
|
container_name: ${NEXT_CONTAINER_NAME}
|
||||||
environment:
|
environment:
|
||||||
@@ -43,12 +55,13 @@ services:
|
|||||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
environment:
|
environment:
|
||||||
- INSTANCE_NAME=${INSTANCE_NAME}
|
- INSTANCE_NAME=${INSTANCE_NAME}
|
||||||
#- INSTANCE_SECRET
|
- INSTANCE_SECRET=${INSTANCE_SECRET}
|
||||||
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}}
|
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}}
|
||||||
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${SITE_PROXY_PORT:-3211}}
|
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${SITE_PROXY_PORT:-3211}}
|
||||||
- DISABLE_BEACON=${DISABLE_BEACON:-true}
|
- DISABLE_BEACON=${DISABLE_BEACON:-true}
|
||||||
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
|
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
|
||||||
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
|
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
|
||||||
|
# Optional: Convex-on-Postgres is not the template default.
|
||||||
#- POSTGRES_URL=${POSTGRES_URL}
|
#- POSTGRES_URL=${POSTGRES_URL}
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
@@ -80,6 +93,8 @@ services:
|
|||||||
stop_grace_period: 10s
|
stop_grace_period: 10s
|
||||||
stop_signal: SIGINT
|
stop_signal: SIGINT
|
||||||
|
|
||||||
|
# Optional production Postgres. Payload may instead continue using an
|
||||||
|
# external/VPN PAYLOAD_DB_URL. Convex does not share this DB by default.
|
||||||
#convexmonorepo-postgresql:
|
#convexmonorepo-postgresql:
|
||||||
#image: postgres:17
|
#image: postgres:17
|
||||||
#container_name: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
|
#container_name: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
|
||||||
|
|||||||
+39
-419
@@ -1,424 +1,44 @@
|
|||||||
# Payload CMS In `convex-monorepo`
|
# Payload CMS
|
||||||
|
|
||||||
This document explains how Payload CMS is integrated into this template, what it manages
|
Payload is embedded in `apps/next` and manages marketing content. Convex remains
|
||||||
today, and how to extend it safely when you want more editable marketing content.
|
the application backend for auth, realtime data, files, and workflows.
|
||||||
|
|
||||||
## Current Scope
|
Configuration lives in `apps/next/src/payload.config.ts`. Payload uses
|
||||||
|
`PAYLOAD_SECRET`, `PAYLOAD_DB_URL`, and `NEXT_PUBLIC_SITE_URL` from the validated
|
||||||
Payload currently powers the editable marketing layer inside the Next.js app.
|
environment contract. Local `dev` values come from Infisical and point Payload
|
||||||
|
to Postgres at `localhost:5432`, because Next runs on the host.
|
||||||
Today that means:
|
|
||||||
|
Start the isolated services with `bun db:up`, then run `bun dev:next`. The local
|
||||||
- the landing page at `/`
|
database persists across `bun db:down`; `bun db:down:wipe` removes it. Local
|
||||||
- the admin UI at `/admin`
|
Convex data is stored in a separate volume and Convex does not receive
|
||||||
- the REST API under `/api`
|
`POSTGRES_URL` by default.
|
||||||
- the GraphQL API under `/api/graphql`
|
|
||||||
- live preview for the landing page
|
To refresh the local seed snapshot from current staging/production-derived
|
||||||
|
Payload content, stop Next and run `bun db:sync:payload`. The command uses
|
||||||
Payload is not replacing Convex.
|
staging only as a read-only dump source, verifies the restore destination is
|
||||||
|
localhost, writes `.local/payload-staging.dump`, and force-applies it locally.
|
||||||
- Convex still handles auth, backend logic, realtime data, files, and app workflows
|
Normal `bun db:up` never contacts staging; it only restores that local snapshot
|
||||||
- Payload owns marketing content editing inside the Next app
|
when `.local/payload-seed-state.env` is absent or does not match the current
|
||||||
|
snapshot. The marker stays outside Postgres so Payload schema push does not see
|
||||||
## High-Level Architecture
|
an unmanaged table. The snapshot is gitignored and contains sensitive
|
||||||
|
production-derived records, including Payload users and password hashes. Do not
|
||||||
### Core config
|
share or commit it.
|
||||||
|
|
||||||
Payload is configured in `apps/next/src/payload.config.ts`.
|
The public landing page is in `apps/next/src/app/(frontend)/page.tsx`; schemas
|
||||||
|
are under `apps/next/src/payload`, and rendering components are under
|
||||||
Important pieces there:
|
`apps/next/src/components/landing`. Custom routes belong in `(frontend)`, not
|
||||||
|
the generated `(payload)` route group.
|
||||||
- `postgresAdapter(...)` connects Payload to Postgres via `PAYLOAD_DB_URL`
|
|
||||||
- `secret: env.PAYLOAD_SECRET` secures Payload
|
Do not manually edit Payload-generated routes, layouts, import maps, or
|
||||||
- `collections: [Users]` currently registers only the Payload admin user collection
|
`apps/next/payload-types.ts`. When schema changes require regeneration, run the
|
||||||
- `globals: [LandingPage]` registers the editable landing-page global
|
Payload generators through the package's environment wrapper:
|
||||||
- `lexicalEditor()` enables the Payload editor setup
|
|
||||||
|
```sh
|
||||||
### Current Payload data model
|
|
||||||
|
|
||||||
This template is intentionally small right now.
|
|
||||||
|
|
||||||
Current Payload entities:
|
|
||||||
|
|
||||||
- `users` collection in `apps/next/src/payload/collections/users.ts`
|
|
||||||
- used by Payload admin itself
|
|
||||||
- `landing-page` global in `apps/next/src/payload/globals/landing-page.ts`
|
|
||||||
- stores the homepage layout as a block list
|
|
||||||
|
|
||||||
There is no `pages` collection in this template yet.
|
|
||||||
|
|
||||||
That means the current pattern is:
|
|
||||||
|
|
||||||
- one global for one marketing page
|
|
||||||
- one frontend route that reads that global
|
|
||||||
- reusable block schemas that editors can reorder inside the global
|
|
||||||
|
|
||||||
## How the Landing Page Works
|
|
||||||
|
|
||||||
### Schema side
|
|
||||||
|
|
||||||
The landing page is defined by:
|
|
||||||
|
|
||||||
- `apps/next/src/payload/globals/landing-page.ts`
|
|
||||||
- `apps/next/src/payload/globals/landing-page-blocks.ts`
|
|
||||||
|
|
||||||
`landing-page.ts` defines a single global with a `layout` field of type `blocks`.
|
|
||||||
|
|
||||||
`landing-page-blocks.ts` defines the actual editable block types, including:
|
|
||||||
|
|
||||||
- `hero`
|
|
||||||
- `logoCloud`
|
|
||||||
- `features`
|
|
||||||
- `stats`
|
|
||||||
- `techStack`
|
|
||||||
- `testimonials`
|
|
||||||
- `pricing`
|
|
||||||
- `faq`
|
|
||||||
- `cta`
|
|
||||||
|
|
||||||
### Frontend side
|
|
||||||
|
|
||||||
The frontend route is:
|
|
||||||
|
|
||||||
- `apps/next/src/app/(frontend)/page.tsx`
|
|
||||||
|
|
||||||
That route calls:
|
|
||||||
|
|
||||||
- `apps/next/src/lib/payload/get-landing-page-content.ts`
|
|
||||||
|
|
||||||
That helper fetches the `landing-page` global from Payload and merges it with fallback
|
|
||||||
content from:
|
|
||||||
|
|
||||||
- `apps/next/src/components/landing/content.ts`
|
|
||||||
|
|
||||||
That fallback layer is important. It means the page can still render even if:
|
|
||||||
|
|
||||||
- the Payload DB is empty
|
|
||||||
- an editor saves partial content
|
|
||||||
- a newly added field is missing from older content
|
|
||||||
|
|
||||||
### Rendering flow
|
|
||||||
|
|
||||||
The homepage flow is:
|
|
||||||
|
|
||||||
1. `/` loads in `apps/next/src/app/(frontend)/page.tsx`
|
|
||||||
2. the page checks whether `?preview=true` is enabled
|
|
||||||
3. `getLandingPageContent(isPreview)` fetches the `landing-page` global
|
|
||||||
4. the fetched global is merged with defaults from `apps/next/src/components/landing/content.ts`
|
|
||||||
5. `LandingPageBuilder` renders the normalized block data
|
|
||||||
6. `RefreshRouteOnSave` keeps preview mode refreshed after saves
|
|
||||||
|
|
||||||
## Live Preview
|
|
||||||
|
|
||||||
Live preview is configured in:
|
|
||||||
|
|
||||||
- `apps/next/src/payload/globals/landing-page.ts`
|
|
||||||
|
|
||||||
The preview URL is:
|
|
||||||
|
|
||||||
- `/?preview=true`
|
|
||||||
|
|
||||||
The frontend bridge is:
|
|
||||||
|
|
||||||
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
|
|
||||||
|
|
||||||
That component uses Payload's live-preview utilities plus Next's router refresh so
|
|
||||||
saved changes show up in the preview iframe.
|
|
||||||
|
|
||||||
Important requirement:
|
|
||||||
|
|
||||||
- `NEXT_PUBLIC_SITE_URL` must point to the correct frontend origin
|
|
||||||
|
|
||||||
If preview appears blank, stale, or disconnected, that is one of the first values to
|
|
||||||
check.
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Payload depends on these env vars:
|
|
||||||
|
|
||||||
- `PAYLOAD_SECRET`
|
|
||||||
- `PAYLOAD_DB_URL`
|
|
||||||
- `NEXT_PUBLIC_SITE_URL`
|
|
||||||
|
|
||||||
Why they matter:
|
|
||||||
|
|
||||||
- `PAYLOAD_SECRET` secures Payload
|
|
||||||
- `PAYLOAD_DB_URL` connects Payload to Postgres
|
|
||||||
- `NEXT_PUBLIC_SITE_URL` is used by live preview and frontend URL generation
|
|
||||||
|
|
||||||
All of them live in the single root `/.env` file.
|
|
||||||
|
|
||||||
## Adding a New Landing-Page Block
|
|
||||||
|
|
||||||
If you want editors to control a new section type, add a new block.
|
|
||||||
|
|
||||||
### 1. Add the block schema
|
|
||||||
|
|
||||||
Update:
|
|
||||||
|
|
||||||
- `apps/next/src/payload/globals/landing-page-blocks.ts`
|
|
||||||
|
|
||||||
Add a new block object to the `landingPageBlocks` array.
|
|
||||||
|
|
||||||
### 2. Extend the frontend content types
|
|
||||||
|
|
||||||
Update:
|
|
||||||
|
|
||||||
- `apps/next/src/components/landing/content.ts`
|
|
||||||
|
|
||||||
You usually need to:
|
|
||||||
|
|
||||||
- add the new block TypeScript shape
|
|
||||||
- add default content for it
|
|
||||||
- add sanitizing / merging logic for it
|
|
||||||
- include it in the landing-page block union
|
|
||||||
|
|
||||||
### 3. Teach the landing-page builder to render it
|
|
||||||
|
|
||||||
Update the landing-page rendering layer in the landing components so the new block type
|
|
||||||
actually appears on the page.
|
|
||||||
|
|
||||||
If you add schema without renderer support, editors can save the block but the frontend
|
|
||||||
will not know what to do with it.
|
|
||||||
|
|
||||||
### 4. Regenerate generated Payload files if needed
|
|
||||||
|
|
||||||
Useful commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/next
|
cd apps/next
|
||||||
bun with-env bunx payload generate:types --config src/payload.config.ts
|
bun with-env bunx payload generate:types --config src/payload.config.ts
|
||||||
bun with-env bunx payload generate:db-schema --config src/payload.config.ts
|
bun with-env bunx payload generate:db-schema --config src/payload.config.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
That refreshes:
|
For staging, use `INFISICAL_ENV=staging`; CI uses temporary Gitea-injected
|
||||||
|
configuration and never Infisical. Production Payload may use the existing
|
||||||
- `apps/next/payload-types.ts`
|
external/VPN Postgres URL or the commented optional Compose Postgres service.
|
||||||
- `apps/next/src/payload-generated-schema.ts`
|
|
||||||
|
|
||||||
Do not hand-edit those files.
|
|
||||||
|
|
||||||
## Copy-Paste Block Template
|
|
||||||
|
|
||||||
Use this as a starting point when you want a new landing-page block.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
slug: 'exampleBlock',
|
|
||||||
labels: {
|
|
||||||
singular: 'Example Block',
|
|
||||||
plural: 'Example Blocks',
|
|
||||||
},
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'heading',
|
|
||||||
type: 'text',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
type: 'textarea',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'items',
|
|
||||||
type: 'array',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'label',
|
|
||||||
type: 'text',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After adding it:
|
|
||||||
|
|
||||||
1. add the block to `landingPageBlocks`
|
|
||||||
2. add the TypeScript/content shape in `apps/next/src/components/landing/content.ts`
|
|
||||||
3. add fallback content and sanitizers
|
|
||||||
4. add rendering support in the landing components
|
|
||||||
|
|
||||||
## Copy-Paste Global-Backed Route Template
|
|
||||||
|
|
||||||
Use this when you want another singular Payload-managed page in this template and you do
|
|
||||||
not need a full `pages` collection yet.
|
|
||||||
|
|
||||||
This is the best next step for one-off pages like `about`, `contact`, or `pricing`.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { draftMode } from 'next/headers';
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
import { RefreshRouteOnSave } from '@/components/payload/refresh-route-on-save';
|
|
||||||
import { getPayloadClient } from '@/lib/payload/get-payload';
|
|
||||||
|
|
||||||
const AboutPage = async () => {
|
|
||||||
const { isEnabled: isPreview } = await draftMode();
|
|
||||||
const payload = await getPayloadClient();
|
|
||||||
|
|
||||||
const aboutPage = await payload.findGlobal({
|
|
||||||
slug: 'about-page',
|
|
||||||
draft: isPreview,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!aboutPage) return notFound();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
{isPreview ? <RefreshRouteOnSave /> : null}
|
|
||||||
{/* render the normalized Payload data here */}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AboutPage;
|
|
||||||
```
|
|
||||||
|
|
||||||
For this pattern you would also:
|
|
||||||
|
|
||||||
1. add a new global in `apps/next/src/payload/globals/`
|
|
||||||
2. register it in `apps/next/src/payload.config.ts`
|
|
||||||
3. create the fetch/normalize helper in `apps/next/src/lib/payload/`
|
|
||||||
4. create fallback content in the relevant component/content files
|
|
||||||
|
|
||||||
## When To Add Another Global vs. A `pages` Collection
|
|
||||||
|
|
||||||
### Add another global when:
|
|
||||||
|
|
||||||
- the page is singular
|
|
||||||
- the route is fixed
|
|
||||||
- the content model is custom
|
|
||||||
- you only need a few editable marketing pages
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- homepage
|
|
||||||
- contact page
|
|
||||||
- about page
|
|
||||||
- pricing page
|
|
||||||
|
|
||||||
### Add a `pages` collection when:
|
|
||||||
|
|
||||||
- you need many CMS-managed pages
|
|
||||||
- page routing should be slug-driven
|
|
||||||
- many pages share the same block system
|
|
||||||
- editors should be able to create pages without new code for each route
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- service pages
|
|
||||||
- case studies
|
|
||||||
- marketing subpages
|
|
||||||
- partner pages
|
|
||||||
|
|
||||||
## How To Migrate Another Hardcoded Page To Payload In This Repo
|
|
||||||
|
|
||||||
Because this template currently uses a global-backed landing page, the most natural next
|
|
||||||
migration path is usually another Payload global.
|
|
||||||
|
|
||||||
### Step 1: Audit the current page
|
|
||||||
|
|
||||||
Identify:
|
|
||||||
|
|
||||||
- route path
|
|
||||||
- metadata
|
|
||||||
- JSON-LD or structured data
|
|
||||||
- reusable sections
|
|
||||||
- what should stay hardcoded vs. what should become editable
|
|
||||||
|
|
||||||
### Step 2: Decide the storage model
|
|
||||||
|
|
||||||
Ask:
|
|
||||||
|
|
||||||
- is this a one-off page? use another global
|
|
||||||
- is this the beginning of many similar pages? consider creating a `pages` collection
|
|
||||||
|
|
||||||
### Step 3: Model the editable fields
|
|
||||||
|
|
||||||
Prefer structured fields and blocks over dumping everything into one rich text area.
|
|
||||||
|
|
||||||
### Step 4: Add fallback content
|
|
||||||
|
|
||||||
Keep the same resilience pattern used by the current landing page:
|
|
||||||
|
|
||||||
- define defaults in component/content files
|
|
||||||
- merge Payload content into those defaults
|
|
||||||
|
|
||||||
This avoids blank pages when content is incomplete.
|
|
||||||
|
|
||||||
### Step 5: Wire the route
|
|
||||||
|
|
||||||
Create a server route in `apps/next/src/app/(frontend)/...` that:
|
|
||||||
|
|
||||||
1. reads preview state
|
|
||||||
2. fetches Payload data
|
|
||||||
3. normalizes or merges it
|
|
||||||
4. renders the page
|
|
||||||
5. includes `RefreshRouteOnSave` in preview mode
|
|
||||||
|
|
||||||
### Step 6: Verify preview and admin flow
|
|
||||||
|
|
||||||
After migration, verify:
|
|
||||||
|
|
||||||
- the route renders normally
|
|
||||||
- the page updates from the admin
|
|
||||||
- preview refresh works
|
|
||||||
- the page still renders when content is partial
|
|
||||||
|
|
||||||
## Common Failure Modes
|
|
||||||
|
|
||||||
### 1. Homepage renders old or partial content
|
|
||||||
|
|
||||||
Check:
|
|
||||||
|
|
||||||
- whether the landing-page global actually saved
|
|
||||||
- whether preview mode is enabled
|
|
||||||
- whether your fallback merge logic is masking a missing field
|
|
||||||
|
|
||||||
### 2. Live preview does not refresh
|
|
||||||
|
|
||||||
Check:
|
|
||||||
|
|
||||||
- `NEXT_PUBLIC_SITE_URL`
|
|
||||||
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
|
|
||||||
- the `admin.livePreview.url` value in the Payload global config
|
|
||||||
|
|
||||||
### 3. New block fields do not show up in the frontend
|
|
||||||
|
|
||||||
Usually means the schema changed but the frontend data contract did not.
|
|
||||||
|
|
||||||
Check:
|
|
||||||
|
|
||||||
- `apps/next/src/payload/globals/landing-page-blocks.ts`
|
|
||||||
- `apps/next/src/components/landing/content.ts`
|
|
||||||
- the landing-page render path
|
|
||||||
|
|
||||||
### 4. Payload admin works but the page looks empty
|
|
||||||
|
|
||||||
Usually means one of these:
|
|
||||||
|
|
||||||
- the block renderer support is missing
|
|
||||||
- the global returned data that your merge layer is not handling yet
|
|
||||||
- the page route is not reading the correct global or preview state
|
|
||||||
|
|
||||||
## Important Files At A Glance
|
|
||||||
|
|
||||||
- config: `apps/next/src/payload.config.ts`
|
|
||||||
- payload global: `apps/next/src/payload/globals/landing-page.ts`
|
|
||||||
- landing-page block schemas: `apps/next/src/payload/globals/landing-page-blocks.ts`
|
|
||||||
- payload client helper: `apps/next/src/lib/payload/get-payload.ts`
|
|
||||||
- landing-page fetch helper: `apps/next/src/lib/payload/get-landing-page-content.ts`
|
|
||||||
- landing-page content defaults: `apps/next/src/components/landing/content.ts`
|
|
||||||
- landing-page route: `apps/next/src/app/(frontend)/page.tsx`
|
|
||||||
- preview refresh bridge: `apps/next/src/components/payload/refresh-route-on-save.tsx`
|
|
||||||
- generated types: `apps/next/payload-types.ts`
|
|
||||||
- generated db schema: `apps/next/src/payload-generated-schema.ts`
|
|
||||||
|
|
||||||
## Practical Rule Of Thumb
|
|
||||||
|
|
||||||
If the task is “make marketing content editable,” reach for Payload.
|
|
||||||
|
|
||||||
If the task is “build application logic, auth, data workflows, or realtime product
|
|
||||||
features,” it probably belongs in Convex instead.
|
|
||||||
|
|||||||
+58
-2
@@ -21,6 +21,18 @@
|
|||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"catalogs": {
|
"catalogs": {
|
||||||
|
"test": {
|
||||||
|
"@edge-runtime/vm": "^5.0.0",
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
|
"convex-test": "^0.0.53",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
|
"vitest": "^4.1.8"
|
||||||
|
},
|
||||||
"convex": {
|
"convex": {
|
||||||
"@convex-dev/auth": "^0.0.87",
|
"@convex-dev/auth": "^0.0.87",
|
||||||
"convex": "^1.34.1"
|
"convex": "^1.34.1"
|
||||||
@@ -42,7 +54,13 @@
|
|||||||
"dev:next:web": "turbo run dev:web -F @gib/next -F @gib/backend",
|
"dev:next:web": "turbo run dev:web -F @gib/next -F @gib/backend",
|
||||||
"dev:expo": "turbo run dev -F @gib/expo -F @gib/backend",
|
"dev:expo": "turbo run dev -F @gib/expo -F @gib/backend",
|
||||||
"dev:backend": "turbo run dev -F @gib/backend",
|
"dev:backend": "turbo run dev -F @gib/backend",
|
||||||
|
"dev:staging": "INFISICAL_ENV=staging turbo run dev -F @gib/next -F @gib/backend",
|
||||||
"dev:expo:tunnel": "turbo run dev:tunnel -F @gib/expo -F @gib/backend",
|
"dev:expo:tunnel": "turbo run dev:tunnel -F @gib/expo -F @gib/backend",
|
||||||
|
"db:up": "bash scripts/db/up",
|
||||||
|
"db:down": "bash scripts/db/down",
|
||||||
|
"db:down:wipe": "bash scripts/db/down --wipe",
|
||||||
|
"db:seed:payload": "bash scripts/db/seed-payload",
|
||||||
|
"db:sync:payload": "bash scripts/db/sync-payload",
|
||||||
"format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache",
|
"format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache",
|
||||||
"format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache",
|
"format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache",
|
||||||
"lint": "turbo run lint --continue -- --cache --cache-location .cache/.eslintcache",
|
"lint": "turbo run lint --continue -- --cache --cache-location .cache/.eslintcache",
|
||||||
@@ -51,19 +69,57 @@
|
|||||||
"patch:usesend": "node scripts/patch-usesend.mjs",
|
"patch:usesend": "node scripts/patch-usesend.mjs",
|
||||||
"postinstall": "bun patch:usesend && bun lint:ws",
|
"postinstall": "bun patch:usesend && bun lint:ws",
|
||||||
"typecheck": "turbo run typecheck",
|
"typecheck": "turbo run typecheck",
|
||||||
|
"test": "turbo run test:unit test:integration test:component",
|
||||||
|
"test:unit": "turbo run test:unit",
|
||||||
|
"test:integration": "turbo run test:integration",
|
||||||
|
"test:component": "turbo run test:component",
|
||||||
|
"test:e2e": "bash scripts/e2e",
|
||||||
|
"test:all": "turbo run test:unit test:integration test:component && bun test:e2e",
|
||||||
|
"ci:check": "bun lint:ws && turbo run lint typecheck test:unit test:integration test:component --concurrency=2 && bun test:e2e",
|
||||||
|
"prepare": "husky",
|
||||||
"ui-add": "turbo run ui-add",
|
"ui-add": "turbo run ui-add",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios"
|
"ios": "expo run:ios"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gib/prettier-config": "workspace:",
|
"@gib/prettier-config": "workspace:",
|
||||||
"@turbo/gen": "^2.8.21",
|
"@turbo/gen": "^2.9.18",
|
||||||
"dotenv-cli": "11.0.0",
|
"dotenv-cli": "11.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^17.0.7",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"turbo": "^2.8.21",
|
"turbo": "^2.9.18",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
},
|
},
|
||||||
"prettier": "@gib/prettier-config",
|
"prettier": "@gib/prettier-config",
|
||||||
|
"lint-staged": {
|
||||||
|
"apps/next/**/*.{ts,tsx}": [
|
||||||
|
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config apps/next/eslint.config.ts",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"apps/expo/**/*.{ts,tsx}": [
|
||||||
|
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config apps/expo/eslint.config.mts",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"packages/backend/**/*.{ts,tsx}": [
|
||||||
|
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config packages/backend/eslint.config.ts",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"packages/ui/**/*.{ts,tsx}": [
|
||||||
|
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config packages/ui/eslint.config.ts",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"tools/tailwind/**/*.{ts,tsx}": [
|
||||||
|
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config tools/tailwind/eslint.config.ts",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"tools/{eslint,prettier,typescript,vitest}/**/*.{ts,tsx}": [
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"**/*.{js,mjs,cjs,md,json,yaml,yml,css}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@sentry/cli",
|
"@sentry/cli",
|
||||||
"core-js-pure",
|
"core-js-pure",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
-5
@@ -11,12 +11,11 @@
|
|||||||
import type {
|
import type {
|
||||||
DataModelFromSchemaDefinition,
|
DataModelFromSchemaDefinition,
|
||||||
DocumentByName,
|
DocumentByName,
|
||||||
SystemTableNames,
|
|
||||||
TableNamesInDataModel,
|
TableNamesInDataModel,
|
||||||
} from 'convex/server';
|
SystemTableNames,
|
||||||
import type { GenericId } from 'convex/values';
|
} from "convex/server";
|
||||||
|
import type { GenericId } from "convex/values";
|
||||||
import schema from '../schema.js';
|
import schema from "../schema.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The names of all of your Convex tables.
|
* The names of all of your Convex tables.
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
|
|||||||
from: process.env.USESEND_FROM_EMAIL ?? 'noreply@example.com',
|
from: process.env.USESEND_FROM_EMAIL ?? 'noreply@example.com',
|
||||||
maxAge: 24 * 60 * 60, // 24 hours
|
maxAge: 24 * 60 * 60, // 24 hours
|
||||||
|
|
||||||
generateVerificationToken() {
|
generateVerificationToken: () => {
|
||||||
const random: RandomReader = {
|
const random: RandomReader = {
|
||||||
read(bytes) {
|
read: (bytes) => {
|
||||||
crypto.getRandomValues(bytes);
|
crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return generateRandomString(random, alphabet('0-9'), 6);
|
return generateRandomString(random, alphabet('0-9'), 6);
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendVerificationRequest(params) {
|
sendVerificationRequest: async (params) => {
|
||||||
const { identifier: to, provider, url, token } = params;
|
const { identifier: to, provider, url, token } = params;
|
||||||
// Derive a display name from the site URL, fallback to 'App'
|
// Derive a display name from the site URL, fallback to 'App'
|
||||||
const siteUrl = process.env.USESEND_FROM_EMAIL ?? '';
|
const siteUrl = process.env.USESEND_FROM_EMAIL ?? '';
|
||||||
|
|||||||
@@ -20,7 +20,10 @@
|
|||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"with-env": "dotenv -e ../../.env --"
|
"test:unit": "vitest run --project unit",
|
||||||
|
"test:integration": "vitest run --project integration --passWithNoTests",
|
||||||
|
"test:component": "vitest run --project component --passWithNoTests",
|
||||||
|
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
@@ -33,14 +36,18 @@
|
|||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@edge-runtime/vm": "catalog:test",
|
||||||
"@gib/eslint-config": "workspace:*",
|
"@gib/eslint-config": "workspace:*",
|
||||||
"@gib/prettier-config": "workspace:*",
|
"@gib/prettier-config": "workspace:*",
|
||||||
"@gib/tsconfig": "workspace:*",
|
"@gib/tsconfig": "workspace:*",
|
||||||
|
"@gib/vitest-config": "workspace:*",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
|
"convex-test": "catalog:test",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"react-email": "5.2.10",
|
"react-email": "5.2.10",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:",
|
||||||
|
"vitest": "catalog:test"
|
||||||
},
|
},
|
||||||
"prettier": "@gib/prettier-config"
|
"prettier": "@gib/prettier-config"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { convexTest } from 'convex-test';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import schema from '../../convex/schema';
|
||||||
|
|
||||||
|
const modules = import.meta.glob('../../convex/**/*.*s');
|
||||||
|
|
||||||
|
describe('convex-test harness', () => {
|
||||||
|
test('boots and executes against the project schema', async () => {
|
||||||
|
const t = convexTest(schema, modules);
|
||||||
|
expect(await t.run(() => Promise.resolve(42))).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "@gib/tsconfig/base.json",
|
||||||
|
"compilerOptions": { "lib": ["ES2022", "DOM"], "types": ["node"] },
|
||||||
|
"include": ["tests", "vitest.config.ts"],
|
||||||
|
"exclude": ["node_modules", "convex/_generated"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
import { convexProject, nodeProject } from '@gib/vitest-config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
projects: [
|
||||||
|
convexProject('unit', ['tests/unit/**/*.test.ts']),
|
||||||
|
convexProject('integration', ['tests/integration/**/*.test.ts']),
|
||||||
|
nodeProject('component', ['tests/component/**/*.test.{ts,tsx}']),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,9 @@
|
|||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
||||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
|
||||||
|
"test:unit": "vitest run --project unit --passWithNoTests",
|
||||||
|
"test:integration": "vitest run --project integration --passWithNoTests",
|
||||||
|
"test:component": "NODE_ENV=test vitest run --project component",
|
||||||
"ui-add": "bunx --bun shadcn@latest add && prettier src --write --list-different"
|
"ui-add": "bunx --bun shadcn@latest add && prettier src --write --list-different"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -53,11 +56,15 @@
|
|||||||
"@gib/eslint-config": "workspace:*",
|
"@gib/eslint-config": "workspace:*",
|
||||||
"@gib/prettier-config": "workspace:*",
|
"@gib/prettier-config": "workspace:*",
|
||||||
"@gib/tsconfig": "workspace:*",
|
"@gib/tsconfig": "workspace:*",
|
||||||
|
"@gib/vitest-config": "workspace:*",
|
||||||
|
"@testing-library/react": "catalog:test",
|
||||||
"@types/react": "catalog:react19",
|
"@types/react": "catalog:react19",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
|
"jsdom": "catalog:test",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"react": "catalog:react19",
|
"react": "catalog:react19",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
"vitest": "catalog:test",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const BasedProgress = ({
|
|||||||
value = 0,
|
value = 0,
|
||||||
...props
|
...props
|
||||||
}: BasedProgressProps) => {
|
}: BasedProgressProps) => {
|
||||||
const [progress, setProgress] = React.useState<number>(value);
|
const [progress, setProgress] = React.useState<number>(value ?? 0);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const id = window.setInterval(() => {
|
const id = window.setInterval(() => {
|
||||||
|
|||||||
+35
-10
@@ -105,6 +105,15 @@ ${colorConfig
|
|||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
|
type ChartPayloadItem = {
|
||||||
|
name?: string | number;
|
||||||
|
value?: number | string;
|
||||||
|
dataKey?: string | number;
|
||||||
|
type?: string;
|
||||||
|
color?: string;
|
||||||
|
payload?: Record<string, unknown> & { fill?: string };
|
||||||
|
};
|
||||||
|
|
||||||
const ChartTooltipContent = ({
|
const ChartTooltipContent = ({
|
||||||
active,
|
active,
|
||||||
payload,
|
payload,
|
||||||
@@ -119,8 +128,23 @@ const ChartTooltipContent = ({
|
|||||||
color,
|
color,
|
||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
}: React.ComponentProps<'div'> & {
|
||||||
React.ComponentProps<'div'> & {
|
active?: boolean;
|
||||||
|
payload?: ChartPayloadItem[];
|
||||||
|
label?: React.ReactNode;
|
||||||
|
labelFormatter?: (
|
||||||
|
value: React.ReactNode,
|
||||||
|
payload: ChartPayloadItem[],
|
||||||
|
) => React.ReactNode;
|
||||||
|
formatter?: (
|
||||||
|
value: number | string,
|
||||||
|
name: string | number,
|
||||||
|
item: ChartPayloadItem,
|
||||||
|
index: number,
|
||||||
|
itemPayload: ChartPayloadItem['payload'],
|
||||||
|
) => React.ReactNode;
|
||||||
|
color?: string;
|
||||||
|
labelClassName?: string;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
hideIndicator?: boolean;
|
hideIndicator?: boolean;
|
||||||
indicator?: 'line' | 'dot' | 'dashed';
|
indicator?: 'line' | 'dot' | 'dashed';
|
||||||
@@ -139,7 +163,7 @@ const ChartTooltipContent = ({
|
|||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const value =
|
const value =
|
||||||
!labelKey && typeof label === 'string'
|
!labelKey && typeof label === 'string'
|
||||||
? config[label]?.label ?? label
|
? (config[label]?.label ?? label)
|
||||||
: itemConfig?.label;
|
: itemConfig?.label;
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
@@ -185,17 +209,17 @@ const ChartTooltipContent = ({
|
|||||||
.map((item, index) => {
|
.map((item, index) => {
|
||||||
const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;
|
const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const indicatorColor = color ?? item.payload.fill ?? item.color;
|
const indicatorColor = color ?? item.payload?.fill ?? item.color;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={item.dataKey ?? index}
|
||||||
className={cn(
|
className={cn(
|
||||||
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
|
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
|
||||||
indicator === 'dot' && 'items-center',
|
indicator === 'dot' && 'items-center',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
{formatter && item.value !== undefined && item.name ? (
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -259,8 +283,9 @@ const ChartLegendContent = ({
|
|||||||
payload,
|
payload,
|
||||||
verticalAlign = 'bottom',
|
verticalAlign = 'bottom',
|
||||||
nameKey,
|
nameKey,
|
||||||
}: React.ComponentProps<'div'> &
|
}: React.ComponentProps<'div'> & {
|
||||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
payload?: ChartPayloadItem[];
|
||||||
|
verticalAlign?: 'top' | 'bottom';
|
||||||
hideIcon?: boolean;
|
hideIcon?: boolean;
|
||||||
nameKey?: string;
|
nameKey?: string;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -280,13 +305,13 @@ const ChartLegendContent = ({
|
|||||||
>
|
>
|
||||||
{payload
|
{payload
|
||||||
.filter((item) => item.type !== 'none')
|
.filter((item) => item.type !== 'none')
|
||||||
.map((item) => {
|
.map((item, index) => {
|
||||||
const key = `${nameKey ?? item.dataKey ?? 'value'}`;
|
const key = `${nameKey ?? item.dataKey ?? 'value'}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.value}
|
key={item.value ?? index}
|
||||||
className={cn(
|
className={cn(
|
||||||
'[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3',
|
'[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -68,14 +68,17 @@ const ComboboxInput = ({
|
|||||||
/>
|
/>
|
||||||
<InputGroupAddon align='inline-end'>
|
<InputGroupAddon align='inline-end'>
|
||||||
{showTrigger && (
|
{showTrigger && (
|
||||||
|
<ComboboxTrigger
|
||||||
|
render={
|
||||||
<InputGroupButton
|
<InputGroupButton
|
||||||
size='icon-xs'
|
size='icon-xs'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
render={<ComboboxTrigger />}
|
|
||||||
data-slot='input-group-button'
|
data-slot='input-group-button'
|
||||||
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
|
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{showClear && <ComboboxClear disabled={disabled} />}
|
{showClear && <ComboboxClear disabled={disabled} />}
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module '*.css';
|
||||||
@@ -294,19 +294,17 @@ export const ImageCropApply = ({
|
|||||||
|
|
||||||
if (asChild) {
|
if (asChild) {
|
||||||
return (
|
return (
|
||||||
<Slot.Root onClick={handleClick} {...(props as any)}>
|
<Slot.Root
|
||||||
|
onClick={handleClick}
|
||||||
|
{...(props as ComponentProps<typeof Slot.Root>)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Slot.Root>
|
</Slot.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button onClick={handleClick} size='icon' variant='ghost' {...props}>
|
||||||
onClick={handleClick}
|
|
||||||
size='icon'
|
|
||||||
variant='ghost'
|
|
||||||
{...(props as any)}
|
|
||||||
>
|
|
||||||
{children ?? <CropIcon className='size-4' />}
|
{children ?? <CropIcon className='size-4' />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -331,19 +329,17 @@ export const ImageCropReset = ({
|
|||||||
|
|
||||||
if (asChild) {
|
if (asChild) {
|
||||||
return (
|
return (
|
||||||
<Slot.Root onClick={handleClick} {...(props as any)}>
|
<Slot.Root
|
||||||
|
onClick={handleClick}
|
||||||
|
{...(props as ComponentProps<typeof Slot.Root>)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Slot.Root>
|
</Slot.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button onClick={handleClick} size='icon' variant='ghost' {...props}>
|
||||||
onClick={handleClick}
|
|
||||||
size='icon'
|
|
||||||
variant='ghost'
|
|
||||||
{...(props as any)}
|
|
||||||
>
|
|
||||||
{children ?? <RotateCcwIcon className='size-4' />}
|
{children ?? <RotateCcwIcon className='size-4' />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -373,7 +369,15 @@ export const Cropper = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
onCrop={onCrop}
|
onCrop={onCrop}
|
||||||
{...(props as any)}
|
{...(props as Omit<
|
||||||
|
ImageCropProps,
|
||||||
|
| 'file'
|
||||||
|
| 'maxImageSize'
|
||||||
|
| 'onChange'
|
||||||
|
| 'onComplete'
|
||||||
|
| 'onCrop'
|
||||||
|
| 'children'
|
||||||
|
>)}
|
||||||
>
|
>
|
||||||
<ImageCropContent className={className} style={style} />
|
<ImageCropContent className={className} style={style} />
|
||||||
</ImageCrop>
|
</ImageCrop>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { Button } from '../../src/button';
|
||||||
|
|
||||||
|
describe('Button', () => {
|
||||||
|
it('renders its children', () => {
|
||||||
|
render(<Button>Click me</Button>);
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'Click me' }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "tests", "vitest.config.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
import { jsdomProject, nodeProject } from '@gib/vitest-config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
projects: [
|
||||||
|
nodeProject('unit', ['tests/unit/**/*.test.{ts,tsx}']),
|
||||||
|
nodeProject('integration', ['tests/integration/**/*.test.{ts,tsx}']),
|
||||||
|
jsdomProject('component', ['tests/component/**/*.test.{ts,tsx}']),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
+9
-16
@@ -1,19 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
ENVIRONMENT="${1:-staging}"
|
||||||
|
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: build-next-app [dev|staging]" >&2; exit 2; }
|
||||||
ENV_FILE="$ROOT_DIR/.env"
|
ENV_FILE="${CI_ENV_FILE:-}"
|
||||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
|
cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then ENV_FILE="$(mktemp)"; sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"; fi
|
||||||
echo "Error: env file not found at $ENV_FILE" >&2
|
args=(); [[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
|
||||||
exit 1
|
docker compose "${args[@]}" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
|
||||||
fi
|
|
||||||
|
|
||||||
set -a
|
|
||||||
source "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build "$NEXT_CONTAINER_NAME"
|
|
||||||
|
|||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
|
||||||
|
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
|
||||||
|
PAYLOAD_SEED_MARKER="$ROOT_DIR/.local/payload-seed-state.env"
|
||||||
|
WIPE=false
|
||||||
|
[ "${1:-}" = "--wipe" ] && WIPE=true
|
||||||
|
|
||||||
|
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
|
||||||
|
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
|
||||||
|
else echo "Docker or Podman not found; nothing to stop." >&2; exit 0; fi
|
||||||
|
|
||||||
|
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
|
||||||
|
trap 'rm -f "$ENV_FILE"' EXIT
|
||||||
|
sh "$ROOT_DIR/scripts/export-env" dev > "$ENV_FILE"
|
||||||
|
|
||||||
|
if [ "$WIPE" = true ]; then
|
||||||
|
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down -v
|
||||||
|
if [ -f "$STATE_FILE" ]; then
|
||||||
|
tmp="${STATE_FILE}.tmp"
|
||||||
|
grep -v '^CONVEX_SELF_HOSTED_ADMIN_KEY=' "$STATE_FILE" > "$tmp" || true
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
rm -f "$PAYLOAD_SEED_MARKER"
|
||||||
|
echo "Local stack and both data volumes removed; generated admin key and Payload seed marker cleared."
|
||||||
|
else
|
||||||
|
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down
|
||||||
|
echo "Local stack stopped; Payload and Convex data preserved."
|
||||||
|
fi
|
||||||
Executable
+114
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Restore the local Payload database from the local snapshot only when the
|
||||||
|
# database has not already been seeded. Does not contact staging/production.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
|
||||||
|
SNAPSHOT="$ROOT_DIR/.local/payload-staging.dump"
|
||||||
|
MARKER="$ROOT_DIR/.local/payload-seed-state.env"
|
||||||
|
DEV_ENV=""
|
||||||
|
FORCE=false
|
||||||
|
ASSUME_YES=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--force) FORCE=true ;;
|
||||||
|
--yes) ASSUME_YES=true ;;
|
||||||
|
*) printf 'usage: seed-payload [--force] [--yes]\n' >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
info() { printf '▶ %s\n' "$*"; }
|
||||||
|
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
|
||||||
|
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
|
||||||
|
else die "Docker or Podman is required."; fi
|
||||||
|
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
|
||||||
|
|
||||||
|
cleanup() { [ -z "$DEV_ENV" ] || rm -f "$DEV_ENV"; }
|
||||||
|
trap cleanup EXIT INT TERM HUP
|
||||||
|
|
||||||
|
if [ ! -s "$SNAPSHOT" ]; then
|
||||||
|
echo "No local Payload snapshot found at .local/payload-staging.dump; skipping Payload seed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ROOT_DIR/.local"
|
||||||
|
chmod 700 "$ROOT_DIR/.local"
|
||||||
|
|
||||||
|
next_is_running=false
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -ltnH 'sport = :3000' | grep -q . && next_is_running=true
|
||||||
|
elif command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1 && next_is_running=true
|
||||||
|
elif curl -sS --max-time 1 http://localhost:3000 >/dev/null 2>&1; then
|
||||||
|
next_is_running=true
|
||||||
|
fi
|
||||||
|
if [ "$next_is_running" = true ]; then
|
||||||
|
die "Next is running on port 3000. Stop bun dev:next before replacing Payload data."
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEV_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-dev.XXXXXX.env")"
|
||||||
|
sh "$ROOT_DIR/scripts/export-env" dev > "$DEV_ENV"
|
||||||
|
|
||||||
|
bunx dotenv -e "$DEV_ENV" -- node -e '
|
||||||
|
const raw = process.env.PAYLOAD_DB_URL;
|
||||||
|
if (!raw) process.exit(2);
|
||||||
|
const url = new URL(raw);
|
||||||
|
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
||||||
|
if (!local) process.exit(3);
|
||||||
|
' || die "Refusing to seed: dev PAYLOAD_DB_URL is not localhost."
|
||||||
|
|
||||||
|
dc() { "$RUNTIME" compose --env-file "$DEV_ENV" -f "$COMPOSE_FILE" "$@"; }
|
||||||
|
POSTGRES_USER="$(bunx dotenv -e "$DEV_ENV" -- sh -c 'printf %s "$POSTGRES_USER"')"
|
||||||
|
POSTGRES_DB="$(bunx dotenv -e "$DEV_ENV" -- sh -c 'printf %s "$POSTGRES_DB"')"
|
||||||
|
[ -n "$POSTGRES_USER" ] && [ -n "$POSTGRES_DB" ] || die "Local Postgres configuration is incomplete."
|
||||||
|
|
||||||
|
info "Ensuring local Payload Postgres is running"
|
||||||
|
dc up -d postgres >/dev/null
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if dc exec -T postgres pg_isready -U "$POSTGRES_USER" >/dev/null 2>&1; then break; fi
|
||||||
|
[ "$i" -lt 30 ] || die "Local Payload Postgres did not become ready."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
SNAPSHOT_HASH="$(sha256sum "$SNAPSHOT" | awk '{print $1}')"
|
||||||
|
marker_exists=false
|
||||||
|
|
||||||
|
# Earlier versions stored this marker in the Payload database. Payload/Drizzle
|
||||||
|
# treats unmanaged tables as drift and prompts to delete them during dev startup,
|
||||||
|
# so keep seed state in .local/ and remove the legacy table from local DBs.
|
||||||
|
dc exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 \
|
||||||
|
-c 'DROP TABLE IF EXISTS _local_seed_state' >/dev/null
|
||||||
|
|
||||||
|
if [ -s "$MARKER" ] && grep -qx "PAYLOAD_SNAPSHOT_SHA256=$SNAPSHOT_HASH" "$MARKER"; then
|
||||||
|
marker_exists=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$marker_exists" = true ] && [ "$FORCE" != true ]; then
|
||||||
|
echo "Payload snapshot seed already applied; skipping. Use --force to restore again."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FORCE" = true ] && [ "$ASSUME_YES" != true ]; then
|
||||||
|
printf 'This will replace the LOCAL Payload database from .local/payload-staging.dump.\n'
|
||||||
|
read -r -p 'Continue? [y/N] ' answer
|
||||||
|
case "$answer" in y|Y|yes|YES) ;; *) echo "Cancelled."; exit 0 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Restoring local Payload database from .local/payload-staging.dump"
|
||||||
|
dc exec -T postgres dropdb --force --if-exists -U "$POSTGRES_USER" "$POSTGRES_DB"
|
||||||
|
dc exec -T postgres createdb -U "$POSTGRES_USER" -O "$POSTGRES_USER" "$POSTGRES_DB"
|
||||||
|
dc exec -T postgres pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
|
||||||
|
--no-owner --no-acl --exit-on-error < "$SNAPSHOT"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf 'PAYLOAD_SNAPSHOT_SHA256=%s\n' "$SNAPSHOT_HASH"
|
||||||
|
printf 'PAYLOAD_SNAPSHOT_SEEDED_AT=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
} > "$MARKER"
|
||||||
|
chmod 600 "$MARKER"
|
||||||
|
|
||||||
|
echo "Payload snapshot seed applied."
|
||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Refresh the local Payload seed snapshot from staging, then force-apply it to
|
||||||
|
# the local development Payload database. Normal db:up never calls staging.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
|
||||||
|
SNAPSHOT="$ROOT_DIR/.local/payload-staging.dump"
|
||||||
|
STAGING_ENV=""
|
||||||
|
DEV_ENV=""
|
||||||
|
SOURCE_PG_ENV=""
|
||||||
|
ASSUME_YES=false
|
||||||
|
[ "${1:-}" = "--yes" ] && ASSUME_YES=true
|
||||||
|
|
||||||
|
info() { printf '▶ %s\n' "$*"; }
|
||||||
|
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
|
||||||
|
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
|
||||||
|
else die "Docker or Podman is required."; fi
|
||||||
|
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
|
||||||
|
|
||||||
|
mkdir -p "$ROOT_DIR/.local"
|
||||||
|
chmod 700 "$ROOT_DIR/.local"
|
||||||
|
cleanup() {
|
||||||
|
[ -z "$STAGING_ENV" ] || rm -f "$STAGING_ENV"
|
||||||
|
[ -z "$DEV_ENV" ] || rm -f "$DEV_ENV"
|
||||||
|
[ -z "$SOURCE_PG_ENV" ] || rm -f "$SOURCE_PG_ENV"
|
||||||
|
rm -f "${SNAPSHOT}.tmp"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM HUP
|
||||||
|
|
||||||
|
STAGING_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-staging.XXXXXX.env")"
|
||||||
|
DEV_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-dev.XXXXXX.env")"
|
||||||
|
sh "$ROOT_DIR/scripts/export-env" staging > "$STAGING_ENV"
|
||||||
|
sh "$ROOT_DIR/scripts/export-env" dev > "$DEV_ENV"
|
||||||
|
|
||||||
|
validate_url() {
|
||||||
|
local env_file="$1" expected="$2"
|
||||||
|
bunx dotenv -e "$env_file" -- node -e '
|
||||||
|
const expected = process.argv[1];
|
||||||
|
const raw = process.env.PAYLOAD_DB_URL;
|
||||||
|
if (!raw) process.exit(2);
|
||||||
|
const url = new URL(raw);
|
||||||
|
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
||||||
|
if ((expected === "local") !== local) process.exit(3);
|
||||||
|
' "$expected" || die "PAYLOAD_DB_URL safety check failed for the $expected environment."
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_url "$STAGING_ENV" remote
|
||||||
|
validate_url "$DEV_ENV" local
|
||||||
|
|
||||||
|
next_is_running=false
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -ltnH 'sport = :3000' | grep -q . && next_is_running=true
|
||||||
|
elif command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1 && next_is_running=true
|
||||||
|
elif curl -sS --max-time 1 http://localhost:3000 >/dev/null 2>&1; then
|
||||||
|
next_is_running=true
|
||||||
|
fi
|
||||||
|
if [ "$next_is_running" = true ]; then
|
||||||
|
die "Next is running on port 3000. Stop bun dev:next before replacing Payload data."
|
||||||
|
fi
|
||||||
|
|
||||||
|
SOURCE_PG_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-pg.XXXXXX.env")"
|
||||||
|
PG_ENV_FILE="$SOURCE_PG_ENV" bunx dotenv -e "$STAGING_ENV" -- sh -c '
|
||||||
|
umask 077
|
||||||
|
printf "PGDATABASE=%s\n" "$PAYLOAD_DB_URL" > "$PG_ENV_FILE"
|
||||||
|
'
|
||||||
|
|
||||||
|
if [ "$ASSUME_YES" != true ]; then
|
||||||
|
printf 'This will download staging Payload data and replace the LOCAL Payload database.\n'
|
||||||
|
printf 'The snapshot may contain user records and password hashes; it stays under .local/.\n'
|
||||||
|
read -r -p 'Continue? [y/N] ' answer
|
||||||
|
case "$answer" in y|Y|yes|YES) ;; *) echo "Cancelled."; exit 0 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Exporting a read-only snapshot from staging Payload Postgres"
|
||||||
|
"$RUNTIME" run --rm --network host --env-file "$SOURCE_PG_ENV" \
|
||||||
|
docker.io/library/postgres:17 \
|
||||||
|
sh -c 'exec pg_dump --dbname="$PGDATABASE" --format=custom --no-owner --no-acl' \
|
||||||
|
> "${SNAPSHOT}.tmp"
|
||||||
|
mv "${SNAPSHOT}.tmp" "$SNAPSHOT"
|
||||||
|
chmod 600 "$SNAPSHOT"
|
||||||
|
|
||||||
|
info "Ensuring local Payload Postgres is running"
|
||||||
|
bash "$ROOT_DIR/scripts/db/seed-payload" --force --yes
|
||||||
|
|
||||||
|
printf '\nLocal Payload seed snapshot refreshed and applied.\n'
|
||||||
|
printf 'Snapshot: .local/payload-staging.dump\n'
|
||||||
|
printf 'Normal db:up will reuse this snapshot and skip after .local/payload-seed-state.env exists.\n'
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
|
||||||
|
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
|
||||||
|
ENV_FILE=""
|
||||||
|
|
||||||
|
info() { printf '▶ %s\n' "$*"; }
|
||||||
|
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
|
||||||
|
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
|
||||||
|
else die "Docker or Podman is required."; fi
|
||||||
|
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
|
||||||
|
|
||||||
|
mkdir -p "$ROOT_DIR/.local"
|
||||||
|
cleanup() { [ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
refresh_env() {
|
||||||
|
local next
|
||||||
|
next="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
|
||||||
|
sh "$ROOT_DIR/scripts/export-env" dev > "$next" || { rm -f "$next"; die "Unable to export Infisical dev."; }
|
||||||
|
[ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"
|
||||||
|
ENV_FILE="$next"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
dc() { "$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
|
||||||
|
upsert_state() {
|
||||||
|
local key="$1" value="$2" tmp escaped
|
||||||
|
tmp="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-state.XXXXXX.env")"
|
||||||
|
[ ! -f "$STATE_FILE" ] || grep -v "^${key}=" "$STATE_FILE" > "$tmp" || true
|
||||||
|
escaped="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
|
||||||
|
printf "%s='%s'\n" "$key" "$escaped" >> "$tmp"
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_env
|
||||||
|
info "Starting local Payload Postgres, Convex, and dashboard"
|
||||||
|
dc up -d
|
||||||
|
|
||||||
|
info "Waiting for Payload Postgres"
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
dc exec -T postgres pg_isready -U "${POSTGRES_USER:-convexmonorepo}" >/dev/null 2>&1 && break
|
||||||
|
[ "$i" -lt 30 ] || die "Postgres did not become ready."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
info "Waiting for Convex at http://localhost:${BACKEND_PORT:-3210}"
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
curl -fs "http://localhost:${BACKEND_PORT:-3210}/version" >/dev/null 2>&1 && break
|
||||||
|
[ "$i" -lt 60 ] || die "Convex did not become ready."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${CONVEX_SELF_HOSTED_ADMIN_KEY:-}" ]; then
|
||||||
|
admin_key="$(dc exec -T convex-backend ./generate_admin_key.sh 2>/dev/null | grep -E '.+\|.+' | tail -n1 | tr -d '\r')"
|
||||||
|
[ -n "$admin_key" ] || die "Unable to generate the Convex admin key."
|
||||||
|
upsert_state CONVEX_SELF_HOSTED_ADMIN_KEY "$admin_key"
|
||||||
|
refresh_env
|
||||||
|
info "Generated the machine-local Convex admin key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Deploying Convex schema and functions"
|
||||||
|
(cd "$ROOT_DIR/packages/backend" && bun run setup)
|
||||||
|
|
||||||
|
convex_env_names="$(
|
||||||
|
sh "$ROOT_DIR/scripts/with-env" dev -- bash -c \
|
||||||
|
'cd packages/backend && bunx convex env list' 2>/dev/null \
|
||||||
|
| sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p'
|
||||||
|
)"
|
||||||
|
if ! printf '%s\n' "$convex_env_names" | grep -qx 'JWT_PRIVATE_KEY' \
|
||||||
|
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'JWKS' \
|
||||||
|
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'SITE_URL'; then
|
||||||
|
info "Configuring local Convex Auth keys"
|
||||||
|
auth_keys="$(node "$ROOT_DIR/scripts/generate-convex-auth-keys.mjs")"
|
||||||
|
jwt="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWT_PRIVATE_KEY="\(.*\)"$/\1/p')"
|
||||||
|
jwks="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWKS=//p')"
|
||||||
|
JWT_VAL="$jwt" JWKS_VAL="$jwks" sh "$ROOT_DIR/scripts/with-env" dev -- bash -c '
|
||||||
|
cd packages/backend
|
||||||
|
bunx convex env set "JWT_PRIVATE_KEY=$JWT_VAL" >/dev/null
|
||||||
|
bunx convex env set "JWKS=$JWKS_VAL" >/dev/null
|
||||||
|
bunx convex env set "SITE_URL=http://localhost:3000" >/dev/null
|
||||||
|
'
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Seeding local Payload from snapshot if needed"
|
||||||
|
bash "$ROOT_DIR/scripts/db/seed-payload" --yes
|
||||||
|
|
||||||
|
printf '\nLocal stack ready:\n App: http://localhost:3000\n Convex: http://localhost:%s\n Dashboard: http://localhost:%s\n Payload Postgres: localhost:%s\n' "${BACKEND_PORT:-3210}" "${DASHBOARD_PORT:-6791}" "${POSTGRES_PORT:-5432}"
|
||||||
+18
-40
@@ -1,49 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
ENV_FILE="$ROOT_DIR/.env"
|
|
||||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
|
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
|
||||||
|
ENVIRONMENT="${1:-staging}"
|
||||||
|
if [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]]; then shift || true; else ENVIRONMENT=staging; fi
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
ENV_FILE="${CI_ENV_FILE:-}"
|
||||||
echo "Error: env file not found at $ENV_FILE" >&2
|
cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
|
||||||
exit 1
|
trap cleanup EXIT
|
||||||
|
if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then
|
||||||
|
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-compose.XXXXXX.env")"
|
||||||
|
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$COMPOSE_FILE" ]; then
|
args=()
|
||||||
echo "Error: compose file not found at $COMPOSE_FILE" >&2
|
[[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
|
||||||
exit 1
|
translated=()
|
||||||
fi
|
|
||||||
|
|
||||||
set -a
|
|
||||||
source "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
translated_args=()
|
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in backend) translated+=(convexmonorepo-backend) ;; dashboard) translated+=(convexmonorepo-dashboard) ;; next) translated+=(convexmonorepo-next) ;; *) translated+=("$arg") ;; esac
|
||||||
backend)
|
|
||||||
: "${BACKEND_CONTAINER_NAME:?BACKEND_CONTAINER_NAME is not set in .env}"
|
|
||||||
translated_args+=("$BACKEND_CONTAINER_NAME")
|
|
||||||
;;
|
|
||||||
dashboard)
|
|
||||||
: "${DASHBOARD_CONTAINER_NAME:?DASHBOARD_CONTAINER_NAME is not set in .env}"
|
|
||||||
translated_args+=("$DASHBOARD_CONTAINER_NAME")
|
|
||||||
;;
|
|
||||||
next)
|
|
||||||
: "${NEXT_CONTAINER_NAME:?NEXT_CONTAINER_NAME is not set in .env}"
|
|
||||||
translated_args+=("$NEXT_CONTAINER_NAME")
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
translated_args+=("$arg")
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
set +e
|
||||||
exec docker compose \
|
docker compose "${args[@]}" -f "$COMPOSE_FILE" "${translated[@]}"
|
||||||
--env-file "$ENV_FILE" \
|
status=$?
|
||||||
-f "$COMPOSE_FILE" \
|
set -e
|
||||||
"${translated_args[@]}"
|
exit "$status"
|
||||||
|
|||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -n "${CI:-}" ]; then echo "CI detected; skipping local e2e."; exit 0; fi
|
||||||
|
if [ -n "${SKIP_E2E:-}" ]; then echo "SKIP_E2E set; skipping local e2e."; exit 0; fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
bash "$ROOT_DIR/scripts/db/up"
|
||||||
|
echo "Local-stack smoke checks passed; no seeded browser flow is configured."
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
[ "$#" -eq 1 ] || { echo "usage: export-env <dev|staging>" >&2; exit 2; }
|
||||||
|
ENVIRONMENT="$1"
|
||||||
|
case "$ENVIRONMENT" in dev|staging) ;; *) echo "export-env: expected dev or staging" >&2; exit 2 ;; esac
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
|
||||||
|
|
||||||
|
if [ -n "${CI:-}" ]; then
|
||||||
|
echo "export-env: refusing to export secrets in CI; use injected variables or CI_ENV_FILE." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "export-env: run 'infisical init' in this repository." >&2; exit 1; }
|
||||||
|
command -v infisical >/dev/null 2>&1 || { echo "export-env: Infisical CLI is required." >&2; exit 1; }
|
||||||
|
|
||||||
|
(cd "$ROOT_DIR" && infisical export --env="$ENVIRONMENT" --format=dotenv --silent) || {
|
||||||
|
echo "export-env: failed to export '$ENVIRONMENT'; check login and project access." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -f "$STATE_FILE" ]; then
|
||||||
|
printf '\n'
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in ''|'#'*) printf '%s\n' "$line"; continue ;; esac
|
||||||
|
key=${line%%=*}
|
||||||
|
value=${line#*=}
|
||||||
|
case "$value" in \'*\') value=${value#\'}; value=${value%\'} ;; \"*\") value=${value#\"}; value=${value%\"} ;; esac
|
||||||
|
escaped=$(printf '%s' "$value" | sed "s/'/'\\\\''/g")
|
||||||
|
printf "%s='%s'\n" "$key" "$escaped"
|
||||||
|
done < "$STATE_FILE"
|
||||||
|
fi
|
||||||
@@ -1,20 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
ENVIRONMENT="${1:-staging}"
|
||||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: generate-convex-admin-key [dev|staging]" >&2; exit 2; }
|
||||||
|
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
|
||||||
ENV_FILE="$ROOT_DIR/.env"
|
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
|
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" exec convexmonorepo-backend ./generate_admin_key.sh
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
|
||||||
echo "Error: env file not found at $ENV_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -a
|
|
||||||
source "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec \
|
|
||||||
"$BACKEND_CONTAINER_NAME" ./generate_admin_key.sh
|
|
||||||
|
|||||||
+7
-20
@@ -1,22 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
ENVIRONMENT="${1:-staging}"
|
||||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-convex [dev|staging]" >&2; exit 2; }
|
||||||
|
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
|
||||||
ENV_FILE="$ROOT_DIR/.env"
|
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
|
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" pull convexmonorepo-backend convexmonorepo-dashboard
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-backend convexmonorepo-dashboard
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
|
||||||
echo "Error: env file not found at $ENV_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -a
|
|
||||||
source "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" pull \
|
|
||||||
"$BACKEND_CONTAINER_NAME" "$DASHBOARD_CONTAINER_NAME"
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d \
|
|
||||||
"$BACKEND_CONTAINER_NAME" "$DASHBOARD_CONTAINER_NAME"
|
|
||||||
|
|||||||
+7
-18
@@ -1,20 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
ENVIRONMENT="${1:-staging}"
|
||||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-next-app [dev|staging]" >&2; exit 2; }
|
||||||
|
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
|
||||||
ENV_FILE="$ROOT_DIR/.env"
|
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
|
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-next
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
|
||||||
echo "Error: env file not found at $ENV_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -a
|
|
||||||
source "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build "$NEXT_CONTAINER_NAME"
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d "$NEXT_CONTAINER_NAME"
|
|
||||||
|
|||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
echo "usage: with-env <dev|staging> -- <command> [args...]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENVIRONMENT="$1"
|
||||||
|
shift
|
||||||
|
[ "${1:-}" = "--" ] && shift
|
||||||
|
[ "$#" -gt 0 ] || { echo "with-env: no command given" >&2; exit 2; }
|
||||||
|
|
||||||
|
case "$ENVIRONMENT" in dev|staging) ;; *) echo "with-env: expected dev or staging" >&2; exit 2 ;; esac
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
|
||||||
|
|
||||||
|
if [ -n "${CI:-}" ]; then
|
||||||
|
export WITH_ENV_SOURCE=ci WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v infisical >/dev/null 2>&1 || {
|
||||||
|
echo "with-env: install Infisical, run 'infisical login', and link this repo with 'infisical init'." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "with-env: .infisical.json is missing." >&2; exit 1; }
|
||||||
|
|
||||||
|
TMP_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-$ENVIRONMENT.XXXXXX.env")"
|
||||||
|
trap 'rm -f "$TMP_ENV"' EXIT INT TERM HUP
|
||||||
|
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$TMP_ENV"
|
||||||
|
|
||||||
|
export WITH_ENV_SOURCE=infisical WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
|
||||||
|
set +e
|
||||||
|
bunx dotenv -e "$TMP_ENV" -- "$@"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
exit "$status"
|
||||||
@@ -1 +0,0 @@
|
|||||||
[["1","2","3","4","5","6"],{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},{"key":"13","value":"14"},{"key":"15","value":"16"},{"key":"17","value":"18"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/base.ts",{"size":2963,"mtime":1774546669459,"hash":"19","data":"20"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/nextjs.ts",{"size":440,"mtime":1768155639000,"hash":"21","data":"22"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/.cache/.prettiercache",{"size":1278,"mtime":1774718093280},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/package.json",{"size":1034,"mtime":1774588268325,"hash":"23","data":"24"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/react.ts",{"size":592,"mtime":1768155639000,"hash":"25","data":"26"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"27","data":"28"},"6a779439826cf31b5561a21273d134a9",{"hashOfOptions":"29"},"25c52c46972131dcc296288599ff108d",{"hashOfOptions":"30"},"a5326aca75246da261fd2e354257b45a",{"hashOfOptions":"31"},"2292935ede6baf909f6a0c61486e15da",{"hashOfOptions":"32"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"33"},"1686097143","2347540204","302976953","3406150487","1582266352"]
|
|
||||||
+23
-1
@@ -11,6 +11,12 @@ import tseslint from 'typescript-eslint';
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const preferArrowPlugin = preferArrowFunctions as any;
|
const preferArrowPlugin = preferArrowFunctions as any;
|
||||||
|
|
||||||
|
const turboRecommendedRules = (
|
||||||
|
turboPlugin as unknown as {
|
||||||
|
configs: { recommended: { rules: Record<string, string> } };
|
||||||
|
}
|
||||||
|
).configs.recommended.rules;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All packages that leverage t3-env should use this rule
|
* All packages that leverage t3-env should use this rule
|
||||||
*/
|
*/
|
||||||
@@ -59,7 +65,7 @@ export const baseConfig = defineConfig(
|
|||||||
...tseslint.configs.stylisticTypeChecked,
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
...turboPlugin.configs.recommended.rules,
|
...turboRecommendedRules,
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
@@ -88,6 +94,22 @@ export const baseConfig = defineConfig(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/tests/**/*.{ts,tsx}',
|
||||||
|
'**/*.test.{ts,tsx}',
|
||||||
|
'**/*.spec.{ts,tsx}',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-return': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||||
|
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
linterOptions: { reportUnusedDisableDirectives: true },
|
linterOptions: { reportUnusedDisableDirectives: true },
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
import type { Linter } from 'eslint';
|
||||||
import reactPlugin from 'eslint-plugin-react';
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import { defineConfig } from 'eslint/config';
|
import { defineConfig } from 'eslint/config';
|
||||||
|
|
||||||
|
const reactFlat = reactPlugin.configs.flat as Record<string, Linter.Config>;
|
||||||
|
|
||||||
export const reactConfig = defineConfig(
|
export const reactConfig = defineConfig(
|
||||||
{
|
{
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
...reactPlugin.configs.flat.recommended,
|
...reactFlat.recommended,
|
||||||
...reactPlugin.configs.flat['jsx-runtime'],
|
...reactFlat['jsx-runtime'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...reactPlugin.configs.flat.recommended?.languageOptions,
|
...reactFlat.recommended?.languageOptions,
|
||||||
...reactPlugin.configs.flat['jsx-runtime']?.languageOptions,
|
...reactFlat['jsx-runtime']?.languageOptions,
|
||||||
globals: {
|
globals: {
|
||||||
React: 'writable',
|
React: 'writable',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "@gib/tsconfig/base.json",
|
"extends": "@gib/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
[["1","2","3","4"],{"key":"5","value":"6"},{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/.cache/.prettiercache",{"size":832,"mtime":1774718093318},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/index.js",{"size":1194,"mtime":1768372320442,"hash":"13","data":"14"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/package.json",{"size":607,"mtime":1774032385569,"hash":"15","data":"16"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"17","data":"18"},"ecbaa91166a940dfcec8117059f52402",{"hashOfOptions":"19"},"11b634ce56ac720ac9a2860d77fbd2cc",{"hashOfOptions":"20"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"21"},"1828250668","802511607","4250532914"]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js":"1"},{"size":70,"mtime":1768155639000,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","suppressedMessages":"6","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"zvlfqu","/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",[],[]]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[["1","2","3","4","5","6","7"],{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},{"key":"16","value":"17"},{"key":"18","value":"19"},{"key":"20","value":"21"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/.cache/.eslintcache",{"size":396,"mtime":1774717490012},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/.cache/.prettiercache",{"size":1450,"mtime":1774718093720},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/package.json",{"size":851,"mtime":1774032407411,"hash":"22","data":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"24","data":"25"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/eslint.config.ts",{"size":143,"mtime":1768155639000,"hash":"26","data":"27"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/theme.css",{"size":7273,"mtime":1768320378000,"hash":"28","data":"29"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",{"size":70,"mtime":1768155639000,"hash":"30","data":"31"},"0d22e47f57739db9de04c6f8420d6fb5",{"hashOfOptions":"32"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"33"},"b8fec960cb32340eea62ca1485093e68",{"hashOfOptions":"34"},"e40c2569ef375a9c828c80c0f9ce1bf2",{"hashOfOptions":"35"},"9a944fbda06979be39571bd9bd00b0d9",{"hashOfOptions":"36"},"286235122","2846522359","1288936688","2683656544","1694755693"]
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@gib/tsconfig/base.json",
|
"extends": "@gib/tsconfig/base.json",
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "eslint.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { defineProject } from 'vitest/config';
|
||||||
|
|
||||||
|
const jsdomSetup = fileURLToPath(new URL('./setup-jsdom.ts', import.meta.url));
|
||||||
|
|
||||||
|
export const nodeProject = (name: string, include: string[]) =>
|
||||||
|
defineProject({ test: { name, environment: 'node', include } });
|
||||||
|
|
||||||
|
export const jsdomProject = (name: string, include: string[]) =>
|
||||||
|
defineProject({
|
||||||
|
plugins: [react()],
|
||||||
|
test: { name, environment: 'jsdom', include, setupFiles: [jsdomSetup] },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const convexProject = (name: string, include: string[]) =>
|
||||||
|
defineProject({
|
||||||
|
test: {
|
||||||
|
name,
|
||||||
|
environment: 'edge-runtime',
|
||||||
|
include,
|
||||||
|
server: { deps: { inline: ['convex-test'] } },
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@gib/vitest-config",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts",
|
||||||
|
"./setup-jsdom": "./setup-jsdom.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "git clean -xdf .cache .turbo node_modules",
|
||||||
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "catalog:test",
|
||||||
|
"@vitejs/plugin-react": "catalog:test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@gib/prettier-config": "workspace:*",
|
||||||
|
"@gib/tsconfig": "workspace:*",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"prettier": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"vitest": "catalog:test"
|
||||||
|
},
|
||||||
|
"prettier": "@gib/prettier-config"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "@gib/tsconfig/base.json",
|
||||||
|
"compilerOptions": { "lib": ["ES2022", "dom", "dom.iterable"] },
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
+33
-2
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://v2-8-20.turborepo.dev/schema.json",
|
"$schema": "https://v2-9-18.turborepo.dev/schema.json",
|
||||||
"globalDependencies": ["**/.env.*local"],
|
"globalDependencies": [".infisical.json", ".local/*.env"],
|
||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
|
"INFISICAL_ENV",
|
||||||
"SENTRY_AUTH_TOKEN",
|
"SENTRY_AUTH_TOKEN",
|
||||||
|
"SENTRY_DISABLE_AUTO_UPLOAD",
|
||||||
"PAYLOAD_SECRET",
|
"PAYLOAD_SECRET",
|
||||||
"PAYLOAD_DB_URL",
|
"PAYLOAD_DB_URL",
|
||||||
"NEXT_PUBLIC_SITE_URL",
|
"NEXT_PUBLIC_SITE_URL",
|
||||||
@@ -22,6 +24,32 @@
|
|||||||
"AUTH_AUTHENTIK_ID",
|
"AUTH_AUTHENTIK_ID",
|
||||||
"AUTH_AUTHENTIK_SECRET",
|
"AUTH_AUTHENTIK_SECRET",
|
||||||
"AUTH_AUTHENTIK_ISSUER",
|
"AUTH_AUTHENTIK_ISSUER",
|
||||||
|
"SKIP_E2E",
|
||||||
|
"BASE_URL",
|
||||||
|
"NETWORK",
|
||||||
|
"NEXT_CONTAINER_NAME",
|
||||||
|
"NEXT_DOMAIN",
|
||||||
|
"NEXT_PORT",
|
||||||
|
"BACKEND_TAG",
|
||||||
|
"DASHBOARD_TAG",
|
||||||
|
"BACKEND_CONTAINER_NAME",
|
||||||
|
"DASHBOARD_CONTAINER_NAME",
|
||||||
|
"BACKEND_DOMAIN",
|
||||||
|
"DASHBOARD_DOMAIN",
|
||||||
|
"INSTANCE_NAME",
|
||||||
|
"INSTANCE_SECRET",
|
||||||
|
"CONVEX_CLOUD_ORIGIN",
|
||||||
|
"CONVEX_SITE_ORIGIN",
|
||||||
|
"NEXT_PUBLIC_DEPLOYMENT_URL",
|
||||||
|
"LOCAL_INSTANCE_NAME",
|
||||||
|
"LOCAL_INSTANCE_SECRET",
|
||||||
|
"POSTGRES_USER",
|
||||||
|
"POSTGRES_PASSWORD",
|
||||||
|
"POSTGRES_DB",
|
||||||
|
"POSTGRES_PORT",
|
||||||
|
"BACKEND_PORT",
|
||||||
|
"SITE_PROXY_PORT",
|
||||||
|
"DASHBOARD_PORT"
|
||||||
],
|
],
|
||||||
"globalPassThroughEnv": ["NODE_ENV"],
|
"globalPassThroughEnv": ["NODE_ENV"],
|
||||||
"ui": "tui",
|
"ui": "tui",
|
||||||
@@ -57,6 +85,9 @@
|
|||||||
"dependsOn": ["^topo", "^build"],
|
"dependsOn": ["^topo", "^build"],
|
||||||
"outputs": [".cache/tsbuildinfo.json"]
|
"outputs": [".cache/tsbuildinfo.json"]
|
||||||
},
|
},
|
||||||
|
"test:unit": { "dependsOn": ["^build"], "cache": false },
|
||||||
|
"test:integration": { "dependsOn": ["^build"], "cache": false },
|
||||||
|
"test:component": { "dependsOn": ["^build"], "cache": false },
|
||||||
"clean": {
|
"clean": {
|
||||||
"cache": false
|
"cache": false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user