@clubedge/create-clubedge-app 0.44.0 → 0.46.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 +27 -0
- package/README.md +27 -11
- package/dist/cli/args.js +1 -1
- package/dist/cli/ui.js +3 -2
- package/dist/core/env.js +17 -1
- package/dist/core/manifest-fields.js +12 -0
- package/dist/core/merge.js +26 -12
- package/dist/core/package-json-merge.js +106 -0
- package/dist/core/plan.js +11 -1
- package/dist/core/update.js +24 -7
- package/dist/index.js +12 -8
- package/dist/modify.js +192 -39
- package/package.json +3 -3
- package/skills/clubedge-starter/SKILL.md +3 -0
- package/template/.github/workflows/ci.yml +1 -1
- package/template/.github/workflows/expo-release.yml +65 -0
- package/template/.github/workflows/expo-update.yml +56 -0
- package/template/.github/workflows/shell-release.yml +145 -0
- package/template/.railway/railway.ts +9 -0
- package/template/README.md +36 -15
- package/template/SETUP.md +169 -27
- package/template/apps/api/.env.example +46 -5
- package/template/apps/api/clubedge.stack.json +12 -0
- package/template/apps/api/package.json +4 -1
- package/template/apps/api/src/app.test.ts +24 -1
- package/template/apps/api/src/app.ts +55 -3
- package/template/apps/api/src/config/site.json +3 -1
- package/template/apps/api/src/env/production.test.ts +18 -0
- package/template/apps/api/src/env/production.ts +28 -0
- 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/routes/push.test.ts +160 -0
- package/template/apps/api/src/routes/push.ts +146 -0
- 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/index.ts +2 -0
- package/template/apps/api/src/server/push.ts +40 -0
- package/template/apps/api/src/server/variants/push.native.ts +78 -0
- package/template/apps/api-fastapi/.env.example +50 -9
- package/template/apps/api-fastapi/Dockerfile +1 -0
- package/template/apps/api-fastapi/clubedge.stack.json +24 -0
- package/template/apps/api-fastapi/package.json +1 -1
- package/template/apps/api-fastapi/pyproject.toml +3 -1
- package/template/apps/api-fastapi/src/clubedge_api/app.py +59 -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 +27 -0
- package/template/apps/api-fastapi/src/clubedge_api/push.py +141 -0
- package/template/apps/api-fastapi/src/clubedge_api/push_expo.py +67 -0
- package/template/apps/api-fastapi/src/clubedge_api/push_native.py +316 -0
- package/template/apps/api-fastapi/src/clubedge_api/push_service.py +21 -0
- package/template/apps/api-fastapi/src/clubedge_api/routes/app_version.py +17 -8
- package/template/apps/api-fastapi/src/clubedge_api/routes/push.py +163 -0
- package/template/apps/api-fastapi/src/clubedge_api/server.py +5 -0
- package/template/apps/api-fastapi/src/clubedge_api/variants/push_service_native.py +19 -0
- 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 +11 -0
- package/template/apps/api-fastapi/tests/test_push.py +289 -0
- package/template/apps/api-fastapi/tests/test_push_expo.py +90 -0
- package/template/apps/api-fastapi/tests/test_push_native.py +202 -0
- package/template/apps/api-fastapi/uv.lock +197 -1
- package/template/apps/capacitor/.env.example +13 -0
- package/template/apps/capacitor/_gitignore +2 -0
- 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/capacitor.config.ts +12 -0
- package/template/apps/capacitor/clubedge.stack.json +9 -1
- package/template/apps/capacitor/package.json +5 -2
- package/template/apps/capacitor/scripts/build.mjs +13 -4
- package/template/apps/capacitor/scripts/icons.mjs +113 -14
- package/template/apps/capacitor/scripts/push.mjs +80 -0
- package/template/apps/capacitor/scripts/release.mjs +133 -0
- package/template/apps/capacitor/scripts/version.mjs +129 -0
- package/template/apps/capacitor/version.json +3 -0
- package/template/apps/expo/.env.example +7 -0
- package/template/apps/expo/app.config.ts +76 -30
- package/template/apps/expo/app.json +5 -0
- package/template/apps/expo/clubedge.stack.json +39 -3
- package/template/apps/expo/eas.json +33 -0
- package/template/apps/expo/package.json +11 -3
- package/template/apps/expo/scripts/set-version.mjs +66 -0
- package/template/apps/expo/src/app/(app)/_layout.tsx +4 -23
- package/template/apps/expo/src/app/(app)/index.tsx +166 -44
- 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 +92 -0
- package/template/apps/expo/src/app/(app)/settings/password.tsx +49 -0
- package/template/apps/expo/src/app/_layout.tsx +12 -1
- package/template/apps/expo/src/app/forgot-password.tsx +47 -39
- package/template/apps/expo/src/app/reset-password.tsx +6 -8
- package/template/apps/expo/src/app/sign-in.tsx +29 -12
- package/template/apps/expo/src/app/sign-up.tsx +42 -14
- package/template/apps/expo/src/app/welcome.tsx +99 -0
- package/template/apps/expo/src/components/ota-banner.tsx +69 -0
- package/template/apps/expo/src/components/push-settings.tsx +56 -0
- package/template/apps/expo/src/components/sign-out-sheet.tsx +141 -0
- package/template/apps/expo/src/components/tab-bar.tsx +143 -0
- package/template/apps/expo/src/components/ui.tsx +545 -81
- package/template/apps/expo/src/components/update-gate.tsx +331 -37
- package/template/apps/expo/src/config/site.json +3 -1
- package/template/apps/expo/src/lib/account.ts +62 -0
- package/template/apps/expo/src/lib/app-update.ts +36 -0
- package/template/apps/expo/src/lib/auth-client.ts +10 -2
- package/template/apps/expo/src/lib/ota.ts +30 -0
- package/template/apps/expo/src/lib/push-token.ts +20 -0
- package/template/apps/expo/src/lib/push.ts +161 -0
- package/template/apps/expo/src/lib/theme.ts +152 -15
- package/template/apps/expo/src/types/images.d.ts +13 -0
- package/template/apps/expo/src/variants/app-layout.auth.tsx +19 -0
- package/template/apps/expo/src/variants/auth.better-auth.ts +12 -2
- package/template/apps/expo/src/variants/auth.supabase.ts +8 -5
- package/template/apps/expo/src/variants/layout.auth.tsx +24 -13
- package/template/apps/expo/src/variants/push-token.native.ts +14 -0
- package/template/apps/expo/variants/eas.updates.json +36 -0
- package/template/apps/start/clubedge.stack.json +3 -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/clubedge.stack.json +5 -0
- package/template/apps/vite/package.json +1 -1
- package/template/apps/vite/server.mjs +5 -1
- package/template/apps/vite/src/components/dashboard/variants/account-button.supabase.tsx +7 -0
- package/template/apps/vite/src/config/site.json +3 -1
- package/template/apps/vite/src/main.tsx +7 -3
- package/template/apps/vite/src/routes/settings.tsx +19 -4
- package/template/apps/web/clubedge.stack.json +3 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/config/site.json +3 -1
- package/template/clubedge/modules/ota.json +15 -0
- package/template/clubedge/modules/push.json +36 -0
- package/template/clubedge.template.json +19 -3
- package/template/package.json +3 -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 +1 -1
- package/template/packages/auth-firebase/package.json +1 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/contract/openapi.yaml +234 -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 +85 -1
- package/template/packages/contract/src/index.ts +2 -0
- package/template/packages/contract/src/schema.ts +223 -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 +5 -2
- 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/push-settings.tsx +95 -0
- package/template/packages/mobile/src/push.test.ts +153 -0
- package/template/packages/mobile/src/push.ts +126 -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/push/drizzle/0000_initial.sql +11 -0
- package/template/packages/push/drizzle/meta/0000_snapshot.json +96 -0
- package/template/packages/push/drizzle/meta/_journal.json +13 -0
- package/template/packages/push/drizzle.config.ts +12 -0
- package/template/packages/push/package.json +35 -0
- package/template/packages/push/scripts/migrate.ts +14 -0
- package/template/packages/push/src/devices.test.ts +133 -0
- package/template/packages/push/src/devices.ts +78 -0
- package/template/packages/push/src/index.ts +12 -0
- package/template/packages/push/src/schema.ts +29 -0
- package/template/packages/push/src/types.ts +59 -0
- package/template/packages/push/tsconfig.json +4 -0
- package/template/packages/push-expo/package.json +21 -0
- package/template/packages/push-expo/src/index.test.ts +82 -0
- package/template/packages/push-expo/src/index.ts +70 -0
- package/template/packages/push-expo/tsconfig.json +4 -0
- package/template/packages/push-native/package.json +21 -0
- package/template/packages/push-native/src/index.test.ts +245 -0
- package/template/packages/push-native/src/index.ts +291 -0
- package/template/packages/push-native/tsconfig.json +4 -0
- 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 +298 -5
- package/template/supabase/config.toml +10 -0
- package/template.lock.json +77 -7
- package/template/apps/expo/src/app/(app)/settings.tsx +0 -168
- package/template/apps/expo/src/lib/update.test.ts +0 -34
- package/template/apps/expo/src/lib/update.ts +0 -40
- 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,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.46.0
|
|
6
|
+
|
|
7
|
+
- **Add and remove the apps installed on devices** (#161), so a project created without them can take them later, and drop them again, without starting over: `pnpm clubedge add mobile expo` for the native app, `pnpm clubedge add shell capacitor` for the shell that packages the website, and `pnpm clubedge remove mobile` or `pnpm clubedge remove shell`. The shell needs Starter v0.27.0 or later, and the native app v0.28.0.
|
|
8
|
+
- Adding an app brings its folder, its CI job and the Starter's other sections for it, its dependencies, and its local env file. `package.json` records it, so later changes and upgrades keep it.
|
|
9
|
+
- The app must fit the project, and the CLI says which project can have it when this one can't. When the project's option for a module is one the app lacks, such as a sign-in the native app doesn't have yet, switch it in the same command: `pnpm clubedge add mobile expo auth better-auth`.
|
|
10
|
+
- Removing an app deletes the Starter's files and lists what was never the Starter's, such as the shell's `android/` and `ios/` projects, to delete once you no longer need them.
|
|
11
|
+
- The framework and the API stay as the project was created.
|
|
12
|
+
- When a change keeps a variable in a local env file, the comments above it that you never edited now follow the example's, so a later change that removes the variable removes them too.
|
|
13
|
+
- **Module options can name the registry packages only they need** (#162), in a new `dependencies` field. A project without the option leaves them out, such as the notification plugins in projects without push. Stack packs can use it too ([stack packs](https://github.com/Clubedge/create-clubedge-app/blob/main/docs/stack-packs.md)).
|
|
14
|
+
- **Starter v0.30.0** by default (#163). Its template schema is still 6. The [Starter's release notes](https://github.com/Clubedge/clubedge-starter/releases/tag/v0.30.0) cover:
|
|
15
|
+
- store releases for both mobile apps, signed, from your computer or a workflow, with build numbers made from each app's version;
|
|
16
|
+
- push notifications for both apps (`--push expo` or `--push native`);
|
|
17
|
+
- over-the-air updates for the native app (`--ota eas`).
|
|
18
|
+
|
|
19
|
+
## 0.45.0
|
|
20
|
+
|
|
21
|
+
- **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:
|
|
22
|
+
- 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`;
|
|
23
|
+
- a new design for the native app (`--mobile expo`);
|
|
24
|
+
- splash screens that are never cropped or stretched.
|
|
25
|
+
- **`package.json` merges by key** (#158) when `add`, `remove`, or `upgrade` merges the Starter's changes into your project:
|
|
26
|
+
- 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.
|
|
27
|
+
- A key both sides changed differently is a conflict, with the usual markers around that key's lines only.
|
|
28
|
+
- 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.
|
|
29
|
+
- **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.
|
|
30
|
+
- CI uses astral-sh/setup-uv 10.3.2 (#154).
|
|
31
|
+
|
|
5
32
|
## 0.44.0
|
|
6
33
|
|
|
7
34
|
- **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):
|
package/README.md
CHANGED
|
@@ -165,6 +165,8 @@ pnpm dlx @clubedge/create-clubedge-app my-app --framework vite --api hono --mobi
|
|
|
165
165
|
|
|
166
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
167
|
|
|
168
|
+
A project created without them can add them later, and drop them again: `pnpm clubedge add mobile expo`, `pnpm clubedge add shell capacitor`, or `pnpm clubedge remove mobile` (since CLI 0.46.0, see [Change an existing project](#change-an-existing-project)).
|
|
169
|
+
|
|
168
170
|
#### Without a website
|
|
169
171
|
|
|
170
172
|
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).
|
|
@@ -241,7 +243,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
241
243
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
242
244
|
|
|
243
245
|
# Reproduce a specific Starter release or commit
|
|
244
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
246
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.30.0
|
|
245
247
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
246
248
|
|
|
247
249
|
# Test local Starter changes before releasing them
|
|
@@ -250,7 +252,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --template-dir ../clubedge-starter
|
|
|
250
252
|
|
|
251
253
|
## Change an existing project
|
|
252
254
|
|
|
253
|
-
Inside a project created by the CLI, `add` and `remove` change modules, and `upgrade` moves the project to a newer Starter release. Projects from Starter v0.8.0 on have a `clubedge` script, so `pnpm clubedge add cache redis` works too. Without a module or option, `add` and `remove` ask for them, with a list where you can pick several modules at once:
|
|
255
|
+
Inside a project created by the CLI, `add` and `remove` change modules and the apps installed on devices, and `upgrade` moves the project to a newer Starter release. Projects from Starter v0.8.0 on have a `clubedge` script, so `pnpm clubedge add cache redis` works too. Without a module or option, `add` and `remove` ask for them, with a list where you can pick several modules and apps at once:
|
|
254
256
|
|
|
255
257
|
```sh
|
|
256
258
|
pnpm clubedge add # choose a module and option from a list
|
|
@@ -258,21 +260,33 @@ pnpm dlx @clubedge/create-clubedge-app add cache redis # or switch: add aut
|
|
|
258
260
|
pnpm dlx @clubedge/create-clubedge-app remove storage
|
|
259
261
|
pnpm clubedge add email resend payments stripe # several modules as one change
|
|
260
262
|
pnpm clubedge remove storage payments
|
|
263
|
+
pnpm clubedge add mobile expo # the native app; add shell capacitor packages the website
|
|
264
|
+
pnpm clubedge remove mobile
|
|
261
265
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
262
266
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
263
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
267
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.30.0 # or a specific tag, branch, or commit
|
|
264
268
|
```
|
|
265
269
|
|
|
266
270
|
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.
|
|
267
271
|
|
|
268
272
|
- Files you never changed are updated, added, or deleted.
|
|
269
273
|
- 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.
|
|
274
|
+
- `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.
|
|
270
275
|
- Your own files are never touched.
|
|
271
276
|
- 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.
|
|
272
277
|
- `pnpm-lock.yaml` is replaced if you never changed your dependencies; otherwise `pnpm install` reconciles it.
|
|
273
278
|
|
|
274
279
|
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`.
|
|
275
280
|
|
|
281
|
+
### Apps installed on devices
|
|
282
|
+
|
|
283
|
+
Since CLI 0.46.0, `add` and `remove` also take the apps that run on phones rather than on a server: the native app (`add mobile expo`) and the shell that packages the website (`add shell capacitor`), with Starter v0.27.0 or later for the shell and v0.28.0 for the native app. The project's framework and API stay as they were created, since its deployment and data are built around them.
|
|
284
|
+
|
|
285
|
+
- Adding an app brings its folder, its CI job and the rest of the Starter's sections for it, its dependencies in `pnpm-lock.yaml`, and its local env file, made from its example as a new project's is. `package.json` records it, so later changes and upgrades keep it.
|
|
286
|
+
- The app must fit the project: the native app calls an API, so it needs `--framework vite` or `none` with an API, and the shell packages the Vite frontend. The CLI says which project can have the app when this one cannot.
|
|
287
|
+
- An app implements the modules it uses, such as the native app's sign-in. When the project's option is one it lacks, switch it in the same command: `add mobile expo auth better-auth`.
|
|
288
|
+
- Removing an app deletes the Starter's files and leaves what was never the Starter's, such as its env file, its `node_modules`, and the shell's `android/` and `ios/` projects. The CLI lists them, to delete once you no longer need them.
|
|
289
|
+
|
|
276
290
|
The project must be a Git repository without uncommitted changes, so you can review the result with `git diff` and undo it. Pass `--force` to skip this check. Add `--no-install` to skip `pnpm install`. Afterward, run `pnpm db:migrate` and `pnpm check:services` if the change added tables or services.
|
|
277
291
|
|
|
278
292
|
pnpm cannot read a `package.json` with conflict markers, so a change that leaves conflicts in one skips `pnpm install` and says so: resolve them, then run `pnpm install`. Until then, `pnpm clubedge` does not start either, and the CLI's own commands (`pnpm dlx @clubedge/create-clubedge-app status`, for example) stop with a message that names the file and the command that compares it with the Starter's version in `.clubedge/incoming/`. When the install fails for another reason, the CLI still lists what is left to resolve, then exits with an error.
|
|
@@ -399,27 +413,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
399
413
|
Each CLI release is tested against one specific Starter release:
|
|
400
414
|
|
|
401
415
|
```text
|
|
402
|
-
create-clubedge-app v0.
|
|
416
|
+
create-clubedge-app v0.46.0
|
|
403
417
|
|
|
|
404
418
|
v
|
|
405
|
-
Clubedge Starter v0.
|
|
406
|
-
commit
|
|
419
|
+
Clubedge Starter v0.30.0
|
|
420
|
+
commit 4d341955bd13cb170c7385acb745c3d374259897
|
|
407
421
|
```
|
|
408
422
|
|
|
409
423
|
The current relationship is:
|
|
410
424
|
|
|
411
425
|
| CLI release | Default Starter release | Meaning |
|
|
412
426
|
| --- | --- | --- |
|
|
413
|
-
| `create-clubedge-app@0.
|
|
427
|
+
| `create-clubedge-app@0.46.0` | `clubedge-starter@v0.30.0` | The CLI's required, tested default |
|
|
414
428
|
|
|
415
429
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
416
430
|
|
|
417
|
-
The `v0.
|
|
418
|
-
`
|
|
431
|
+
The `v0.30.0` tag is expected to resolve to commit
|
|
432
|
+
`4d341955bd13cb170c7385acb745c3d374259897`. The CLI verifies this before
|
|
419
433
|
scaffolding with the default reference and fails rather than silently using a
|
|
420
434
|
moved tag.
|
|
421
435
|
|
|
422
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
436
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.30.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.
|
|
423
437
|
|
|
424
438
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
425
439
|
|
|
@@ -474,7 +488,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
474
488
|
1. Make and validate changes in `clubedge-starter`.
|
|
475
489
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
476
490
|
3. Update the Starter package versions.
|
|
477
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
491
|
+
4. Commit `main`, create a release tag such as `v0.30.0`, and publish the Starter release.
|
|
478
492
|
|
|
479
493
|
### CLI maintainers
|
|
480
494
|
|
|
@@ -529,6 +543,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
529
543
|
| 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 |
|
|
530
544
|
| 0.43.0 | v0.27.1 | Upgrades keep each app's own version; add, remove, and upgrade that change nothing skip the Starter download |
|
|
531
545
|
| 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 |
|
|
546
|
+
| 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 |
|
|
547
|
+
| 0.46.0 | v0.30.0 | Add and remove the mobile apps in an existing project; Starter v0.30.0: store releases, push notifications, and over-the-air updates; module options' `dependencies` |
|
|
532
548
|
|
|
533
549
|
## Community
|
|
534
550
|
|
package/dist/cli/args.js
CHANGED
|
@@ -153,7 +153,7 @@ function parseCommand(positionals) {
|
|
|
153
153
|
const module = rest[0];
|
|
154
154
|
if (name === "add") {
|
|
155
155
|
if (rest.length > 1 && rest.length % 2 === 1) {
|
|
156
|
-
throw new UsageError("Usage: add [module option]..., for example: add cache redis,
|
|
156
|
+
throw new UsageError("Usage: add [module option]..., for example: add cache redis, add email resend payments stripe, or add mobile expo");
|
|
157
157
|
}
|
|
158
158
|
const changes = rest.length === 1
|
|
159
159
|
? [{ module: rest[0] }]
|
package/dist/cli/ui.js
CHANGED
|
@@ -101,8 +101,9 @@ Usage:
|
|
|
101
101
|
upgrade, status, doctor, try, deploy, or neon. Without a module or option, add and remove ask
|
|
102
102
|
for them.
|
|
103
103
|
|
|
104
|
-
add and remove change modules of an existing project,
|
|
105
|
-
|
|
104
|
+
add and remove change modules of an existing project, and its apps installed on
|
|
105
|
+
devices (add mobile expo, add shell capacitor, remove mobile), several at once as
|
|
106
|
+
one change, and upgrade moves it to a newer Starter (this CLI's release unless --ref
|
|
106
107
|
is given) and lists the Starter releases it crosses. The change is merged into
|
|
107
108
|
your code; lines you changed that it also changes get conflict markers. Commit
|
|
108
109
|
first so you can review the result with git diff.
|
package/dist/core/env.js
CHANGED
|
@@ -49,12 +49,28 @@ export function updateEnvFile(local, baseExample, targetExample) {
|
|
|
49
49
|
const start = generated ? at - comments.length : at;
|
|
50
50
|
lines.splice(start, at - start + 1, ...(generated ? replacement : replacement.slice(-1)));
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Gives a variable whose value stays the target's comments, if the developer kept the base's:
|
|
54
|
+
* another change of the selection then still finds them as generated, and removes them with it.
|
|
55
|
+
*/
|
|
56
|
+
function refreshComments(key) {
|
|
57
|
+
const at = find(lines, key);
|
|
58
|
+
const comments = commentsAbove(lines, at);
|
|
59
|
+
const baseComments = commentsAbove(base, find(base, key));
|
|
60
|
+
const targetComments = commentsAbove(target, find(target, key));
|
|
61
|
+
if (sameLines(comments, baseComments) && !sameLines(baseComments, targetComments)) {
|
|
62
|
+
lines.splice(at - comments.length, comments.length, ...targetComments);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
52
65
|
for (const [key, before] of baseValues) {
|
|
53
66
|
if (!mine.has(key))
|
|
54
67
|
continue;
|
|
55
68
|
const after = targetValues.get(key);
|
|
56
|
-
if (after === before || mine.get(key) === after)
|
|
69
|
+
if (after === before || mine.get(key) === after) {
|
|
70
|
+
if (after !== undefined)
|
|
71
|
+
refreshComments(key);
|
|
57
72
|
continue;
|
|
73
|
+
}
|
|
58
74
|
const generated = mine.get(key) === before;
|
|
59
75
|
if (after === undefined) {
|
|
60
76
|
(generated ? result.removed : result.unused).push(key);
|
|
@@ -89,6 +89,17 @@ export function parseRequires(file, value, prefix) {
|
|
|
89
89
|
}
|
|
90
90
|
return requires;
|
|
91
91
|
}
|
|
92
|
+
/** An npm package name, scoped or not, such as expo-notifications or @capacitor/push-notifications. */
|
|
93
|
+
const PACKAGE_NAME = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
94
|
+
/** `dependencies`: packages from the registry, by name. */
|
|
95
|
+
function parseDependencies(file, value, prefix) {
|
|
96
|
+
if (value === undefined)
|
|
97
|
+
return [];
|
|
98
|
+
if (!isStringArray(value) || !value.every((name) => PACKAGE_NAME.test(name))) {
|
|
99
|
+
throw new ManifestError(`${file}: ${prefix}dependencies must be an array of package names.`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
92
103
|
/** A module's name, default, and options; `prefix` locates it in its file. */
|
|
93
104
|
export function parseModule(file, entry, prefix) {
|
|
94
105
|
const { fail, requireString } = fields(file, prefix);
|
|
@@ -111,6 +122,7 @@ export function parseModule(file, entry, prefix) {
|
|
|
111
122
|
options[optionId] = {
|
|
112
123
|
name: read.requireString(values, "name"),
|
|
113
124
|
packages: read.optionalStringRecord(values, "packages"),
|
|
125
|
+
dependencies: parseDependencies(file, values.dependencies, optionPrefix),
|
|
114
126
|
files: read.optionalStringArray(values, "files"),
|
|
115
127
|
replace: read.optionalStringRecord(values, "replace"),
|
|
116
128
|
requires: parseRequires(file, values.requires, optionPrefix),
|
package/dist/core/merge.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Three-way merge between what the CLI generated (base), what it would generate now (target),
|
|
2
2
|
// and the project as it is (current). Changes from base to target are applied to the project
|
|
3
3
|
// without losing the project's own edits. Pure: callers read and write the files.
|
|
4
|
-
import { diff3Merge } from "node-diff3";
|
|
4
|
+
import { diff3Merge, diffComm } from "node-diff3";
|
|
5
|
+
import { mergePackageJson } from "./package-json-merge.js";
|
|
5
6
|
export const CONFLICT_LABELS = { yours: "yours", theirs: "clubedge" };
|
|
6
7
|
/**
|
|
7
8
|
* Where the Starter's versions of files left to resolve are saved, next to the project's own,
|
|
@@ -35,28 +36,41 @@ export function sameContent(a, b) {
|
|
|
35
36
|
return a.equals(b);
|
|
36
37
|
return normalize(a) === normalize(b);
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
if (isBinary(current) || isBinary(base) || isBinary(target))
|
|
41
|
-
return null;
|
|
42
|
-
const split = (content) => normalize(content).split("\n");
|
|
43
|
-
const regions = diff3Merge(split(current), split(base), split(target), { excludeFalseConflicts: true });
|
|
39
|
+
/** The merged text, with conflict markers around each region where the two sides differ. */
|
|
40
|
+
function withMarkers(regions) {
|
|
44
41
|
const lines = [];
|
|
45
42
|
let conflict = false;
|
|
46
43
|
for (const region of regions) {
|
|
47
|
-
if (region
|
|
44
|
+
if ("ok" in region) {
|
|
48
45
|
lines.push(...region.ok);
|
|
49
46
|
continue;
|
|
50
47
|
}
|
|
51
|
-
if (!region.conflict)
|
|
52
|
-
continue;
|
|
53
48
|
conflict = true;
|
|
54
|
-
lines.push(`<<<<<<< ${CONFLICT_LABELS.yours}`, ...region.
|
|
49
|
+
lines.push(`<<<<<<< ${CONFLICT_LABELS.yours}`, ...region.yours, "=======", ...region.theirs, `>>>>>>> ${CONFLICT_LABELS.theirs}`);
|
|
55
50
|
}
|
|
56
51
|
return { text: lines.join("\n"), conflict };
|
|
57
52
|
}
|
|
53
|
+
/** Merges base -> target into current. Resolves null for binary files, which cannot be merged. */
|
|
54
|
+
export function mergeText(current, base, target) {
|
|
55
|
+
if (isBinary(current) || isBinary(base) || isBinary(target))
|
|
56
|
+
return null;
|
|
57
|
+
const split = (content) => normalize(content).split("\n");
|
|
58
|
+
const regions = diff3Merge(split(current), split(base), split(target), { excludeFalseConflicts: true });
|
|
59
|
+
return withMarkers(regions.flatMap((region) => region.ok ? [{ ok: region.ok }] : region.conflict ? [{ yours: region.conflict.a, theirs: region.conflict.b }] : []));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Merges a package.json by key (see package-json-merge.ts), so edits to different keys never
|
|
63
|
+
* conflict, and markers wrap only the lines of a key both sides changed differently. Resolves
|
|
64
|
+
* null when a side does not parse, for mergeText to merge it by line.
|
|
65
|
+
*/
|
|
66
|
+
export function mergePackageJsonText(current, base, target) {
|
|
67
|
+
const merged = mergePackageJson(normalize(current), normalize(base), normalize(target));
|
|
68
|
+
if (!merged)
|
|
69
|
+
return null;
|
|
70
|
+
return withMarkers(diffComm(merged.yours.split("\n"), merged.theirs.split("\n")).map((region) => region.common !== undefined ? { ok: region.common } : { yours: region.buffer1, theirs: region.buffer2 }));
|
|
71
|
+
}
|
|
58
72
|
function merged(path, current, base, target, binaryReason) {
|
|
59
|
-
const result = mergeText(current, base, target);
|
|
73
|
+
const result = (/(^|\/)package\.json$/.test(path) ? mergePackageJsonText(current, base, target) : null) ?? mergeText(current, base, target);
|
|
60
74
|
if (!result)
|
|
61
75
|
return { path, kind: "keep", reason: binaryReason, incoming: target };
|
|
62
76
|
const content = withLineEndings(result.text, current);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// package.json files merge by key instead of by line (#142), so edits to different keys never
|
|
2
|
+
// conflict: a script the project adds after the Starter's, or an override next to one the
|
|
3
|
+
// Starter adds. A key both sides changed to different values is a conflict. The merged file is
|
|
4
|
+
// written twice, with your value for each such key and with the Starter's, and merge.ts puts
|
|
5
|
+
// conflict markers around the lines where the two differ, which are that key's.
|
|
6
|
+
/** A key both sides changed to different values; undefined is a side that removed it. */
|
|
7
|
+
class Conflict {
|
|
8
|
+
yours;
|
|
9
|
+
theirs;
|
|
10
|
+
constructor(yours, theirs) {
|
|
11
|
+
this.yours = yours;
|
|
12
|
+
this.theirs = theirs;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Conflict);
|
|
16
|
+
const own = (object, key) => (Object.hasOwn(object, key) ? object[key] : undefined);
|
|
17
|
+
/** Sets a key as data, so a key named __proto__ cannot change the object's prototype. */
|
|
18
|
+
function set(object, key, value) {
|
|
19
|
+
Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
|
|
20
|
+
}
|
|
21
|
+
/** Equal JSON values; objects regardless of their key order. */
|
|
22
|
+
function same(a, b) {
|
|
23
|
+
if (a === b)
|
|
24
|
+
return true;
|
|
25
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
26
|
+
return a.length === b.length && a.every((item, i) => same(item, b[i]));
|
|
27
|
+
if (isObject(a) && isObject(b)) {
|
|
28
|
+
const keys = Object.keys(a);
|
|
29
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && same(a[key], b[key]));
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One key's value: whoever changed it, or a conflict when both did, differently. Objects both
|
|
35
|
+
* changed merge key by key; arrays and scalars are single values. Undefined is a removed key.
|
|
36
|
+
*/
|
|
37
|
+
function merge(base, yours, theirs) {
|
|
38
|
+
if (same(yours, theirs) || same(base, theirs))
|
|
39
|
+
return yours;
|
|
40
|
+
if (same(base, yours))
|
|
41
|
+
return theirs;
|
|
42
|
+
if (isObject(yours) && isObject(theirs))
|
|
43
|
+
return mergeObjects(isObject(base) ? base : {}, yours, theirs);
|
|
44
|
+
return new Conflict(yours, theirs);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Your keys in your order; a key only the Starter has goes after the key it follows in the
|
|
48
|
+
* Starter's file, or first. At the root, `clubedge` (the provenance) is always the Starter's.
|
|
49
|
+
*/
|
|
50
|
+
function mergeObjects(base, yours, theirs, root = false) {
|
|
51
|
+
const order = Object.keys(yours);
|
|
52
|
+
let previous;
|
|
53
|
+
for (const key of Object.keys(theirs)) {
|
|
54
|
+
if (!order.includes(key))
|
|
55
|
+
order.splice(previous === undefined ? 0 : order.indexOf(previous) + 1, 0, key);
|
|
56
|
+
previous = key;
|
|
57
|
+
}
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const key of order) {
|
|
60
|
+
const value = root && key === "clubedge" ? own(theirs, key) : merge(own(base, key), own(yours, key), own(theirs, key));
|
|
61
|
+
if (value !== undefined)
|
|
62
|
+
set(result, key, value);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** The merged value with one side's value for each conflict. */
|
|
67
|
+
function resolve(value, side) {
|
|
68
|
+
if (value instanceof Conflict)
|
|
69
|
+
return value[side];
|
|
70
|
+
if (!isObject(value))
|
|
71
|
+
return value;
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const [key, item] of Object.entries(value)) {
|
|
74
|
+
const resolved = resolve(item, side);
|
|
75
|
+
if (resolved !== undefined)
|
|
76
|
+
set(result, key, resolved);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The file's indentation, as npm reads it: the whitespace before the first key, after the
|
|
82
|
+
* opening brace's line. A file on one line has none, and stays on one line.
|
|
83
|
+
*/
|
|
84
|
+
function indentation(text) {
|
|
85
|
+
return /^\s*\{\n([ \t]*)"/.exec(text)?.[1] ?? "";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Merges base -> target into current, all LF text. Resolves the result written with your value
|
|
89
|
+
* for each conflict and with the Starter's (equal when there are none), in your file's
|
|
90
|
+
* indentation and final newline; null when a side is not a JSON object, to merge by line.
|
|
91
|
+
*/
|
|
92
|
+
export function mergePackageJson(current, base, target) {
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = [JSON.parse(current), JSON.parse(base), JSON.parse(target)];
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const [yours, before, theirs] = parsed;
|
|
101
|
+
if (!isObject(yours) || !isObject(before) || !isObject(theirs))
|
|
102
|
+
return null;
|
|
103
|
+
const merged = mergeObjects(before, yours, theirs, true);
|
|
104
|
+
const write = (side) => JSON.stringify(resolve(merged, side), null, indentation(current)) + (current.endsWith("\n") ? "\n" : "");
|
|
105
|
+
return { yours: write("yours"), theirs: write("theirs") };
|
|
106
|
+
}
|
package/dist/core/plan.js
CHANGED
|
@@ -238,6 +238,8 @@ function resolveModuleFiles(manifest, selection, stacks) {
|
|
|
238
238
|
const keptPaths = new Set();
|
|
239
239
|
const usedPackages = new Map();
|
|
240
240
|
const offeredPackages = new Map();
|
|
241
|
+
const usedDependencies = new Set();
|
|
242
|
+
const offeredDependencies = new Set();
|
|
241
243
|
const variantSources = new Set();
|
|
242
244
|
/** Variant source path to the default file it replaces. */
|
|
243
245
|
const replacements = new Map();
|
|
@@ -254,6 +256,11 @@ function resolveModuleFiles(manifest, selection, stacks) {
|
|
|
254
256
|
if (selected)
|
|
255
257
|
usedPackages.set(name, directory);
|
|
256
258
|
}
|
|
259
|
+
for (const name of option.dependencies) {
|
|
260
|
+
offeredDependencies.add(name);
|
|
261
|
+
if (selected)
|
|
262
|
+
usedDependencies.add(name);
|
|
263
|
+
}
|
|
257
264
|
for (const [target, variant] of Object.entries(option.replace)) {
|
|
258
265
|
variantSources.add(variant);
|
|
259
266
|
if (selected)
|
|
@@ -303,9 +310,12 @@ function resolveModuleFiles(manifest, selection, stacks) {
|
|
|
303
310
|
if (Object.keys(rename).length)
|
|
304
311
|
scripts.rename = rename;
|
|
305
312
|
const removedPackages = [...offeredPackages].filter(([name]) => !usedPackages.has(name));
|
|
313
|
+
// Registry packages only left-out options list go the way of left-out workspace packages:
|
|
314
|
+
// out of each app's package.json and the lockfile's importers.
|
|
315
|
+
const removedDependencies = [...offeredDependencies].filter((name) => !usedDependencies.has(name));
|
|
306
316
|
return {
|
|
307
317
|
scripts,
|
|
308
|
-
removedPackageNames: removedPackages.map(([name]) => name),
|
|
318
|
+
removedPackageNames: [...removedPackages.map(([name]) => name), ...removedDependencies],
|
|
309
319
|
removedPackageDirectories: removedPackages.map(([, directory]) => directory),
|
|
310
320
|
replacedTargets: new Set(replacements.values()),
|
|
311
321
|
replacements,
|
package/dist/core/update.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Changes an existing project from one generated state (base) to another (target): different
|
|
2
|
-
// modules for add and remove, or a newer Starter for upgrade. The project's own edits
|
|
3
|
-
// through a three-way merge, see merge.ts.
|
|
2
|
+
// modules or apps for add and remove, or a newer Starter for upgrade. The project's own edits
|
|
3
|
+
// survive through a three-way merge, see merge.ts.
|
|
4
4
|
import { mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { customizeAppManifest } from "./customize.js";
|
|
@@ -14,15 +14,17 @@ export async function planUpdate(project, base, target) {
|
|
|
14
14
|
source: side.source,
|
|
15
15
|
manifest: side.manifest,
|
|
16
16
|
identity: project.identity,
|
|
17
|
-
stacks: project.stacks,
|
|
17
|
+
stacks: side.stacks ?? project.stacks,
|
|
18
18
|
modules: side.modules,
|
|
19
19
|
cliVersion: side.cliVersion,
|
|
20
20
|
});
|
|
21
21
|
const before = await render(base);
|
|
22
22
|
const after = await render(target);
|
|
23
|
-
// The lockfile and the local env files are handled on their own below.
|
|
24
|
-
|
|
25
|
-
const
|
|
23
|
+
// The lockfile and the local env files are handled on their own below. The env file of an app
|
|
24
|
+
// the change removes is left as it is: it is the developer's, and Git ignores it.
|
|
25
|
+
const envFiles = after.envFiles;
|
|
26
|
+
const removedEnvFiles = before.envFiles.filter(({ to }) => !after.envFiles.some((file) => file.to === to));
|
|
27
|
+
const special = [LOCKFILE, ...[...envFiles, ...removedEnvFiles].map(({ to }) => to)];
|
|
26
28
|
const strip = (files) => new Map([...files].filter(([path]) => !special.includes(path)));
|
|
27
29
|
const baseFiles = strip(before.files);
|
|
28
30
|
const targetFiles = strip(after.files);
|
|
@@ -80,7 +82,14 @@ export async function planUpdate(project, base, target) {
|
|
|
80
82
|
const text = (files) => files.get(envFile.from)?.toString("utf8") ?? "";
|
|
81
83
|
const local = current.get(envFile.to)?.toString("utf8");
|
|
82
84
|
const update = local === undefined ? null : updateEnvFile(local, text(before.files), text(after.files));
|
|
83
|
-
|
|
85
|
+
const added = !before.envFiles.some(({ to }) => to === envFile.to);
|
|
86
|
+
const example = after.files.get(envFile.from);
|
|
87
|
+
return {
|
|
88
|
+
path: envFile.to,
|
|
89
|
+
update,
|
|
90
|
+
changed: update !== null && update.text !== local,
|
|
91
|
+
...(local === undefined && added && example ? { create: example } : {}),
|
|
92
|
+
};
|
|
84
93
|
});
|
|
85
94
|
return { project, actions, lockfile, envs };
|
|
86
95
|
}
|
|
@@ -137,5 +146,13 @@ export async function applyUpdate(plan) {
|
|
|
137
146
|
for (const env of plan.envs) {
|
|
138
147
|
if (env.update && env.changed)
|
|
139
148
|
await writeFile(join(root, env.path), env.update.text);
|
|
149
|
+
if (env.create) {
|
|
150
|
+
await mkdir(dirname(join(root, env.path)), { recursive: true });
|
|
151
|
+
// An env file written since the plan was made is never replaced.
|
|
152
|
+
await writeFile(join(root, env.path), env.create, { flag: "wx" }).catch((error) => {
|
|
153
|
+
if (error.code !== "EEXIST")
|
|
154
|
+
throw error;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
140
157
|
}
|
|
141
158
|
}
|
package/dist/index.js
CHANGED
|
@@ -77,13 +77,12 @@ async function chooseStacks(manifest, requested, interactive) {
|
|
|
77
77
|
* offered. Returns null when a prompt is cancelled.
|
|
78
78
|
*/
|
|
79
79
|
async function chooseModules(manifest, stacks, requested, interactive) {
|
|
80
|
-
|
|
80
|
+
// Modules with a choice for these apps: a module only a mobile app uses has one for a website.
|
|
81
|
+
const unasked = Object.keys(manifest.modules).filter((id) => requested[id] === undefined && moduleChoices(manifest, stacks, id, requested).length > 1);
|
|
81
82
|
if (!interactive || unasked.length === 0)
|
|
82
83
|
return requested;
|
|
83
84
|
const customize = await confirm({
|
|
84
|
-
message: `Customize the included services (${
|
|
85
|
-
.map((module) => module.name.toLowerCase())
|
|
86
|
-
.join(", ")})?`,
|
|
85
|
+
message: `Customize the included services (${unasked.map((id) => manifest.modules[id].name.toLowerCase()).join(", ")})?`,
|
|
87
86
|
initialValue: false,
|
|
88
87
|
});
|
|
89
88
|
if (isCancel(customize))
|
|
@@ -101,8 +100,13 @@ async function chooseModules(manifest, stacks, requested, interactive) {
|
|
|
101
100
|
// Modules answered later are checked when the plan is created.
|
|
102
101
|
return value === undefined || allowed.includes(value);
|
|
103
102
|
}));
|
|
104
|
-
|
|
103
|
+
// With one option left, there is nothing to ask. It is still the answer: later modules'
|
|
104
|
+
// requirements check it, and the plan takes it rather than the default.
|
|
105
|
+
if (available.length < 2) {
|
|
106
|
+
if (available[0] !== undefined)
|
|
107
|
+
chosen[id] = available[0][0];
|
|
105
108
|
continue;
|
|
109
|
+
}
|
|
106
110
|
const value = await select({
|
|
107
111
|
message: module.name,
|
|
108
112
|
initialValue: available.some(([optionId]) => optionId === module.default)
|
|
@@ -259,7 +263,7 @@ async function createProject(args) {
|
|
|
259
263
|
const directory = relative(process.cwd(), targetDirectory) || ".";
|
|
260
264
|
const scripts = JSON.parse(await readFile(join(targetDirectory, "package.json"), "utf8")).scripts ?? {};
|
|
261
265
|
const later = scripts.clubedge
|
|
262
|
-
? `Change modules later with \`pnpm clubedge add\` or \`pnpm clubedge remove\`, and get Starter updates with \`pnpm clubedge upgrade\`.\n\n`
|
|
266
|
+
? `Change modules and mobile apps later with \`pnpm clubedge add\` or \`pnpm clubedge remove\`, and get Starter updates with \`pnpm clubedge upgrade\`.\n\n`
|
|
263
267
|
: "";
|
|
264
268
|
outro(`Next steps:\n cd ${directory}\n${migrate ? ` ${pnpmCommand} ${migrate}\n` : ""} ${pnpmCommand} dev\n\n` +
|
|
265
269
|
`See SETUP.md for database and provider configuration.\n\n` +
|
|
@@ -318,8 +322,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
318
322
|
}
|
|
319
323
|
else {
|
|
320
324
|
const intros = {
|
|
321
|
-
add: "Add a module to this project",
|
|
322
|
-
remove: "Remove a module from this project",
|
|
325
|
+
add: "Add a module or an app to this project",
|
|
326
|
+
remove: "Remove a module or an app from this project",
|
|
323
327
|
upgrade: "Upgrade this project to a newer Starter",
|
|
324
328
|
};
|
|
325
329
|
intro(intros[args.command.name]);
|