@alfredmouelle/create-stack 0.1.0 → 0.1.2

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.
Files changed (81) hide show
  1. package/README.md +101 -40
  2. package/_stack/apps/next-base/.turbo/turbo-typecheck.log +1 -0
  3. package/_stack/apps/next-base/.vscode/settings.json +35 -0
  4. package/_stack/apps/next-base/.zed/settings.json +45 -0
  5. package/_stack/apps/next-base/src/components/ui/spinner.tsx +1 -1
  6. package/_stack/apps/next-base/src/emails/components/components.tsx +4 -2
  7. package/_stack/apps/next-base/src/emails/components/context.tsx +3 -1
  8. package/_stack/apps/next-base/src/emails/components/theme.ts +6 -7
  9. package/_stack/apps/next-base/src/env.ts +2 -3
  10. package/_stack/apps/next-base/src/lib/date.ts +1 -1
  11. package/_stack/apps/next-base/src/server/auth/guards.ts +1 -1
  12. package/_stack/apps/next-base/src/server/better-auth/config.ts +1 -1
  13. package/_stack/apps/next-base/src/server/better-auth/server.ts +2 -2
  14. package/_stack/apps/next-base/src/server/db/schemas/index.ts +1 -1
  15. package/_stack/apps/next-base/src/server/db/seed.ts +2 -2
  16. package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +3 -3
  17. package/_stack/apps/next-base/src/server/email/core/address.ts +3 -3
  18. package/_stack/apps/next-base/src/server/email/core/port.ts +13 -20
  19. package/_stack/apps/next-base/src/server/email/core/render.ts +2 -2
  20. package/_stack/apps/next-base/src/server/email/factory.ts +7 -9
  21. package/_stack/apps/next-base/src/server/email/index.ts +1 -1
  22. package/_stack/apps/next-base/src/trpc/react.tsx +2 -2
  23. package/_stack/apps/next-base/src/trpc/server.ts +1 -1
  24. package/_stack/apps/tanstack-base/.turbo/turbo-typecheck.log +1 -0
  25. package/_stack/apps/tanstack-base/.vscode/settings.json +35 -0
  26. package/_stack/apps/tanstack-base/.zed/settings.json +45 -0
  27. package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +1 -1
  28. package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +1 -1
  29. package/_stack/apps/tanstack-base/src/emails/components/components.tsx +2 -2
  30. package/_stack/apps/tanstack-base/src/emails/components/context.tsx +1 -1
  31. package/_stack/apps/tanstack-base/src/emails/components/theme.ts +6 -7
  32. package/_stack/apps/tanstack-base/src/env.ts +2 -6
  33. package/_stack/apps/tanstack-base/src/lib/date.ts +1 -1
  34. package/_stack/apps/tanstack-base/src/routes/__root.tsx +1 -1
  35. package/_stack/apps/tanstack-base/src/routes/_authed.tsx +1 -4
  36. package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +1 -1
  37. package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +1 -2
  38. package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +1 -1
  39. package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +12 -5
  40. package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +1 -1
  41. package/_stack/apps/tanstack-base/src/server/db/seed.ts +2 -2
  42. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +3 -3
  43. package/_stack/apps/tanstack-base/src/server/email/core/address.ts +3 -3
  44. package/_stack/apps/tanstack-base/src/server/email/core/port.ts +12 -22
  45. package/_stack/apps/tanstack-base/src/server/email/core/render.ts +1 -1
  46. package/_stack/apps/tanstack-base/src/server/email/factory.ts +7 -8
  47. package/_stack/apps/tanstack-base/src/server/email/index.ts +1 -1
  48. package/_stack/packages/mailer/src/adapters/brevo/index.ts +3 -3
  49. package/_stack/packages/mailer/src/adapters/resend/index.ts +3 -3
  50. package/_stack/packages/mailer/src/adapters/ses/config.ts +3 -3
  51. package/_stack/packages/mailer/src/adapters/ses/index.ts +4 -5
  52. package/index.mjs +33 -44
  53. package/lib/build.mjs +7 -15
  54. package/lib/env.mjs +5 -6
  55. package/lib/foundations.mjs +35 -0
  56. package/lib/identity.mjs +4 -5
  57. package/lib/mailer.mjs +9 -13
  58. package/lib/paths.mjs +15 -0
  59. package/lib/scaffold.mjs +65 -10
  60. package/lib/strip.mjs +9 -24
  61. package/lib/util.mjs +8 -9
  62. package/package.json +1 -1
  63. package/_stack/packages/analytics/capability.json +0 -26
  64. package/_stack/packages/cache/capability.json +0 -21
  65. package/_stack/packages/error-tracking/capability.json +0 -21
  66. package/_stack/packages/jobs/capability.json +0 -26
  67. package/_stack/packages/logger/capability.json +0 -21
  68. package/_stack/packages/mailer/capability.json +0 -28
  69. package/_stack/packages/storage/capability.json +0 -32
  70. package/_stack/patterns/README.md +0 -58
  71. package/_stack/patterns/_baseline/env.ts +0 -31
  72. package/_stack/patterns/_baseline/tsconfig.json +0 -27
  73. package/_stack/patterns/better-auth/pattern.json +0 -73
  74. package/_stack/patterns/better-auth-next/pattern.json +0 -76
  75. package/_stack/patterns/data-table/pattern.json +0 -43
  76. package/_stack/patterns/drizzle/pattern.json +0 -61
  77. package/_stack/patterns/trpc/pattern.json +0 -61
  78. package/_stack/patterns/trpc-next/pattern.json +0 -64
  79. package/lib/manifests.mjs +0 -61
  80. /package/{_stack/patterns/_baseline → templates}/README-author.md +0 -0
  81. /package/{_stack/patterns/_baseline → templates}/biome.jsonc +0 -0
