@clubedge/create-clubedge-app 0.43.1 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +45 -24
- package/dist/cli/ui.js +2 -1
- package/dist/core/customize.js +34 -8
- package/dist/core/deploy.js +39 -12
- package/dist/core/manifest-fields.js +5 -1
- package/dist/core/manifest-packs.js +107 -14
- package/dist/core/manifest.js +5 -1
- package/dist/core/merge.js +26 -12
- package/dist/core/package-json-merge.js +106 -0
- package/dist/core/plan.js +95 -24
- package/dist/core/project.js +5 -2
- package/dist/deploy-docker.js +8 -4
- package/dist/deploy-preview.js +6 -1
- package/dist/deploy.js +95 -46
- package/dist/index.js +1 -1
- package/dist/mcp-server.js +4 -2
- package/dist/modify.js +1 -1
- package/package.json +3 -3
- package/skills/clubedge-starter/SKILL.md +1 -1
- package/template/.github/workflows/ci.yml +107 -6
- package/template/.github/workflows/preview.yml +13 -5
- package/template/.prettierignore +10 -0
- package/template/.railway/railway.ts +9 -0
- package/template/AGENTS.md +9 -3
- package/template/README.md +143 -35
- package/template/SETUP.md +273 -61
- package/template/_gitignore +13 -0
- package/template/apps/api/.env.example +40 -13
- package/template/apps/api/clubedge.stack.json +3 -1
- package/template/apps/api/package.json +3 -2
- package/template/apps/api/src/app.test.ts +24 -1
- package/template/apps/api/src/app.ts +68 -3
- package/template/apps/api/src/config/site.json +3 -1
- package/template/apps/api/src/env/production.test.ts +28 -6
- package/template/apps/api/src/env/production.ts +17 -2
- package/template/apps/api/src/rate-limit.ts +1 -1
- package/template/apps/api/src/routes/app-version.test.ts +68 -7
- package/template/apps/api/src/routes/app-version.ts +10 -3
- package/template/apps/api/src/server/app-version.test.ts +109 -7
- package/template/apps/api/src/server/app-version.ts +94 -44
- package/template/apps/api/src/server/auth.ts +8 -0
- package/template/apps/api/src/server/index.ts +2 -0
- package/template/apps/api/src/server/variants/auth.better-auth.ts +60 -3
- package/template/apps/api-fastapi/.env.example +31 -12
- package/template/apps/api-fastapi/clubedge.stack.json +1 -0
- package/template/apps/api-fastapi/package.json +1 -1
- package/template/apps/api-fastapi/pyproject.toml +1 -1
- package/template/apps/api-fastapi/src/clubedge_api/app.py +41 -3
- package/template/apps/api-fastapi/src/clubedge_api/app_version.py +160 -28
- package/template/apps/api-fastapi/src/clubedge_api/contract.py +4 -0
- package/template/apps/api-fastapi/src/clubedge_api/production_env.py +2 -0
- package/template/apps/api-fastapi/src/clubedge_api/routes/app_version.py +17 -8
- package/template/apps/api-fastapi/src/config/site.json +3 -1
- package/template/apps/api-fastapi/tests/test_app_version.py +247 -20
- package/template/apps/api-fastapi/tests/test_production_env.py +9 -2
- package/template/apps/api-fastapi/uv.lock +1 -1
- package/template/apps/capacitor/assets/splash-dark.svg +1 -0
- package/template/apps/capacitor/assets/splash-icon.png +0 -0
- package/template/apps/capacitor/assets/splash.svg +1 -0
- package/template/apps/capacitor/package.json +1 -1
- package/template/apps/capacitor/scripts/icons.mjs +113 -14
- package/template/apps/expo/.env.example +24 -0
- package/template/apps/expo/app.config.ts +51 -0
- package/template/apps/expo/assets/adaptive-icon.png +0 -0
- package/template/apps/expo/assets/icon.png +0 -0
- package/template/apps/expo/assets/splash-icon.png +0 -0
- package/template/apps/expo/clubedge.stack.json +51 -0
- package/template/apps/expo/package.json +47 -0
- package/template/apps/expo/src/app/(app)/_layout.tsx +12 -0
- package/template/apps/expo/src/app/(app)/index.tsx +193 -0
- package/template/apps/expo/src/app/(app)/settings/_layout.tsx +12 -0
- package/template/apps/expo/src/app/(app)/settings/delete-account.tsx +76 -0
- package/template/apps/expo/src/app/(app)/settings/edit-profile.tsx +44 -0
- package/template/apps/expo/src/app/(app)/settings/index.tsx +90 -0
- package/template/apps/expo/src/app/(app)/settings/password.tsx +49 -0
- package/template/apps/expo/src/app/_layout.tsx +28 -0
- package/template/apps/expo/src/app/forgot-password.tsx +80 -0
- package/template/apps/expo/src/app/reset-password.tsx +78 -0
- package/template/apps/expo/src/app/sign-in.tsx +84 -0
- package/template/apps/expo/src/app/sign-up.tsx +106 -0
- package/template/apps/expo/src/app/welcome.tsx +99 -0
- package/template/apps/expo/src/components/sign-out-sheet.tsx +138 -0
- package/template/apps/expo/src/components/tab-bar.tsx +143 -0
- package/template/apps/expo/src/components/ui.tsx +670 -0
- package/template/apps/expo/src/components/update-gate.tsx +344 -0
- package/template/apps/expo/src/config/site.json +13 -0
- package/template/apps/expo/src/lib/account.ts +62 -0
- package/template/apps/expo/src/lib/api.ts +6 -0
- package/template/apps/expo/src/lib/app-update.ts +36 -0
- package/template/apps/expo/src/lib/auth-client.ts +80 -0
- package/template/apps/expo/src/lib/auth-enabled.ts +1 -0
- package/template/apps/expo/src/lib/auth.ts +2 -0
- package/template/apps/expo/src/lib/config.ts +21 -0
- package/template/apps/expo/src/lib/session.tsx +44 -0
- package/template/apps/expo/src/lib/theme.ts +176 -0
- package/template/apps/expo/src/types/expo.d.ts +3 -0
- package/template/apps/expo/src/types/images.d.ts +13 -0
- package/template/apps/expo/src/variants/api.auth.ts +18 -0
- package/template/apps/expo/src/variants/app-layout.auth.tsx +17 -0
- package/template/apps/expo/src/variants/auth-enabled.auth.ts +1 -0
- package/template/apps/expo/src/variants/auth.better-auth.ts +108 -0
- package/template/apps/expo/src/variants/auth.supabase.ts +149 -0
- package/template/apps/expo/src/variants/layout.auth.tsx +67 -0
- package/template/apps/expo/tsconfig.json +12 -0
- package/template/apps/start/package.json +1 -1
- package/template/apps/start/src/config/site.json +3 -1
- package/template/apps/vite/.env.example +4 -4
- package/template/apps/vite/auth.server.mjs +34 -10
- package/template/apps/vite/package.json +3 -2
- package/template/apps/vite/server.mjs +2 -1
- package/template/apps/vite/src/config/site.json +3 -1
- package/template/apps/vite/src/main.tsx +4 -3
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/config/site.json +3 -1
- package/template/clubedge.template.json +42 -3
- package/template/docker-compose.yml +15 -12
- package/template/eslint.config.mjs +9 -0
- package/template/package.json +9 -2
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth-appwrite/package.json +1 -1
- package/template/packages/auth-better-auth/package.json +2 -2
- package/template/packages/auth-better-auth/src/index.ts +26 -5
- package/template/packages/auth-firebase/package.json +1 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/auth-supabase/src/browser.ts +3 -3
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/contract/openapi.yaml +85 -3
- package/template/packages/contract/package.json +1 -1
- package/template/packages/contract/src/app-update.test.ts +321 -0
- package/template/packages/contract/src/app-update.ts +237 -0
- package/template/packages/contract/src/app-version.test.ts +76 -0
- package/template/packages/contract/src/app-version.ts +71 -0
- package/template/packages/contract/src/conformance.ts +42 -1
- package/template/packages/contract/src/index.ts +2 -0
- package/template/packages/contract/src/schema.ts +44 -4
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs-bullmq/package.json +1 -1
- package/template/packages/jobs-inngest/package.json +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/mobile/package.json +1 -1
- package/template/packages/mobile/src/demo/api.ts +7 -1
- package/template/packages/mobile/src/demo/demo.test.ts +2 -0
- package/template/packages/mobile/src/update-notice.test.ts +26 -2
- package/template/packages/mobile/src/update-notice.tsx +55 -41
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/pnpm-lock.yaml +5985 -410
- package/template/supabase/config.toml +10 -0
- package/template.lock.json +52 -4
- package/template/packages/mobile/src/version.test.ts +0 -65
- package/template/packages/mobile/src/version.ts +0 -37
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.45.0
|
|
6
|
+
|
|
7
|
+
- **Starter v0.29.0** by default (#159). Its template schema is still 6. The [Starter's release notes](https://github.com/Clubedge/clubedge-starter/releases/tag/v0.29.0) cover:
|
|
8
|
+
- an update policy for apps installed on phones: a minimum announced for a date, values per platform, and APIs that answer apps below the minimum `426 update_required`;
|
|
9
|
+
- a new design for the native app (`--mobile expo`);
|
|
10
|
+
- splash screens that are never cropped or stretched.
|
|
11
|
+
- **`package.json` merges by key** (#158) when `add`, `remove`, or `upgrade` merges the Starter's changes into your project:
|
|
12
|
+
- A key only one side changed takes that side's value, and objects merge key by key, so your dependencies and the Starter's no longer collide line by line.
|
|
13
|
+
- A key both sides changed differently is a conflict, with the usual markers around that key's lines only.
|
|
14
|
+
- Your key order and indentation are kept. A file that doesn't parse, such as one with markers left from an earlier merge, merges by line as before.
|
|
15
|
+
- **The Starter pin** keeps the release a change came with in notes such as "since CLI 0.44.0, with Starter v0.28.0", which moving the pin to v0.29.0 had changed.
|
|
16
|
+
- CI uses astral-sh/setup-uv 10.3.2 (#154).
|
|
17
|
+
|
|
18
|
+
## 0.44.0
|
|
19
|
+
|
|
20
|
+
- **The native app with Expo**: default to Starter `v0.28.0` (#151). `--mobile expo` adds `apps/mobile`, an Android and iOS app with its own native screens, which calls the project's API (#147):
|
|
21
|
+
- Template schema 6 adds the `mobile` role: an app with its own screens that calls an API. Like a shell, it runs no server, so it has no Dockerfile or deployment, and `deploy` leaves it out. Older CLIs refuse a schema 6 Starter and ask for an update.
|
|
22
|
+
- It needs an API, `--api hono` or `--api fastapi`: with React + Vite, or without a website. Next.js and TanStack Start have no separate API for it to call, so they refuse `--mobile expo`. It can sit next to the Capacitor app (`--shell capacitor`).
|
|
23
|
+
- A mobile stack lists the module options it implements, and the others are refused with it: Expo signs in with Better Auth, Supabase, or nothing.
|
|
24
|
+
- Interactive runs ask about it after the framework and the API.
|
|
25
|
+
- The [Starter's release notes](https://github.com/Clubedge/clubedge-starter/releases/tag/v0.28.0) cover the app's screens, sign-in, update notice, and builds.
|
|
26
|
+
- **Projects without a website** (#148): `--framework none --api hono` (or `fastapi`), with or without `--mobile expo`, creates an API, and the native app that calls it directly:
|
|
27
|
+
- Schema 6 slots can be optional. The project records `framework: none`, so `add`, `remove`, and `upgrade` keep it without a web app.
|
|
28
|
+
- A slot names the files and root scripts only its apps use, left out when it is empty, such as the browser tests, and the slot whose package the root scripts use instead, so `pnpm dev` starts the API.
|
|
29
|
+
- Templates list the selections they refuse, with the reason (`rules`). Prompts and `add` offer only what the rules allow, and a refused option fails with the reason. Without a website, the Starter refuses payments, Inngest, FastAPI with Better Auth, and deploy targets other than Docker and Railway.
|
|
30
|
+
- `deploy` publishes the API itself: as the public app behind Caddy with Docker Compose, or as one public service on Railway, with Better Auth's URL and secret. Vercel, Netlify, Fly.io, and Render deploy a web app, so they refuse a project without one, and `deploy --preview` refuses it with the Railway commands for a preview by hand.
|
|
31
|
+
- **Audit exceptions per stack** (#150): a stack lists the advisories `pnpm audit` skips because of its dependencies (`auditIgnores`), and projects without the stack audit them again. The Starter's Expo stack skips two in Expo's development CLI that have no usable fix.
|
|
32
|
+
- The showcase waits for the billing portal's subscription before recording it, so its video no longer ends on a spinner (#146).
|
|
33
|
+
|
|
5
34
|
## 0.43.1
|
|
6
35
|
|
|
7
36
|
- **`try` shows Railway's reason when Railway refuses a VM** (#144):
|
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ pnpm dev
|
|
|
102
102
|
|
|
103
103
|
Or start it with npm: `npx @clubedge/create-clubedge-app my-app`. The generated project is a pnpm workspace, so dependencies are installed with pnpm either way; the CLI falls back to `corepack pnpm` when `pnpm` is not on your path. Without a destination, the CLI asks where to create the project.
|
|
104
104
|
|
|
105
|
-
The generated project includes `apps/web/.env.local` (and `apps/api/.env.local` with a separate API, `apps/shell/.env.local` with a mobile app), created from the example and pointing at the local services you chose. Its `SETUP.md` describes every variable and provider. The root route is a landing page named after your project, and the dashboard is at `/dashboard`.
|
|
105
|
+
The generated project includes `apps/web/.env.local` (and `apps/api/.env.local` with a separate API, `apps/shell/.env.local` with a mobile shell, `apps/mobile/.env.local` with a native app), created from the example and pointing at the local services you chose. Its `SETUP.md` describes every variable and provider. The root route is a landing page named after your project, and the dashboard is at `/dashboard`.
|
|
106
106
|
|
|
107
107
|
The default Starter release ships inside the CLI package, so GitHub is only contacted when you pass `--ref`. Internet access is needed to install dependencies.
|
|
108
108
|
|
|
@@ -145,18 +145,35 @@ With the Hono API, the decoupled stacks support every module option the full-sta
|
|
|
145
145
|
|
|
146
146
|
### A mobile app
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
A project can have a mobile app in two ways, and both at once:
|
|
149
|
+
|
|
150
|
+
- **A shell** packages the Vite frontend as an Android and iOS app with [Capacitor](https://capacitorjs.com), so the app shows the same pages as the website (since CLI 0.42.0, with Starter v0.27.0).
|
|
151
|
+
- **A native app** has its own screens, built with [Expo](https://expo.dev) and React Native, and calls the same API (since CLI 0.44.0, with Starter v0.28.0).
|
|
152
|
+
|
|
153
|
+
Earlier Starters have neither, and the CLI refuses `--shell` and `--mobile` with them.
|
|
149
154
|
|
|
150
155
|
```sh
|
|
151
156
|
pnpm dlx @clubedge/create-clubedge-app my-app --framework vite --api hono --shell capacitor
|
|
157
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --framework vite --api hono --mobile expo
|
|
152
158
|
```
|
|
153
159
|
|
|
154
|
-
| Flag
|
|
155
|
-
|
|
|
156
|
-
| `--shell capacitor`
|
|
157
|
-
| `--
|
|
160
|
+
| Flag | App | Where |
|
|
161
|
+
| ------------------------------- | ------------------------------------------------------------ | ------------- |
|
|
162
|
+
| `--shell capacitor` | The Vite app, packaged for Android and iOS with Capacitor | `apps/shell` |
|
|
163
|
+
| `--mobile expo` | A native Android and iOS app with Expo, calling the same API | `apps/mobile` |
|
|
164
|
+
| `--shell none`, `--mobile none` | No mobile app (the default) | |
|
|
165
|
+
|
|
166
|
+
A shell packages the frontend's static files, so it needs `--framework vite`, and the frontend still needs its API (`--api hono` or `--api fastapi`). A native app calls the API through the website, as the shell does, so it needs one too. Next.js and TanStack Start render their pages on their own server and have no separate API for an app to call: the CLI refuses `--shell` and `--mobile` with them, and interactive runs only ask about a mobile app after a frontend and its API. A shell and a native app run no server, so `deploy` deploys the frontend and the API as above and leaves them out. The native app's Starter lists the sign-in options it supports, so the CLI refuses an `--auth` option the app does not have yet.
|
|
167
|
+
|
|
168
|
+
#### Without a website
|
|
169
|
+
|
|
170
|
+
A project can also be only an API and its native app: `--framework none` leaves the web app out (since CLI 0.44.0, with Starter v0.28.0).
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --framework none --api hono --mobile expo
|
|
174
|
+
```
|
|
158
175
|
|
|
159
|
-
|
|
176
|
+
The app then calls the API at the API's own address. A project needs a web app or an API, so `--framework none` needs `--api`. The project records `framework: none`, so `add`, `remove`, and `upgrade` keep it without a web app, and the commands that manage env files leave `apps/web/.env.local` out. `deploy` publishes the API itself on Railway or with Docker Compose (see their sections below); Fly.io, Vercel, Netlify, and Render deploy a web app, so it refuses them for a project without one.
|
|
160
177
|
|
|
161
178
|
## Choosing modules
|
|
162
179
|
|
|
@@ -194,9 +211,10 @@ Usage:
|
|
|
194
211
|
pnpm dlx @clubedge/create-clubedge-app upgrade [--ref <tag>] (inside a project)
|
|
195
212
|
|
|
196
213
|
Options:
|
|
197
|
-
--framework <id> App framework, such as next or tanstack-start (or vite, a frontend)
|
|
214
|
+
--framework <id> App framework, such as next or tanstack-start (or vite, a frontend, or none)
|
|
198
215
|
--api <id> A separate API for a frontend, such as hono or fastapi
|
|
199
216
|
--shell <id> A mobile app that packages a frontend, such as capacitor
|
|
217
|
+
--mobile <id> A native mobile app that calls the API, such as expo
|
|
200
218
|
--<module> <option> A module option, such as --auth none; the Starter lists its modules
|
|
201
219
|
--ref <ref> Download this Starter tag, branch, or commit from GitHub instead
|
|
202
220
|
(upgrade: the Starter to upgrade to)
|
|
@@ -223,7 +241,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
223
241
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
224
242
|
|
|
225
243
|
# Reproduce a specific Starter release or commit
|
|
226
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
244
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.29.0
|
|
227
245
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
228
246
|
|
|
229
247
|
# Test local Starter changes before releasing them
|
|
@@ -242,15 +260,16 @@ pnpm clubedge add email resend payments stripe # several modules as
|
|
|
242
260
|
pnpm clubedge remove storage payments
|
|
243
261
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
244
262
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
245
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
263
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.29.0 # or a specific tag, branch, or commit
|
|
246
264
|
```
|
|
247
265
|
|
|
248
266
|
The CLI renders the project twice, from the Starter commit and modules recorded in your `package.json` and from the new state, and merges the difference into your project. The new state has other modules for `add` and `remove`, and the newer Starter with the same modules for `upgrade`. Modules a newer Starter introduces start as `none` where possible, so an upgrade never adds a service on its own.
|
|
249
267
|
|
|
250
268
|
- Files you never changed are updated, added, or deleted.
|
|
251
269
|
- Files you changed keep your edits. Where the module change touches the same lines, the file gets conflict markers (`<<<<<<< yours` ... `>>>>>>> clubedge`) for you to resolve. The Starter's whole new version of each such file is saved in `.clubedge/incoming/` to compare with yours (`git diff --no-index <file> .clubedge/incoming/<file>`); Git ignores that folder, so delete it when you are done.
|
|
270
|
+
- `package.json` files merge by key, not by line: a script, dependency, or override you added next to the Starter's keeps both, and markers wrap only a key you both changed to different values, such as the same dependency's range. The file keeps your key order, indentation, and line endings.
|
|
252
271
|
- Your own files are never touched.
|
|
253
|
-
- Each app's local env file (`apps/web/.env.local`, and `apps/api/.env.local` or `apps/
|
|
272
|
+
- Each app's local env file (`apps/web/.env.local`, and `apps/api/.env.local`, `apps/shell/.env.local`, or `apps/mobile/.env.local` when the project has them) keeps your values. Values still at their generated defaults follow the new selection, and new variables are added.
|
|
254
273
|
- `pnpm-lock.yaml` is replaced if you never changed your dependencies; otherwise `pnpm install` reconciles it.
|
|
255
274
|
|
|
256
275
|
Several modules in one command are applied as one change, and their requirements are checked together, so `add auth supabase storage supabase` works even though Supabase Storage needs Supabase Auth. When lines conflict, the CLI lists each file with its number of conflict blocks. Before an upgrade, it lists the Starter releases it crosses with the sections of their release notes, such as `v0.13.0 Observability, Tests, Also`.
|
|
@@ -322,7 +341,7 @@ pnpm clubedge deploy --env-file .env.staging # with another file of product
|
|
|
322
341
|
pnpm clubedge deploy --dry-run # show the plan, create nothing
|
|
323
342
|
```
|
|
324
343
|
|
|
325
|
-
With [Railway's CLI](https://docs.railway.com/cli) (the installed `railway`, or `npx @railway/cli`) and your Railway login, it creates or reuses a project named after the package and the databases selected by the modules. Full-stack projects get one public app service. A Vite project gets two services: the frontend has the public `*.up.railway.app` domain, and the API stays private; the frontend's Node server (`node server.mjs`) forwards the API routes to it over Railway's private network. API variables, including `FRONTEND_URL`, go on the API service; `VITE_*` values go on the frontend service for its build. With Better Auth, the frontend's Node server runs auth: it gets the same `DATABASE_URL` as the API, a generated `BETTER_AUTH_SECRET`, the email settings, and `BETTER_AUTH_URL`, the public frontend URL both services use. Each service's unset variables are reported from its own env example. The API's migration command runs before the API release, and the frontend and API deployments are both awaited. Database variables are references to Railway's Postgres and Redis by default. Values from `.env.production` (or `--env-file`) take precedence; external database URLs stay external, and localhost URLs are ignored. `.railway/railway.ts` describes the selected service topology and health checks; `deploy` applies it and adds Railway's TypeScript SDK (`railway`) as a dev dependency. Commit the generated file and package changes. It needs Railway CLI 5.42.1 or newer. Running `deploy` again reuses the services, so it also adds variables and ships new releases. Railway bills usage on your plan.
|
|
344
|
+
With [Railway's CLI](https://docs.railway.com/cli) (the installed `railway`, or `npx @railway/cli`) and your Railway login, it creates or reuses a project named after the package and the databases selected by the modules. Full-stack projects get one public app service. A project without a website (`--framework none`) also gets one, the API, with the public `*.up.railway.app` domain its mobile app calls, its migrations before each release, and, with Better Auth, which then runs in the API, a generated `BETTER_AUTH_SECRET` and its own public URL as `BETTER_AUTH_URL`. A Vite project gets two services: the frontend has the public `*.up.railway.app` domain, and the API stays private; the frontend's Node server (`node server.mjs`) forwards the API routes to it over Railway's private network. API variables, including `FRONTEND_URL`, go on the API service; `VITE_*` values go on the frontend service for its build. With Better Auth, the frontend's Node server runs auth: it gets the same `DATABASE_URL` as the API, a generated `BETTER_AUTH_SECRET`, the email settings, and `BETTER_AUTH_URL`, the public frontend URL both services use. Each service's unset variables are reported from its own env example. The API's migration command runs before the API release, and the frontend and API deployments are both awaited. Database variables are references to Railway's Postgres and Redis by default. Values from `.env.production` (or `--env-file`) take precedence; external database URLs stay external, and localhost URLs are ignored. `.railway/railway.ts` describes the selected service topology and health checks; `deploy` applies it and adds Railway's TypeScript SDK (`railway`) as a dev dependency. Commit the generated file and package changes. It needs Railway CLI 5.42.1 or newer. Running `deploy` again reuses the services, so it also adds variables and ships new releases. Railway bills usage on your plan.
|
|
326
345
|
|
|
327
346
|
### Deploy to Fly.io (experimental)
|
|
328
347
|
|
|
@@ -344,7 +363,7 @@ In a project with `--deploy render`, `pnpm clubedge deploy` uses Render's API. R
|
|
|
344
363
|
|
|
345
364
|
In a project with `--deploy docker` (the default), `pnpm clubedge deploy` prepares the project for a server of your own and creates nothing anywhere else. It writes three files to commit:
|
|
346
365
|
|
|
347
|
-
- `compose.yaml` runs each app's image behind [Caddy](https://caddyserver.com), the only service that publishes ports (80, and 443 over TCP and UDP): it serves `DOMAIN` over HTTPS, with certificates it obtains and renews by itself. A full-stack app (Next.js or TanStack Start) is one service, `web`, built from the root `Dockerfile`; a frontend and its API are two, the frontend `web`, which Caddy serves, and `api`, which stays on the Compose network behind the frontend's `/api` proxy
|
|
366
|
+
- `compose.yaml` runs each app's image behind [Caddy](https://caddyserver.com), the only service that publishes ports (80, and 443 over TCP and UDP): it serves `DOMAIN` over HTTPS, with certificates it obtains and renews by itself. A full-stack app (Next.js or TanStack Start) is one service, `web`, built from the root `Dockerfile`; a frontend and its API are two, the frontend `web`, which Caddy serves, and `api`, which stays on the Compose network behind the frontend's `/api` proxy; without a website, `api` is the one service, which Caddy serves, and Better Auth in it gets `BETTER_AUTH_URL`. Each service has an image, `${IMAGE_PREFIX:-<project>}-<service>:${TAG:-latest}`, and its build: `docker compose up -d` pulls the released images when `IMAGE_PREFIX` points at GitHub's registry, and `--build` builds them on the server. The public URL defaults to `https://$DOMAIN`: Next.js also gets it as a build argument, and Better Auth on a Vite frontend as `BETTER_AUTH_URL`.
|
|
348
367
|
- `Caddyfile`: every request for `DOMAIN` goes to the public app.
|
|
349
368
|
- `.github/workflows/release-images.yml`: each published GitHub release builds every app's image for `linux/amd64` and `linux/arm64` and pushes it to `ghcr.io/<owner>/<repository>-<service>`, tagged with its version (`1.2.3`), `1.2`, and `latest`; run by hand on the default branch, it pushes `edge`. It never runs for tag pushes, so a deleted tag leaves no images behind. The values an image compiles in come from repository variables: `NEXT_PUBLIC_APP_URL` for Next.js, the `VITE_*` values for a Vite frontend.
|
|
350
369
|
|
|
@@ -354,7 +373,7 @@ Then point the domain's DNS A and AAAA records at the server, publish a release,
|
|
|
354
373
|
# .env.production: DOMAIN=example.com, the app's variables, and, to run the released images,
|
|
355
374
|
# IMAGE_PREFIX=ghcr.io/<owner>/<repository> (TAG=1.2.3 pins a release; latest otherwise)
|
|
356
375
|
docker login ghcr.io # while the packages are private: a token with read:packages
|
|
357
|
-
docker compose --env-file .env.production run --rm web node /app/migrate.mjs # api
|
|
376
|
+
docker compose --env-file .env.production run --rm web node /app/migrate.mjs # api when the project has one
|
|
358
377
|
docker compose --env-file .env.production up -d # --build builds the images on the server
|
|
359
378
|
```
|
|
360
379
|
|
|
@@ -372,7 +391,7 @@ In a project that deploys to Vercel or Railway, `pnpm clubedge deploy --preview`
|
|
|
372
391
|
NEON_API_KEY=... NEON_PROJECT_ID=... pnpm clubedge deploy --preview
|
|
373
392
|
```
|
|
374
393
|
|
|
375
|
-
With `NEON_API_KEY` and `NEON_PROJECT_ID` (in the shell, or in the env file), the preview gets its own [Neon branch](https://neon.com/docs/introduction/branching), `preview/<git branch>`: a copy of the production database made in seconds that shares its storage until it changes. `deploy` creates it, or reuses it on later runs, applies this branch's migrations to it, and points the preview's `DATABASE_URL` and `DATABASE_URL_DIRECT` at it. On Vercel, the preview is a preview deployment with the project's Preview variables, and the branch's URLs are set as Preview variables of your Git branch, which needs the Vercel project connected to its Git repository; without the Neon settings, it uses the Preview `DATABASE_URL`. On Railway, it is an environment named `preview-<git branch>`, copied from production with its own domain; without the Neon settings, it gets its own empty copy of Railway's Postgres, and when production's database comes from elsewhere, `deploy --preview` refuses, since the copy would use production's data. Production is not changed. It deploys a single app for now: a Vite frontend and its API are two services, so `deploy --preview` refuses them and explains how to copy production's Railway environment yourself.
|
|
394
|
+
With `NEON_API_KEY` and `NEON_PROJECT_ID` (in the shell, or in the env file), the preview gets its own [Neon branch](https://neon.com/docs/introduction/branching), `preview/<git branch>`: a copy of the production database made in seconds that shares its storage until it changes. `deploy` creates it, or reuses it on later runs, applies this branch's migrations to it, and points the preview's `DATABASE_URL` and `DATABASE_URL_DIRECT` at it. On Vercel, the preview is a preview deployment with the project's Preview variables, and the branch's URLs are set as Preview variables of your Git branch, which needs the Vercel project connected to its Git repository; without the Neon settings, it uses the Preview `DATABASE_URL`. On Railway, it is an environment named `preview-<git branch>`, copied from production with its own domain; without the Neon settings, it gets its own empty copy of Railway's Postgres, and when production's database comes from elsewhere, `deploy --preview` refuses, since the copy would use production's data. Production is not changed. It deploys a single web app for now: a Vite frontend and its API are two services, and a project without a website runs only its API, so `deploy --preview` refuses them and explains how to copy production's Railway environment yourself.
|
|
376
395
|
|
|
377
396
|
Projects created with `--infra neon` also have `.github/workflows/preview.yml`, which does this for every pull request in CI: a Neon branch per pull request with its migrations, the schema diff as a comment, a Vercel or Railway preview, and cleanup when the pull request closes. The Starter's SETUP.md lists the secrets it needs.
|
|
378
397
|
|
|
@@ -381,27 +400,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
381
400
|
Each CLI release is tested against one specific Starter release:
|
|
382
401
|
|
|
383
402
|
```text
|
|
384
|
-
create-clubedge-app v0.
|
|
403
|
+
create-clubedge-app v0.45.0
|
|
385
404
|
|
|
|
386
405
|
v
|
|
387
|
-
Clubedge Starter v0.
|
|
388
|
-
commit
|
|
406
|
+
Clubedge Starter v0.29.0
|
|
407
|
+
commit 29350332db4bc44acc1e7b989c04825ff4c0f7b7
|
|
389
408
|
```
|
|
390
409
|
|
|
391
410
|
The current relationship is:
|
|
392
411
|
|
|
393
412
|
| CLI release | Default Starter release | Meaning |
|
|
394
413
|
| --- | --- | --- |
|
|
395
|
-
| `create-clubedge-app@0.
|
|
414
|
+
| `create-clubedge-app@0.45.0` | `clubedge-starter@v0.29.0` | The CLI's required, tested default |
|
|
396
415
|
|
|
397
416
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
398
417
|
|
|
399
|
-
The `v0.
|
|
400
|
-
`
|
|
418
|
+
The `v0.29.0` tag is expected to resolve to commit
|
|
419
|
+
`29350332db4bc44acc1e7b989c04825ff4c0f7b7`. The CLI verifies this before
|
|
401
420
|
scaffolding with the default reference and fails rather than silently using a
|
|
402
421
|
moved tag.
|
|
403
422
|
|
|
404
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
423
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.29.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
|
|
405
424
|
|
|
406
425
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
407
426
|
|
|
@@ -409,7 +428,7 @@ Every generated project records its CLI version, Starter repository, Starter ref
|
|
|
409
428
|
|
|
410
429
|
Each CLI release bundles one Starter release. When the release is built, `scripts/bundle-template.mjs` clones the pinned Starter tag, verifies that it resolves to the pinned commit, and copies its tracked files into the package (`template/` and `template.lock.json`). Running the CLI then needs no GitHub access for the default Starter.
|
|
411
430
|
|
|
412
|
-
The Starter describes itself in `clubedge.template.json`: its app directory, site config, environment files, Docker image name, and files that stay out of generated projects. Schema 2 manifests also list each framework's app directory, environment example, and Dockerfile. The CLI moves the selected framework's app to `apps/web` (renaming its package to `@clubedge/web`), moves its environment example and Dockerfile to the project root, leaves the other frameworks out, and removes their entries from `pnpm-lock.yaml` so `pnpm install --frozen-lockfile` keeps working. Schema 3 adds `modules`: for each option, the workspace packages it uses, the files it owns, the variant files that replace defaults, the root `package.json` scripts it sets, and the options it requires. Files listed under `conditional` contain `clubedge:if <condition>` / `clubedge:end` blocks (in `#`, `//`, `<!-- -->`, or JSX comments) that the CLI keeps or drops for the selection, always removing the markers; `clubedge:if starter-repository` marks sections for Starter maintainers only. Schema 4 moves each app's description into its own stack pack, placed in a slot (`--framework`, `--api`),
|
|
431
|
+
The Starter describes itself in `clubedge.template.json`: its app directory, site config, environment files, Docker image name, and files that stay out of generated projects. Schema 2 manifests also list each framework's app directory, environment example, and Dockerfile. The CLI moves the selected framework's app to `apps/web` (renaming its package to `@clubedge/web`), moves its environment example and Dockerfile to the project root, leaves the other frameworks out, and removes their entries from `pnpm-lock.yaml` so `pnpm install --frozen-lockfile` keeps working. Schema 3 adds `modules`: for each option, the workspace packages it uses, the files it owns, the variant files that replace defaults, the root `package.json` scripts it sets, and the options it requires. Files listed under `conditional` contain `clubedge:if <condition>` / `clubedge:end` blocks (in `#`, `//`, `<!-- -->`, or JSX comments) that the CLI keeps or drops for the selection, always removing the markers; `clubedge:if starter-repository` marks sections for Starter maintainers only. Schema 4 moves each app's description into its own stack pack, placed in a slot (`--framework`, `--api`), schema 5 adds the shell role for a mobile app that packages the frontend (`--shell`), and schema 6 the mobile role for a native app (`--mobile`); see [docs/stack-packs.md](docs/stack-packs.md). The CLI refuses a Starter whose manifest schema is newer than it understands and asks you to update. Starters before v0.3.0 have no manifest and use the previous layout.
|
|
413
432
|
|
|
414
433
|
The CLI first builds a plan (the files to copy and the edits to make), which `--dry-run` prints, and then applies it. The source is organized the same way:
|
|
415
434
|
|
|
@@ -456,7 +475,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
456
475
|
1. Make and validate changes in `clubedge-starter`.
|
|
457
476
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
458
477
|
3. Update the Starter package versions.
|
|
459
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
478
|
+
4. Commit `main`, create a release tag such as `v0.29.0`, and publish the Starter release.
|
|
460
479
|
|
|
461
480
|
### CLI maintainers
|
|
462
481
|
|
|
@@ -510,6 +529,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
510
529
|
| 0.41.0 | v0.26.0 | A conflicted package.json no longer breaks later commands (`--detailed-exit-code`); doctor finds conflict markers; projects own their version and README |
|
|
511
530
|
| 0.42.0 | v0.27.0 | Starter v0.27.0: mobile apps with Capacitor (`--shell capacitor`, template schema 5); client addresses per platform; Docker Compose behind Caddy, with images on GHCR |
|
|
512
531
|
| 0.43.0 | v0.27.1 | Upgrades keep each app's own version; add, remove, and upgrade that change nothing skip the Starter download |
|
|
532
|
+
| 0.44.0 | v0.28.0 | Starter v0.28.0: the native app with Expo (`--mobile expo`, template schema 6); projects without a website (`--framework none`), with template rules; audit exceptions per stack |
|
|
533
|
+
| 0.45.0 | v0.29.0 | Starter v0.29.0: an update policy for installed apps, a new design for the native app, and clean splash screens; `package.json` merged by key |
|
|
513
534
|
|
|
514
535
|
## Community
|
|
515
536
|
|
package/dist/cli/ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
+
import { isOptionalSlot } from "../core/manifest.js";
|
|
2
3
|
import { NO_STACK, stacksIn } from "../core/plan.js";
|
|
3
4
|
import { cliUrl, cliVersion, siteUrl, starterPin } from "../package-info.js";
|
|
4
5
|
// Each glyph is 6 rows tall. Rows are padded to a common width when rendered.
|
|
@@ -72,7 +73,7 @@ export function selectionHelp(manifest) {
|
|
|
72
73
|
}
|
|
73
74
|
return [
|
|
74
75
|
...Object.entries(manifest.slots).flatMap(([id, slot]) => {
|
|
75
|
-
const stacks = [...stacksIn(manifest, id), ...(slot
|
|
76
|
+
const stacks = [...stacksIn(manifest, id), ...(isOptionalSlot(slot) ? [NO_STACK] : [])];
|
|
76
77
|
return stacks.length > 1 ? [line(`--${id} <id>`, `${slot.name}: ${choices(stacks, slot.default ?? NO_STACK)}`)] : [];
|
|
77
78
|
}),
|
|
78
79
|
...Object.entries(manifest.modules).map(([id, module]) => line(`--${id} <id>`, `${module.name}: ${choices(Object.keys(module.options), module.default)}`)),
|
package/dist/core/customize.js
CHANGED
|
@@ -21,7 +21,12 @@ function recordStacks(stacks) {
|
|
|
21
21
|
...(Object.keys(apps).length ? { apps } : {}),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
/** A command with one package name replaced where it stands on its own, as in `--filter=<name>`. */
|
|
25
|
+
function renamePackage(command, from, to) {
|
|
26
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
return command.replace(new RegExp(`(^|[\\s='"])${escaped}(?=$|[\\s'"])`, "g"), `$1${to}`);
|
|
28
|
+
}
|
|
29
|
+
export function customizeRootManifest(source, { packageName, version = "0.1.0" }, provenance, dockerImage, scripts = { set: {}, remove: [] }, droppedAdvisories = []) {
|
|
25
30
|
const manifest = JSON.parse(source);
|
|
26
31
|
manifest.name = packageName;
|
|
27
32
|
manifest.version = version;
|
|
@@ -31,6 +36,23 @@ export function customizeRootManifest(source, { packageName, version = "0.1.0" }
|
|
|
31
36
|
}
|
|
32
37
|
if (Object.keys(scripts.set).length)
|
|
33
38
|
manifest.scripts = { ...manifest.scripts, ...scripts.set };
|
|
39
|
+
for (const [from, to] of Object.entries(scripts.rename ?? {})) {
|
|
40
|
+
for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
|
|
41
|
+
if (typeof command === "string")
|
|
42
|
+
manifest.scripts[name] = renamePackage(command, from, to);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Advisories that only left-out stacks need are audited again; settings left empty go too.
|
|
46
|
+
const auditConfig = manifest.pnpm?.auditConfig;
|
|
47
|
+
if (droppedAdvisories.length && Array.isArray(auditConfig?.ignoreGhsas)) {
|
|
48
|
+
auditConfig.ignoreGhsas = auditConfig.ignoreGhsas.filter((id) => !droppedAdvisories.includes(id));
|
|
49
|
+
if (!auditConfig.ignoreGhsas.length)
|
|
50
|
+
delete auditConfig.ignoreGhsas;
|
|
51
|
+
if (!Object.keys(auditConfig).length)
|
|
52
|
+
delete manifest.pnpm.auditConfig;
|
|
53
|
+
if (!Object.keys(manifest.pnpm).length)
|
|
54
|
+
delete manifest.pnpm;
|
|
55
|
+
}
|
|
34
56
|
manifest.clubedge = {
|
|
35
57
|
cliVersion: provenance.cliVersion,
|
|
36
58
|
starterRepository: provenance.starterRepository,
|
|
@@ -173,20 +195,24 @@ export class ConditionalError extends Error {
|
|
|
173
195
|
const MARKER = /^\s*(?:\/\/|#|%%|<!--|\{?\/\*)\s*clubedge:(if|end)\b\s*(.*?)\s*(?:-->|\*\/\}?)?\s*$/;
|
|
174
196
|
// A single line kept only when its condition holds: "- Item <!-- clubedge:only auth=supabase -->".
|
|
175
197
|
const LINE_MARKER = /\s*(?:\/\/|#|<!--|\{?\/\*)\s*clubedge:only\s+(.+?)\s*(?:-->|\*\/\}?)?\s*$/;
|
|
198
|
+
/** The parts of a condition such as `framework=none && payments!=none`, as markers and rules write them. */
|
|
199
|
+
export function conditionParts(expression, location) {
|
|
200
|
+
return expression.split("&&").map((part) => {
|
|
201
|
+
const match = /^\s*([a-z][a-z0-9-]*)\s*(!?=)\s*([a-z0-9|-]+)\s*$/.exec(part);
|
|
202
|
+
if (!match)
|
|
203
|
+
throw new ConditionalError(`${location}: cannot read the condition "${expression}".`);
|
|
204
|
+
return { key: match[1], equals: match[2] === "=", values: match[3].split("|") };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
176
207
|
function evaluateCondition(expression, selection, location) {
|
|
177
208
|
// Maintainer-only sections of the Starter repository never reach a generated project.
|
|
178
209
|
if (expression.trim() === "starter-repository")
|
|
179
210
|
return false;
|
|
180
|
-
return expression
|
|
181
|
-
const match = /^\s*([a-z][a-z0-9-]*)\s*(!?=)\s*([a-z0-9|-]+)\s*$/.exec(part);
|
|
182
|
-
if (!match)
|
|
183
|
-
throw new ConditionalError(`${location}: cannot read the condition "${expression}".`);
|
|
184
|
-
const [, key, operator, values] = match;
|
|
211
|
+
return conditionParts(expression, location).every(({ key, equals, values }) => {
|
|
185
212
|
const selected = selection[key];
|
|
186
213
|
if (selected === undefined)
|
|
187
214
|
throw new ConditionalError(`${location}: unknown key "${key}".`);
|
|
188
|
-
|
|
189
|
-
return operator === "=" ? matches : !matches;
|
|
215
|
+
return values.includes(selected) === equals;
|
|
190
216
|
});
|
|
191
217
|
}
|
|
192
218
|
/**
|
package/dist/core/deploy.js
CHANGED
|
@@ -188,6 +188,21 @@ export function planServicePair(input) {
|
|
|
188
188
|
const unset = frontendKeys.filter((key) => set[key] === undefined && !existing.frontend[key] && !optional.includes(key));
|
|
189
189
|
return { frontend: { set, unset }, api };
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The variables of an API deployed without a web app, as the public service its mobile app calls.
|
|
193
|
+
* It gets what planVariables provides, with the platform's client-address settings. With Better
|
|
194
|
+
* Auth, which then runs in the API, BETTER_AUTH_URL is the API's public URL, unless the env file
|
|
195
|
+
* names another public one, and the secret is generated unless the service already has one.
|
|
196
|
+
*/
|
|
197
|
+
export function planApiAlone(input) {
|
|
198
|
+
const plan = planVariables(input);
|
|
199
|
+
const configured = input.envFile.BETTER_AUTH_URL;
|
|
200
|
+
if (input.modules.auth === "better-auth")
|
|
201
|
+
plan.set.BETTER_AUTH_URL = configured && !isLocalUrl(configured) ? configured : input.url;
|
|
202
|
+
// The image listens on every interface: its Dockerfile sets HOST.
|
|
203
|
+
plan.unset = plan.unset.filter((key) => plan.set[key] === undefined && key !== "HOST");
|
|
204
|
+
return plan;
|
|
205
|
+
}
|
|
191
206
|
/**
|
|
192
207
|
* DATABASE_URL_DIRECT for a database hosted elsewhere: the env file's, or for a pooled Neon URL the
|
|
193
208
|
* same URL without -pooler. Never next to the platform's own database (no external URL), where it
|
|
@@ -307,7 +322,8 @@ export const railwayIacPath = ".railway/railway.ts";
|
|
|
307
322
|
* the variables stay managed by `deploy` and the dashboard, and the region is Railway's default.
|
|
308
323
|
*/
|
|
309
324
|
export function railwayIacSource(service, services) {
|
|
310
|
-
|
|
325
|
+
// A frontend and its API, or an API without a web app: each app builds its own Dockerfile.
|
|
326
|
+
if (services && Object.values(services).some(({ role }) => role !== "fullstack")) {
|
|
311
327
|
const identifier = (slot) => slot.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
312
328
|
const apiEntry = Object.entries(services).find(([, item]) => item.role === "api");
|
|
313
329
|
const api = apiEntry?.[1];
|
|
@@ -651,10 +667,16 @@ export const fullstackService = {
|
|
|
651
667
|
healthPath: "/api/health",
|
|
652
668
|
platforms: ["docker"],
|
|
653
669
|
};
|
|
654
|
-
/**
|
|
670
|
+
/**
|
|
671
|
+
* The service the public reaches: a frontend, whose API stays private, else the full-stack app,
|
|
672
|
+
* else an API without a web app, which its mobile app calls directly.
|
|
673
|
+
*/
|
|
655
674
|
export function publicService(services) {
|
|
656
675
|
const apps = Object.values(services);
|
|
657
|
-
return apps.find(({ role }) => role === "frontend") ??
|
|
676
|
+
return (apps.find(({ role }) => role === "frontend") ??
|
|
677
|
+
apps.find(({ role }) => role === "fullstack") ??
|
|
678
|
+
apps.find(({ role }) => role === "api") ??
|
|
679
|
+
fullstackService);
|
|
658
680
|
}
|
|
659
681
|
/**
|
|
660
682
|
* The values an app's image compiles in, which its Dockerfile takes as build arguments, from the
|
|
@@ -671,8 +693,8 @@ export function buildArgumentKeys(service, keys) {
|
|
|
671
693
|
* ports: it serves DOMAIN over HTTPS, with certificates it obtains and renews. `frontendKeys` are
|
|
672
694
|
* the variables the public app's env example lists: the values its image compiles in become build
|
|
673
695
|
* arguments, and a frontend's others runtime variables, such as Better Auth's when the frontend's
|
|
674
|
-
* server runs auth.
|
|
675
|
-
* https://DOMAIN.
|
|
696
|
+
* server runs auth. For an API without a web app, the public app, they are the API's. Compose reads
|
|
697
|
+
* their values from its --env-file; the public URL defaults to https://DOMAIN.
|
|
676
698
|
*/
|
|
677
699
|
export function dockerComposeSource(project, services, frontendKeys = []) {
|
|
678
700
|
const apps = Object.values(services);
|
|
@@ -686,22 +708,27 @@ export function dockerComposeSource(project, services, frontendKeys = []) {
|
|
|
686
708
|
// client-address settings default to Caddy's.
|
|
687
709
|
const runtimeKeys = frontendKeys.filter((key) => !key.startsWith("VITE_") && !["API_URL", "PORT", ...clientAddressKeys].includes(key));
|
|
688
710
|
// Better Auth runs in the frontend's server: the API asks it for the session over the Compose network.
|
|
689
|
-
const authUrl = api && frontendKeys.includes("BETTER_AUTH_URL") ? `http://${web.id}:${web.port}` : null;
|
|
711
|
+
const authUrl = api && api !== web && frontendKeys.includes("BETTER_AUTH_URL") ? `http://${web.id}:${web.port}` : null;
|
|
690
712
|
const environment = (service) => {
|
|
713
|
+
// A frontend has no env_file: CLIENT_IP_HEADER from the env file needs a line of its own.
|
|
714
|
+
const address = [
|
|
715
|
+
" # Caddy, the one proxy in front, sets X-Forwarded-For to the client's address.",
|
|
716
|
+
...(service.role === "frontend" ? clientAddressKeys : Object.keys(behindCaddy)).map((key) => ` ${key}: ${fromEnv(key, behindCaddy[key])}`),
|
|
717
|
+
];
|
|
691
718
|
if (service.role === "api") {
|
|
719
|
+
// The API listens on every interface, whatever HOST the env file has from development.
|
|
720
|
+
if (service === web) {
|
|
721
|
+
// Without a web app, the API is the public app, and runs Better Auth itself.
|
|
722
|
+
const auth = frontendKeys.includes("BETTER_AUTH_URL") ? [` BETTER_AUTH_URL: ${fromEnv("BETTER_AUTH_URL", publicUrl)}`] : [];
|
|
723
|
+
return [" HOST: 0.0.0.0", ...address, ...auth];
|
|
724
|
+
}
|
|
692
725
|
return [
|
|
693
|
-
// The API listens on every interface, whatever HOST the env file has from development.
|
|
694
726
|
" HOST: 0.0.0.0",
|
|
695
727
|
` # Reached only through ${web.id}, which passes on the client's address as the one X-Forwarded-For entry.`,
|
|
696
728
|
...clientAddressKeys.map((key) => ` ${key}: ${JSON.stringify(behindWeb[key] ?? "")}`),
|
|
697
729
|
...(authUrl ? [` BETTER_AUTH_URL: ${authUrl}`] : []),
|
|
698
730
|
];
|
|
699
731
|
}
|
|
700
|
-
// A frontend has no env_file: CLIENT_IP_HEADER from the env file needs a line of its own.
|
|
701
|
-
const address = [
|
|
702
|
-
" # Caddy, the one proxy in front, sets X-Forwarded-For to the client's address.",
|
|
703
|
-
...(service.role === "frontend" ? clientAddressKeys : Object.keys(behindCaddy)).map((key) => ` ${key}: ${fromEnv(key, behindCaddy[key])}`),
|
|
704
|
-
];
|
|
705
732
|
if (service.role !== "frontend")
|
|
706
733
|
return [...appUrlKeys.map((key) => ` ${key}: ${fromEnv(key, publicUrl)}`), ...address];
|
|
707
734
|
return [
|
|
@@ -4,7 +4,11 @@ export const MANIFEST_FILE = "clubedge.template.json";
|
|
|
4
4
|
export class ManifestError extends Error {
|
|
5
5
|
name = "ManifestError";
|
|
6
6
|
}
|
|
7
|
-
export const stackRoles = ["fullstack", "frontend", "api", "shell"];
|
|
7
|
+
export const stackRoles = ["fullstack", "frontend", "api", "shell", "mobile"];
|
|
8
|
+
/** The template schema each role came with: a Starter on an older schema is read without it. */
|
|
9
|
+
export const roleSchema = { fullstack: 4, frontend: 4, api: 4, shell: 5, mobile: 6 };
|
|
10
|
+
/** Whether a role's apps run a server: a shell and a mobile app are installed on devices instead. */
|
|
11
|
+
export const runsServer = (role) => role !== "shell" && role !== "mobile";
|
|
8
12
|
/**
|
|
9
13
|
* The slot for the app itself. Starters before schema 4 call it the framework, and projects
|
|
10
14
|
* record their choice for it as `framework`.
|