@clubedge/create-clubedge-app 0.32.0 → 0.33.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 +14 -0
- package/README.md +30 -11
- package/dist/cli/ui.js +5 -4
- package/dist/core/customize.js +27 -7
- package/dist/core/execute.js +2 -2
- package/dist/core/manifest-fields.js +120 -0
- package/dist/core/manifest-legacy.js +110 -0
- package/dist/core/manifest-packs.js +200 -0
- package/dist/core/manifest.js +121 -180
- package/dist/core/plan.js +215 -56
- package/dist/core/project.js +5 -3
- package/dist/core/update.js +13 -14
- package/dist/deploy.js +6 -0
- package/dist/index.js +77 -33
- package/dist/inspect.js +1 -1
- package/dist/modify.js +23 -18
- package/package.json +3 -3
- package/template/SETUP.md +22 -0
- package/template/apps/api/.env.example +8 -0
- package/template/apps/api/Dockerfile +39 -0
- package/template/apps/api/build.mjs +18 -0
- package/template/apps/api/clubedge.stack.json +33 -0
- package/template/apps/api/package.json +29 -0
- package/template/apps/api/scripts/check-env.ts +10 -0
- package/template/apps/api/scripts/check-services.ts +11 -0
- package/template/apps/api/src/app.test.ts +31 -0
- package/template/apps/api/src/app.ts +43 -0
- package/template/apps/api/src/config/site.json +11 -0
- package/template/apps/api/src/config/site.ts +14 -0
- package/template/apps/api/src/env.ts +37 -0
- package/template/apps/api/src/index.ts +11 -0
- package/template/apps/api/src/server/auth.ts +9 -0
- package/template/apps/api/src/server/db.ts +24 -0
- package/template/apps/api/src/server/index.ts +12 -0
- package/template/apps/api/tsconfig.json +13 -0
- package/template/apps/start/clubedge.stack.json +186 -0
- package/template/apps/start/package.json +1 -1
- package/template/apps/vite/.env.example +3 -0
- package/template/apps/vite/Caddyfile +22 -0
- package/template/apps/vite/Dockerfile +35 -0
- package/template/apps/vite/clubedge.stack.json +15 -0
- package/template/apps/vite/index.html +29 -0
- package/template/apps/vite/package.json +32 -0
- package/template/apps/vite/public/.well-known/logo.svg +1 -0
- package/template/apps/vite/public/favicon.ico +0 -0
- package/template/apps/vite/src/components/branding/clubedge-mark.tsx +24 -0
- package/template/apps/vite/src/components/dashboard/account-button.tsx +9 -0
- package/template/apps/vite/src/components/dashboard/dashboard-shell.tsx +590 -0
- package/template/apps/vite/src/components/landing/copy-command.tsx +46 -0
- package/template/apps/vite/src/components/not-found.tsx +17 -0
- package/template/apps/vite/src/components/route-error.tsx +23 -0
- package/template/apps/vite/src/components/theme/theme-provider.tsx +41 -0
- package/template/apps/vite/src/components/theme/theme-toggle.tsx +21 -0
- package/template/apps/vite/src/config/site.json +11 -0
- package/template/apps/vite/src/config/site.ts +20 -0
- package/template/apps/vite/src/main.tsx +14 -0
- package/template/apps/vite/src/router.tsx +52 -0
- package/template/apps/vite/src/routes/dashboard.tsx +313 -0
- package/template/apps/vite/src/routes/index.tsx +226 -0
- package/template/apps/vite/src/styles.css +55 -0
- package/template/apps/vite/tsconfig.json +19 -0
- package/template/apps/vite/turbo.json +15 -0
- package/template/apps/vite/vite.config.ts +39 -0
- package/template/apps/web/clubedge.stack.json +173 -0
- package/template/apps/web/package.json +1 -1
- package/template/clubedge/modules/auth.json +70 -0
- package/template/clubedge/modules/cache.json +15 -0
- package/template/clubedge/modules/deploy.json +27 -0
- package/template/clubedge/modules/email.json +23 -0
- package/template/clubedge/modules/infra.json +53 -0
- package/template/clubedge/modules/jobs.json +39 -0
- package/template/clubedge/modules/observability.json +25 -0
- package/template/clubedge/modules/payments.json +20 -0
- package/template/clubedge/modules/storage.json +26 -0
- package/template/clubedge.template.json +40 -600
- package/template/docker-compose.yml +19 -0
- package/template/e2e/app.ts +12 -1
- package/template/eslint.config.mjs +1 -0
- package/template/package.json +2 -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 +101 -0
- package/template/packages/contract/package.json +24 -0
- package/template/packages/contract/src/conformance.ts +83 -0
- package/template/packages/contract/src/index.ts +24 -0
- package/template/packages/contract/src/remote.test.ts +14 -0
- package/template/packages/contract/src/schema.test.ts +15 -0
- package/template/packages/contract/src/schema.ts +167 -0
- package/template/packages/contract/tsconfig.json +7 -0
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/db/scripts/load-env.ts +3 -2
- 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/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/playwright.config.ts +11 -4
- package/template/pnpm-lock.yaml +278 -23
- package/template/turbo.json +5 -1
- package/template.lock.json +64 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.33.0
|
|
6
|
+
|
|
7
|
+
- **A frontend and a separate API**: default to Starter `v0.22.0` (#98). `--framework vite --api hono` creates a React + Vite frontend in `apps/web` and a Hono API in `apps/api`, joined by one HTTP contract:
|
|
8
|
+
- `packages/contract/openapi.yaml` is the contract. The frontend calls the API only through the client generated from it, and `@clubedge/contract/conformance` is the test suite every API passes, in process or over HTTP against any running API.
|
|
9
|
+
- `pnpm dev` and `pnpm start` run both, and the frontend forwards `/api` to the API, so the browser sees one origin. `docker compose up --build` runs both images.
|
|
10
|
+
- A first version: on these stacks the modules are `none` (cache: memory, deploy: Docker). `deploy` explains that it sets up one app and doesn't deploy the pair yet (#96).
|
|
11
|
+
- CI generates the pair from the packed CLI and runs its full pipeline; the smoke check accepts a single-page app's shell for a missing page (#97).
|
|
12
|
+
- **Stack packs** (template schema 4, #95): a Starter describes each app in its own `clubedge.stack.json` and each module in `clubedge/modules/<id>.json`, so adding a stack adds a folder and changes nothing shared. See [docs/stack-packs.md](docs/stack-packs.md).
|
|
13
|
+
- Slots place apps in generated projects: `--framework` for the web app and `--api` for a separate API. A frontend needs an API, and a full-stack app takes none.
|
|
14
|
+
- The CLI checks that the selected stacks implement each chosen module option, and only offers those options in prompts and `add`.
|
|
15
|
+
- Stacks outside Node.js declare a toolchain, such as uv, whose install runs after `pnpm install`.
|
|
16
|
+
- A slot can name packages only its apps use, left out when it is empty, and its own site config, named after the project (#96).
|
|
17
|
+
- Starters with schema 1 to 3 are read into the same model, so projects created from them still add, remove, and upgrade. Projects generated from the same selection are byte-identical to 0.32.0.
|
|
18
|
+
|
|
5
19
|
## 0.32.0
|
|
6
20
|
|
|
7
21
|
- **Pluggable authentication**: default to Starter `v0.21.0` (#93). Authentication now has four providers behind one interface, so the pages, settings, payments, and jobs stay the same whichever signs users in:
|
package/README.md
CHANGED
|
@@ -66,6 +66,23 @@ pnpm dlx @clubedge/create-clubedge-app my-app --framework tanstack-start
|
|
|
66
66
|
|
|
67
67
|
Either way, the app lives in `apps/web` and uses the same shared packages, so the project layout, root scripts, and Docker commands are identical.
|
|
68
68
|
|
|
69
|
+
### A frontend and a separate API
|
|
70
|
+
|
|
71
|
+
A frontend can instead call a separate API, so the API can be written in another language later:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --framework vite --api hono
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Flag | App | Where |
|
|
78
|
+
| ------------------ | ----------------------------------------------------- | ----------- |
|
|
79
|
+
| `--framework vite` | React + Vite single-page app (TanStack Router) | `apps/web` |
|
|
80
|
+
| `--api hono` | Hono API on Node, reusing the Starter's packages | `apps/api` |
|
|
81
|
+
|
|
82
|
+
They meet at one HTTP contract, `packages/contract/openapi.yaml`: the frontend calls the API only through the client generated from it, and every API passes the same conformance suite. `pnpm dev` starts both, and the frontend forwards `/api` to the API. A full-stack framework (`next`, `tanstack-start`) has its own server, so it takes no `--api`.
|
|
83
|
+
|
|
84
|
+
This is a first version: on these stacks the modules are `none` (cache: memory, deploy: Docker), and `deploy` doesn't set them up yet; SETUP.md in the project describes running both images. See [docs/stack-packs.md](docs/stack-packs.md) for how stacks are described.
|
|
85
|
+
|
|
69
86
|
## Choosing modules
|
|
70
87
|
|
|
71
88
|
Authentication, file storage, caching, email, payments, background jobs, observability, local infrastructure, and deployment are modules. Each has a default, and anything you leave out is removed completely. The Starter's `clubedge.template.json` defines the modules, so a module a newer Starter adds gets its `--<module>` flag without a CLI change; `--help` lists the modules of the Starter this CLI release creates.
|
|
@@ -102,7 +119,8 @@ Usage:
|
|
|
102
119
|
pnpm dlx @clubedge/create-clubedge-app upgrade [--ref <tag>] (inside a project)
|
|
103
120
|
|
|
104
121
|
Options:
|
|
105
|
-
--framework <id> App framework, such as next or tanstack-start
|
|
122
|
+
--framework <id> App framework, such as next or tanstack-start (or vite, a frontend)
|
|
123
|
+
--api <id> A separate API for a frontend, such as hono
|
|
106
124
|
--<module> <option> A module option, such as --auth none; the Starter lists its modules
|
|
107
125
|
--ref <ref> Download this Starter tag, branch, or commit from GitHub instead
|
|
108
126
|
(upgrade: the Starter to upgrade to)
|
|
@@ -129,7 +147,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
129
147
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
130
148
|
|
|
131
149
|
# Reproduce a specific Starter release or commit
|
|
132
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
150
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.22.0
|
|
133
151
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
134
152
|
|
|
135
153
|
# Test local Starter changes before releasing them
|
|
@@ -148,7 +166,7 @@ pnpm clubedge add email resend payments stripe # several modules as
|
|
|
148
166
|
pnpm clubedge remove storage payments
|
|
149
167
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
150
168
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
151
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
169
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.22.0 # or a specific tag, branch, or commit
|
|
152
170
|
```
|
|
153
171
|
|
|
154
172
|
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.
|
|
@@ -259,27 +277,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
259
277
|
Each CLI release is tested against one specific Starter release:
|
|
260
278
|
|
|
261
279
|
```text
|
|
262
|
-
create-clubedge-app v0.
|
|
280
|
+
create-clubedge-app v0.33.0
|
|
263
281
|
|
|
|
264
282
|
v
|
|
265
|
-
Clubedge Starter v0.
|
|
266
|
-
commit
|
|
283
|
+
Clubedge Starter v0.22.0
|
|
284
|
+
commit d0f3f9cdd4d568504cec4cf4681c7faf97842405
|
|
267
285
|
```
|
|
268
286
|
|
|
269
287
|
The current relationship is:
|
|
270
288
|
|
|
271
289
|
| CLI release | Default Starter release | Meaning |
|
|
272
290
|
| --- | --- | --- |
|
|
273
|
-
| `create-clubedge-app@0.
|
|
291
|
+
| `create-clubedge-app@0.33.0` | `clubedge-starter@v0.22.0` | The CLI's required, tested default |
|
|
274
292
|
|
|
275
293
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
276
294
|
|
|
277
|
-
The `v0.
|
|
278
|
-
`
|
|
295
|
+
The `v0.22.0` tag is expected to resolve to commit
|
|
296
|
+
`d0f3f9cdd4d568504cec4cf4681c7faf97842405`. The CLI verifies this before
|
|
279
297
|
scaffolding with the default reference and fails rather than silently using a
|
|
280
298
|
moved tag.
|
|
281
299
|
|
|
282
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
300
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.22.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.
|
|
283
301
|
|
|
284
302
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
285
303
|
|
|
@@ -334,7 +352,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
334
352
|
1. Make and validate changes in `clubedge-starter`.
|
|
335
353
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
336
354
|
3. Update the Starter package versions.
|
|
337
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
355
|
+
4. Commit `main`, create a release tag such as `v0.22.0`, and publish the Starter release.
|
|
338
356
|
|
|
339
357
|
### CLI maintainers
|
|
340
358
|
|
|
@@ -377,6 +395,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
377
395
|
| 0.30.1 | v0.19.1 | Railway previews work with a workspace token and answer on the port their domain targets |
|
|
378
396
|
| 0.31.0 | v0.20.0 | Pluggable background jobs: `--jobs bullmq` and `--jobs inngest` next to pg-boss, and a conformance suite every provider passes |
|
|
379
397
|
| 0.32.0 | v0.21.0 | Pluggable authentication: `--auth firebase` and `--auth appwrite` next to Supabase and Better Auth, and a conformance suite every provider passes |
|
|
398
|
+
| 0.33.0 | v0.22.0 | Starter v0.22.0: stack packs (template schema 4), and a React + Vite frontend with a separate Hono API (`--framework vite --api hono`) |
|
|
380
399
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
381
400
|
|
|
382
401
|
## Community
|
package/dist/cli/ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
+
import { NO_STACK, stacksIn } from "../core/plan.js";
|
|
2
3
|
import { cliUrl, cliVersion, siteUrl, starterPin } from "../package-info.js";
|
|
3
4
|
// Each glyph is 6 rows tall. Rows are padded to a common width when rendered.
|
|
4
5
|
const glyphs = {
|
|
@@ -69,11 +70,11 @@ export function selectionHelp(manifest) {
|
|
|
69
70
|
line("--<module> <option>", "A module option, such as --auth none; the Starter lists its modules"),
|
|
70
71
|
].join("\n");
|
|
71
72
|
}
|
|
72
|
-
const frameworks = Object.keys(manifest.frameworks);
|
|
73
73
|
return [
|
|
74
|
-
...(
|
|
75
|
-
|
|
76
|
-
: []
|
|
74
|
+
...Object.entries(manifest.slots).flatMap(([id, slot]) => {
|
|
75
|
+
const stacks = [...stacksIn(manifest, id), ...(slot.default === undefined ? [NO_STACK] : [])];
|
|
76
|
+
return stacks.length > 1 ? [line(`--${id} <id>`, `${slot.name}: ${choices(stacks, slot.default ?? NO_STACK)}`)] : [];
|
|
77
|
+
}),
|
|
77
78
|
...Object.entries(manifest.modules).map(([id, module]) => line(`--${id} <id>`, `${module.name}: ${choices(Object.keys(module.options), module.default)}`)),
|
|
78
79
|
].join("\n");
|
|
79
80
|
}
|
package/dist/core/customize.js
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
// Pure text transforms applied to template files. Keeping them free of I/O makes each one easy
|
|
2
2
|
// to test and lets --dry-run describe them without touching the disk.
|
|
3
3
|
import { pruneOptionalPeers } from "./lockfile-peers.js";
|
|
4
|
+
import { FRAMEWORK_SLOT } from "./manifest-fields.js";
|
|
4
5
|
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
6
|
+
/**
|
|
7
|
+
* The stacks a project recorded in its package.json `clubedge` field, by slot. Projects have
|
|
8
|
+
* recorded the framework slot as `framework` since before slots existed; other slots are
|
|
9
|
+
* recorded under `apps`, such as `"apps": { "api": "fastapi" }`. */
|
|
10
|
+
export function recordedStacks(origin) {
|
|
11
|
+
const apps = origin.apps && typeof origin.apps === "object" ? origin.apps : {};
|
|
12
|
+
return {
|
|
13
|
+
...(typeof origin.framework === "string" ? { [FRAMEWORK_SLOT]: origin.framework } : {}),
|
|
14
|
+
...apps,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function recordStacks(stacks) {
|
|
18
|
+
const { [FRAMEWORK_SLOT]: framework, ...apps } = stacks;
|
|
19
|
+
return {
|
|
20
|
+
...(framework ? { framework } : {}),
|
|
21
|
+
...(Object.keys(apps).length ? { apps } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
5
24
|
export function customizeRootManifest(source, { packageName }, provenance, dockerImage, scripts = { set: {}, remove: [] }) {
|
|
6
25
|
const manifest = JSON.parse(source);
|
|
7
26
|
manifest.name = packageName;
|
|
@@ -16,7 +35,7 @@ export function customizeRootManifest(source, { packageName }, provenance, docke
|
|
|
16
35
|
starterRepository: provenance.starterRepository,
|
|
17
36
|
starterRef: provenance.starterRef,
|
|
18
37
|
...(provenance.starterCommit ? { starterCommit: provenance.starterCommit } : {}),
|
|
19
|
-
...(provenance.
|
|
38
|
+
...recordStacks(provenance.stacks ?? {}),
|
|
20
39
|
...(provenance.modules ? { modules: provenance.modules } : {}),
|
|
21
40
|
};
|
|
22
41
|
for (const script of ["docker:build", "docker:start"]) {
|
|
@@ -30,13 +49,13 @@ export function customizeReadme(source, { displayName }, provenance) {
|
|
|
30
49
|
const commitLine = provenance.starterCommit
|
|
31
50
|
? `\n- Starter commit: \`${provenance.starterCommit}\``
|
|
32
51
|
: "";
|
|
33
|
-
const
|
|
52
|
+
const stackLines = (provenance.stackSummary ?? []).map((line) => `\n- ${line}`).join("");
|
|
34
53
|
const modulesLine = provenance.moduleSummary?.length
|
|
35
54
|
? `\n- Modules: ${provenance.moduleSummary.join(", ")}`
|
|
36
55
|
: "";
|
|
37
56
|
const origin = `## Generated from\n\n` +
|
|
38
57
|
`- Starter repository: \`${provenance.starterRepository}\`\n` +
|
|
39
|
-
`- Starter ref: \`${provenance.starterRef}\`${commitLine}${
|
|
58
|
+
`- Starter ref: \`${provenance.starterRef}\`${commitLine}${stackLines}${modulesLine}\n` +
|
|
40
59
|
`- CLI version: \`${provenance.cliVersion}\``;
|
|
41
60
|
// Only the first heading is the project title; leave every other line as written.
|
|
42
61
|
return source.replace(/^# .+$/m, `# ${displayName}\n\n${origin}`);
|
|
@@ -67,20 +86,20 @@ export function customizeLegacySource(source, { displayName }) {
|
|
|
67
86
|
.replaceAll(">Clubedge<", `>${displayName}<`)
|
|
68
87
|
.replaceAll("Starter workspace", "Application workspace");
|
|
69
88
|
}
|
|
70
|
-
/** Gives
|
|
89
|
+
/** Gives a selected stack's app the package name generated projects use. */
|
|
71
90
|
export function customizeAppManifest(source, appPackage) {
|
|
72
91
|
const manifest = JSON.parse(source);
|
|
73
92
|
manifest.name = appPackage;
|
|
74
93
|
return json(manifest);
|
|
75
94
|
}
|
|
76
95
|
/**
|
|
77
|
-
* Keeps pnpm-lock.yaml in step with the selected
|
|
96
|
+
* Keeps pnpm-lock.yaml in step with the selected stacks so `pnpm install --frozen-lockfile`
|
|
78
97
|
* works. Importers are keyed by workspace path at two-space indentation; package entries that
|
|
79
98
|
* are no longer used are harmless, and pnpm prunes them on the next lockfile update. Optional
|
|
80
99
|
* peer links to packages that only left-out importers installed are removed, so those packages
|
|
81
100
|
* are not installed either.
|
|
82
101
|
*/
|
|
83
|
-
export function customizeLockfile(source, { remove, rename, removeDependencies = [] }) {
|
|
102
|
+
export function customizeLockfile(source, { remove, rename = [], removeDependencies = [] }) {
|
|
84
103
|
const output = [];
|
|
85
104
|
let inImporters = false;
|
|
86
105
|
let skippingImporter = false;
|
|
@@ -106,8 +125,9 @@ export function customizeLockfile(source, { remove, rename, removeDependencies =
|
|
|
106
125
|
skippingImporter = remove.includes(importer);
|
|
107
126
|
skippingDependency = false;
|
|
108
127
|
pendingGroup = null;
|
|
128
|
+
const renamed = rename.find(({ from }) => from === importer);
|
|
109
129
|
if (!skippingImporter)
|
|
110
|
-
output.push(
|
|
130
|
+
output.push(renamed ? ` ${renamed.to}:` : line);
|
|
111
131
|
continue;
|
|
112
132
|
}
|
|
113
133
|
if (skippingImporter)
|
package/dist/core/execute.js
CHANGED
|
@@ -48,8 +48,8 @@ export async function executePlan(plan, source) {
|
|
|
48
48
|
const path = join(target, edit.path);
|
|
49
49
|
await writeFile(path, edit.apply(await readFile(path, "utf8")));
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
await copyFile(join(target,
|
|
51
|
+
for (const envFile of plan.envFiles) {
|
|
52
|
+
await copyFile(join(target, envFile.from), join(target, envFile.to), constants.COPYFILE_EXCL);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
catch (error) {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Field readers shared by the manifest schemas. Each one names the file and the field in its
|
|
2
|
+
// error, so a Starter maintainer sees exactly what to fix.
|
|
3
|
+
export const MANIFEST_FILE = "clubedge.template.json";
|
|
4
|
+
export class ManifestError extends Error {
|
|
5
|
+
name = "ManifestError";
|
|
6
|
+
}
|
|
7
|
+
export const stackRoles = ["fullstack", "frontend", "api"];
|
|
8
|
+
/**
|
|
9
|
+
* The slot for the app itself. Starters before schema 4 call it the framework, and projects
|
|
10
|
+
* record their choice for it as `framework`.
|
|
11
|
+
*/
|
|
12
|
+
export const FRAMEWORK_SLOT = "framework";
|
|
13
|
+
/** Ids of slots, stacks, modules, and options: lowercase words joined by dashes. */
|
|
14
|
+
export const ID = /^[a-z][a-z0-9-]*$/;
|
|
15
|
+
export function isRecord(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
export function isStringArray(value) {
|
|
19
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
20
|
+
}
|
|
21
|
+
/** Reads the fields of one JSON object, reporting errors against `file` and `prefix`. */
|
|
22
|
+
export function fields(file, prefix = "") {
|
|
23
|
+
const fail = (message) => {
|
|
24
|
+
throw new ManifestError(`${file}: ${message}`);
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
fail,
|
|
28
|
+
requireString(record, key) {
|
|
29
|
+
const value = record[key];
|
|
30
|
+
if (typeof value !== "string" || !value)
|
|
31
|
+
fail(`${prefix}${key} must be a non-empty string.`);
|
|
32
|
+
return value;
|
|
33
|
+
},
|
|
34
|
+
optionalString(record, key) {
|
|
35
|
+
const value = record[key];
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (typeof value !== "string" || !value)
|
|
39
|
+
fail(`${prefix}${key} must be a non-empty string.`);
|
|
40
|
+
return value;
|
|
41
|
+
},
|
|
42
|
+
optionalStringArray(record, key) {
|
|
43
|
+
const value = record[key];
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return [];
|
|
46
|
+
if (!isStringArray(value))
|
|
47
|
+
fail(`${prefix}${key} must be an array of paths.`);
|
|
48
|
+
return value;
|
|
49
|
+
},
|
|
50
|
+
optionalStringRecord(record, key) {
|
|
51
|
+
const value = record[key];
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return {};
|
|
54
|
+
if (!isRecord(value) || !Object.values(value).every((item) => typeof item === "string" && item)) {
|
|
55
|
+
fail(`${prefix}${key} must map names to paths.`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
},
|
|
59
|
+
requireRecord(record, key) {
|
|
60
|
+
const value = record[key];
|
|
61
|
+
if (!isRecord(value))
|
|
62
|
+
fail(`${prefix}${key} must be an object.`);
|
|
63
|
+
return value;
|
|
64
|
+
},
|
|
65
|
+
requireId(id, kind) {
|
|
66
|
+
if (!ID.test(id))
|
|
67
|
+
fail(`"${id}" is not a valid ${kind} id.`);
|
|
68
|
+
return id;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** `requires`: module id to the options allowed, as one option or several. */
|
|
73
|
+
export function parseRequires(file, value, prefix) {
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
return {};
|
|
76
|
+
if (!isRecord(value))
|
|
77
|
+
throw new ManifestError(`${file}: ${prefix}requires must be an object.`);
|
|
78
|
+
const requires = {};
|
|
79
|
+
for (const [module, allowed] of Object.entries(value)) {
|
|
80
|
+
const list = typeof allowed === "string" ? [allowed] : allowed;
|
|
81
|
+
if (!isStringArray(list) || list.length === 0) {
|
|
82
|
+
throw new ManifestError(`${file}: ${prefix}requires.${module} must name one or more options.`);
|
|
83
|
+
}
|
|
84
|
+
requires[module] = list;
|
|
85
|
+
}
|
|
86
|
+
return requires;
|
|
87
|
+
}
|
|
88
|
+
/** A module's name, default, and options; `prefix` locates it in its file. */
|
|
89
|
+
export function parseModule(file, entry, prefix) {
|
|
90
|
+
const { fail, requireString } = fields(file, prefix);
|
|
91
|
+
if (!isRecord(entry))
|
|
92
|
+
fail(`${prefix.slice(0, -1) || "the module"} must be an object.`);
|
|
93
|
+
const record = entry;
|
|
94
|
+
const optionsValue = record.options;
|
|
95
|
+
if (!isRecord(optionsValue) || Object.keys(optionsValue).length === 0) {
|
|
96
|
+
fail(`${prefix}options must describe at least one option.`);
|
|
97
|
+
}
|
|
98
|
+
const options = {};
|
|
99
|
+
for (const [optionId, option] of Object.entries(optionsValue)) {
|
|
100
|
+
const optionPrefix = `${prefix}options.${optionId}.`;
|
|
101
|
+
if (!ID.test(optionId))
|
|
102
|
+
fail(`"${optionId}" is not a valid option id in ${prefix ? prefix.slice(0, -1) : "options"}.`);
|
|
103
|
+
if (!isRecord(option))
|
|
104
|
+
fail(`${optionPrefix.slice(0, -1)} must be an object.`);
|
|
105
|
+
const read = fields(file, optionPrefix);
|
|
106
|
+
const values = option;
|
|
107
|
+
options[optionId] = {
|
|
108
|
+
name: read.requireString(values, "name"),
|
|
109
|
+
packages: read.optionalStringRecord(values, "packages"),
|
|
110
|
+
files: read.optionalStringArray(values, "files"),
|
|
111
|
+
replace: read.optionalStringRecord(values, "replace"),
|
|
112
|
+
requires: parseRequires(file, values.requires, optionPrefix),
|
|
113
|
+
scripts: read.optionalStringRecord(values, "scripts"),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const defaultOption = requireString(record, "default");
|
|
117
|
+
if (!options[defaultOption])
|
|
118
|
+
fail(`${prefix}default "${defaultOption}" is not in its options.`);
|
|
119
|
+
return { name: requireString(record, "name"), default: defaultOption, options };
|
|
120
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Schema 1 to 3: one clubedge.template.json with every framework and module file. These
|
|
2
|
+
// Starters predate slots and stack packs, so each framework becomes a full-stack stack in the
|
|
3
|
+
// "framework" slot, implementing every module option.
|
|
4
|
+
import { fields, FRAMEWORK_SLOT, ID, isRecord, isStringArray, MANIFEST_FILE, ManifestError, parseModule } from "./manifest-fields.js";
|
|
5
|
+
const frameworkSlot = (path, appPackage, env, defaultFramework) => ({
|
|
6
|
+
name: "Framework",
|
|
7
|
+
question: "Which framework should the app use?",
|
|
8
|
+
path,
|
|
9
|
+
...(appPackage ? { package: appPackage } : {}),
|
|
10
|
+
env,
|
|
11
|
+
dockerfile: "Dockerfile",
|
|
12
|
+
roles: ["fullstack"],
|
|
13
|
+
default: defaultFramework,
|
|
14
|
+
packages: {},
|
|
15
|
+
});
|
|
16
|
+
const fullstack = (name, app, envExample, dockerfile) => ({
|
|
17
|
+
name,
|
|
18
|
+
role: "fullstack",
|
|
19
|
+
slot: FRAMEWORK_SLOT,
|
|
20
|
+
app,
|
|
21
|
+
envExample,
|
|
22
|
+
dockerfile,
|
|
23
|
+
modules: {},
|
|
24
|
+
});
|
|
25
|
+
/** Starter releases before v0.3.0 have no manifest; this describes their layout. */
|
|
26
|
+
export const legacyManifest = {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
name: "clubedge-starter",
|
|
29
|
+
packageManager: "pnpm",
|
|
30
|
+
siteConfig: "apps/web/src/config/site.json",
|
|
31
|
+
dockerImage: "clubedge-starter",
|
|
32
|
+
slots: {
|
|
33
|
+
[FRAMEWORK_SLOT]: frameworkSlot("apps/web", undefined, { example: ".env.example", target: "apps/web/.env.local" }, "next"),
|
|
34
|
+
},
|
|
35
|
+
stacks: { next: fullstack("Next.js", "apps/web", ".env.example", "Dockerfile") },
|
|
36
|
+
modules: {},
|
|
37
|
+
conditional: [],
|
|
38
|
+
exclude: [],
|
|
39
|
+
};
|
|
40
|
+
export function parseLegacyManifest(data, schemaVersion) {
|
|
41
|
+
const { fail, requireString, optionalStringArray } = fields(MANIFEST_FILE);
|
|
42
|
+
const env = data.env;
|
|
43
|
+
if (data.packageManager !== "pnpm")
|
|
44
|
+
fail(`packageManager must be "pnpm".`);
|
|
45
|
+
if (!isRecord(env))
|
|
46
|
+
fail("env must be an object.");
|
|
47
|
+
if (data.exclude !== undefined && !isStringArray(data.exclude))
|
|
48
|
+
fail("exclude must be an array of paths.");
|
|
49
|
+
const app = requireString(data, "app");
|
|
50
|
+
const envFiles = {
|
|
51
|
+
example: fields(MANIFEST_FILE, "env.").requireString(env, "example"),
|
|
52
|
+
target: fields(MANIFEST_FILE, "env.").requireString(env, "target"),
|
|
53
|
+
};
|
|
54
|
+
const appPackage = schemaVersion >= 2 ? requireString(data, "appPackage") : undefined;
|
|
55
|
+
const { defaultFramework, stacks } = schemaVersion >= 2
|
|
56
|
+
? parseFrameworks(data)
|
|
57
|
+
: { defaultFramework: "next", stacks: { next: fullstack("Next.js", app, envFiles.example, "Dockerfile") } };
|
|
58
|
+
const modules = schemaVersion >= 3 ? parseModules(data.modules) : {};
|
|
59
|
+
// Every framework of these Starters implements every option.
|
|
60
|
+
for (const stack of Object.values(stacks)) {
|
|
61
|
+
stack.modules = Object.fromEntries(Object.entries(modules).map(([id, module]) => [id, Object.keys(module.options)]));
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion,
|
|
65
|
+
name: requireString(data, "name"),
|
|
66
|
+
packageManager: "pnpm",
|
|
67
|
+
siteConfig: requireString(data, "siteConfig"),
|
|
68
|
+
dockerImage: requireString(data, "dockerImage"),
|
|
69
|
+
slots: { [FRAMEWORK_SLOT]: frameworkSlot(app, appPackage, envFiles, defaultFramework) },
|
|
70
|
+
stacks,
|
|
71
|
+
modules,
|
|
72
|
+
conditional: schemaVersion >= 3 ? optionalStringArray(data, "conditional") : [],
|
|
73
|
+
exclude: data.exclude ?? [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parseModules(value) {
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
return {};
|
|
79
|
+
if (!isRecord(value))
|
|
80
|
+
throw new ManifestError(`${MANIFEST_FILE}: modules must be an object.`);
|
|
81
|
+
const modules = {};
|
|
82
|
+
for (const [id, entry] of Object.entries(value)) {
|
|
83
|
+
if (!ID.test(id) || id === FRAMEWORK_SLOT) {
|
|
84
|
+
throw new ManifestError(`${MANIFEST_FILE}: "${id}" is not a valid module id.`);
|
|
85
|
+
}
|
|
86
|
+
modules[id] = { ...parseModule(MANIFEST_FILE, entry, `modules.${id}.`), scope: "app" };
|
|
87
|
+
}
|
|
88
|
+
return modules;
|
|
89
|
+
}
|
|
90
|
+
function parseFrameworks(data) {
|
|
91
|
+
const { fail, requireString } = fields(MANIFEST_FILE);
|
|
92
|
+
const { frameworks } = data;
|
|
93
|
+
if (!isRecord(frameworks) || Object.keys(frameworks).length === 0) {
|
|
94
|
+
fail("frameworks must describe at least one framework.");
|
|
95
|
+
}
|
|
96
|
+
const stacks = {};
|
|
97
|
+
for (const [id, entry] of Object.entries(frameworks)) {
|
|
98
|
+
if (!ID.test(id))
|
|
99
|
+
fail(`"${id}" is not a valid framework id.`);
|
|
100
|
+
if (!isRecord(entry))
|
|
101
|
+
fail(`frameworks.${id} must be an object.`);
|
|
102
|
+
const read = fields(MANIFEST_FILE, `frameworks.${id}.`);
|
|
103
|
+
const framework = entry;
|
|
104
|
+
stacks[id] = fullstack(read.requireString(framework, "name"), read.requireString(framework, "app"), read.requireString(framework, "envExample"), read.requireString(framework, "dockerfile"));
|
|
105
|
+
}
|
|
106
|
+
const defaultFramework = requireString(data, "defaultFramework");
|
|
107
|
+
if (!stacks[defaultFramework])
|
|
108
|
+
fail(`defaultFramework "${defaultFramework}" is not in frameworks.`);
|
|
109
|
+
return { defaultFramework, stacks };
|
|
110
|
+
}
|