package/README.md CHANGED
@@ -1,56 +1,117 @@
1
1
  # create-stack
2
2
 
3
- Interactive, **deterministic** installer for the reference stack — the non-LLM
4
- counterpart of the `bootstrap` skill's CREATE mode. It forks a base app
5
- (`apps/tanstack-base` or `apps/next-base`) and strips it down to your selection.
3
+ > `@alfredmouelle/create-stack`
6
4
 
7
- ## Usage
5
+ Interactive, **deterministic** project installer. It forks a fully-wired base app
6
+ (**Next.js App Router** or **TanStack Start**) and strips it down to exactly the
7
+ foundations and provider you pick — Drizzle, tRPC, better-auth, data tables and a
8
+ mailer — then stamps identity, generates `.env`, initializes git and verifies the
9
+ result (typecheck + Biome).
10
+
11
+ No template guesswork: the output is a real, buildable app from day one.
12
+
13
+ ## Quick start
8
14
 
9
15
  ```bash
10
- # from anywhere
11
- node /path/to/stack/cli/index.mjs my-app
16
+ pnpm dlx @alfredmouelle/create-stack my-app
17
+ # or, using the create-* convention:
18
+ pnpm create @alfredmouelle/stack my-app
19
+ # npm / yarn:
20
+ npm create @alfredmouelle/stack my-app
21
+ yarn create @alfredmouelle/stack my-app
22
+ ```
23
+
24
+ Run with no extra flags → an **interactive wizard** asks every question. Pass any
25
+ selection flag → **non-interactive** mode (scriptable / CI).
26
+
27
+ ## Requirements
28
+
29
+ - **Node** ≥ 22
30
+ - **pnpm** (the generated project is a pnpm project)
31
+ - **git** and **rsync** available on `PATH` (macOS/Linux ship both)
12
32
 
13
- # from the stack repo
14
- pnpm create-stack my-app
33
+ ## Usage
34
+
35
+ ```
36
+ create-stack [project] [flags]
15
37
  ```
16
38
 
17
- The wizard asks for:
39
+ `project` is the target directory (and default package name). It must be empty or
40
+ not exist yet. In non-interactive mode it is required.
41
+
42
+ ### Flags
43
+
44
+ | Flag | Values | Default | Description |
45
+ | --- | --- | --- | --- |
46
+ | `--framework` | `tanstack` \| `next` | `tanstack` | Base app to fork. |
47
+ | `--foundations` | csv of `drizzle,trpc,better-auth,data-table` | all | Foundations to keep; the rest are stripped. |
48
+ | `--mailer` | `resend` \| `brevo` \| `ses` \| `none` | `resend` | Mailer provider. `none` is rejected when `better-auth` is kept. |
49
+ | `--no-install` | — | install on | Skip `pnpm install` + verification. |
50
+ | `--yes`, `-y` | — | — | Non-interactive with all defaults. |
51
+
52
+ Passing any of `--framework`, `--foundations`, `--mailer` or `--no-install`
53
+ (or `--yes`) switches the CLI to non-interactive mode; missing values fall back
54
+ to the defaults above.
18
55
 
