@clubedge/create-clubedge-app 0.9.0 → 0.10.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 +7 -0
- package/README.md +13 -12
- package/dist/cli/args.js +56 -27
- package/dist/cli/ui.js +27 -6
- package/dist/core/env.js +10 -3
- package/dist/index.js +19 -4
- package/package.json +3 -3
- package/template/.env.example +20 -0
- package/template/README.md +14 -1
- package/template/SETUP.md +38 -4
- package/template/apps/start/.env.example +20 -0
- package/template/apps/start/package.json +4 -1
- package/template/apps/start/scripts/check-services.ts +38 -0
- package/template/apps/start/src/components/auth/auth-form-controls.tsx +8 -2
- package/template/apps/start/src/components/auth/auth-page.tsx +50 -0
- package/template/apps/start/src/components/auth/notice.tsx +33 -0
- package/template/apps/start/src/env/auth.ts +9 -1
- package/template/apps/start/src/lib/login-url.ts +10 -1
- package/template/apps/start/src/routes/auth/forgot-password.ts +11 -0
- package/template/apps/start/src/routes/auth/reset-password.ts +11 -0
- package/template/apps/start/src/routes/forgot-password.tsx +46 -0
- package/template/apps/start/src/routes/login.tsx +23 -32
- package/template/apps/start/src/routes/reset-password.tsx +52 -0
- package/template/apps/start/src/server/auth-email.ts +18 -0
- package/template/apps/start/src/server/auth-forms.ts +58 -2
- package/template/apps/start/src/server/auth.functions.ts +9 -5
- package/template/apps/start/src/server/email.ts +39 -0
- package/template/apps/start/src/server/variants/auth.better-auth.ts +8 -1
- package/template/apps/start/src/server/variants/email.resend.ts +39 -0
- package/template/apps/web/package.json +4 -1
- package/template/apps/web/scripts/check-services.ts +38 -0
- package/template/apps/web/src/app/actions/auth.ts +49 -2
- package/template/apps/web/src/app/auth/callback/route.ts +11 -2
- package/template/apps/web/src/app/forgot-password/page.tsx +40 -0
- package/template/apps/web/src/app/login/_components/auth-form-controls.tsx +8 -2
- package/template/apps/web/src/app/login/_components/auth-page.tsx +50 -0
- package/template/apps/web/src/app/login/_components/notice.tsx +33 -0
- package/template/apps/web/src/app/login/page.tsx +23 -32
- package/template/apps/web/src/app/reset-password/page.tsx +47 -0
- package/template/apps/web/src/env/auth.ts +4 -0
- package/template/apps/web/src/lib/login-url.ts +10 -1
- package/template/apps/web/src/server/auth-email.ts +18 -0
- package/template/apps/web/src/server/email.ts +39 -0
- package/template/apps/web/src/server/variants/auth.better-auth.ts +8 -1
- package/template/apps/web/src/server/variants/email.resend.ts +39 -0
- package/template/clubedge.template.json +49 -2
- package/template/docker-compose.dev.yml +13 -0
- package/template/e2e/email-verification.spec.ts +29 -0
- package/template/e2e/inbox.ts +53 -0
- package/template/e2e/password-reset.spec.ts +48 -0
- package/template/package.json +1 -1
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth/src/types.ts +19 -1
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-better-auth/src/emails.ts +78 -0
- package/template/packages/auth-better-auth/src/index.test.ts +133 -2
- package/template/packages/auth-better-auth/src/index.ts +135 -7
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/auth-supabase/src/index.test.ts +45 -0
- package/template/packages/auth-supabase/src/index.ts +12 -0
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/email/package.json +18 -0
- package/template/packages/email/src/index.test.ts +57 -0
- package/template/packages/email/src/index.ts +50 -0
- package/template/packages/email/tsconfig.json +4 -0
- package/template/packages/email-resend/package.json +21 -0
- package/template/packages/email-resend/src/index.test.ts +56 -0
- package/template/packages/email-resend/src/index.ts +56 -0
- package/template/packages/email-resend/tsconfig.json +4 -0
- package/template/packages/email-smtp/package.json +22 -0
- package/template/packages/email-smtp/src/index.test.ts +73 -0
- package/template/packages/email-smtp/src/index.ts +26 -0
- package/template/packages/email-smtp/tsconfig.json +4 -0
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/pnpm-lock.yaml +53 -0
- package/template/supabase/config.toml +6 -1
- package/template.lock.json +34 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.10.0
|
|
6
|
+
|
|
7
|
+
- Default to Starter `v0.9.0`, which adds the email module (`--email smtp|resend|none`), password reset for both auth providers, and email verification for Better Auth. `--email` needs no CLI code: it comes from the Starter's manifest.
|
|
8
|
+
- Fix a doubled blank line in `.env.local` when `add` or `remove` swaps one section of variables for another, such as switching `--email smtp` to `resend`.
|
|
9
|
+
- Take module flags from the Starter's manifest: any `--<module> <option>` flag is accepted and validated against the modules the Starter describes, so new Starter modules need no CLI release. A mistyped module lists the Starter's modules; an unknown flag without a value is still an error.
|
|
10
|
+
- Build the help text's framework and module lines, and the "customize the included services" prompt, from the Starter's manifest.
|
|
11
|
+
|
|
5
12
|
## 0.9.0
|
|
6
13
|
|
|
7
14
|
- Default to Starter `v0.8.0`, whose projects have a `clubedge` script: `pnpm clubedge add`, `remove`, and `upgrade`.
|
package/README.md
CHANGED
|
@@ -12,27 +12,27 @@ The CLI downloads a tested Starter release, names the generated project, creates
|
|
|
12
12
|
Each CLI release is tested against one specific Starter release:
|
|
13
13
|
|
|
14
14
|
```text
|
|
15
|
-
create-clubedge-app v0.
|
|
15
|
+
create-clubedge-app v0.10.0
|
|
16
16
|
|
|
|
17
17
|
v
|
|
18
|
-
Clubedge Starter v0.
|
|
19
|
-
commit
|
|
18
|
+
Clubedge Starter v0.9.0
|
|
19
|
+
commit e858d0b5b60bbb05f1b6aed3f4d9e841ea2df2a5
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The current relationship is:
|
|
23
23
|
|
|
24
24
|
| CLI release | Default Starter release | Meaning |
|
|
25
25
|
| --- | --- | --- |
|
|
26
|
-
| `create-clubedge-app@0.
|
|
26
|
+
| `create-clubedge-app@0.10.0` | `clubedge-starter@v0.9.0` | The CLI's required, tested default |
|
|
27
27
|
|
|
28
28
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
29
29
|
|
|
30
|
-
The `v0.
|
|
31
|
-
`
|
|
30
|
+
The `v0.9.0` tag is expected to resolve to commit
|
|
31
|
+
`e858d0b5b60bbb05f1b6aed3f4d9e841ea2df2a5`. The CLI verifies this before
|
|
32
32
|
scaffolding with the default reference and fails rather than silently using a
|
|
33
33
|
moved tag.
|
|
34
34
|
|
|
35
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
35
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.9.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.
|
|
36
36
|
|
|
37
37
|
## Requirements
|
|
38
38
|
|
|
@@ -85,13 +85,14 @@ Either way, the app lives in `apps/web` and uses the same shared packages, so th
|
|
|
85
85
|
|
|
86
86
|
### Choosing services
|
|
87
87
|
|
|
88
|
-
Authentication, file storage, and
|
|
88
|
+
Authentication, file storage, caching, email, and local infrastructure are modules. Each has a default, and anything you leave out is removed completely: its package, its provider SDK, its environment variables, its documentation, and its lockfile entries. 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.
|
|
89
89
|
|
|
90
90
|
| Flag | Options | Default |
|
|
91
91
|
| ----------- | ------------------------------------------ | ---------- |
|
|
92
92
|
| `--auth` | `supabase`, `better-auth`, `none` | `supabase` |
|
|
93
93
|
| `--storage` | `s3`, `supabase` (needs Supabase Auth), `none` | `s3` |
|
|
94
94
|
| `--cache` | `redis` (falls back to memory without `REDIS_URL`), `memory` | `redis` |
|
|
95
|
+
| `--email` | `smtp` (any provider, or a local inbox), `resend`, `none` | `smtp` |
|
|
95
96
|
| `--infra` | `docker` (Docker Compose), `supabase` (Supabase CLI), `local` (no Docker: PGlite and a local folder), `none` | `docker` |
|
|
96
97
|
|
|
97
98
|
```sh
|
|
@@ -136,7 +137,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
136
137
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
137
138
|
|
|
138
139
|
# Reproduce a specific Starter release or commit
|
|
139
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
140
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.9.0
|
|
140
141
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
141
142
|
|
|
142
143
|
# Test local Starter changes before releasing them
|
|
@@ -153,7 +154,7 @@ pnpm dlx @clubedge/create-clubedge-app add cache redis # or switch: add aut
|
|
|
153
154
|
pnpm dlx @clubedge/create-clubedge-app remove storage
|
|
154
155
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
155
156
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
156
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
157
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.9.0 # or a specific tag, branch, or commit
|
|
157
158
|
```
|
|
158
159
|
|
|
159
160
|
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.
|
|
@@ -204,7 +205,7 @@ CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarb
|
|
|
204
205
|
1. Make and validate changes in `clubedge-starter`.
|
|
205
206
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
206
207
|
3. Update the Starter package versions.
|
|
207
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
208
|
+
4. Commit `main`, create a release tag such as `v0.9.0`, and publish the Starter release.
|
|
208
209
|
|
|
209
210
|
### CLI maintainers
|
|
210
211
|
|
|
@@ -218,7 +219,7 @@ The required CI job always tests the pinned default. The scheduled compatibility
|
|
|
218
219
|
|
|
219
220
|
## Contributing
|
|
220
221
|
|
|
221
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for development and pull request guidance. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
|
|
222
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development and pull request guidance, and [ROADMAP.md](ROADMAP.md) for the planned phases. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
|
|
222
223
|
|
|
223
224
|
## License
|
|
224
225
|
|
package/dist/cli/args.js
CHANGED
|
@@ -1,32 +1,70 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
/** Module flags the CLI accepts. Which options exist is up to the Starter's manifest. */
|
|
3
|
-
export const MODULE_FLAGS = ["auth", "storage", "cache", "infra"];
|
|
4
2
|
export class UsageError extends Error {
|
|
5
3
|
name = "UsageError";
|
|
6
4
|
}
|
|
5
|
+
/** The CLI's own long options; every other --name value flag selects a module option. */
|
|
6
|
+
const OPTIONS = {
|
|
7
|
+
framework: { type: "string" },
|
|
8
|
+
ref: { type: "string" },
|
|
9
|
+
"template-dir": { type: "string" },
|
|
10
|
+
install: { type: "boolean", default: true },
|
|
11
|
+
git: { type: "boolean", default: true },
|
|
12
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
13
|
+
"dry-run": { type: "boolean", default: false },
|
|
14
|
+
force: { type: "boolean", default: false },
|
|
15
|
+
help: { type: "boolean", short: "h", default: false },
|
|
16
|
+
version: { type: "boolean", short: "v", default: false },
|
|
17
|
+
};
|
|
18
|
+
const isOwnOption = (name) => name in OPTIONS || (name.startsWith("no-") && OPTIONS[name.slice(3)]?.type === "boolean");
|
|
19
|
+
/**
|
|
20
|
+
* Takes module flags (`--email smtp` or `--email=smtp`) out of the arguments, leaving the CLI's
|
|
21
|
+
* own options for the strict parser. A flag that is neither needs a value, so a stray unknown
|
|
22
|
+
* flag such as --verbose is still reported.
|
|
23
|
+
*/
|
|
24
|
+
function extractModuleFlags(argv) {
|
|
25
|
+
const rest = [];
|
|
26
|
+
const modules = {};
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const argument = argv[i];
|
|
29
|
+
if (argument === "--") {
|
|
30
|
+
rest.push(...argv.slice(i));
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
const match = /^--([^=]+)(?:=(.*))?$/.exec(argument);
|
|
34
|
+
if (!match || isOwnOption(match[1])) {
|
|
35
|
+
rest.push(argument);
|
|
36
|
+
// A string option's value is never a flag of its own.
|
|
37
|
+
if (match && match[2] === undefined && OPTIONS[match[1]]?.type === "string") {
|
|
38
|
+
if (argv[i + 1] !== undefined)
|
|
39
|
+
rest.push(argv[++i]);
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const [, name, inline] = match;
|
|
44
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name))
|
|
45
|
+
throw new UsageError(`Unknown option --${name}. Run with --help to see available options.`);
|
|
46
|
+
const next = argv[i + 1];
|
|
47
|
+
const value = inline ?? (next !== undefined && !next.startsWith("-") ? argv[++i] : undefined);
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
throw new UsageError(`Unknown option --${name}. Module flags take an option, such as --${name} none. Run with --help to see available options.`);
|
|
50
|
+
}
|
|
51
|
+
if (!value.trim())
|
|
52
|
+
throw new UsageError(`--${name} requires an option, such as none.`);
|
|
53
|
+
modules[name] = value.trim();
|
|
54
|
+
}
|
|
55
|
+
return { rest, modules };
|
|
56
|
+
}
|
|
7
57
|
export function parseCliArgs(argv) {
|
|
58
|
+
const { rest, modules } = extractModuleFlags(argv);
|
|
8
59
|
let parsed;
|
|
9
60
|
try {
|
|
10
61
|
parsed = parseArgs({
|
|
11
|
-
args:
|
|
62
|
+
args: rest,
|
|
12
63
|
allowPositionals: true,
|
|
13
64
|
allowNegative: true,
|
|
14
65
|
strict: true,
|
|
15
66
|
options: {
|
|
16
|
-
|
|
17
|
-
auth: { type: "string" },
|
|
18
|
-
storage: { type: "string" },
|
|
19
|
-
cache: { type: "string" },
|
|
20
|
-
infra: { type: "string" },
|
|
21
|
-
ref: { type: "string" },
|
|
22
|
-
"template-dir": { type: "string" },
|
|
23
|
-
install: { type: "boolean", default: true },
|
|
24
|
-
git: { type: "boolean", default: true },
|
|
25
|
-
yes: { type: "boolean", short: "y", default: false },
|
|
26
|
-
"dry-run": { type: "boolean", default: false },
|
|
27
|
-
force: { type: "boolean", default: false },
|
|
28
|
-
help: { type: "boolean", short: "h", default: false },
|
|
29
|
-
version: { type: "boolean", short: "v", default: false },
|
|
67
|
+
...OPTIONS,
|
|
30
68
|
},
|
|
31
69
|
});
|
|
32
70
|
}
|
|
@@ -39,7 +77,7 @@ export function parseCliArgs(argv) {
|
|
|
39
77
|
if (command.name === "create" && positionals.length > 1) {
|
|
40
78
|
throw new UsageError("Only one project directory can be provided.");
|
|
41
79
|
}
|
|
42
|
-
if (command.name !== "create" && (values.framework !== undefined ||
|
|
80
|
+
if (command.name !== "create" && (values.framework !== undefined || Object.keys(modules).length)) {
|
|
43
81
|
throw new UsageError("--framework and module flags only apply when creating a project; use add or remove to change one module.");
|
|
44
82
|
}
|
|
45
83
|
if ((command.name === "add" || command.name === "remove") && values.ref !== undefined) {
|
|
@@ -48,15 +86,6 @@ export function parseCliArgs(argv) {
|
|
|
48
86
|
if (values.framework !== undefined && !values.framework.trim()) {
|
|
49
87
|
throw new UsageError("--framework requires a framework id, such as next or tanstack-start.");
|
|
50
88
|
}
|
|
51
|
-
const modules = {};
|
|
52
|
-
for (const module of MODULE_FLAGS) {
|
|
53
|
-
const value = values[module];
|
|
54
|
-
if (value === undefined)
|
|
55
|
-
continue;
|
|
56
|
-
if (!value.trim())
|
|
57
|
-
throw new UsageError(`--${module} requires an option, such as none.`);
|
|
58
|
-
modules[module] = value.trim();
|
|
59
|
-
}
|
|
60
89
|
if (values.ref !== undefined && !values.ref.trim()) {
|
|
61
90
|
throw new UsageError("--ref requires a tag, branch, or commit.");
|
|
62
91
|
}
|
package/dist/cli/ui.js
CHANGED
|
@@ -52,7 +52,32 @@ export function showBanner() {
|
|
|
52
52
|
console.log(` ${dim("By Clubedge")} ${dim("·")} ${paint(gradientFrom, siteUrl)}`);
|
|
53
53
|
console.log("");
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
/** "a (default), b, or c" */
|
|
56
|
+
function choices(ids, defaultId) {
|
|
57
|
+
const labels = ids.map((id) => (id === defaultId ? `${id} (default)` : id));
|
|
58
|
+
return labels.length > 1 ? `${labels.slice(0, -1).join(", ")}${labels.length > 2 ? "," : ""} or ${labels.at(-1)}` : labels[0] ?? "";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The framework and module flags of a Starter, from its manifest, so new modules appear in the
|
|
62
|
+
* help without CLI changes. Without a manifest, a generic line explains module flags.
|
|
63
|
+
*/
|
|
64
|
+
export function selectionHelp(manifest) {
|
|
65
|
+
const line = (flag, text) => ` ${flag.padEnd(24)}${text}`;
|
|
66
|
+
if (!manifest) {
|
|
67
|
+
return [
|
|
68
|
+
line("--framework <id>", "App framework, such as next or tanstack-start"),
|
|
69
|
+
line("--<module> <option>", "A module option, such as --auth none; the Starter lists its modules"),
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
const frameworks = Object.keys(manifest.frameworks);
|
|
73
|
+
return [
|
|
74
|
+
...(frameworks.length > 1
|
|
75
|
+
? [line("--framework <id>", `App framework: ${choices(frameworks, manifest.defaultFramework)}`)]
|
|
76
|
+
: []),
|
|
77
|
+
...Object.entries(manifest.modules).map(([id, module]) => line(`--${id} <id>`, `${module.name}: ${choices(Object.keys(module.options), module.default)}`)),
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
export function helpText(manifest = null) {
|
|
56
81
|
return `
|
|
57
82
|
create-clubedge-app ${cliVersion}
|
|
58
83
|
|
|
@@ -74,11 +99,7 @@ Usage:
|
|
|
74
99
|
markers. Commit first so you can review the result with git diff.
|
|
75
100
|
|
|
76
101
|
Options:
|
|
77
|
-
|
|
78
|
-
--auth <id> Authentication: supabase (default), better-auth, or none
|
|
79
|
-
--storage <id> File storage: s3 (default), supabase, or none
|
|
80
|
-
--cache <id> Cache and rate limits: redis (default) or memory
|
|
81
|
-
--infra <id> Local services: docker (default), supabase, local, or none
|
|
102
|
+
${selectionHelp(manifest)}
|
|
82
103
|
--ref <ref> Download this Starter tag, branch, or commit from GitHub instead
|
|
83
104
|
(upgrade: the Starter to upgrade to)
|
|
84
105
|
--template-dir <path> Scaffold from a local Starter checkout (for Starter development)
|
package/dist/core/env.js
CHANGED
|
@@ -76,10 +76,17 @@ export function updateEnvFile(local, baseExample, targetExample) {
|
|
|
76
76
|
while (anchor >= 0 && !keyOf(target[anchor]))
|
|
77
77
|
anchor--;
|
|
78
78
|
const position = anchor >= 0 ? find(lines, keyOf(target[anchor])) : -1;
|
|
79
|
-
if (position === -1)
|
|
79
|
+
if (position === -1) {
|
|
80
80
|
appended.push(...commentsAbove(target, at), line);
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const between = target.slice(anchor + 1, at + 1);
|
|
84
|
+
// A removed section can leave its blank separator after the anchor; reuse it rather than
|
|
85
|
+
// adding a second one.
|
|
86
|
+
// The last element is the end of the file after its final newline, not a blank line.
|
|
87
|
+
const followedByBlank = position + 1 < lines.length - 1 && lines[position + 1].trim() === "";
|
|
88
|
+
const reuseBlank = between[0]?.trim() === "" && followedByBlank;
|
|
89
|
+
lines.splice(position + (reuseBlank ? 2 : 1), 0, ...(reuseBlank ? between.slice(1) : between));
|
|
83
90
|
});
|
|
84
91
|
let text = lines.join(eol);
|
|
85
92
|
// Removed variables can leave blank lines in a row, as removed template blocks do.
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { dim, helpText, showBanner } from "./cli/ui.js";
|
|
|
7
7
|
import { assertTargetIsUsable, executePlan } from "./core/execute.js";
|
|
8
8
|
import { readTemplateManifest } from "./core/manifest.js";
|
|
9
9
|
import { createPlan, describePlan, selectFramework } from "./core/plan.js";
|
|
10
|
-
import { resolveTemplateSource } from "./core/source.js";
|
|
11
|
-
import { cliUrl, cliVersion, siteUrl } from "./package-info.js";
|
|
10
|
+
import { readBundledTemplate, resolveTemplateSource } from "./core/source.js";
|
|
11
|
+
import { cliUrl, cliVersion, siteUrl, starterPin } from "./package-info.js";
|
|
12
12
|
import { changeProject } from "./modify.js";
|
|
13
13
|
import { initializeGit } from "./steps/git.js";
|
|
14
14
|
import { detectLauncher, installDependencies, resolvePnpm } from "./steps/install.js";
|
|
@@ -63,7 +63,9 @@ async function chooseModules(manifest, requested, interactive) {
|
|
|
63
63
|
if (!interactive || unasked.length === 0)
|
|
64
64
|
return requested;
|
|
65
65
|
const customize = await confirm({
|
|
66
|
-
message:
|
|
66
|
+
message: `Customize the included services (${Object.values(manifest.modules)
|
|
67
|
+
.map((module) => module.name.toLowerCase())
|
|
68
|
+
.join(", ")})?`,
|
|
67
69
|
initialValue: false,
|
|
68
70
|
});
|
|
69
71
|
if (isCancel(customize))
|
|
@@ -193,6 +195,19 @@ async function createProject(args) {
|
|
|
193
195
|
later +
|
|
194
196
|
`${dim("Docs")} ${cliUrl}\n${dim("Clubedge")} ${siteUrl}`);
|
|
195
197
|
}
|
|
198
|
+
/** The bundled Starter's manifest, for the help text; null in a checkout without a bundle. */
|
|
199
|
+
async function bundledManifest() {
|
|
200
|
+
try {
|
|
201
|
+
const bundled = await readBundledTemplate();
|
|
202
|
+
// A checkout can hold an older bundle; only the pinned release describes what this CLI creates.
|
|
203
|
+
if (!bundled || bundled.commit !== starterPin.starterCommit)
|
|
204
|
+
return null;
|
|
205
|
+
return await readTemplateManifest(bundled.root);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
196
211
|
export async function main(argv = process.argv.slice(2)) {
|
|
197
212
|
try {
|
|
198
213
|
const args = parseCliArgs(argv);
|
|
@@ -202,7 +217,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
202
217
|
}
|
|
203
218
|
showBanner();
|
|
204
219
|
if (args.help) {
|
|
205
|
-
console.log(helpText());
|
|
220
|
+
console.log(helpText(await bundledManifest()));
|
|
206
221
|
return;
|
|
207
222
|
}
|
|
208
223
|
assertSupportedNode();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Create a Next.js or TanStack Start application from the Clubedge Starter reference implementation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.9.0",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"clubedge": {
|
|
24
24
|
"starterRepository": "Clubedge/clubedge-starter",
|
|
25
|
-
"starterRef": "v0.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.9.0",
|
|
26
|
+
"starterCommit": "e858d0b5b60bbb05f1b6aed3f4d9e841ea2df2a5"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/.env.example
CHANGED
|
@@ -22,6 +22,8 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
|
|
|
22
22
|
# Better Auth keeps users and sessions in DATABASE_URL (run pnpm db:migrate first).
|
|
23
23
|
# Generate a secret with: openssl rand -base64 32
|
|
24
24
|
BETTER_AUTH_SECRET=
|
|
25
|
+
# true refuses sign-in until the email address is confirmed through the emailed link. # clubedge:only email!=none
|
|
26
|
+
BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=false # clubedge:only email!=none
|
|
25
27
|
# clubedge:end
|
|
26
28
|
|
|
27
29
|
# clubedge:if cache=redis
|
|
@@ -31,6 +33,24 @@ REDIS_URL= # clubedge:only infra!=docker
|
|
|
31
33
|
REDIS_URL=redis://localhost:6379 # clubedge:only infra=docker
|
|
32
34
|
# clubedge:end
|
|
33
35
|
|
|
36
|
+
# clubedge:if email=smtp
|
|
37
|
+
# Email over SMTP: any provider's SMTP endpoint (smtps:// for TLS). Without it, development
|
|
38
|
+
# prints emails to the server log and production refuses to send.
|
|
39
|
+
SMTP_URL= # clubedge:only infra!=docker|supabase
|
|
40
|
+
# Mailpit from docker-compose.dev.yml; read the emails at http://localhost:8025. # clubedge:only infra=docker
|
|
41
|
+
SMTP_URL=smtp://localhost:1025 # clubedge:only infra=docker
|
|
42
|
+
# The local Supabase inbox; read the emails at http://127.0.0.1:54324. # clubedge:only infra=supabase
|
|
43
|
+
SMTP_URL=smtp://127.0.0.1:54325 # clubedge:only infra=supabase
|
|
44
|
+
EMAIL_FROM=App <no-reply@example.com>
|
|
45
|
+
# clubedge:end
|
|
46
|
+
|
|
47
|
+
# clubedge:if email=resend
|
|
48
|
+
# Resend (https://resend.com). EMAIL_FROM must use a domain verified in Resend. Without the
|
|
49
|
+
# key, development prints emails to the server log and production refuses to send.
|
|
50
|
+
RESEND_API_KEY=
|
|
51
|
+
EMAIL_FROM=App <no-reply@example.com>
|
|
52
|
+
# clubedge:end
|
|
53
|
+
|
|
34
54
|
# clubedge:if storage=s3
|
|
35
55
|
# S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
|
|
36
56
|
STORAGE_BUCKET= # clubedge:only infra!=docker
|
package/template/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Clubedge Starter is a production-oriented, modular React application foundation.
|
|
|
7
7
|
|
|
8
8
|
<!-- clubedge:if starter-repository -->
|
|
9
9
|
|
|
10
|
-
This repository contains the same application for two frameworks, Next.js (`apps/web`) and TanStack Start (`apps/start`), plus every optional module. [create-clubedge-app](https://github.com/Clubedge/create-clubedge-app) generates a project with one framework and only the modules you choose; sections of this README marked for a framework or module appear only in projects that use it. Contributions and bug reports are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Repository maintainers can use [PUBLISHING.md](PUBLISHING.md) for the GitHub launch checklist.
|
|
10
|
+
This repository contains the same application for two frameworks, Next.js (`apps/web`) and TanStack Start (`apps/start`), plus every optional module. [create-clubedge-app](https://github.com/Clubedge/create-clubedge-app) generates a project with one framework and only the modules you choose; sections of this README marked for a framework or module appear only in projects that use it. Contributions and bug reports are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Repository maintainers can use [PUBLISHING.md](PUBLISHING.md) for the GitHub launch checklist, and [ROADMAP.md](ROADMAP.md) describes the planned phases.
|
|
11
11
|
|
|
12
12
|
<!-- clubedge:end -->
|
|
13
13
|
|
|
@@ -19,10 +19,14 @@ This repository contains the same application for two frameworks, Next.js (`apps
|
|
|
19
19
|
- Drizzle ORM and PostgreSQL schema, migrations, and seed commands.
|
|
20
20
|
- Supabase Auth with server-verified sessions in secure cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=supabase -->
|
|
21
21
|
- Better Auth with users and sessions in your own PostgreSQL, secure session cookies, a protected dashboard, and rate-limited sign-in. <!-- clubedge:only auth=better-auth -->
|
|
22
|
+
- Password reset by email, and email verification that can be required before sign-in. <!-- clubedge:only auth=better-auth && email!=none -->
|
|
23
|
+
- Password reset by email, sent by Supabase. <!-- clubedge:only auth=supabase -->
|
|
22
24
|
- S3-compatible storage adapter for AWS S3, Cloudflare R2, and MinIO. <!-- clubedge:only storage=s3 -->
|
|
23
25
|
- Supabase Storage adapter that uses the signed-in user's session. <!-- clubedge:only storage=supabase -->
|
|
24
26
|
- Fixed-window rate limiter and cache on Redis, falling back to process memory without `REDIS_URL`. <!-- clubedge:only cache=redis -->
|
|
25
27
|
- Fixed-window rate limiter and cache in process memory, suited to a single server instance. <!-- clubedge:only cache=memory -->
|
|
28
|
+
- Email over SMTP, for any provider or a local inbox; development prints emails to the server log until SMTP is configured. <!-- clubedge:only email=smtp -->
|
|
29
|
+
- Email through Resend; development prints emails to the server log until the API key is set. <!-- clubedge:only email=resend -->
|
|
26
30
|
- Zod environment validation, security headers, structured errors, and `/api/health`.
|
|
27
31
|
- Docker support, GitHub Actions CI, Vitest, Playwright, ESLint, and Prettier.
|
|
28
32
|
|
|
@@ -47,6 +51,12 @@ flowchart TB
|
|
|
47
51
|
%% clubedge:if cache=redis
|
|
48
52
|
App -->|cache and rate limits| Redis[(Redis, or memory)]
|
|
49
53
|
%% clubedge:end
|
|
54
|
+
%% clubedge:if email=smtp
|
|
55
|
+
App -->|email interface| SMTP[SMTP server, or the log]
|
|
56
|
+
%% clubedge:end
|
|
57
|
+
%% clubedge:if email=resend
|
|
58
|
+
App -->|email interface| Resend[Resend, or the log]
|
|
59
|
+
%% clubedge:end
|
|
50
60
|
%% clubedge:if storage=s3
|
|
51
61
|
App -->|storage interface| S3[S3-compatible storage: AWS S3, R2, MinIO]
|
|
52
62
|
%% clubedge:end
|
|
@@ -201,6 +211,9 @@ The TanStack Start app is `apps/start`, run with `pnpm --filter @clubedge/start
|
|
|
201
211
|
- `packages/storage-supabase/`: Supabase Storage adapter <!-- clubedge:only storage=supabase -->
|
|
202
212
|
- `packages/cache/`: `Cache` and `RateLimiter` interfaces with in-memory implementations
|
|
203
213
|
- `packages/cache-redis/`: Redis rate limiter and cache <!-- clubedge:only cache=redis -->
|
|
214
|
+
- `packages/email/`: `EmailProvider` interface with console and in-memory providers <!-- clubedge:only email!=none -->
|
|
215
|
+
- `packages/email-smtp/`: SMTP email provider <!-- clubedge:only email=smtp -->
|
|
216
|
+
- `packages/email-resend/`: Resend email provider <!-- clubedge:only email=resend -->
|
|
204
217
|
- `packages/ui/`: shared shadcn/ui components, utilities, and global theme styles
|
|
205
218
|
- `.github/`: CI workflow, issue forms, and pull request template
|
|
206
219
|
- `SETUP.md`, `CONTRIBUTING.md`, `SECURITY.md`: setup, contribution, and security guides
|
package/template/SETUP.md
CHANGED
|
@@ -43,6 +43,8 @@ The variables are:
|
|
|
43
43
|
- `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` (for sign-in): see [Supabase Auth](#supabase-auth). <!-- clubedge:only framework=tanstack-start && auth=supabase -->
|
|
44
44
|
- `BETTER_AUTH_SECRET` (for sign-in): see [Better Auth](#better-auth). <!-- clubedge:only auth=better-auth -->
|
|
45
45
|
- `REDIS_URL` (optional): see [Redis](#redis). <!-- clubedge:only cache=redis -->
|
|
46
|
+
- `SMTP_URL` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=smtp -->
|
|
47
|
+
- `RESEND_API_KEY` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=resend -->
|
|
46
48
|
- `STORAGE_*` (for file storage): see [File storage](#file-storage). <!-- clubedge:only storage=s3 -->
|
|
47
49
|
- `SUPABASE_STORAGE_BUCKET` (for file storage): see [File storage](#file-storage). <!-- clubedge:only storage=supabase -->
|
|
48
50
|
|
|
@@ -62,13 +64,13 @@ The app is available at <http://localhost:3000> and the liveness endpoint at <ht
|
|
|
62
64
|
|
|
63
65
|
Create or select a Supabase project and copy its URL and publishable key from the project's API settings into the Supabase variables above. Authentication runs on the server, which validates every user with Supabase; never use a service role key.
|
|
64
66
|
|
|
65
|
-
In Supabase Auth URL configuration, add this redirect URL for local email confirmation:
|
|
67
|
+
In Supabase Auth URL configuration, add this redirect URL for local email confirmation and password reset links:
|
|
66
68
|
|
|
67
69
|
```text
|
|
68
|
-
http://localhost:3000/auth/callback
|
|
70
|
+
http://localhost:3000/auth/callback**
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
For deployed environments, add your deployed origin followed by `/auth/callback`. Until the Supabase variables are set, the dashboard stays viewable as a demo and the login page explains what is missing; once they are set, the dashboard redirects signed-out visitors to the login page.
|
|
73
|
+
For deployed environments, add your deployed origin followed by `/auth/callback**`. The `**` lets links carry the page to open next, such as `/reset-password`. Supabase sends the confirmation and password reset emails itself; configure its SMTP settings in the Supabase project to send them from your own domain. Until the Supabase variables are set, the dashboard stays viewable as a demo and the login page explains what is missing; once they are set, the dashboard redirects signed-out visitors to the login page.
|
|
72
74
|
|
|
73
75
|
<!-- clubedge:end -->
|
|
74
76
|
|
|
@@ -82,7 +84,13 @@ Better Auth keeps users, passwords, and sessions in your PostgreSQL database, in
|
|
|
82
84
|
openssl rand -base64 32
|
|
83
85
|
```
|
|
84
86
|
|
|
85
|
-
Put the result in `BETTER_AUTH_SECRET`. Until it is set, the dashboard stays viewable as a demo and the login page explains what is missing; once it is set, the dashboard redirects signed-out visitors to the login page.
|
|
87
|
+
Put the result in `BETTER_AUTH_SECRET`. Until it is set, the dashboard stays viewable as a demo and the login page explains what is missing; once it is set, the dashboard redirects signed-out visitors to the login page. Signing in again on another device creates another session row, and signing out deletes it.
|
|
88
|
+
|
|
89
|
+
Sign-up signs the user in immediately; add email verification in `packages/auth-better-auth` if your application needs it. <!-- clubedge:only email=none -->
|
|
90
|
+
Better Auth sends its emails through the [email module](#email): <!-- clubedge:only email!=none -->
|
|
91
|
+
|
|
92
|
+
- **Password reset:** "Forgot password?" on the login page emails a link to `/reset-password`, valid for one hour and usable once. <!-- clubedge:only email!=none -->
|
|
93
|
+
- **Email verification:** every new account gets a confirmation link. Sign-up signs the user in right away unless `BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=true`, which refuses sign-in until the link is opened. <!-- clubedge:only email!=none -->
|
|
86
94
|
|
|
87
95
|
<!-- clubedge:end -->
|
|
88
96
|
|
|
@@ -101,6 +109,7 @@ pnpm check:services
|
|
|
101
109
|
- PostgreSQL 17 on port 5432, database `app`, user and password `postgres`. Data survives restarts in a Docker volume.
|
|
102
110
|
- Redis on port 6379. <!-- clubedge:only cache=redis -->
|
|
103
111
|
- S3-compatible storage (adobe/s3mock) on port 9090 with the bucket `app`. It accepts any credentials and is for development only. <!-- clubedge:only storage=s3 -->
|
|
112
|
+
- Mailpit, which catches every email the app sends: SMTP on port 1025, and the inbox at http://localhost:8025. <!-- clubedge:only email=smtp -->
|
|
104
113
|
|
|
105
114
|
`pnpm infra:down` stops the services, `pnpm infra:reset` also deletes their data, and `pnpm infra:logs` follows their logs. The file is for development; in production, point the same variables at managed services.
|
|
106
115
|
|
|
@@ -118,6 +127,8 @@ pnpm check:services
|
|
|
118
127
|
|
|
119
128
|
The database is on port 54322 and the API on port 54321, matching the example environment. Copy the publishable key from `pnpm infra:status` into the Supabase variables. Email confirmation is off locally, so sign-up signs the user in directly. `pnpm infra:down` stops the stack and `pnpm infra:reset` also deletes its data.
|
|
120
129
|
|
|
130
|
+
The app's own email goes to the same inbox over SMTP, on port 54325. <!-- clubedge:only email=smtp -->
|
|
131
|
+
|
|
121
132
|
<!-- clubedge:end -->
|
|
122
133
|
<!-- clubedge:if infra=local -->
|
|
123
134
|
|
|
@@ -126,6 +137,7 @@ This project runs its development services without Docker or any other installat
|
|
|
126
137
|
- **PostgreSQL:** `pnpm dev` also starts [PGlite](https://pglite.dev), PostgreSQL compiled to WebAssembly, and serves it on `127.0.0.1:5433`. Each start applies the migrations. Data is kept in `.data/pglite`. `pnpm infra:up` runs the database on its own, for `pnpm check:services` or browser tests.
|
|
127
138
|
- **Files:** while `STORAGE_DIRECTORY` is set, uploads are stored in that folder (`.data/storage`) instead of S3. Set the S3 variables and remove `STORAGE_DIRECTORY` to use object storage. <!-- clubedge:only storage=s3 -->
|
|
128
139
|
- **Cache:** without `REDIS_URL`, the cache and rate limits use process memory. <!-- clubedge:only cache=redis -->
|
|
140
|
+
- **Email:** without a provider configured, emails are printed to the `pnpm dev` output, links included. <!-- clubedge:only email!=none -->
|
|
129
141
|
|
|
130
142
|
PGlite runs a single database session, so connections take turns. That is plenty for development but not for production: deploy against a managed PostgreSQL by setting `DATABASE_URL`. Supabase Auth and Supabase Storage need a Supabase project, local or hosted. Better Auth works fully offline. `pnpm infra:reset` deletes everything in `.data/`.
|
|
131
143
|
|
|
@@ -174,6 +186,28 @@ Without `REDIS_URL`, the rate limiter and cache use process memory. Memory limit
|
|
|
174
186
|
|
|
175
187
|
The rate limiter and cache keep their state in process memory, which suits a single server instance. Limits and cached values are not shared between instances and reset on restart.
|
|
176
188
|
|
|
189
|
+
<!-- clubedge:end -->
|
|
190
|
+
<!-- clubedge:if email!=none -->
|
|
191
|
+
|
|
192
|
+
## Email
|
|
193
|
+
|
|
194
|
+
The app sends email through the `EmailProvider` in `apps/web/src/server/email.ts`. Server code imports `email` from `@/server/email` and calls `email.send({ to, subject, text, html })`.
|
|
195
|
+
|
|
196
|
+
<!-- clubedge:end -->
|
|
197
|
+
<!-- clubedge:if email=smtp -->
|
|
198
|
+
|
|
199
|
+
Set `SMTP_URL` to any SMTP server, for example `smtps://user:password@smtp.example.com:465`, and `EMAIL_FROM` to the sender, as `Name <address>` or an address. Most email providers (Resend, Postmark, Amazon SES, SendGrid, Mailgun) offer an SMTP endpoint.
|
|
200
|
+
|
|
201
|
+
<!-- clubedge:end -->
|
|
202
|
+
<!-- clubedge:if email=resend -->
|
|
203
|
+
|
|
204
|
+
Set `RESEND_API_KEY` to a [Resend](https://resend.com) API key and `EMAIL_FROM` to a sender on a domain you verified in Resend, as `Name <address>` or an address. A key limited to sending is enough.
|
|
205
|
+
|
|
206
|
+
<!-- clubedge:end -->
|
|
207
|
+
<!-- clubedge:if email!=none -->
|
|
208
|
+
|
|
209
|
+
Without a provider configured, development prints each email to the server log, so you can open links from the terminal, and production refuses to send. `pnpm check:services` confirms that the provider accepts the settings without sending anything.
|
|
210
|
+
|
|
177
211
|
<!-- clubedge:end -->
|
|
178
212
|
<!-- clubedge:if storage!=none -->
|
|
179
213
|
|
|
@@ -24,6 +24,8 @@ SUPABASE_PUBLISHABLE_KEY=
|
|
|
24
24
|
# Better Auth keeps users and sessions in DATABASE_URL (run pnpm db:migrate first).
|
|
25
25
|
# Generate a secret with: openssl rand -base64 32
|
|
26
26
|
BETTER_AUTH_SECRET=
|
|
27
|
+
# true refuses sign-in until the email address is confirmed through the emailed link. # clubedge:only email!=none
|
|
28
|
+
BETTER_AUTH_REQUIRE_EMAIL_VERIFICATION=false # clubedge:only email!=none
|
|
27
29
|
# clubedge:end
|
|
28
30
|
|
|
29
31
|
# clubedge:if cache=redis
|
|
@@ -33,6 +35,24 @@ REDIS_URL= # clubedge:only infra!=docker
|
|
|
33
35
|
REDIS_URL=redis://localhost:6379 # clubedge:only infra=docker
|
|
34
36
|
# clubedge:end
|
|
35
37
|
|
|
38
|
+
# clubedge:if email=smtp
|
|
39
|
+
# Email over SMTP: any provider's SMTP endpoint (smtps:// for TLS). Without it, development
|
|
40
|
+
# prints emails to the server log and production refuses to send.
|
|
41
|
+
SMTP_URL= # clubedge:only infra!=docker|supabase
|
|
42
|
+
# Mailpit from docker-compose.dev.yml; read the emails at http://localhost:8025. # clubedge:only infra=docker
|
|
43
|
+
SMTP_URL=smtp://localhost:1025 # clubedge:only infra=docker
|
|
44
|
+
# The local Supabase inbox; read the emails at http://127.0.0.1:54324. # clubedge:only infra=supabase
|
|
45
|
+
SMTP_URL=smtp://127.0.0.1:54325 # clubedge:only infra=supabase
|
|
46
|
+
EMAIL_FROM=App <no-reply@example.com>
|
|
47
|
+
# clubedge:end
|
|
48
|
+
|
|
49
|
+
# clubedge:if email=resend
|
|
50
|
+
# Resend (https://resend.com). EMAIL_FROM must use a domain verified in Resend. Without the
|
|
51
|
+
# key, development prints emails to the server log and production refuses to send.
|
|
52
|
+
RESEND_API_KEY=
|
|
53
|
+
EMAIL_FROM=App <no-reply@example.com>
|
|
54
|
+
# clubedge:end
|
|
55
|
+
|
|
36
56
|
# clubedge:if storage=s3
|
|
37
57
|
# S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
|
|
38
58
|
STORAGE_BUCKET= # clubedge:only infra!=docker
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"@clubedge/cache-redis": "workspace:*",
|
|
20
20
|
"@clubedge/core": "workspace:*",
|
|
21
21
|
"@clubedge/db": "workspace:*",
|
|
22
|
+
"@clubedge/email": "workspace:*",
|
|
23
|
+
"@clubedge/email-resend": "workspace:*",
|
|
24
|
+
"@clubedge/email-smtp": "workspace:*",
|
|
22
25
|
"@clubedge/storage": "workspace:*",
|
|
23
26
|
"@clubedge/storage-s3": "workspace:*",
|
|
24
27
|
"@clubedge/storage-supabase": "workspace:*",
|
|
@@ -8,6 +8,12 @@ import { createRedisCache } from "@clubedge/cache-redis";
|
|
|
8
8
|
// clubedge:if storage=s3
|
|
9
9
|
import { createS3Storage } from "@clubedge/storage-s3";
|
|
10
10
|
// clubedge:end
|
|
11
|
+
// clubedge:if email=smtp
|
|
12
|
+
import { createSmtpEmail } from "@clubedge/email-smtp";
|
|
13
|
+
// clubedge:end
|
|
14
|
+
// clubedge:if email=resend
|
|
15
|
+
import { createResendEmail } from "@clubedge/email-resend";
|
|
16
|
+
// clubedge:end
|
|
11
17
|
// clubedge:if storage=s3 && infra=local
|
|
12
18
|
import { createFileSystemStorage } from "@clubedge/storage/file-system";
|
|
13
19
|
// clubedge:end
|
|
@@ -62,6 +68,38 @@ async function main() {
|
|
|
62
68
|
},
|
|
63
69
|
},
|
|
64
70
|
// clubedge:end
|
|
71
|
+
// clubedge:if email=smtp
|
|
72
|
+
{
|
|
73
|
+
name: "Email (SMTP)",
|
|
74
|
+
skip: env.SMTP_URL ? undefined : "SMTP_URL is not set; development prints emails to the log",
|
|
75
|
+
async run() {
|
|
76
|
+
// Connects and authenticates without sending anything.
|
|
77
|
+
const email = createSmtpEmail({
|
|
78
|
+
url: env.SMTP_URL!,
|
|
79
|
+
from: env.EMAIL_FROM || "no-reply@example.com",
|
|
80
|
+
});
|
|
81
|
+
await email.verify();
|
|
82
|
+
return `connected to ${new URL(env.SMTP_URL!).host}`;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
// clubedge:end
|
|
86
|
+
// clubedge:if email=resend
|
|
87
|
+
{
|
|
88
|
+
name: "Email (Resend)",
|
|
89
|
+
skip: env.RESEND_API_KEY
|
|
90
|
+
? undefined
|
|
91
|
+
: "RESEND_API_KEY is not set; development prints emails to the log",
|
|
92
|
+
async run() {
|
|
93
|
+
// Checks the API key without sending anything.
|
|
94
|
+
const email = createResendEmail({
|
|
95
|
+
apiKey: env.RESEND_API_KEY!,
|
|
96
|
+
from: env.EMAIL_FROM || "no-reply@example.com",
|
|
97
|
+
});
|
|
98
|
+
await email.verify();
|
|
99
|
+
return "API key accepted";
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// clubedge:end
|
|
65
103
|
// clubedge:if storage=s3 && infra=local
|
|
66
104
|
{
|
|
67
105
|
name: "Local folder storage",
|