Template
Move to infisical. Create local dev environment. Add ci gates. Modernize repo
This commit is contained in:
1 parent
86e2fdc82e
commit
a12bf6071b
79 files changed
+1612
-42168
No files matched your search
+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 <[email protected]>
|
|
||||||
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 <[email protected]>
|
|
||||||
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 <[email protected]>"
|
|
||||||
```
|
|
||||||
|
|
||||||
**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';
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/cta.tsx":"1","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/features.tsx":"2","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/hero.tsx":"3","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/index.tsx":"4","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx":"5","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx":"6","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx":"7","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/avatar-upload.tsx":"8","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/header.tsx":"9","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/index.tsx":"10","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx":"11","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx":"12","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/user-info.tsx":"13","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/footer/index.tsx":"14","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx":"15","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx":"16","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/index.tsx":"17","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/ConvexClientProvider.tsx":"18","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/index.tsx":"19","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts":"20","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation.ts":"21","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/metadata.ts":"22","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/proxy/ban-sus-ips.ts":"23","/home/gib/Documents/Code/convex-monorepo/apps/next/src/proxy.ts":"24","/home/gib/Documents/Code/convex-monorepo/apps/next/payload-types.ts":"25","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/forgot-password/layout.tsx":"26","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/forgot-password/page.tsx":"27","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/profile/layout.tsx":"28","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/profile/page.tsx":"29","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/sign-in/layout.tsx":"30","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/sign-in/page.tsx":"31","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/global-error.tsx":"32","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/layout.tsx":"33","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/page.tsx":"34","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/[[...segments]]/not-found.tsx":"35","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/[[...segments]]/page.tsx":"36","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/importMap.js":"37","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/[...slug]/route.ts":"38","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/graphql/route.ts":"39","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/graphql-playground/route.ts":"40","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/layout.tsx":"41","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/content.ts":"42","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/faq.tsx":"43","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/logo-cloud.tsx":"44","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/page-builder.tsx":"45","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/pricing.tsx":"46","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/stats.tsx":"47","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/testimonials.tsx":"48","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/navigation.tsx":"49","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/payload/refresh-route-on-save.tsx":"50","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/payload/get-landing-page-content.ts":"51","/home/gib/Documents/Code/coLine truncated
|
|
||||||
@@ -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","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69","70","71","72","73","74","75","76","77","78","79","80","81"],{"key":"82","value":"83"},{"key":"84","value":"85"},{"key":"86","value":"87"},{"key":"88","value":"89"},{"key":"90","value":"91"},{"key":"92","value":"93"},{"key":"94","value":"95"},{"key":"96","value":"97"},{"key":"98","value":"99"},{"key":"100","value":"101"},{"key":"102","value":"103"},{"key":"104","value":"105"},{"key":"106","value":"107"},{"key":"108","value":"109"},{"key":"110","value":"111"},{"key":"112","value":"113"},{"key":"114","value":"115"},{"key":"116","value":"117"},{"key":"118","value":"119"},{"key":"120","value":"121"},{"key":"122","value":"123"},{"key":"124","value":"125"},{"key":"126","value":"127"},{"key":"128","value":"129"},{"key":"130","value":"131"},{"key":"132","value":"133"},{"key":"134","value":"135"},{"key":"136","value":"137"},{"key":"138","value":"139"},{"key":"140","value":"141"},{"key":"142","value":"143"},{"key":"144","value":"145"},{"key":"146","value":"147"},{"key":"148","value":"149"},{"key":"150","value":"151"},{"key":"152","value":"153"},{"key":"154","value":"155"},{"key":"156","value":"157"},{"key":"158","value":"159"},{"key":"160","value":"161"},{"key":"162","value":"163"},{"key":"164","value":"165"},{"key":"166","value":"167"},{"key":"168","value":"169"},{"key":"170","value":"171"},{"key":"172","value":"173"},{"key":"174","value":"175"},{"key":"176","value":"177"},{"key":"178","value":"179"},{"key":"180","value":"181"},{"key":"182","value":"183"},{"key":"184","value":"185"},{"key":"186","value":"187"},{"key":"188","value":"189"},{"key":"190","value":"191"},{"key":"192","value":"193"},{"key":"194","value":"195"},{"key":"196","value":"197"},{"key":"198","value":"199"},{"key":"200","value":"201"},{"key":"202","value":"203"},{"key":"204","value":"205"},{"key":"206","value":"207"},{"key":"208","value":"209"},{"key":"210","value":"211"},{"key":"212","value":"213"},{"key":"214","value":"215"},{"key":"216","value":"217"},{"key":"218","value":"219"},{"key":"220","value":"221"},{"key":"222","value":"223"},{"key":"224","value":"225"},{"key":"226","value":"227"},{"key":"228","value":"229"},{"key":"230","value":"231"},{"key":"232","value":"233"},{"key":"234","value":"235"},{"key":"236","value":"237"},{"key":"238","value":"239"},{"key":"240","value":"241"},{"key":"242","value":"243"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-black.svg",{"size":948,"mtime":1717458243000,"hash":"244"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx",{"size":499,"mtime":1768372347633,"hash":"245","data":"246"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx",{"size":1081,"mtime":1768372347496,"hash":"247","data":"248"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-wordmark-black.svg",{"size":3110,"mtime":1717458243000,"hash":"249"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/gitea/gitea.svg",{"size":1844,"mtime":1747415494000,"hash":"250"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/sentry.server.config.ts",{"size":184,"mtime":1774546669448,"hash":"251","data":"252"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx",{"size":5681,"mtime":1768372347569,"hash":"253","data":"254"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/[...slug]/route.ts",{"size":561,"mtime":1774715746548,"data":"255"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/logo-cloud.tsx",{"size":1142,"mtime":1774603139720,"data":"256"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/payload.config.ts",{"size":614,"mtime":1774716796806,"data":"257"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/payload/globals/landing-page-blocks.ts",{"size":8875,"mtime":1774603243829,"data":"258"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-color.svg",{"size":948,"mtime":1717458243000,"hash":"259"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts",{"size":982,"mtime":1774667904119,"hash":"260","data":"261"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx",{"size":1440,"mtime":1774601252818,"hash":"262","data":"263"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/favicon-light.png",{"size":14406,"mtime":1717458300000,"hash":"264"},"/home/gib/Documents/Code/convex-monorepo/apps/next/.cache/tsbuildinfo.json",{"size":1198363,"mtime":1774718095241,"hash":"265","data":"266"},"/home/gib/Documents/Code/conveLine truncated
|
|
||||||
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:
|
||||||
+38
-23
@@ -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,25 +93,27 @@ 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}
|
||||||
#hostname: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
|
#hostname: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
|
||||||
#domainname: postgres.${NEXT_DOMAIN:-convexmonorepo.gbrown.org}
|
#domainname: postgres.${NEXT_DOMAIN:-convexmonorepo.gbrown.org}
|
||||||
#networks: ['${NETWORK:-nginx-bridge}']
|
#networks: ['${NETWORK:-nginx-bridge}']
|
||||||
#ports: ['5432:5432']
|
#ports: ['5432:5432']
|
||||||
#environment:
|
#environment:
|
||||||
#- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo}
|
#- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo}
|
||||||
#- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
#- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
#- POSTGRES_DB=${POSTGRES_DB:-convexmonorepo_payload}
|
#- POSTGRES_DB=${POSTGRES_DB:-convexmonorepo_payload}
|
||||||
#labels: ['com.centurylinklabs.watchtower.enable=true']
|
#labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
#volumes: ['./volumes/postgres:/var/lib/postgresql/data']
|
#volumes: ['./volumes/postgres:/var/lib/postgresql/data']
|
||||||
#tty: true
|
#tty: true
|
||||||
#stdin_open: true
|
#stdin_open: true
|
||||||
#restart: unless-stopped
|
#restart: unless-stopped
|
||||||
#healthcheck:
|
#healthcheck:
|
||||||
#test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
#test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||||
#start_period: 20s
|
#start_period: 20s
|
||||||
#interval: 30s
|
#interval: 30s
|
||||||
#retries: 5
|
#retries: 5
|
||||||
#timeout: 5s
|
#timeout: 5s
|
||||||
+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",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
[{"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/auth.ts":"1","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/crons.ts":"2","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/index.ts":"3","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/password.ts":"4","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/usesend.ts":"5","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/files.ts":"6","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/http.ts":"7","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/schema.ts":"8","/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/utils.ts":"9"},{"size":3664,"mtime":1774717261375,"results":"10","hashOfConfig":"11"},{"size":528,"mtime":1774717276314,"results":"12","hashOfConfig":"11"},{"size":142,"mtime":1768157708000,"results":"13","hashOfConfig":"11"},{"size":966,"mtime":1774546669452,"results":"14","hashOfConfig":"11"},{"size":3419,"mtime":1774717294835,"results":"15","hashOfConfig":"11"},{"size":582,"mtime":1768239263763,"results":"16","hashOfConfig":"11"},{"size":153,"mtime":1768157491000,"results":"17","hashOfConfig":"11"},{"size":1199,"mtime":1774583096378,"results":"18","hashOfConfig":"11"},{"size":635,"mtime":1768155639000,"results":"19","hashOfConfig":"11"},{"filePath":"20","messages":"21","suppressedMessages":"22","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"pdxq0k",{"filePath":"23","messages":"24","suppressedMessages":"25","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"26","messages":"27","suppressedMessages":"28","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"29","messages":"30","suppressedMessages":"31","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":1,"source":null},{"filePath":"32","messages":"33","suppressedMessages":"34","errorCount":20,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":3,"source":null},{"filePath":"35","messages":"36","suppressedMessages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"38","messages":"39","suppressedMessages":"40","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"41","messages":"42","suppressedMessages":"43","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"44","messages":"45","suppressedMessages":"46","errorCount":3,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":null},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/auth.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/crons.ts",["47"],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/index.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/password.ts",["48","49"],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/usesend.ts",["50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69","70","71","72"],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/files.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/http.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/schema.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/utils.ts",["73","74","75","76","77"],[],{"ruleId":"78","severity":1,"message":"79","line":3,"column":10,"messageId":"80","endLine":3,"endColumn":13,"suggestions":"81"},{"ruleId":"82","severity":1,"message":"83","line":8,"column":10,"messageId":"84","endLine":13,"endColumn":4,"fix":"85"},{"ruleId":"78","severity":1,"message":"86","line":8,"column":19,"messageId":"80","endLine":8,"endColumn":22},{"ruleId":"87","severity":2,"message":"88","line":11,"column":5,"messageId":"89","endLine":11,"endColumn":66},{"ruleId":"90","severity":2,"message":"91","line":11,"column":19,"messageId":"92","endLine":11,"endColumn":22},{"ruleId":"82","severity":1,"message":"83","line":14,"column":30,"messageId":"84","endLine":21,"endColumn":6,"fix":"93"},{"ruleId":"82","severity":1,"message":"83","line":16,"column":13,"messageId":"84","endLine":18,"endColumn":10,"fix":"94"},{"ruleId":"82","severity":1,"message":"83","line":23,"column":34,"messageId":"84","endLine":77,"endColumn":6,"fix":"95"},{"ruleId":"87","severity":2,"message":"88","line":26,"column":13,"messageId":"89","endLine":26,"endColumn":59},{"ruleId":"90","severity":2,"message":"91","line":26,"column":31,"messageId":"92","endLine":2Line truncated
|
|
||||||
@@ -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","23","24","25"],{"key":"26","value":"27"},{"key":"28","value":"29"},{"key":"30","value":"31"},{"key":"32","value":"33"},{"key":"34","value":"35"},{"key":"36","value":"37"},{"key":"38","value":"39"},{"key":"40","value":"41"},{"key":"42","value":"43"},{"key":"44","value":"45"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},{"key":"58","value":"59"},{"key":"60","value":"61"},{"key":"62","value":"63"},{"key":"64","value":"65"},{"key":"66","value":"67"},{"key":"68","value":"69"},{"key":"70","value":"71"},{"key":"72","value":"73"},{"key":"74","value":"75"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/.gitignore",{"size":16,"mtime":1766222924000,"hash":"76"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/usesend.ts",{"size":3419,"mtime":1774717294835,"hash":"77","data":"78"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/scripts/generateKeys.mjs",{"size":520,"mtime":1768368324432,"hash":"79","data":"80"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/tsconfig.json",{"size":732,"mtime":1766222924000,"hash":"81","data":"82"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/http.ts",{"size":153,"mtime":1768157491000,"hash":"83","data":"84"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/schema.ts",{"size":1199,"mtime":1774583096378,"hash":"85","data":"86"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/eslint.config.ts",{"size":233,"mtime":1774717251737,"data":"87"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/auth.ts",{"size":3664,"mtime":1774717261375,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/files.ts",{"size":582,"mtime":1768239263763,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/index.ts",{"size":142,"mtime":1768157708000,"hash":"92","data":"93"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/types/auth.ts",{"size":174,"mtime":1768188156942,"hash":"94","data":"95"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/crons.ts",{"size":528,"mtime":1774717276314,"hash":"96","data":"97"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/auth.config.ts",{"size":127,"mtime":1768156477000,"hash":"98","data":"99"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/server.d.ts",{"size":5600,"mtime":1774031968455,"hash":"100","data":"101"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/globals.d.ts",{"size":358,"mtime":1774718189069,"data":"102"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/README.md",{"size":2525,"mtime":1768155639000,"hash":"103","data":"104"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/api.d.ts",{"size":1566,"mtime":1774718095030,"hash":"105","data":"106"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/dataModel.d.ts",{"size":1737,"mtime":1774718420526,"hash":"107","data":"108"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/server.js",{"size":3696,"mtime":1774031968444,"hash":"109","data":"110"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/types/index.ts",{"size":69,"mtime":1768239263861,"hash":"111","data":"112"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/password.ts",{"size":966,"mtime":1774546669452,"hash":"113","data":"114"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/.cache/.prettiercache",{"size":5782,"mtime":1774718094147},"/home/gib/Documents/Code/convex-monorepo/packages/backend/.cache/.eslintcache",{"size":12307,"mtime":1774717492533},"/home/gib/Documents/Code/convex-monorepo/packages/backend/package.json",{"size":1374,"mtime":1774717505903,"hash":"115","data":"116"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/api.js",{"size":480,"mtime":1768372369903,"hash":"117","data":"118"},"175b1a771387d8b7e26145c3d04e0475","14f5e2d18e21f65282ac744b382f2d5f",{"hashOfOptions":"119"},"ccf5b9105ad4ff66fce0a7133fa3db4b",{"hashOfOptions":"120"},"cfa98923457caed911ec68b626ef4234",{"hashOfOptions":"121"},"4bbb95a66a1e615c30ea7c5098a4a609",{"hashOfOptions":"122"},"7e6dcc47831030bfaa3e235d2226cec6",{"hashOfOptions":"123"},{"hashOfOptions":"124"},"3d304208139ef0b2775272a5a4a2a970",{"hashOfOptions":"125"},"5ff3856f1a61b946df899f7c9880b72a",{"hashOfOptions":"126"},"8776a23ed6f2ab3681e4ba5b4bcf3d74",{"hashOfOptions":"127"},"f0f9ef72dc6fef350f3521d3fd8adfc7",{"hashOfOptions":"128"},"0d76c3867783aec5435b0c716f1bfc65",{"hashOfOptions":"129"},"2d4f4ccfc784af9d668d8c64726c2f6b",{"hashOfOptions":"130"},"6719b019702fc173de16b1bdc8e8ed47",{"hashOfOptions":"1Line truncated
|
|
||||||
+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 ?? '[email protected]',
|
from: process.env.USESEND_FROM_EMAIL ?? '[email protected]',
|
||||||
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}']),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/accordion.tsx":"1","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/alert-dialog.tsx":"2","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/alert.tsx":"3","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/aspect-ratio.tsx":"4","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/avatar.tsx":"5","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/badge.tsx":"6","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/based-avatar.tsx":"7","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/based-progress.tsx":"8","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/breadcrumb.tsx":"9","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/button-group.tsx":"10","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/button.tsx":"11","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/calendar.tsx":"12","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/card.tsx":"13","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/carousel.tsx":"14","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/chart.tsx":"15","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/checkbox.tsx":"16","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/collapsible.tsx":"17","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/combobox.tsx":"18","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/command.tsx":"19","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/context-menu.tsx":"20","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/dialog.tsx":"21","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/drawer.tsx":"22","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/dropdown-menu.tsx":"23","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/empty.tsx":"24","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/field.tsx":"25","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/form.tsx":"26","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/hooks/index.tsx":"27","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/hooks/use-mobile.ts":"28","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/hooks/use-on-click-outside.tsx":"29","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/hover-card.tsx":"30","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/image-crop.tsx":"31","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/index.tsx":"32","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/input-group.tsx":"33","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/input-otp.tsx":"34","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/input.tsx":"35","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/item.tsx":"36","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/kbd.tsx":"37","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/label.tsx":"38","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/menubar.tsx":"39","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/native-select.tsx":"40","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/navigation-menu.tsx":"41","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/pagination.tsx":"42","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/popover.tsx":"43","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/progress.tsx":"44","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/radio-group.tsx":"45","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/resizable.tsx":"46","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/scroll-area.tsx":"47","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/select.tsx":"48","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/separator.tsx":"49","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/sheet.tsx":"50","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/sidebar.tsx":"51","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/skeleton.tsx":"52","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/slider.tsx":"53","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/sonner.tsx":"54","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/spinner.tsx":"55","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/status-message.tsx":"56","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/submit-button.tsx":"57","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/switch.tsx":"58","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/table.tsx":"59","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/tabs.tsx":"60","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/textarea.tsx":"61","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/theme.tsx":"62","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/toggle-group.tsx":"63","/home/gib/Documents/Code/convex-monorepo/packages/ui/src/toggle.tsx":"64","/home/gib/Documents/Code/convex-monorepo/pacLine truncated
|
|
||||||
@@ -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","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69","70","71","72"],{"key":"73","value":"74"},{"key":"75","value":"76"},{"key":"77","value":"78"},{"key":"79","value":"80"},{"key":"81","value":"82"},{"key":"83","value":"84"},{"key":"85","value":"86"},{"key":"87","value":"88"},{"key":"89","value":"90"},{"key":"91","value":"92"},{"key":"93","value":"94"},{"key":"95","value":"96"},{"key":"97","value":"98"},{"key":"99","value":"100"},{"key":"101","value":"102"},{"key":"103","value":"104"},{"key":"105","value":"106"},{"key":"107","value":"108"},{"key":"109","value":"110"},{"key":"111","value":"112"},{"key":"113","value":"114"},{"key":"115","value":"116"},{"key":"117","value":"118"},{"key":"119","value":"120"},{"key":"121","value":"122"},{"key":"123","value":"124"},{"key":"125","value":"126"},{"key":"127","value":"128"},{"key":"129","value":"130"},{"key":"131","value":"132"},{"key":"133","value":"134"},{"key":"135","value":"136"},{"key":"137","value":"138"},{"key":"139","value":"140"},{"key":"141","value":"142"},{"key":"143","value":"144"},{"key":"145","value":"146"},{"key":"147","value":"148"},{"key":"149","value":"150"},{"key":"151","value":"152"},{"key":"153","value":"154"},{"key":"155","value":"156"},{"key":"157","value":"158"},{"key":"159","value":"160"},{"key":"161","value":"162"},{"key":"163","value":"164"},{"key":"165","value":"166"},{"key":"167","value":"168"},{"key":"169","value":"170"},{"key":"171","value":"172"},{"key":"173","value":"174"},{"key":"175","value":"176"},{"key":"177","value":"178"},{"key":"179","value":"180"},{"key":"181","value":"182"},{"key":"183","value":"184"},{"key":"185","value":"186"},{"key":"187","value":"188"},{"key":"189","value":"190"},{"key":"191","value":"192"},{"key":"193","value":"194"},{"key":"195","value":"196"},{"key":"197","value":"198"},{"key":"199","value":"200"},{"key":"201","value":"202"},{"key":"203","value":"204"},{"key":"205","value":"206"},{"key":"207","value":"208"},{"key":"209","value":"210"},{"key":"211","value":"212"},{"key":"213","value":"214"},{"key":"215","value":"216"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/based-avatar.tsx",{"size":1745,"mtime":1774589278255,"data":"217"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/hooks/index.tsx",{"size":104,"mtime":1774546669457,"data":"218"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/hover-card.tsx",{"size":1476,"mtime":1774546669457,"data":"219"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/popover.tsx",{"size":2261,"mtime":1774546669458,"data":"220"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/tooltip.tsx",{"size":1771,"mtime":1774546669459,"data":"221"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/aspect-ratio.tsx",{"size":273,"mtime":1774546669455,"data":"222"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/button.tsx",{"size":3380,"mtime":1774546669455,"data":"223"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/toggle-group.tsx",{"size":2238,"mtime":1774716762755,"data":"224"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/hooks/use-mobile.ts",{"size":589,"mtime":1774546669457,"data":"225"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/submit-button.tsx",{"size":1192,"mtime":1773778628581,"data":"226"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/.cache/.eslintcache",{"size":34794,"mtime":1774717495793},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/carousel.tsx",{"size":5695,"mtime":1774718094545,"data":"227"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/textarea.tsx",{"size":736,"mtime":1774546669459,"data":"228"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/.cache/.prettiercache",{"size":13631,"mtime":1774718095584},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/avatar.tsx",{"size":2764,"mtime":1774546669455,"data":"229"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/command.tsx",{"size":4628,"mtime":1774546669456,"data":"230"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/dropdown-menu.tsx",{"size":8511,"mtime":1774546669456,"data":"231"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/empty.tsx",{"size":2265,"mtime":1774546669456,"data":"232"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/item.tsx",{"size":4575,"mtime":1774546669457,"data":"233"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/spinner.tsx",{"size":307,"mtime":1774546669458,"data":"234"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/package.json",{"size":2137,"mtime":1774590118182,"data":"235"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/dialog.tsx",{"size":3925,"mtime":1774546669456,"data":"236"},"/Line truncated
|
|
||||||
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(() => {
|
||||||
|
|||||||
+44
-19
@@ -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,14 +128,29 @@ const ChartTooltipContent = ({
|
|||||||
color,
|
color,
|
||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
}: React.ComponentProps<'div'> & {
|
||||||
React.ComponentProps<'div'> & {
|
active?: boolean;
|
||||||
hideLabel?: boolean;
|
payload?: ChartPayloadItem[];
|
||||||
hideIndicator?: boolean;
|
label?: React.ReactNode;
|
||||||
indicator?: 'line' | 'dot' | 'dashed';
|
labelFormatter?: (
|
||||||
nameKey?: string;
|
value: React.ReactNode,
|
||||||
labelKey?: string;
|
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;
|
||||||
|
hideIndicator?: boolean;
|
||||||
|
indicator?: 'line' | 'dot' | 'dashed';
|
||||||
|
nameKey?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
}) => {
|
||||||
const { config } = useChart();
|
const { config } = useChart();
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
@@ -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,11 +283,12 @@ const ChartLegendContent = ({
|
|||||||
payload,
|
payload,
|
||||||
verticalAlign = 'bottom',
|
verticalAlign = 'bottom',
|
||||||
nameKey,
|
nameKey,
|
||||||
}: React.ComponentProps<'div'> &
|
}: React.ComponentProps<'div'> & {
|
||||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
payload?: ChartPayloadItem[];
|
||||||
hideIcon?: boolean;
|
verticalAlign?: 'top' | 'bottom';
|
||||||
nameKey?: string;
|
hideIcon?: boolean;
|
||||||
}) => {
|
nameKey?: string;
|
||||||
|
}) => {
|
||||||
const { config } = useChart();
|
const { config } = useChart();
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
@@ -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,13 +68,16 @@ const ComboboxInput = ({
|
|||||||
/>
|
/>
|
||||||
<InputGroupAddon align='inline-end'>
|
<InputGroupAddon align='inline-end'>
|
||||||
{showTrigger && (
|
{showTrigger && (
|
||||||
<InputGroupButton
|
<ComboboxTrigger
|
||||||
size='icon-xs'
|
render={
|
||||||
variant='ghost'
|
<InputGroupButton
|
||||||
render={<ComboboxTrigger />}
|
size='icon-xs'
|
||||||
data-slot='input-group-button'
|
variant='ghost'
|
||||||
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
|
data-slot='input-group-button'
|
||||||
disabled={disabled}
|
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showClear && <ComboboxClear disabled={disabled} />}
|
{showClear && <ComboboxClear disabled={disabled} />}
|
||||||
|
|||||||
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