19
- - **Framework** — TanStack Start or Next.js (App Router)
20
- - **Foundations** — Drizzle, tRPC, better-auth, data-table (hard deps resolved:
21
- tRPC/better-auth ⇒ Drizzle; better-auth ⇒ mailer)
22
- - **Mailer provider** — Resend, Brevo, Amazon SES (or none, if no better-auth)
23
- - **Extra capabilities** — storage, jobs, cache, … (added afterwards via the
24
- `add-capability` skill; not baked into the base)
56
+ ### Dependency resolution
25
57
 
26
- Then it forks, strips unselected foundations, swaps the mailer adapter, stamps
27
- identity, generates `.env(.example)`, and optionally installs + verifies
28
- (typecheck + Biome).
58
+ Selections are normalized for you:
29
59
 
30
- ## How it works
60
+ - `trpc` pulls in `drizzle`
61
+ - `better-auth` ⇒ pulls in `drizzle` **and** forces a real mailer (not `none`)
31
62
 
32
- - **Single source of truth**: the base apps (`apps/*-base`) hold the real code;
33
- the `patterns/*/pattern.json` manifests drive deps/scripts/env/file lists.
34
- - **Deterministic strip**: whole-directory deletes + dep/env/script diffs from the
35
- manifests, plus a few hardcoded code *seams* (tRPC↔auth context, root provider
36
- wiring) handled via shipped reduced variants in `templates/`.
37
- - If you edit a seam file in a base app (`server/api/trpc.ts`, the root
38
- `router.tsx` / `__root.tsx` / `layout.tsx`, the schema barrel), update the
39
- matching transform in `lib/strip.mjs` or the `templates/` variant.
63
+ ### Examples
40
64
 
41
- ## Layout
65
+ ```bash
66
+ # Full interactive wizard
67
+ pnpm dlx @alfredmouelle/create-stack my-app
68
+
69
+ # Everything, defaults (TanStack Start + all foundations + Resend), no questions
70
+ pnpm dlx @alfredmouelle/create-stack my-app --yes
42
71
 
72
+ # Next.js with just Drizzle + tRPC, Amazon SES mailer, don't install
73
+ pnpm dlx @alfredmouelle/create-stack api --framework next \
74
+ --foundations drizzle,trpc --mailer ses --no-install
75
+
76
+ # Minimal: Drizzle only, no mailer
77
+ pnpm dlx @alfredmouelle/create-stack db-svc --foundations drizzle --mailer none
43
78
  ```
44
- cli/
45
- index.mjs wizard (prompts) + install/verify + report
46
- lib/
47
- build.mjs pure build phase (fork strip mailer env identity)
48
- manifests.mjs load patterns + capabilities; logical→manifest mapping
49
- scaffold.mjs fork base app, make it standalone
50
- strip.mjs reverse-strip unselected foundations + code seams
51
- mailer.mjs mailer provider swap
52
- env.mjs rebuild src/env.ts blocks + generate .env files
53
- identity.mjs title/meta + README with the # Author footer
54
- util.mjs fs / exec / package.json helpers
55
- templates/ reduced "no-trpc" root-wiring variants
79
+
80
+ ## What you get
81
+
82
+ - **Framework** Next.js App Router *or* TanStack Start, fully wired (SSR, routing).
83
+ - **Drizzle ORM** Postgres client, schema barrel, keyset pagination, seed harness.
84
+ - **tRPC v11** — typed API, SSR/RSC integration, health router.
85
+ - **better-auth** email+password + verification, optional Google OAuth, session
86
+ guards, auth UI pages.
87
+ - **Mailer** — Resend / Brevo / SES behind one port; React Email templates.
88
+ - **Data tables** TanStack Table primitives (DataTable, InfiniteDataTable, …).
89
+ - **Baseline** Tailwind v4 + shadcn, Geist, theme toggle, strict Biome, typed
90
+ env (`src/env.ts`), Dockerfile, a generated `.gitignore` and `.env`/`.env.example`.
91
+
92
+ Unselected foundations are removed cleanly (files, deps, env vars and wiring),
93
+ and the project is left **bootable and green** (typecheck + Biome).
94
+
95
+ ## Adding more capabilities
96
+
97
+ The base bakes in the **mailer** (chosen via `--mailer`). Other capabilities —
98
+ storage, jobs, cache, logger, analytics, error-tracking, http — are added *after*
99
+ scaffolding with the `add-capability` skill (it wires each adapter's env/config per
100
+ provider, which this CLI deliberately leaves out).
101
+
102
+ ## After scaffolding
103
+
104
+ ```bash
105
+ cd my-app
106
+ pnpm install # only if you passed --no-install
107
+ cp .env.example .env # fill in the values
108
+ pnpm dev
56
109
  ```
