@alfredmouelle/create-stack 0.1.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/README.md +56 -0
- package/_stack/apps/next-base/.dockerignore +10 -0
- package/_stack/apps/next-base/Dockerfile +34 -0
- package/_stack/apps/next-base/README.md +32 -0
- package/_stack/apps/next-base/components.json +25 -0
- package/_stack/apps/next-base/drizzle.config.ts +16 -0
- package/_stack/apps/next-base/next.config.ts +8 -0
- package/_stack/apps/next-base/package.json +70 -0
- package/_stack/apps/next-base/postcss.config.mjs +7 -0
- package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
- package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
- package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
- package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
- package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
- package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
- package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
- package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
- package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
- package/_stack/apps/next-base/src/app/globals.css +171 -0
- package/_stack/apps/next-base/src/app/layout.tsx +23 -0
- package/_stack/apps/next-base/src/app/page.tsx +15 -0
- package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
- package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
- package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
- package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
- package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
- package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
- package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/next-base/src/env.ts +41 -0
- package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
- package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/next-base/src/lib/date.ts +4 -0
- package/_stack/apps/next-base/src/lib/utils.ts +6 -0
- package/_stack/apps/next-base/src/server/api/root.ts +10 -0
- package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
- package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
- package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
- package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
- package/_stack/apps/next-base/src/server/db/index.ts +6 -0
- package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/next-base/src/server/email/index.ts +36 -0
- package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
- package/_stack/apps/next-base/src/trpc/server.ts +23 -0
- package/_stack/apps/next-base/tsconfig.json +37 -0
- package/_stack/apps/tanstack-base/.dockerignore +13 -0
- package/_stack/apps/tanstack-base/Dockerfile +28 -0
- package/_stack/apps/tanstack-base/README.md +31 -0
- package/_stack/apps/tanstack-base/components.json +25 -0
- package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
- package/_stack/apps/tanstack-base/package.json +85 -0
- package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
- package/_stack/apps/tanstack-base/public/logo192.png +0 -0
- package/_stack/apps/tanstack-base/public/logo512.png +0 -0
- package/_stack/apps/tanstack-base/public/manifest.json +25 -0
- package/_stack/apps/tanstack-base/public/robots.txt +3 -0
- package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
- package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
- package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
- package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
- package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
- package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/tanstack-base/src/env.ts +41 -0
- package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
- package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
- package/_stack/apps/tanstack-base/src/router.tsx +40 -0
- package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
- package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
- package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
- package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
- package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
- package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
- package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
- package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
- package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
- package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
- package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
- package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
- package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
- package/_stack/apps/tanstack-base/src/styles.css +171 -0
- package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
- package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
- package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
- package/_stack/apps/tanstack-base/tsconfig.json +27 -0
- package/_stack/apps/tanstack-base/tsr.config.json +3 -0
- package/_stack/apps/tanstack-base/vite.config.ts +15 -0
- package/_stack/packages/analytics/capability.json +26 -0
- package/_stack/packages/cache/capability.json +21 -0
- package/_stack/packages/error-tracking/capability.json +21 -0
- package/_stack/packages/jobs/capability.json +26 -0
- package/_stack/packages/logger/capability.json +21 -0
- package/_stack/packages/mailer/capability.json +28 -0
- package/_stack/packages/mailer/package.json +37 -0
- package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
- package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
- package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
- package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
- package/_stack/packages/storage/capability.json +32 -0
- package/_stack/patterns/README.md +58 -0
- package/_stack/patterns/_baseline/README-author.md +10 -0
- package/_stack/patterns/_baseline/biome.jsonc +119 -0
- package/_stack/patterns/_baseline/env.ts +31 -0
- package/_stack/patterns/_baseline/tsconfig.json +27 -0
- package/_stack/patterns/better-auth/pattern.json +73 -0
- package/_stack/patterns/better-auth-next/pattern.json +76 -0
- package/_stack/patterns/data-table/pattern.json +43 -0
- package/_stack/patterns/drizzle/pattern.json +61 -0
- package/_stack/patterns/trpc/pattern.json +61 -0
- package/_stack/patterns/trpc-next/pattern.json +64 -0
- package/index.mjs +216 -0
- package/lib/build.mjs +64 -0
- package/lib/env.mjs +56 -0
- package/lib/identity.mjs +33 -0
- package/lib/mailer.mjs +95 -0
- package/lib/manifests.mjs +61 -0
- package/lib/scaffold.mjs +49 -0
- package/lib/strip.mjs +132 -0
- package/lib/util.mjs +82 -0
- package/package.json +51 -0
- package/templates/next/layout.no-trpc.tsx +22 -0
- package/templates/tanstack/__root.no-trpc.tsx +63 -0
- package/templates/tanstack/router.no-trpc.tsx +24 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReactElement } from 'react'
|
|
2
|
+
|
|
3
|
+
/** A structured email address. */
|
|
4
|
+
export interface MailAddress {
|
|
5
|
+
email: string
|
|
6
|
+
name?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A recipient, either as a plain string (`"hi@acme.com"` or
|
|
11
|
+
* `"Acme <hi@acme.com>"`) or a structured {@link MailAddress}.
|
|
12
|
+
*/
|
|
13
|
+
export type MailRecipient = string | MailAddress
|
|
14
|
+
|
|
15
|
+
export interface MailAttachment {
|
|
16
|
+
filename: string
|
|
17
|
+
/** Raw bytes or a base64 string. */
|
|
18
|
+
content: Uint8Array | string
|
|
19
|
+
contentType?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A message as authored by application code. We never send raw HTML strings:
|
|
24
|
+
* the body is always a React Email component, rendered to HTML + plain text by
|
|
25
|
+
* the mailer before it reaches the adapter.
|
|
26
|
+
*/
|
|
27
|
+
export interface MailMessage {
|
|
28
|
+
to: MailRecipient | MailRecipient[]
|
|
29
|
+
subject: string
|
|
30
|
+
/** A React Email component. Rendered to HTML and plain text automatically. */
|
|
31
|
+
react: ReactElement
|
|
32
|
+
/** Overrides the default `from` configured on the mailer. */
|
|
33
|
+
from?: MailRecipient
|
|
34
|
+
replyTo?: MailRecipient
|
|
35
|
+
cc?: MailRecipient | MailRecipient[]
|
|
36
|
+
bcc?: MailRecipient | MailRecipient[]
|
|
37
|
+
headers?: Record<string, string>
|
|
38
|
+
/** Provider-agnostic key/value tags for analytics & filtering. */
|
|
39
|
+
tags?: Record<string, string>
|
|
40
|
+
attachments?: MailAttachment[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SentMail {
|
|
44
|
+
/** Provider message id. */
|
|
45
|
+
id: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The port the application depends on. Swapping providers means swapping the
|
|
50
|
+
* adapter passed to {@link createMailer}; this interface never changes.
|
|
51
|
+
*/
|
|
52
|
+
export interface Mailer {
|
|
53
|
+
send(message: MailMessage): Promise<SentMail>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The message shape an adapter receives: addresses are normalized and the body
|
|
58
|
+
* is already rendered to `html` + `text`. Adapters do no rendering.
|
|
59
|
+
*/
|
|
60
|
+
export interface RenderedMessage {
|
|
61
|
+
to: MailAddress[]
|
|
62
|
+
from: MailAddress
|
|
63
|
+
subject: string
|
|
64
|
+
html: string
|
|
65
|
+
text: string
|
|
66
|
+
replyTo?: MailAddress
|
|
67
|
+
cc?: MailAddress[]
|
|
68
|
+
bcc?: MailAddress[]
|
|
69
|
+
headers?: Record<string, string>
|
|
70
|
+
tags?: Record<string, string>
|
|
71
|
+
attachments?: MailAttachment[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The contract each provider implements. Kept intentionally tiny. */
|
|
75
|
+
export interface MailerAdapter {
|
|
76
|
+
readonly name: string
|
|
77
|
+
send(message: RenderedMessage): Promise<SentMail>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Normalized error thrown by adapters so callers never catch provider types. */
|
|
81
|
+
export class MailerError extends Error {
|
|
82
|
+
readonly adapter: string
|
|
83
|
+
|
|
84
|
+
constructor(message: string, options: { adapter: string; cause?: unknown }) {
|
|
85
|
+
super(message, { cause: options.cause })
|
|
86
|
+
this.name = 'MailerError'
|
|
87
|
+
this.adapter = options.adapter
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement } from 'react'
|
|
2
|
+
import { render } from 'react-email'
|
|
3
|
+
|
|
4
|
+
export interface RenderedBody {
|
|
5
|
+
html: string
|
|
6
|
+
text: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A function that turns a React Email component into HTML + plain text. */
|
|
10
|
+
export type EmailRenderer = (react: ReactElement) => Promise<RenderedBody>
|
|
11
|
+
|
|
12
|
+
/** Default renderer backed by `@react-email/render`. */
|
|
13
|
+
export const renderEmail: EmailRenderer = async (react) => {
|
|
14
|
+
const [html, text] = await Promise.all([render(react), render(react, { plainText: true })])
|
|
15
|
+
return { html, text }
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { normalizeAddress, normalizeRecipients } from './core/address'
|
|
2
|
+
import type { Mailer, MailerAdapter, MailRecipient, RenderedMessage } from './core/port'
|
|
3
|
+
import { type EmailRenderer, renderEmail } from './core/render'
|
|
4
|
+
|
|
5
|
+
export interface CreateMailerOptions {
|
|
6
|
+
/** The provider implementation (Resend, Brevo, …). */
|
|
7
|
+
adapter: MailerAdapter
|
|
8
|
+
/** Default sender used when a message omits `from`. */
|
|
9
|
+
from: MailRecipient
|
|
10
|
+
/** Override the React Email renderer (mostly for tests). */
|
|
11
|
+
render?: EmailRenderer
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a {@link Mailer}. This is the composition root: pick the adapter here,
|
|
16
|
+
* the rest of the app depends only on the `Mailer` port.
|
|
17
|
+
*
|
|
18
|
+
* The mailer renders the React body to HTML + plain text, normalizes every
|
|
19
|
+
* address, applies the default sender, then hands a {@link RenderedMessage} to
|
|
20
|
+
* the adapter.
|
|
21
|
+
*/
|
|
22
|
+
export function createMailer(options: CreateMailerOptions): Mailer {
|
|
23
|
+
const defaultFrom = normalizeAddress(options.from)
|
|
24
|
+
const render = options.render ?? renderEmail
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
async send(message) {
|
|
28
|
+
const { html, text } = await render(message.react)
|
|
29
|
+
|
|
30
|
+
const rendered: RenderedMessage = {
|
|
31
|
+
to: normalizeRecipients(message.to),
|
|
32
|
+
from: message.from ? normalizeAddress(message.from) : defaultFrom,
|
|
33
|
+
subject: message.subject,
|
|
34
|
+
html,
|
|
35
|
+
text,
|
|
36
|
+
replyTo: message.replyTo ? normalizeAddress(message.replyTo) : undefined,
|
|
37
|
+
cc: message.cc ? normalizeRecipients(message.cc) : undefined,
|
|
38
|
+
bcc: message.bcc ? normalizeRecipients(message.bcc) : undefined,
|
|
39
|
+
headers: message.headers,
|
|
40
|
+
tags: message.tags,
|
|
41
|
+
attachments: message.attachments,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return options.adapter.send(rendered)
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactElement } from 'react'
|
|
2
|
+
import { env } from '~/env'
|
|
3
|
+
import { resendAdapter } from './adapters/resend/index'
|
|
4
|
+
import type { MailAddress, Mailer } from './core/port'
|
|
5
|
+
import { createMailer } from './factory'
|
|
6
|
+
|
|
7
|
+
export type EmailRecipient = MailAddress
|
|
8
|
+
|
|
9
|
+
function required(value: string | undefined, name: string): string {
|
|
10
|
+
if (!value) throw new Error(`${name} is required to send email`)
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Lazy so the app boots without RESEND_API_KEY; the adapter is built on first send.
|
|
15
|
+
let mailer: Mailer | null = null
|
|
16
|
+
function getMailer(): Mailer {
|
|
17
|
+
if (!mailer) {
|
|
18
|
+
mailer = createMailer({
|
|
19
|
+
from: env.EMAIL_FROM,
|
|
20
|
+
adapter: resendAdapter({ apiKey: required(env.RESEND_API_KEY, 'RESEND_API_KEY') }),
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
return mailer
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function sendEmail(params: {
|
|
27
|
+
to: EmailRecipient
|
|
28
|
+
subject: string
|
|
29
|
+
template: ReactElement
|
|
30
|
+
}) {
|
|
31
|
+
return getMailer().send({
|
|
32
|
+
to: params.to,
|
|
33
|
+
subject: params.subject,
|
|
34
|
+
react: params.template,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "@fontsource-variable/geist";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--background: oklch(1 0 0);
|
|
9
|
+
--foreground: oklch(0.145 0 0);
|
|
10
|
+
--card: oklch(1 0 0);
|
|
11
|
+
--card-foreground: oklch(0.145 0 0);
|
|
12
|
+
--popover: oklch(1 0 0);
|
|
13
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
14
|
+
--primary: oklch(0.511 0.096 186.391);
|
|
15
|
+
--primary-foreground: oklch(0.984 0.014 180.72);
|
|
16
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
17
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
18
|
+
--muted: oklch(0.97 0 0);
|
|
19
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
20
|
+
--accent: oklch(0.97 0 0);
|
|
21
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
22
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
23
|
+
--destructive-foreground: oklch(0.577 0.245 27.325);
|
|
24
|
+
--border: oklch(0.922 0 0);
|
|
25
|
+
--input: oklch(0.922 0 0);
|
|
26
|
+
--ring: oklch(0.708 0 0);
|
|
27
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
28
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
29
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
30
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
31
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
32
|
+
--radius: 0.45rem;
|
|
33
|
+
--sidebar: oklch(0.985 0 0);
|
|
34
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
35
|
+
--sidebar-primary: oklch(0.6 0.118 184.704);
|
|
36
|
+
--sidebar-primary-foreground: oklch(0.984 0.014 180.72);
|
|
37
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
38
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
39
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
40
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.dark {
|
|
44
|
+
--background: oklch(0.145 0 0);
|
|
45
|
+
--foreground: oklch(0.985 0 0);
|
|
46
|
+
--card: oklch(0.205 0 0);
|
|
47
|
+
--card-foreground: oklch(0.985 0 0);
|
|
48
|
+
--popover: oklch(0.205 0 0);
|
|
49
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
50
|
+
--primary: oklch(0.437 0.078 188.216);
|
|
51
|
+
--primary-foreground: oklch(0.984 0.014 180.72);
|
|
52
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
53
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
54
|
+
--muted: oklch(0.269 0 0);
|
|
55
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
56
|
+
--accent: oklch(0.269 0 0);
|
|
57
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
58
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
59
|
+
--destructive-foreground: oklch(0.637 0.237 25.331);
|
|
60
|
+
--border: oklch(1 0 0 / 10%);
|
|
61
|
+
--input: oklch(1 0 0 / 15%);
|
|
62
|
+
--ring: oklch(0.556 0 0);
|
|
63
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
64
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
65
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
66
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
67
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
68
|
+
--sidebar: oklch(0.205 0 0);
|
|
69
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
70
|
+
--sidebar-primary: oklch(0.704 0.14 182.503);
|
|
71
|
+
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
|
72
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
73
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
74
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
75
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@theme inline {
|
|
79
|
+
--font-sans: "Geist Variable", sans-serif;
|
|
80
|
+
--font-heading: var(--font-sans);
|
|
81
|
+
--color-background: var(--background);
|
|
82
|
+
--color-foreground: var(--foreground);
|
|
83
|
+
--color-card: var(--card);
|
|
84
|
+
--color-card-foreground: var(--card-foreground);
|
|
85
|
+
--color-popover: var(--popover);
|
|
86
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
87
|
+
--color-primary: var(--primary);
|
|
88
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
89
|
+
--color-secondary: var(--secondary);
|
|
90
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
91
|
+
--color-muted: var(--muted);
|
|
92
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
93
|
+
--color-accent: var(--accent);
|
|
94
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
95
|
+
--color-destructive: var(--destructive);
|
|
96
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
97
|
+
--color-border: var(--border);
|
|
98
|
+
--color-input: var(--input);
|
|
99
|
+
--color-ring: var(--ring);
|
|
100
|
+
--color-chart-1: var(--chart-1);
|
|
101
|
+
--color-chart-2: var(--chart-2);
|
|
102
|
+
--color-chart-3: var(--chart-3);
|
|
103
|
+
--color-chart-4: var(--chart-4);
|
|
104
|
+
--color-chart-5: var(--chart-5);
|
|
105
|
+
--color-sidebar: var(--sidebar);
|
|
106
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
107
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
108
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
109
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
110
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
111
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
112
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
113
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
114
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
115
|
+
--radius-lg: var(--radius);
|
|
116
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
117
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
118
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
119
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@layer base {
|
|
123
|
+
* {
|
|
124
|
+
@apply border-border outline-ring/50;
|
|
125
|
+
}
|
|
126
|
+
body {
|
|
127
|
+
@apply bg-background text-foreground;
|
|
128
|
+
}
|
|
129
|
+
html {
|
|
130
|
+
@apply font-sans;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@layer utilities {
|
|
135
|
+
.scrollbar {
|
|
136
|
+
scrollbar-width: thin;
|
|
137
|
+
scrollbar-color: var(--border) transparent;
|
|
138
|
+
}
|
|
139
|
+
.scrollbar::-webkit-scrollbar {
|
|
140
|
+
width: 8px;
|
|
141
|
+
height: 8px;
|
|
142
|
+
}
|
|
143
|
+
.scrollbar::-webkit-scrollbar-track {
|
|
144
|
+
background: transparent;
|
|
145
|
+
}
|
|
146
|
+
.scrollbar::-webkit-scrollbar-thumb {
|
|
147
|
+
background-color: var(--border);
|
|
148
|
+
border-radius: var(--radius-3xl);
|
|
149
|
+
}
|
|
150
|
+
.scrollbar:hover::-webkit-scrollbar-thumb {
|
|
151
|
+
background-color: var(--muted-foreground);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.scrollbar-padded-x::-webkit-scrollbar {
|
|
155
|
+
width: 16px;
|
|
156
|
+
}
|
|
157
|
+
.scrollbar-padded-x::-webkit-scrollbar-thumb {
|
|
158
|
+
border-left: 4px solid transparent;
|
|
159
|
+
border-right: 4px solid transparent;
|
|
160
|
+
background-clip: padding-box;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.scrollbar-padded-y::-webkit-scrollbar {
|
|
164
|
+
height: 16px;
|
|
165
|
+
}
|
|
166
|
+
.scrollbar-padded-y::-webkit-scrollbar-thumb {
|
|
167
|
+
border-top: 4px solid transparent;
|
|
168
|
+
border-bottom: 4px solid transparent;
|
|
169
|
+
background-clip: padding-box;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defaultShouldDehydrateQuery, QueryClient } from '@tanstack/react-query'
|
|
2
|
+
import superjson from 'superjson'
|
|
3
|
+
|
|
4
|
+
export const createQueryClient = () =>
|
|
5
|
+
new QueryClient({
|
|
6
|
+
defaultOptions: {
|
|
7
|
+
queries: {
|
|
8
|
+
staleTime: 30 * 1000,
|
|
9
|
+
},
|
|
10
|
+
dehydrate: {
|
|
11
|
+
serializeData: superjson.serialize,
|
|
12
|
+
shouldDehydrateQuery: (query) =>
|
|
13
|
+
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
|
|
14
|
+
},
|
|
15
|
+
hydrate: {
|
|
16
|
+
deserializeData: superjson.deserialize,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { createTRPCClient, httpBatchStreamLink, loggerLink } from '@trpc/client'
|
|
3
|
+
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'
|
|
4
|
+
import { createTRPCContext, createTRPCOptionsProxy } from '@trpc/tanstack-react-query'
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import superjson from 'superjson'
|
|
7
|
+
import type { AppRouter } from '~/server/api/root'
|
|
8
|
+
|
|
9
|
+
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>()
|
|
10
|
+
|
|
11
|
+
export type RouterInputs = inferRouterInputs<AppRouter>
|
|
12
|
+
|
|
13
|
+
export type RouterOutputs = inferRouterOutputs<AppRouter>
|
|
14
|
+
|
|
15
|
+
function getBaseUrl() {
|
|
16
|
+
if (typeof window !== 'undefined') return window.location.origin
|
|
17
|
+
return `http://localhost:${process.env.PORT ?? 3000}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const trpcClient = createTRPCClient<AppRouter>({
|
|
21
|
+
links: [
|
|
22
|
+
loggerLink({
|
|
23
|
+
enabled: (op) =>
|
|
24
|
+
process.env.NODE_ENV === 'development' ||
|
|
25
|
+
(op.direction === 'down' && op.result instanceof Error),
|
|
26
|
+
}),
|
|
27
|
+
httpBatchStreamLink({
|
|
28
|
+
transformer: superjson,
|
|
29
|
+
url: `${getBaseUrl()}/api/trpc`,
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export function createServerHelpers(queryClient: QueryClient) {
|
|
35
|
+
return createTRPCOptionsProxy<AppRouter>({
|
|
36
|
+
client: trpcClient,
|
|
37
|
+
queryClient,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function TRPCReactProvider(props: { children: ReactNode; queryClient: QueryClient }) {
|
|
42
|
+
return (
|
|
43
|
+
<QueryClientProvider client={props.queryClient}>
|
|
44
|
+
<TRPCProvider queryClient={props.queryClient} trpcClient={trpcClient}>
|
|
45
|
+
{props.children}
|
|
46
|
+
</TRPCProvider>
|
|
47
|
+
</QueryClientProvider>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getRequest } from '@tanstack/react-start/server'
|
|
2
|
+
import { createCaller } from '~/server/api/root'
|
|
3
|
+
import { createTRPCContext } from '~/server/api/trpc'
|
|
4
|
+
|
|
5
|
+
export const getServerCaller = async () => {
|
|
6
|
+
const request = getRequest()
|
|
7
|
+
|
|
8
|
+
const context = await createTRPCContext({ headers: request.headers })
|
|
9
|
+
|
|
10
|
+
return createCaller(context)
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"paths": {
|
|
8
|
+
"~/*": ["./src/*"]
|
|
9
|
+
},
|
|
10
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
11
|
+
"types": ["vite/client"],
|
|
12
|
+
|
|
13
|
+
/* Bundler mode */
|
|
14
|
+
"moduleResolution": "bundler",
|
|
15
|
+
"allowImportingTsExtensions": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"strict": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
2
|
+
import { devtools } from '@tanstack/devtools-vite'
|
|
3
|
+
|
|
4
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
5
|
+
|
|
6
|
+
import viteReact from '@vitejs/plugin-react'
|
|
7
|
+
import { nitro } from 'nitro/vite'
|
|
8
|
+
import { defineConfig } from 'vite'
|
|
9
|
+
|
|
10
|
+
const config = defineConfig({
|
|
11
|
+
resolve: { tsconfigPaths: true },
|
|
12
|
+
plugins: [devtools(), nitro(), tailwindcss(), tanstackStart(), viteReact()],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export default config
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "analytics",
|
|
4
|
+
"description": "Product analytics behind a swappable port. Capture events and identify users; flush/shutdown to drain pending events.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"defaultAdapter": "posthog",
|
|
7
|
+
"adapters": {
|
|
8
|
+
"posthog": {
|
|
9
|
+
"deps": ["posthog-node"],
|
|
10
|
+
"env": ["POSTHOG_API_KEY", "POSTHOG_HOST"],
|
|
11
|
+
"files": ["src/adapters/posthog"]
|
|
12
|
+
},
|
|
13
|
+
"plausible": {
|
|
14
|
+
"deps": ["@alfredmouelle/http"],
|
|
15
|
+
"env": ["PLAUSIBLE_DOMAIN", "PLAUSIBLE_API_HOST"],
|
|
16
|
+
"files": ["src/adapters/plausible"]
|
|
17
|
+
},
|
|
18
|
+
"noop": {
|
|
19
|
+
"deps": [],
|
|
20
|
+
"env": [],
|
|
21
|
+
"files": ["src/adapters/noop"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"sharedDeps": ["valibot"],
|
|
25
|
+
"sharedFiles": ["src/core", "src/index.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "cache",
|
|
4
|
+
"description": "Key/value cache behind a swappable port. Values are serialized as JSON for remote stores.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"defaultAdapter": "redis",
|
|
7
|
+
"adapters": {
|
|
8
|
+
"redis": {
|
|
9
|
+
"deps": ["ioredis"],
|
|
10
|
+
"env": ["REDIS_URL"],
|
|
11
|
+
"files": ["src/adapters/redis"]
|
|
12
|
+
},
|
|
13
|
+
"memory": {
|
|
14
|
+
"deps": [],
|
|
15
|
+
"env": [],
|
|
16
|
+
"files": ["src/adapters/memory"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"sharedDeps": ["valibot"],
|
|
20
|
+
"sharedFiles": ["src/core", "src/index.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "error-tracking",
|
|
4
|
+
"description": "Error reporting behind a swappable port. Capture exceptions, messages, breadcrumbs and user context, then ship them to a provider.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"defaultAdapter": "sentry",
|
|
7
|
+
"adapters": {
|
|
8
|
+
"sentry": {
|
|
9
|
+
"deps": ["@sentry/node"],
|
|
10
|
+
"env": ["SENTRY_DSN", "SENTRY_ENVIRONMENT"],
|
|
11
|
+
"files": ["src/adapters/sentry"]
|
|
12
|
+
},
|
|
13
|
+
"console": {
|
|
14
|
+
"deps": [],
|
|
15
|
+
"env": [],
|
|
16
|
+
"files": ["src/adapters/console"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"sharedDeps": ["valibot"],
|
|
20
|
+
"sharedFiles": ["src/core", "src/index.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "jobs",
|
|
4
|
+
"description": "Background jobs / events behind a swappable port. Event-driven: define jobs against named events and trigger them; the adapter handles delivery and execution.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"defaultAdapter": "inngest",
|
|
7
|
+
"adapters": {
|
|
8
|
+
"inngest": {
|
|
9
|
+
"deps": ["inngest"],
|
|
10
|
+
"env": ["INNGEST_EVENT_KEY", "INNGEST_SIGNING_KEY"],
|
|
11
|
+
"files": ["src/adapters/inngest"]
|
|
12
|
+
},
|
|
13
|
+
"trigger": {
|
|
14
|
+
"deps": ["@trigger.dev/sdk"],
|
|
15
|
+
"env": ["TRIGGER_SECRET_KEY"],
|
|
16
|
+
"files": ["src/adapters/trigger"]
|
|
17
|
+
},
|
|
18
|
+
"memory": {
|
|
19
|
+
"deps": [],
|
|
20
|
+
"env": [],
|
|
21
|
+
"files": ["src/adapters/memory"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"sharedDeps": ["valibot"],
|
|
25
|
+
"sharedFiles": ["src/core", "src/index.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "logger",
|
|
4
|
+
"description": "Structured logging behind a swappable port. Application code depends only on the Logger interface; pick an adapter (pino, console) at the composition root.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"defaultAdapter": "pino",
|
|
7
|
+
"adapters": {
|
|
8
|
+
"pino": {
|
|
9
|
+
"deps": ["pino"],
|
|
10
|
+
"env": [],
|
|
11
|
+
"files": ["src/adapters/pino"]
|
|
12
|
+
},
|
|
13
|
+
"console": {
|
|
14
|
+
"deps": [],
|
|
15
|
+
"env": [],
|
|
16
|
+
"files": ["src/adapters/console"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"sharedDeps": ["valibot"],
|
|
20
|
+
"sharedFiles": ["src/core", "src/index.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "mailer",
|
|
4
|
+
"description": "Transactional email. Bodies are always React Email components, rendered to HTML + plain text.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"factory": "src/factory.ts",
|
|
7
|
+
"defaultAdapter": "resend",
|
|
8
|
+
"adapters": {
|
|
9
|
+
"resend": {
|
|
10
|
+
"deps": ["resend"],
|
|
11
|
+
"env": ["RESEND_API_KEY"],
|
|
12
|
+
"files": ["src/adapters/resend"]
|
|
13
|
+
},
|
|
14
|
+
"brevo": {
|
|
15
|
+
"deps": ["@getbrevo/brevo"],
|
|
16
|
+
"env": ["BREVO_API_KEY"],
|
|
17
|
+
"files": ["src/adapters/brevo"]
|
|
18
|
+
},
|
|
19
|
+
"ses": {
|
|
20
|
+
"deps": ["@aws-sdk/client-sesv2"],
|
|
21
|
+
"env": ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
|
22
|
+
"files": ["src/adapters/ses"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"sharedDeps": ["valibot", "@react-email/render"],
|
|
26
|
+
"peerDeps": ["react", "react-dom"],
|
|
27
|
+
"sharedFiles": ["src/core", "src/factory.ts", "src/index.ts"]
|
|
28
|
+
}
|