@clubedge/create-clubedge-app 0.46.0 → 0.47.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 +26 -0
- package/README.md +29 -11
- package/dist/core/execute.js +16 -2
- package/dist/index.js +11 -3
- package/dist/mcp-progress.js +57 -0
- package/dist/mcp-server.js +7 -3
- package/dist/utils/names.js +11 -0
- package/dist/utils/open.js +22 -1
- package/dist/utils/process.js +90 -6
- package/package.json +5 -5
- package/skills/clubedge-starter/SKILL.md +1 -0
- package/template/.dockerignore +6 -0
- package/template/.github/workflows/ci.yml +2 -8
- package/template/CONTRIBUTING.md +1 -1
- package/template/README.md +23 -17
- package/template/SETUP.md +147 -4
- package/template/_gitignore +7 -0
- package/template/apps/api/.env.example +3 -0
- package/template/apps/api/cloudflare.json +5 -0
- package/template/apps/api/clubedge.stack.json +6 -2
- package/template/apps/api/package.json +1 -1
- package/template/apps/api/src/app.ts +9 -2
- package/template/apps/api/src/cors.test.ts +79 -0
- package/template/apps/api/src/env/production.test.ts +21 -0
- package/template/apps/api/src/env/production.ts +11 -0
- package/template/apps/api/src/env.ts +21 -0
- package/template/apps/api/src/server/index.ts +8 -0
- package/template/apps/api/src/server/storage.test.ts +30 -0
- package/template/apps/api/src/server/storage.ts +9 -2
- package/template/apps/api/src/server/variants/storage.supabase.ts +9 -2
- package/template/apps/api-fastapi/.env.example +3 -0
- package/template/apps/api-fastapi/cloudflare.json +5 -0
- package/template/apps/api-fastapi/clubedge.stack.json +10 -2
- 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 +6 -2
- package/template/apps/api-fastapi/src/clubedge_api/production_env.py +17 -0
- package/template/apps/api-fastapi/src/clubedge_api/server.py +1 -0
- package/template/apps/api-fastapi/src/clubedge_api/settings.py +42 -0
- package/template/apps/api-fastapi/src/clubedge_api/storage.py +3 -2
- package/template/apps/api-fastapi/src/clubedge_api/storage_local.py +11 -5
- package/template/apps/api-fastapi/src/clubedge_api/storage_provider.py +16 -1
- package/template/apps/api-fastapi/src/clubedge_api/storage_range.py +45 -0
- package/template/apps/api-fastapi/src/clubedge_api/storage_s3.py +4 -3
- package/template/apps/api-fastapi/src/clubedge_api/storage_supabase.py +8 -3
- package/template/apps/api-fastapi/src/clubedge_api/variants/storage_supabase.py +3 -2
- package/template/apps/api-fastapi/tests/test_cors.py +75 -0
- package/template/apps/api-fastapi/tests/test_production_env.py +13 -0
- package/template/apps/api-fastapi/tests/test_storage_local.py +19 -0
- package/template/apps/api-fastapi/tests/test_storage_range.py +54 -0
- package/template/apps/api-fastapi/tests/test_storage_s3.py +19 -0
- package/template/apps/api-fastapi/tests/test_storage_supabase.py +17 -0
- package/template/apps/api-fastapi/uv.lock +1 -1
- package/template/apps/capacitor/capacitor.config.ts +7 -0
- package/template/apps/capacitor/e2e/README.md +23 -0
- package/template/apps/capacitor/e2e/account.yaml +116 -0
- package/template/apps/capacitor/package.json +1 -1
- package/template/apps/capacitor/scripts/build.mjs +4 -2
- package/template/apps/expo/app.config.ts +18 -1
- package/template/apps/expo/clubedge.stack.json +0 -1
- package/template/apps/expo/e2e/README.md +26 -0
- package/template/apps/expo/e2e/account.yaml +111 -0
- package/template/apps/expo/package.json +15 -15
- package/template/apps/expo/src/components/update-gate.tsx +10 -2
- package/template/apps/expo/src/lib/api.ts +2 -1
- package/template/apps/expo/src/lib/send.ts +27 -0
- package/template/apps/expo/src/variants/api.auth.ts +6 -11
- package/template/apps/start/clubedge.stack.json +2 -1
- package/template/apps/start/package.json +3 -3
- package/template/apps/start/src/server/storage.test.ts +31 -0
- package/template/apps/start/src/server/storage.ts +9 -2
- package/template/apps/start/src/server/variants/storage.supabase.ts +9 -2
- package/template/apps/vite/clubedge.stack.json +4 -1
- package/template/apps/vite/package.json +3 -3
- package/template/apps/vite/public/_headers +7 -0
- package/template/apps/web/package.json +3 -3
- package/template/apps/web/src/server/storage.ts +9 -2
- package/template/apps/web/src/server/variants/storage.supabase.ts +9 -2
- package/template/clubedge/modules/deploy.json +10 -0
- package/template/clubedge.template.json +16 -1
- package/template/deploy/cloudflare/package.json +17 -0
- package/template/deploy/cloudflare/scripts/config.mjs +41 -0
- package/template/deploy/cloudflare/scripts/config.test.ts +43 -0
- package/template/deploy/cloudflare/scripts/wrangler.mjs +63 -0
- package/template/deploy/cloudflare/src/index.ts +128 -0
- package/template/deploy/cloudflare/src/keep-awake.test.ts +26 -0
- package/template/deploy/cloudflare/src/keep-awake.ts +17 -0
- package/template/deploy/cloudflare/src/routing.test.ts +52 -0
- package/template/deploy/cloudflare/src/routing.ts +50 -0
- package/template/deploy/cloudflare/src/website.ts +33 -0
- package/template/deploy/cloudflare/tsconfig.json +9 -0
- package/template/deploy/cloudflare/wrangler.jsonc +49 -0
- package/template/deploy/cloudflare/wrangler.website.jsonc +24 -0
- package/template/e2e/auth-flow.spec.ts +10 -2
- package/template/e2e/auth.spec.ts +3 -0
- package/template/e2e/email-verification.spec.ts +9 -1
- package/template/e2e/firebase-auth-flow.spec.ts +2 -1
- package/template/e2e/jobs.spec.ts +2 -1
- package/template/e2e/observability.spec.ts +2 -0
- package/template/e2e/password-reset.spec.ts +15 -1
- package/template/e2e/payments.spec.ts +6 -2
- package/template/e2e/settings.spec.ts +9 -1
- package/template/e2e/unique.ts +13 -0
- package/template/package.json +3 -7
- 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/auth-supabase/src/bearer.test.ts +90 -0
- package/template/packages/auth-supabase/src/index.test.ts +82 -2
- package/template/packages/auth-supabase/src/index.ts +45 -12
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/contract/package.json +1 -1
- package/template/packages/contract/src/app-update.test.ts +21 -0
- package/template/packages/contract/src/app-update.ts +4 -2
- package/template/packages/contract/src/index.ts +1 -1
- 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/email-smtp/src/index.test.ts +45 -1
- package/template/packages/email-smtp/src/index.ts +26 -2
- 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 -5
- 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/package.json +1 -1
- package/template/packages/push-expo/package.json +1 -1
- package/template/packages/push-native/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/file-system.test.ts +18 -0
- package/template/packages/storage/src/file-system.ts +5 -2
- package/template/packages/storage/src/index.ts +8 -1
- package/template/packages/storage/src/range.test.ts +47 -0
- package/template/packages/storage/src/range.ts +28 -0
- package/template/packages/storage/src/types.ts +13 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-s3/src/index.test.ts +21 -0
- package/template/packages/storage-s3/src/index.ts +9 -2
- package/template/packages/storage-supabase/package.json +2 -2
- package/template/packages/storage-supabase/src/index.test.ts +75 -0
- package/template/packages/storage-supabase/src/index.ts +6 -2
- package/template/packages/ui/package.json +2 -2
- package/template/playwright.config.ts +9 -1
- package/template/pnpm-lock.yaml +743 -703
- package/template/pnpm-workspace.yaml +1 -0
- package/template/vitest.config.ts +6 -1
- package/template.lock.json +34 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.47.0
|
|
6
|
+
|
|
7
|
+
- **Starter v0.31.0** by default (#183). Its template schema is still 6. The [Starter's release notes](https://github.com/Clubedge/clubedge-starter/releases/tag/v0.31.0) cover:
|
|
8
|
+
- deploying React + Vite and its API to Cloudflare (`--deploy cloudflare`), which has not yet run on an account with Workers Paid;
|
|
9
|
+
- downloads by byte range from storage, which browsers ask for to play audio and video;
|
|
10
|
+
- Supabase sessions that the Hono API verifies itself;
|
|
11
|
+
- fixes to the APIs' CORS and to SMTP sends that never answer;
|
|
12
|
+
- browser tests in Chromium, Firefox, and WebKit.
|
|
13
|
+
- **On Windows, an argument can no longer run a second command or expand a variable** (#176). The CLI ran commands through cmd.exe as one line, where `%VAR%` is expanded inside quotes, a line break ends the command, and a quote inside an argument can leave `&` and `|` outside the quotes. No argument from outside reached these commands, so this closes the gap before one does:
|
|
14
|
+
- A program (`.exe`, `.com`) now starts directly, with no shell, so `git`, `node`, `docker`, and `uv` no longer go through cmd.exe.
|
|
15
|
+
- A batch file (`.cmd`, `.bat`), such as the npm shims for `pnpm` and `npx`, still needs cmd.exe. Each argument is quoted the way Windows programs split their command line, then escaped for both of cmd.exe's readings. An argument with a line break is refused.
|
|
16
|
+
- **Windows device names are refused as project directories** (#169, #171): `CON`, `PRN`, `AUX`, `NUL`, `COM1` to `COM9`, and `LPT1` to `LPT9`, with or without an extension, which on Windows could create a folder that can't be deleted. The CLI checks the project's own name and each parent folder it would create, in the prompt and in the MCP server's `create_project` too. Folders that already exist are left alone.
|
|
17
|
+
- **The browser opens web addresses only** (#175). Commands that open a page, such as `deploy`, `neon`, and `try`, hand the system's opener only an `http://` or `https://` address with a host, and open nothing otherwise.
|
|
18
|
+
- **`create_project` reports progress over MCP** (#178) while it installs dependencies. A client that sends a progress token gets a notification when each install starts, every 5 seconds while it runs, and when it ends or fails, so a client that resets its timeout on progress keeps waiting. A client that sends none gets the same result as before.
|
|
19
|
+
- **Sponsors** (#181, #182): the README has a Sponsors section. [Neon](https://neon.com) supports Clubedge Starter through its Open Source Program.
|
|
20
|
+
- CI runs generated projects' browser checks in Chromium, by name (#180), since the Starter now has three browser projects.
|
|
21
|
+
- `@modelcontextprotocol/server` 2.4.0 or newer (#173), vitest 5.1.0 (#174), and astral-sh/setup-uv 10.3.3 in CI (#172).
|
|
22
|
+
|
|
23
|
+
## 0.46.1
|
|
24
|
+
|
|
25
|
+
- **Starter v0.30.1** by default (#167). Its template schema is still 6. The [Starter's release notes](https://github.com/Clubedge/clubedge-starter/releases/tag/v0.30.1) cover:
|
|
26
|
+
- the native app (`--mobile expo`) reaching its API again: since v0.29.0, each of its calls to the API threw after the API had answered, so the account never loaded, and saving a name or deleting the account failed;
|
|
27
|
+
- getting the fix to installed apps: a new build in the stores, then `APP_MINIMUM_VERSION` on the API, which sends older installs to the update screen, or an over-the-air update with `--ota eas`;
|
|
28
|
+
- end-to-end tests of both mobile apps on an Android emulator;
|
|
29
|
+
- Expo SDK 58's latest patch versions.
|
|
30
|
+
|
|
5
31
|
## 0.46.0
|
|
6
32
|
|
|
7
33
|
- **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.
|
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ cwd = "D:/path/to/workspace"
|
|
|
48
48
|
|
|
49
49
|
The MCP server is included in the same package and uses the CLI's bundled Starter and compatibility rules. Its create tool only writes beneath the configured working directory and refuses a non-empty destination.
|
|
50
50
|
|
|
51
|
+
With `install_dependencies`, `create_project` answers once `pnpm install` and each app's own install, such as `uv sync` for a FastAPI API, have finished, which can take minutes. A client that sends a progress token with the call (`_meta.progressToken`) gets a `notifications/progress` message when each install starts, every 5 seconds while it runs, and when it ends or fails, such as `pnpm install: running for 40s`. Clients that reset their request timeout on progress, as the MCP TypeScript SDK does with `resetTimeoutOnProgress`, then wait for as long as the installs take. `progress` counts the notifications sent and there is no `total`, since how far along an install is isn't known. Without a token nothing is sent, and the result is the same either way.
|
|
52
|
+
|
|
51
53
|
### Agent skill
|
|
52
54
|
|
|
53
55
|
The npm package includes the Agent Skill at [`skills/clubedge-starter/SKILL.md`](skills/clubedge-starter/SKILL.md). Download and extract the package in PowerShell:
|
|
@@ -230,7 +232,7 @@ Options:
|
|
|
230
232
|
-v, --version Show the CLI version
|
|
231
233
|
```
|
|
232
234
|
|
|
233
|
-
The target directory must be empty or not exist. The CLI never deletes or overwrites existing files, and if scaffolding fails it removes whatever it wrote, so a failed run leaves nothing behind.
|
|
235
|
+
The target directory must be empty or not exist, and its name can't be one Windows reserves for a device, such as `CON`, `NUL`, or `COM1`, even on macOS or Linux; the same goes for any folder the CLI creates above it. The CLI never deletes or overwrites existing files, and if scaffolding fails it removes whatever it wrote, so a failed run leaves nothing behind.
|
|
234
236
|
|
|
235
237
|
```sh
|
|
236
238
|
# Preview the files and edits without writing anything
|
|
@@ -243,7 +245,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
243
245
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
244
246
|
|
|
245
247
|
# Reproduce a specific Starter release or commit
|
|
246
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
248
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.31.0
|
|
247
249
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
248
250
|
|
|
249
251
|
# Test local Starter changes before releasing them
|
|
@@ -264,7 +266,7 @@ pnpm clubedge add mobile expo # the native app; ad
|
|
|
264
266
|
pnpm clubedge remove mobile
|
|
265
267
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
266
268
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
267
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
269
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.31.0 # or a specific tag, branch, or commit
|
|
268
270
|
```
|
|
269
271
|
|
|
270
272
|
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.
|
|
@@ -413,27 +415,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
413
415
|
Each CLI release is tested against one specific Starter release:
|
|
414
416
|
|
|
415
417
|
```text
|
|
416
|
-
create-clubedge-app v0.
|
|
418
|
+
create-clubedge-app v0.47.0
|
|
417
419
|
|
|
|
418
420
|
v
|
|
419
|
-
Clubedge Starter v0.
|
|
420
|
-
commit
|
|
421
|
+
Clubedge Starter v0.31.0
|
|
422
|
+
commit e6c47d7b8ec153443ee35abab7fc61d37f55e36a
|
|
421
423
|
```
|
|
422
424
|
|
|
423
425
|
The current relationship is:
|
|
424
426
|
|
|
425
427
|
| CLI release | Default Starter release | Meaning |
|
|
426
428
|
| --- | --- | --- |
|
|
427
|
-
| `create-clubedge-app@0.
|
|
429
|
+
| `create-clubedge-app@0.47.0` | `clubedge-starter@v0.31.0` | The CLI's required, tested default |
|
|
428
430
|
|
|
429
431
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
430
432
|
|
|
431
|
-
The `v0.
|
|
432
|
-
`
|
|
433
|
+
The `v0.31.0` tag is expected to resolve to commit
|
|
434
|
+
`e6c47d7b8ec153443ee35abab7fc61d37f55e36a`. The CLI verifies this before
|
|
433
435
|
scaffolding with the default reference and fails rather than silently using a
|
|
434
436
|
moved tag.
|
|
435
437
|
|
|
436
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
438
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.31.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.
|
|
437
439
|
|
|
438
440
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
439
441
|
|
|
@@ -488,7 +490,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
488
490
|
1. Make and validate changes in `clubedge-starter`.
|
|
489
491
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
490
492
|
3. Update the Starter package versions.
|
|
491
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
493
|
+
4. Commit `main`, create a release tag such as `v0.31.0`, and publish the Starter release.
|
|
492
494
|
|
|
493
495
|
### CLI maintainers
|
|
494
496
|
|
|
@@ -545,6 +547,22 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
545
547
|
| 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
548
|
| 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
549
|
| 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` |
|
|
550
|
+
| 0.47.0 | v0.31.0 | Starter v0.31.0: deploying to Cloudflare (`--deploy cloudflare`), downloads by byte range, and browser tests in three engines; on Windows, arguments that can't start a second command and device names refused as project directories; install progress over MCP |
|
|
551
|
+
|
|
552
|
+
## Sponsors
|
|
553
|
+
|
|
554
|
+
<!-- To add a sponsor: put its light and dark logos in .github/assets/sponsors/ and copy the entry below. -->
|
|
555
|
+
|
|
556
|
+
<p>
|
|
557
|
+
<a href="https://neon.com">
|
|
558
|
+
<picture>
|
|
559
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Clubedge/create-clubedge-app/main/.github/assets/sponsors/neon-dark.svg">
|
|
560
|
+
<img src="https://raw.githubusercontent.com/Clubedge/create-clubedge-app/main/.github/assets/sponsors/neon-light.svg" alt="Neon" height="36">
|
|
561
|
+
</picture>
|
|
562
|
+
</a>
|
|
563
|
+
</p>
|
|
564
|
+
|
|
565
|
+
[Neon](https://neon.com) supports Clubedge Starter through its Open Source Program.
|
|
548
566
|
|
|
549
567
|
## Community
|
|
550
568
|
|
package/dist/core/execute.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, parse } from "node:path";
|
|
3
|
+
import { basename, dirname, join, parse } from "node:path";
|
|
4
4
|
import { copyTemplateFiles, emptyDirectory, pathExists } from "../utils/fs.js";
|
|
5
|
+
import { isReservedDeviceName } from "../utils/names.js";
|
|
5
6
|
export class TargetError extends Error {
|
|
6
7
|
name = "TargetError";
|
|
7
8
|
}
|
|
8
|
-
/** Refuses filesystem roots and non-empty directories. Never deletes or overwrites anything. */
|
|
9
|
+
/** Refuses filesystem roots, reserved Windows device names, and non-empty directories. Never deletes or overwrites anything. */
|
|
9
10
|
export async function assertTargetIsUsable(directory) {
|
|
10
11
|
if (parse(directory).root === directory) {
|
|
11
12
|
throw new TargetError("Choose a project subdirectory rather than a filesystem root.");
|
|
12
13
|
}
|
|
14
|
+
// Check the project's own name and each missing parent that executePlan would create. A parent
|
|
15
|
+
// that already exists is left alone, such as a home folder named con on macOS.
|
|
16
|
+
let folder = directory;
|
|
17
|
+
while (true) {
|
|
18
|
+
const name = basename(folder);
|
|
19
|
+
if (isReservedDeviceName(name)) {
|
|
20
|
+
throw new TargetError(`"${name}" is a reserved device name on Windows. Choose another directory name.`);
|
|
21
|
+
}
|
|
22
|
+
const parent = dirname(folder);
|
|
23
|
+
if (parent === folder || (await pathExists(parent)))
|
|
24
|
+
break;
|
|
25
|
+
folder = parent;
|
|
26
|
+
}
|
|
13
27
|
try {
|
|
14
28
|
const entries = await readdir(directory);
|
|
15
29
|
if (entries.length > 0)
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { parseCliArgs, UsageError } from "./cli/args.js";
|
|
6
6
|
import { dim, helpText, showBanner } from "./cli/ui.js";
|
|
@@ -17,7 +17,7 @@ import { describeNewDatabase, neonCommand, provisionNeonDatabase } from "./neon.
|
|
|
17
17
|
import { initializeGit } from "./steps/git.js";
|
|
18
18
|
import { detectLauncher, detectPnpm, installDependencies } from "./steps/install.js";
|
|
19
19
|
import { commandSucceeds, run } from "./utils/process.js";
|
|
20
|
-
import { displayNameFromPackageName, packageNameFromDirectory } from "./utils/names.js";
|
|
20
|
+
import { displayNameFromPackageName, isReservedDeviceName, packageNameFromDirectory } from "./utils/names.js";
|
|
21
21
|
const minimumNode = [22, 12];
|
|
22
22
|
export function assertSupportedNode(version = process.versions.node) {
|
|
23
23
|
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
@@ -29,7 +29,15 @@ async function askProjectDirectory() {
|
|
|
29
29
|
const value = await text({
|
|
30
30
|
message: "Where should the new project be created?",
|
|
31
31
|
placeholder: "my-app",
|
|
32
|
-
validate: (input) =>
|
|
32
|
+
validate: (input) => {
|
|
33
|
+
const trimmed = input?.trim();
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
return "Enter a project directory name.";
|
|
36
|
+
if (isReservedDeviceName(basename(trimmed))) {
|
|
37
|
+
return `"${basename(trimmed)}" is a reserved device name on Windows. Choose another directory name.`;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
},
|
|
33
41
|
});
|
|
34
42
|
if (isCancel(value)) {
|
|
35
43
|
cancel("Operation cancelled.");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How often a running step reports progress. Clients that reset their request timeout on
|
|
3
|
+
* progress wait tens of seconds, so a few seconds between notifications keeps the call alive.
|
|
4
|
+
*/
|
|
5
|
+
const intervalMs = 5_000;
|
|
6
|
+
/** A duration for a message: "45s", and "2m 5s" from a minute on. */
|
|
7
|
+
function formatElapsed(milliseconds) {
|
|
8
|
+
const seconds = Math.round(milliseconds / 1000);
|
|
9
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Progress notifications for the long steps of a tool call, such as `pnpm install`, which can
|
|
13
|
+
* outlast a client's request timeout. Only a client that sent a progress token with its request
|
|
14
|
+
* (`_meta.progressToken`) is notified; without one the steps run and nothing is sent.
|
|
15
|
+
*/
|
|
16
|
+
export function stepProgress(request) {
|
|
17
|
+
const token = request._meta?.progressToken;
|
|
18
|
+
// The protocol wants each value above the one before. How far along an install is isn't known,
|
|
19
|
+
// so the value counts the notifications sent, and no total is given.
|
|
20
|
+
let sent = 0;
|
|
21
|
+
const notify = async (message) => {
|
|
22
|
+
// A client that cancelled the call no longer knows the token.
|
|
23
|
+
if (token === undefined || request.signal.aborted)
|
|
24
|
+
return;
|
|
25
|
+
sent += 1;
|
|
26
|
+
try {
|
|
27
|
+
await request.notify({ method: "notifications/progress", params: { progressToken: token, progress: sent, message } });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// The client left or its transport closed. The step is not affected.
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
async step(name, run) {
|
|
35
|
+
if (token === undefined)
|
|
36
|
+
return run();
|
|
37
|
+
const startedAt = Date.now();
|
|
38
|
+
const elapsed = () => formatElapsed(Date.now() - startedAt);
|
|
39
|
+
// Notifications are not awaited: a client that reads slowly must not hold the step up.
|
|
40
|
+
void notify(`${name}: started`);
|
|
41
|
+
const timer = setInterval(() => void notify(`${name}: running for ${elapsed()}`), intervalMs);
|
|
42
|
+
try {
|
|
43
|
+
const result = await run();
|
|
44
|
+
void notify(`${name}: finished in ${elapsed()}`);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
void notify(`${name}: failed after ${elapsed()}`);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
// Cleared however the step ends, so the timer never keeps the server running.
|
|
53
|
+
clearInterval(timer);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -9,6 +9,7 @@ import { isOptionalSlot, readTemplateManifest } from "./core/manifest.js";
|
|
|
9
9
|
import { neonSetupStep } from "./core/neon.js";
|
|
10
10
|
import { createPlan, describePlan } from "./core/plan.js";
|
|
11
11
|
import { resolveTemplateSource } from "./core/source.js";
|
|
12
|
+
import { stepProgress } from "./mcp-progress.js";
|
|
12
13
|
import { cliVersion, cliUrl, siteUrl } from "./package-info.js";
|
|
13
14
|
import { initializeGit } from "./steps/git.js";
|
|
14
15
|
import { detectPnpm, installDependencies } from "./steps/install.js";
|
|
@@ -202,7 +203,7 @@ function createServer() {
|
|
|
202
203
|
initialize_git: z.boolean().default(false).describe("Initialize a Git repository after generating files."),
|
|
203
204
|
install_dependencies: z.boolean().default(false).describe("Run pnpm install and declared secondary toolchain installs after generating files."),
|
|
204
205
|
}),
|
|
205
|
-
}, async ({ directory, initialize_git, install_dependencies, ...options }) => withTemplate(async (source) => {
|
|
206
|
+
}, async ({ directory, initialize_git, install_dependencies, ...options }, ctx) => withTemplate(async (source) => {
|
|
206
207
|
const target = await workspaceDestination(directory);
|
|
207
208
|
const plan = await planProject(source, options, target, { git: initialize_git, install: install_dependencies });
|
|
208
209
|
await executePlan(plan, source);
|
|
@@ -227,8 +228,11 @@ function createServer() {
|
|
|
227
228
|
: "Project created, but pnpm was not found. Install pnpm and run it in the project directory.");
|
|
228
229
|
}
|
|
229
230
|
else {
|
|
231
|
+
// Installs can outlast a client's request timeout. A client that sent a progress token
|
|
232
|
+
// hears from each one while it runs, and can keep waiting.
|
|
233
|
+
const progress = stepProgress(ctx.mcpReq);
|
|
230
234
|
try {
|
|
231
|
-
await installDependencies(target, pnpm, [], mcpStdio);
|
|
235
|
+
await progress.step([...pnpm, "install"].join(" "), () => installDependencies(target, pnpm, [], mcpStdio));
|
|
232
236
|
for (const { directory: appDirectory, app, toolchain } of plan.toolchains) {
|
|
233
237
|
const [check, ...checkArgs] = toolchain.check;
|
|
234
238
|
if (!(await commandSucceeds(check, checkArgs))) {
|
|
@@ -237,7 +241,7 @@ function createServer() {
|
|
|
237
241
|
}
|
|
238
242
|
const [command, ...args] = toolchain.install;
|
|
239
243
|
try {
|
|
240
|
-
await run(command, args, resolve(target, appDirectory), mcpStdio);
|
|
244
|
+
await progress.step(`${toolchain.install.join(" ")} in ${appDirectory}`, () => run(command, args, resolve(target, appDirectory), mcpStdio));
|
|
241
245
|
}
|
|
242
246
|
catch (error) {
|
|
243
247
|
warnings.push(`${app} dependencies did not install: ${error instanceof Error ? error.message : String(error)}`);
|
package/dist/utils/names.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
// Windows reserves these names (and the CONIN$/CONOUT$ console handles) for legacy devices,
|
|
3
|
+
// in any case and regardless of extension, even though the Win32 namespace can bypass the
|
|
4
|
+
// check and leave an undeletable directory behind. COM/LPT also reserve the ¹²³ superscript
|
|
5
|
+
// digits Windows treats as equivalent to 1, 2, 3.
|
|
6
|
+
const RESERVED_DEVICE_NAMES = /^(CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[0-9¹²³]|LPT[0-9¹²³])(\..*)?$/i;
|
|
7
|
+
export function isReservedDeviceName(name) {
|
|
8
|
+
const dotIndex = name.indexOf(".");
|
|
9
|
+
const base = dotIndex === -1 ? name : name.slice(0, dotIndex);
|
|
10
|
+
const extension = dotIndex === -1 ? "" : name.slice(dotIndex);
|
|
11
|
+
return RESERVED_DEVICE_NAMES.test(base.replace(/ +$/, "") + extension);
|
|
12
|
+
}
|
|
2
13
|
export function packageNameFromDirectory(directory) {
|
|
3
14
|
return (basename(directory)
|
|
4
15
|
.toLowerCase()
|
package/dist/utils/open.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import process from "node:process";
|
|
3
|
+
/** Only web URLs may reach an OS opener (which can otherwise launch local programs). */
|
|
4
|
+
export function assertWebUrl(url) {
|
|
5
|
+
// Reject URL-parser fixups (backslashes, missing //, or surrounding whitespace)
|
|
6
|
+
// before passing the original value to a platform-specific command.
|
|
7
|
+
if (!/^https?:\/\//.test(url) || /[\\\s]/.test(url)) {
|
|
8
|
+
throw new Error("Refusing to open a non-web URL");
|
|
9
|
+
}
|
|
10
|
+
const parsed = new URL(url);
|
|
11
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || !parsed.hostname) {
|
|
12
|
+
throw new Error("Refusing to open a non-web URL");
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
3
16
|
/** The command that opens a URL in the default browser on this platform. */
|
|
4
17
|
export function openCommand(url, platform = process.platform) {
|
|
18
|
+
assertWebUrl(url);
|
|
5
19
|
if (platform === "win32")
|
|
6
20
|
return ["rundll32", ["url.dll,FileProtocolHandler", url]];
|
|
7
21
|
if (platform === "darwin")
|
|
@@ -10,7 +24,14 @@ export function openCommand(url, platform = process.platform) {
|
|
|
10
24
|
}
|
|
11
25
|
/** Opens a URL in the default browser. Best effort: returns false when nothing could open it. */
|
|
12
26
|
export async function openUrl(url) {
|
|
13
|
-
|
|
27
|
+
let command;
|
|
28
|
+
let args;
|
|
29
|
+
try {
|
|
30
|
+
[command, args] = openCommand(url);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
14
35
|
return await new Promise((resolve) => {
|
|
15
36
|
try {
|
|
16
37
|
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
package/dist/utils/process.js
CHANGED
|
@@ -1,21 +1,105 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
2
3
|
import { access } from "node:fs/promises";
|
|
3
4
|
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
// Package manager shims on Windows are .cmd files, which need a shell to start.
|
|
6
8
|
const useShell = process.platform === "win32";
|
|
7
|
-
/** An
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/** An environment variable by name, whatever its case: Windows keeps Path as it was written. */
|
|
10
|
+
function envValue(env, name) {
|
|
11
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
12
|
+
return key === undefined ? undefined : env[key];
|
|
13
|
+
}
|
|
14
|
+
function isFile(path) {
|
|
15
|
+
try {
|
|
16
|
+
return statSync(path).isFile();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The file cmd.exe runs for a command: the path as given when it names a folder, else the first
|
|
24
|
+
* match in the current folder and then PATH's folders, each with PATHEXT's extensions in order
|
|
25
|
+
* unless the command already ends with one. Null when there is none.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveForCmd(command, env = process.env, cwd = process.cwd()) {
|
|
28
|
+
const extensions = (envValue(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
29
|
+
const named = extensions.some((extension) => command.toLowerCase().endsWith(extension.toLowerCase()));
|
|
30
|
+
const candidates = (base) => (named ? [base] : extensions.map((extension) => base + extension));
|
|
31
|
+
if (/[\\/:]/.test(command))
|
|
32
|
+
return candidates(resolve(cwd, command)).find(isFile) ?? null;
|
|
33
|
+
// A folder in PATH may be written in quotes.
|
|
34
|
+
const path = (envValue(env, "PATH") ?? "").split(delimiter).map((folder) => folder.replace(/^"(.*)"$/, "$1"));
|
|
35
|
+
const folders = [cwd, ...path.filter(Boolean)];
|
|
36
|
+
for (const folder of folders) {
|
|
37
|
+
const found = candidates(join(isAbsolute(folder) ? folder : resolve(cwd, folder), command)).find(isFile);
|
|
38
|
+
if (found)
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** The characters cmd.exe gives a meaning to. A caret before one makes cmd.exe read it as text. */
|
|
44
|
+
const cmdSpecial = /[()\][%!^"`<>&|;, *?]/g;
|
|
45
|
+
const caret = (text) => text.replace(cmdSpecial, "^$&");
|
|
46
|
+
/**
|
|
47
|
+
* An argument as one word of a command line that cmd.exe runs, so the program receives exactly
|
|
48
|
+
* the argument: plain words as they are, and anything else quoted the way Windows programs split
|
|
49
|
+
* their command line (CommandLineToArgvW: backslashes before a quote doubled, the quote escaped),
|
|
50
|
+
* then with a caret before each character cmd.exe would act on. That covers quotes, so cmd.exe
|
|
51
|
+
* never reads the argument as its own quoted text, and `%`, which breaks up `%VAR%`. A batch file
|
|
52
|
+
* such as an npm shim passes its arguments on in a line cmd.exe reads again, so for one the carets
|
|
53
|
+
* are added twice (`passes`). The method is https://qntm.org/cmd, as cross-spawn applies it.
|
|
54
|
+
*
|
|
55
|
+
* A line break ends the command for cmd.exe whatever surrounds it, so it's refused.
|
|
56
|
+
*/
|
|
57
|
+
export function quoteForCmd(argument, passes = 1) {
|
|
58
|
+
if (/[\r\n]/.test(argument)) {
|
|
59
|
+
throw new Error(`An argument with a line break can't be passed to a command through cmd.exe: ${JSON.stringify(argument)}`);
|
|
60
|
+
}
|
|
61
|
+
if (/^[\w@+=:,./\\-]+$/.test(argument))
|
|
62
|
+
return argument;
|
|
63
|
+
let quoted = '"';
|
|
64
|
+
let backslashes = 0;
|
|
65
|
+
for (const character of argument) {
|
|
66
|
+
if (character === "\\") {
|
|
67
|
+
backslashes++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
quoted += character === '"' ? "\\".repeat(backslashes * 2 + 1) + '"' : "\\".repeat(backslashes) + character;
|
|
71
|
+
backslashes = 0;
|
|
72
|
+
}
|
|
73
|
+
quoted += "\\".repeat(backslashes * 2) + '"';
|
|
74
|
+
for (let pass = 0; pass < passes; pass++)
|
|
75
|
+
quoted = caret(quoted);
|
|
76
|
+
return quoted;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The command line for cmd.exe: the command with a caret before each special character (a path
|
|
80
|
+
* with spaces stays one word), then its arguments, escaped twice for a batch file.
|
|
81
|
+
*/
|
|
82
|
+
export function cmdLine(command, args, batch) {
|
|
83
|
+
if (/[\r\n]/.test(command))
|
|
84
|
+
throw new Error(`A command with a line break can't run through cmd.exe: ${JSON.stringify(command)}`);
|
|
85
|
+
return [caret(command), ...args.map((argument) => quoteForCmd(argument, batch ? 2 : 1))].join(" ");
|
|
10
86
|
}
|
|
11
87
|
/**
|
|
12
|
-
* Starts a command,
|
|
13
|
-
*
|
|
88
|
+
* Starts a command. On Windows, with `shell`, it finds the file cmd.exe would run:
|
|
89
|
+
* - a program (.exe, .com) starts directly, with no shell, so nothing reads its arguments but it;
|
|
90
|
+
* - a batch file (.cmd, .bat), such as an npm shim, can only start through cmd.exe, and gets one
|
|
91
|
+
* command line with every argument escaped for it (Node warns, DEP0190, when arguments are only
|
|
92
|
+
* concatenated for a shell);
|
|
93
|
+
* - anything else goes to cmd.exe too, which runs it or reports that it's missing.
|
|
14
94
|
*/
|
|
15
95
|
export function spawnCommand(command, args, options, shell = useShell) {
|
|
16
96
|
if (!shell)
|
|
17
97
|
return spawn(command, args, options);
|
|
18
|
-
|
|
98
|
+
const cwd = options.cwd === undefined ? process.cwd() : typeof options.cwd === "string" ? options.cwd : fileURLToPath(options.cwd);
|
|
99
|
+
const file = resolveForCmd(command, options.env ?? process.env, cwd);
|
|
100
|
+
if (file && /\.(exe|com)$/i.test(file))
|
|
101
|
+
return spawn(file, args, options);
|
|
102
|
+
return spawn(cmdLine(file ?? command, args, file !== null && /\.(cmd|bat)$/i.test(file)), { ...options, shell: true });
|
|
19
103
|
}
|
|
20
104
|
/** Runs a command with inherited output. Rejects on a non-zero exit. */
|
|
21
105
|
export async function run(command, args, cwd, stdio = "inherit") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Create React apps from Clubedge Starter with a CLI or local MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.9.0",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"clubedge": {
|
|
26
26
|
"starterRepository": "Clubedge/clubedge-starter",
|
|
27
|
-
"starterRef": "v0.
|
|
28
|
-
"starterCommit": "
|
|
27
|
+
"starterRef": "v0.31.0",
|
|
28
|
+
"starterCommit": "e6c47d7b8ec153443ee35abab7fc61d37f55e36a"
|
|
29
29
|
},
|
|
30
30
|
"license": "Apache-2.0",
|
|
31
31
|
"repository": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"release": "node scripts/release.mjs"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@modelcontextprotocol/server": "^2.
|
|
60
|
+
"@modelcontextprotocol/server": "^2.4.0",
|
|
61
61
|
"@clack/prompts": "^1.8.1",
|
|
62
62
|
"giget": "^3.3.1",
|
|
63
63
|
"node-diff3": "^3.2.1",
|
|
@@ -68,6 +68,6 @@
|
|
|
68
68
|
"@types/node": "^26.6.5",
|
|
69
69
|
"@types/ssh2": "^1.15.6",
|
|
70
70
|
"typescript": "^5.9.3",
|
|
71
|
-
"vitest": "^5.0
|
|
71
|
+
"vitest": "^5.1.0"
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -16,6 +16,7 @@ Create and manage Clubedge Starter projects through the supported CLI or MCP too
|
|
|
16
16
|
- If the `clubedge-starter` MCP server is available, call `get_starter_capabilities` first. Treat its response as authoritative for framework/API choices, module options, defaults, and requirements.
|
|
17
17
|
- Before scaffolding through MCP, call `plan_project` with the destination `directory` and requested `stacks` and `modules`. It validates compatibility and previews the files and setup actions without writing to disk. Resolve invalid combinations and any open choices before creating the project. Use `validate_project_config` when only a compatibility check is needed and no destination plan is useful.
|
|
18
18
|
- Use `create_project` to scaffold through MCP. Give `directory` as a relative path beneath the MCP server's working directory. Set `initialize_git` and `install_dependencies` to `true` only when appropriate for the user's request; both default to `false`.
|
|
19
|
+
- With `install_dependencies`, `create_project` answers once `pnpm install` and any toolchain install, such as `uv sync`, have finished, which can take minutes. The server sends progress notifications while they run, so a client that asks for them keeps waiting. If the call times out anyway, the project is already on disk and the server may still be installing: do not create it again. Check the directory, and run `pnpm install` there if the dependencies are missing.
|
|
19
20
|
- If MCP is unavailable, use the CLI. Prefer the installed project-local executable when present; otherwise use `pnpm dlx @clubedge/create-clubedge-app <directory> --yes` and provide the requested options as CLI flags.
|
|
20
21
|
- Use the CLI's `add`, `remove`, or `upgrade` commands for changes to an existing generated project. Do not reproduce those changes by manually copying Starter files.
|
|
21
22
|
|
package/template/.dockerignore
CHANGED
|
@@ -9,3 +9,9 @@ playwright-report
|
|
|
9
9
|
.output
|
|
10
10
|
.nitro
|
|
11
11
|
.tanstack
|
|
12
|
+
# clubedge:if deploy=cloudflare
|
|
13
|
+
# Wrangler's working files and local secrets (deploy/cloudflare), which change between builds.
|
|
14
|
+
deploy/cloudflare/.wrangler
|
|
15
|
+
deploy/cloudflare/.dev.vars*
|
|
16
|
+
deploy/cloudflare/*.named.jsonc
|
|
17
|
+
# clubedge:end
|
|
@@ -88,13 +88,6 @@ jobs:
|
|
|
88
88
|
done
|
|
89
89
|
curl --fail --silent --show-error --output /dev/null http://127.0.0.1:3001/api/health
|
|
90
90
|
# clubedge:end
|
|
91
|
-
# clubedge:if mobile!=none
|
|
92
|
-
# package.json's pnpm.auditConfig skips two high advisories in Expo's development CLI
|
|
93
|
-
# (@expo/cli), which builds and serves the native app but is not part of it: node-forge's,
|
|
94
|
-
# which has no fixed release, and zod 3's, fixed only in zod 4. pnpm skips them wherever they
|
|
95
|
-
# appear, so remove them once Expo's CLI no longer needs them. Last checked on 2026-10-26,
|
|
96
|
-
# with @expo/cli 58.1.15.
|
|
97
|
-
# clubedge:end
|
|
98
91
|
- run: pnpm audit --audit-level high
|
|
99
92
|
|
|
100
93
|
# clubedge:if framework!=none
|
|
@@ -108,7 +101,8 @@ jobs:
|
|
|
108
101
|
node-version: 22
|
|
109
102
|
cache: pnpm
|
|
110
103
|
- run: pnpm install --frozen-lockfile
|
|
111
|
-
|
|
104
|
+
# Each engine visitors use (playwright.config.ts): Chrome's, Firefox's, and Safari's WebKit.
|
|
105
|
+
- run: pnpm exec playwright install --with-deps chromium firefox webkit
|
|
112
106
|
- run: pnpm test:e2e
|
|
113
107
|
# clubedge:end
|
|
114
108
|
|
package/template/CONTRIBUTING.md
CHANGED
|
@@ -21,7 +21,7 @@ pnpm format:check
|
|
|
21
21
|
pnpm build
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Run `pnpm test:e2e` for changes that affect browser behavior. It requires
|
|
24
|
+
Run `pnpm test:e2e` for changes that affect browser behavior. It runs in Chromium, Firefox, and WebKit, and requires them installed locally (`pnpm exec playwright install chromium firefox webkit`). CI runs the full configured verification workflow for pull requests.
|
|
25
25
|
|
|
26
26
|
## Contribution workflow
|
|
27
27
|
|