110
+
111
+ ## Notes
112
+
113
+ - The published package is **self-contained**: the base apps, pattern manifests
114
+ and mailer adapters are bundled at publish time, so `pnpm dlx` needs nothing but
115
+ this package.
116
+ - The generated project is a fresh git repo (`git init`, files staged) — make your
117
+ first commit when ready.
@@ -0,0 +1 @@
1
+ $ tsc --noEmit
@@ -0,0 +1,35 @@
1
+ {
2
+ "files.watcherExclude": {
3
+ "**/routeTree.gen.ts": true
4
+ },
5
+ "search.exclude": {
6
+ "**/routeTree.gen.ts": true
7
+ },
8
+ "files.readonlyInclude": {
9
+ "**/routeTree.gen.ts": true
10
+ },
11
+ "[javascript]": {
12
+ "editor.defaultFormatter": "biomejs.biome"
13
+ },
14
+ "[javascriptreact]": {
15
+ "editor.defaultFormatter": "biomejs.biome"
16
+ },
17
+ "[typescript]": {
18
+ "editor.defaultFormatter": "biomejs.biome"
19
+ },
20
+ "[typescriptreact]": {
21
+ "editor.defaultFormatter": "biomejs.biome"
22
+ },
23
+ "[json]": {
24
+ "editor.defaultFormatter": "biomejs.biome"
25
+ },
26
+ "[jsonc]": {
27
+ "editor.defaultFormatter": "biomejs.biome"
28
+ },
29
+ "[css]": {
30
+ "editor.defaultFormatter": "biomejs.biome"
31
+ },
32
+ "editor.codeActionsOnSave": {
33
+ "source.organizeImports.biome": "explicit"
34
+ }
35
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "lsp": {
3
+ "biome": {
4
+ "settings": {
5
+ "require_config_file": true,
6
+ "config_path": "biome.jsonc"
7
+ }
8
+ }
9
+ },
10
+
11
+ "languages": {
12
+ "Markdown": {
13
+ "formatter": { "language_server": { "name": "biome" } }
14
+ },
15
+ "JSONC": {
16
+ "formatter": { "language_server": { "name": "biome" } }
17
+ },
18
+ "JSON": {
19
+ "formatter": { "language_server": { "name": "biome" } }
20
+ },
21
+ "HTML": { "formatter": { "language_server": { "name": "biome" } } },
22
+ "CSS": { "formatter": { "language_server": { "name": "biome" } } },
23
+ "JavaScript": {
24
+ "formatter": { "language_server": { "name": "biome" } },
25
+ "code_actions_on_format": {
26
+ "source.fixAll.biome": true,
27
+ "source.organizeImports.biome": true
28
+ }
29
+ },
30
+ "TypeScript": {
31
+ "formatter": { "language_server": { "name": "biome" } },
32
+ "code_actions_on_format": {
33
+ "source.fixAll.biome": true,
34
+ "source.organizeImports.biome": true
35
+ }
36
+ },
37
+ "TSX": {
38
+ "formatter": { "language_server": { "name": "biome" } },
39
+ "code_actions_on_format": {
40
+ "source.fixAll.biome": true,
41
+ "source.organizeImports.biome": true
42
+ }
43
+ }
44
+ }
45
+ }
@@ -2,7 +2,7 @@ import { Loader2 } from 'lucide-react'
2
2
  import type { ComponentProps } from 'react'
3
3
  import { cn } from '~/lib/utils'
4
4
 
5
- /** Loading indicator — the lucide `loader` icon, spinning. */
5
+ /** Spinning lucide loader icon. */
6
6
  export function Spinner({ className, ...props }: ComponentProps<typeof Loader2>) {
7
7
  return <Loader2 aria-hidden="true" className={cn('size-4 animate-spin', className)} {...props} />
8
8
  }
@@ -1,3 +1,5 @@
1
+ 'use client'
2
+
1
3
  import { Body, Container, Head, Html, Link, Preview, Section, Tailwind, Text } from 'react-email'
2
4
  import { EmailThemeProvider, useEmailTheme } from './context'
3
5
  import { createEmailTheme, type EmailTheme, type EmailThemeOverride } from './theme'
@@ -5,9 +7,9 @@ import { createEmailTheme, type EmailTheme, type EmailThemeOverride } from './th
5
7
  export interface EmailLayoutProps {
6
8
  preview: string
7
9
  children: React.ReactNode
8
- /** Per-email theme override, merged onto the default theme. */
10
+ /** Theme override, merged onto default. */
9
11
  theme?: EmailTheme | EmailThemeOverride
10
- /** Footer year. Pass explicitly to keep rendered output deterministic. */
12
+ /** Footer year. Pass explicitly for deterministic output. */
11
13
  year?: number
12
14
  }
13
15
 
@@ -1,3 +1,5 @@
1
+ 'use client'
2
+
1
3
  import { createContext, useContext } from 'react'
2
4
  import { defaultTheme, type EmailTheme } from './theme'
3
5
 
@@ -12,7 +14,7 @@ export function EmailThemeProvider({ theme, children }: EmailThemeProviderProps)
12
14
  return <EmailThemeContext.Provider value={theme}>{children}</EmailThemeContext.Provider>
13
15
  }
14
16
 
15
- /** Read the active email theme. Falls back to {@link defaultTheme}. */
17
+ /** Active email theme; falls back to {@link defaultTheme}. */
16
18
  export function useEmailTheme(): EmailTheme {
17
19
  return useContext(EmailThemeContext)
18
20
  }
@@ -1,13 +1,12 @@
1
1
  /**
2
- * The swappable design tokens for emails. Override any subset via
3
- * {@link createEmailTheme} and pass the result to `<EmailLayout theme={...}>`,
4
- * or wrap a tree in `<EmailThemeProvider>`.
2
+ * Swappable email design tokens. Override a subset via {@link createEmailTheme}
3
+ * and pass to `<EmailLayout theme={...}>`, or wrap in `<EmailThemeProvider>`.
5
4
  */
6
5
  export interface EmailTheme {
7
6
  brand: {
8
- /** Shown in the header band and footer. */
7
+ /** Header band + footer. */
9
8
  name: string
10
- /** Footer line (the year is prefixed automatically). */
9
+ /** Footer line (year prefixed automatically). */
11
10
  footer: string
12
11
  }
13
12
  fontFamily: string
@@ -21,7 +20,7 @@ export interface EmailTheme {
21
20
  border: string
22
21
  borderSubtle: string
23
22
  destructive: string
24
- /** Text color on top of accent/destructive buttons. */
23
+ /** Text on accent/destructive buttons. */
25
24
  onAccent: string
26
25
  }
27
26
  }
@@ -52,7 +51,7 @@ export type EmailThemeOverride = {
52
51
  colors?: Partial<EmailTheme['colors']>
53
52
  }
54
53
 
55
- /** Deep-merge an override onto a base theme (defaults to {@link defaultTheme}). */
54
+ /** Deep-merge override onto base (defaults to {@link defaultTheme}). */
56
55
  export function createEmailTheme(
57
56
  override: EmailThemeOverride = {},
58
57
  base: EmailTheme = defaultTheme,
@@ -1,13 +1,12 @@
1
1
  import { createEnv } from '@t3-oss/env-core'
2
2
  import * as v from 'valibot'
3
3
 
4
- /** Makes a var required only in production (optional in dev/test). */
4
+ /** Required in production, optional in dev/test. */
5
5
  export const requiredInProduction = <T extends v.GenericSchema>(schema: T) =>
6
6
  process.env.NODE_ENV === 'production' ? schema : v.optional(schema)
7
7
 
8
8
  /**
9
- * Typed environment. Start minimal patterns (drizzle, better-auth, …) and
10
- * capabilities (add-capability) extend the `server` block and `runtimeEnv` with
9
+ * Typed env. Foundations + capabilities extend `server` and `runtimeEnv` with
11
10
  * the keys they need.
12
11
  */
13
12
  export const env = createEnv({
@@ -1,4 +1,4 @@
1
1
  import { format } from 'date-fns'
2
2
 
3
- /** Format a Date as an ISO date string (yyyy-MM-dd). */
3
+ /** Format Date as ISO date (yyyy-MM-dd). */
4
4
  export const toISODate = (date: Date): string => format(date, 'yyyy-MM-dd')
@@ -2,7 +2,7 @@ import 'server-only'
2
2
  import { redirect } from 'next/navigation'
3
3
  import { getSession } from '~/server/better-auth/server'
4
4
 
5
- /** Require a signed-in user in a Server Component / page; redirect otherwise. */
5
+ /** Require signed-in user in a Server Component/page; else redirect. */
6
6
  export async function requireAuth() {
7
7
  const session = await getSession()
8
8
  if (!session) redirect('/auth/sign-in')
@@ -36,7 +36,7 @@ export const auth = betterAuth({
36
36
  },
37
37
  socialProviders,
38
38
  user: {
39
- // Extend the user with extra columns here (mirror them in auth.schema.ts):
39
+ // Extra user columns here (mirror in auth.schema.ts):
40
40
  // additionalFields: {
41
41
  // role: { type: 'string', defaultValue: 'user', input: false },
42
42
  // },
@@ -2,10 +2,10 @@ import { headers } from 'next/headers'
2
2
  import { cache } from 'react'
3
3
  import { auth } from '.'
4
4
 
5
- /** Per-request cached session lookup (uses the cookie cache). */
5
+ /** Per-request cached session (uses cookie cache). */
6
6
  export const getSession = cache(async () => auth.api.getSession({ headers: await headers() }))
7
7
 
8
- /** Bypasses the cookie cache use for sensitive checks. */
8
+ /** Bypasses cookie cache; use for sensitive checks. */
9
9
  export const getFreshSession = cache(async () =>
10
10
  auth.api.getSession({
11
11
  headers: await headers(),
@@ -1,2 +1,2 @@
1
- // Barrel of every Drizzle schema. Add one `export *` line per `*.schema.ts`.
1
+ // Drizzle schema barrel. One `export *` per `*.schema.ts`.
2
2
  export * from './auth.schema'
@@ -3,11 +3,11 @@ import { config } from 'dotenv'
3
3
 
4
4
  config({ path: ['.env.local', '.env'] })
5
5
 
6
- // Deterministic data across runs.
6
+ // Deterministic across runs.
7
7
  faker.seed(42)
8
8
 
9
9
  async function main() {
10
- // Import the db client and seed your tables here (idempotent), using faker:
10
+ // Seed tables here (idempotent), via faker:
11
11
  // const { db } = await import('~/server/db')
12
12
  // await db.insert(user).values({
13
13
  // id: faker.string.uuid(),
@@ -4,7 +4,7 @@ import { formatAddress } from '../../core/address'
4
4
  import { type MailerAdapter, MailerError, type RenderedMessage } from '../../core/port'
5
5
  import { ResendConfigSchema } from './config'
6
6
 
7
- /** Minimal structural view of the Resend client we depend on (eases testing). */
7
+ /** Structural view of the Resend client we depend on (eases testing). */
8
8
  export interface ResendClient {
9
9
  emails: {
10
10
  send(payload: ResendSendPayload): Promise<{
@@ -30,7 +30,7 @@ interface ResendSendPayload {
30
30
 
31
31
  export interface ResendAdapterOptions {
32
32
  apiKey: string
33
- /** Inject a custom/mock client. Defaults to a real `Resend` instance. */
33
+ /** Inject a custom/mock client. Defaults to real `Resend`. */
34
34
  client?: ResendClient
35
35
  }
36
36
 
@@ -39,7 +39,7 @@ function toAttachmentContent(content: Uint8Array | string): Buffer | string {
39
39
  }
40
40
 
41
41
  export function resendAdapter(options: ResendAdapterOptions): MailerAdapter {
42
- // Validate config early so a missing key fails at construction, not at send().
42
+ // Validate early so a missing key fails at construction, not send().
43
43
  const config = v.parse(ResendConfigSchema, { apiKey: options.apiKey })
44
44
  const client: ResendClient =
45
45
  options.client ?? (new Resend(config.apiKey) as unknown as ResendClient)
@@ -2,7 +2,7 @@ import type { MailAddress, MailRecipient } from './port'
2
2
 
3
3
  const ADDRESS_RE = /^\s*(.*?)\s*<([^>]+)>\s*$/
4
4
 
5
- /** Coerce a recipient into a structured {@link MailAddress}. */
5
+ /** Coerce recipient to {@link MailAddress}. */
6
6
  export function normalizeAddress(input: MailRecipient): MailAddress {
7
7
  if (typeof input !== 'string') return input
8
8
  const match = input.match(ADDRESS_RE)
@@ -10,12 +10,12 @@ export function normalizeAddress(input: MailRecipient): MailAddress {
10
10
  return { email: input.trim() }
11
11
  }
12
12
 
13
- /** Coerce a single recipient or list into a normalized array. */
13
+ /** Coerce recipient(s) to normalized array. */
14
14
  export function normalizeRecipients(input: MailRecipient | MailRecipient[]): MailAddress[] {
15
15
  return (Array.isArray(input) ? input : [input]).map(normalizeAddress)
16
16
  }
17
17
 
18
- /** Render an address back to an RFC-style string (`Name <email>`). */
18
+ /** Render address to RFC string (`Name <email>`). */
19
19
  export function formatAddress(address: MailAddress): string {
20
20
  return address.name ? `${address.name} <${address.email}>` : address.email
21
21
  }
@@ -1,41 +1,37 @@
1
1
  import type { ReactElement } from 'react'
2
2
 
3
- /** A structured email address. */
3
+ /** Structured email address. */
4
4
  export interface MailAddress {
5
5
  email: string
6
6
  name?: string
7
7
  }
8
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
- */
9
+ /** Recipient: string (`"hi@acme.com"` / `"Acme <hi@acme.com>"`) or {@link MailAddress}. */
13
10
  export type MailRecipient = string | MailAddress
14
11
 
15
12
  export interface MailAttachment {
16
13
  filename: string
17
- /** Raw bytes or a base64 string. */
14
+ /** Raw bytes or base64 string. */
18
15
  content: Uint8Array | string
19
16
  contentType?: string
20
17
  }
21
18
 
22
19
  /**
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.
20
+ * Message authored by app code. Never raw HTML: body is a React Email
21
+ * component, rendered to HTML + text by the mailer before the adapter.
26
22
  */
27
23
  export interface MailMessage {
28
24
  to: MailRecipient | MailRecipient[]
29
25
  subject: string
30
- /** A React Email component. Rendered to HTML and plain text automatically. */
26
+ /** React Email component; rendered to HTML + text automatically. */
31
27
  react: ReactElement
32
- /** Overrides the default `from` configured on the mailer. */
28
+ /** Overrides the mailer's default `from`. */
33
29
  from?: MailRecipient
34
30
  replyTo?: MailRecipient
35
31
  cc?: MailRecipient | MailRecipient[]
36
32
  bcc?: MailRecipient | MailRecipient[]
37
33
  headers?: Record<string, string>
38
- /** Provider-agnostic key/value tags for analytics & filtering. */
34
+ /** Provider-agnostic tags for analytics/filtering. */
39
35
  tags?: Record<string, string>
40
36
  attachments?: MailAttachment[]
41
37
  }
@@ -46,17 +42,14 @@ export interface SentMail {
46
42
  }
47
43
 
48
44
  /**
49
- * The port the application depends on. Swapping providers means swapping the
50
- * adapter passed to {@link createMailer}; this interface never changes.
45
+ * The port the app depends on. Swap providers by swapping the adapter passed
46
+ * to {@link createMailer}; this interface never changes.
51
47
  */
52
48
  export interface Mailer {
53
49
  send(message: MailMessage): Promise<SentMail>
54
50
  }
55
51
 
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
- */
52
+ /** Shape an adapter receives: addresses normalized, body pre-rendered to `html` + `text`. */
60
53
  export interface RenderedMessage {
61
54
  to: MailAddress[]
62
55
  from: MailAddress
@@ -71,13 +64,13 @@ export interface RenderedMessage {
71
64
  attachments?: MailAttachment[]
72
65
  }
73
66
 
74
- /** The contract each provider implements. Kept intentionally tiny. */
67
+ /** Contract each provider implements. */
75
68
  export interface MailerAdapter {
76
69
  readonly name: string
77
70
  send(message: RenderedMessage): Promise<SentMail>
78
71
  }
79
72
 
80
- /** Normalized error thrown by adapters so callers never catch provider types. */
73
+ /** Normalized adapter error so callers never catch provider types. */
81
74
  export class MailerError extends Error {
82
75
  readonly adapter: string
83
76
 
@@ -6,10 +6,10 @@ export interface RenderedBody {
6
6
  text: string
7
7
  }
8
8
 
9
- /** A function that turns a React Email component into HTML + plain text. */
9
+ /** Turns a React Email component into HTML + text. */
10
10
  export type EmailRenderer = (react: ReactElement) => Promise<RenderedBody>
11
11
 
12
- /** Default renderer backed by `@react-email/render`. */
12
+ /** Default renderer via `@react-email/render`. */
13
13
  export const renderEmail: EmailRenderer = async (react) => {
14
14
  const [html, text] = await Promise.all([render(react), render(react, { plainText: true })])
15
15
  return { html, text }
@@ -3,21 +3,19 @@ import type { Mailer, MailerAdapter, MailRecipient, RenderedMessage } from './co
3
3
  import { type EmailRenderer, renderEmail } from './core/render'
4
4
 
5
5
  export interface CreateMailerOptions {
6
- /** The provider implementation (Resend, Brevo, …). */
6
+ /** Provider implementation (Resend, Brevo, …). */
7
7
  adapter: MailerAdapter
8
- /** Default sender used when a message omits `from`. */
8
+ /** Default sender when a message omits `from`. */
9
9
  from: MailRecipient
10
- /** Override the React Email renderer (mostly for tests). */
10
+ /** Override the renderer (mostly for tests). */
11
11
  render?: EmailRenderer
12
12
  }
13
13
 
14
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.
15
+ * Build a {@link Mailer}. Composition root: pick the adapter here; the rest of
16
+ * the app depends only on the `Mailer` port. Renders the React body to
17
+ * HTML + text, normalizes addresses, applies the default sender, then hands a
18
+ * {@link RenderedMessage} to the adapter.
21
19
  */
22
20
  export function createMailer(options: CreateMailerOptions): Mailer {
23
21
  const defaultFrom = normalizeAddress(options.from)
@@ -11,7 +11,7 @@ function required(value: string | undefined, name: string): string {
11
11
  return value
12
12
  }
13
13
 
14
- // Lazy so the app boots without RESEND_API_KEY; the adapter is built on first send.
14
+ // Lazy: app boots without RESEND_API_KEY; adapter built on first send.
15
15
  let mailer: Mailer | null = null
16
16
  function getMailer(): Mailer {
17
17
  if (!mailer) {
@@ -13,10 +13,10 @@ import { createQueryClient } from './query-client'
13
13
  let clientQueryClientSingleton: QueryClient | undefined
14
14
  const getQueryClient = () => {
15
15
  if (typeof window === 'undefined') {
16
- // Server: always make a new query client.
16
+ // Server: always new client.
17
17
  return createQueryClient()
18
18
  }
19
- // Browser: reuse the same query client across renders.
19
+ // Browser: reuse across renders.
20
20
  clientQueryClientSingleton ??= createQueryClient()
21
21
  return clientQueryClientSingleton
22
22
  }
@@ -7,7 +7,7 @@ import { type AppRouter, createCaller } from '~/server/api/root'
7
7
  import { createTRPCContext } from '~/server/api/trpc'
8
8
  import { createQueryClient } from './query-client'
9
9
 
10
- /** Context for tRPC calls made from React Server Components. */
10
+ /** Context for tRPC calls from RSCs. */
11
11
  const createContext = cache(async () => {
12
12
  const heads = new Headers(await headers())
13
13
  heads.set('x-trpc-source', 'rsc')
@@ -0,0 +1 @@
1
+ $ tsr generate && tsc --noEmit