@alfredmouelle/create-stack 0.1.1 → 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 +8 -13
  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 +2 -2
  7. package/_stack/apps/next-base/src/emails/components/context.tsx +1 -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 +6 -7
  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 +30 -47
  53. package/lib/build.mjs +6 -14
  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 +11 -11
  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
@@ -46,13 +46,12 @@ not exist yet. In non-interactive mode it is required.
46
46
  | `--framework` | `tanstack` \| `next` | `tanstack` | Base app to fork. |
47
47
  | `--foundations` | csv of `drizzle,trpc,better-auth,data-table` | all | Foundations to keep; the rest are stripped. |
48
48
  | `--mailer` | `resend` \| `brevo` \| `ses` \| `none` | `resend` | Mailer provider. `none` is rejected when `better-auth` is kept. |
49
- | `--caps` | csv of capability names | — | Extra capabilities to add afterwards via `add-capability` (see below). |
50
49
  | `--no-install` | — | install on | Skip `pnpm install` + verification. |
51
50
  | `--yes`, `-y` | — | — | Non-interactive with all defaults. |
52
51
 
53
- Passing any of `--framework`, `--foundations`, `--mailer`, `--caps` or
54
- `--no-install` (or `--yes`) switches the CLI to non-interactive mode; missing
55
- values fall back to the defaults above.
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.
56
55
 
57
56
  ### Dependency resolution
58
57
 
@@ -76,10 +75,6 @@ pnpm dlx @alfredmouelle/create-stack api --framework next \
76
75
 
77
76
  # Minimal: Drizzle only, no mailer
78
77
  pnpm dlx @alfredmouelle/create-stack db-svc --foundations drizzle --mailer none
79
-
80
- # TanStack + auth, and queue up extra capabilities for later
81
- pnpm dlx @alfredmouelle/create-stack app --foundations drizzle,trpc,better-auth \
82
- --caps storage,jobs
83
78
  ```
84
79
 
85
80
  ## What you get
@@ -97,12 +92,12 @@ pnpm dlx @alfredmouelle/create-stack app --foundations drizzle,trpc,better-auth
97
92
  Unselected foundations are removed cleanly (files, deps, env vars and wiring),
98
93
  and the project is left **bootable and green** (typecheck + Biome).
99
94
 
100
- ## Extra capabilities
95
+ ## Adding more capabilities
101
96
 
102
- `--caps` lists capabilities that aren't baked into the base (storage, jobs, cache,
103
- logger, analytics, error-tracking, http). They are **not** scaffolded by this CLI;
104
- the final report prints the follow-up to add them with the `add-capability` skill.
105
- Mailer is the exception it's built in and chosen via `--mailer`.
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).
106
101
 
107
102
  ## After scaffolding
108
103
 
@@ -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
  }
@@ -7,9 +7,9 @@ import { createEmailTheme, type EmailTheme, type EmailThemeOverride } from './th
7
7
  export interface EmailLayoutProps {
8
8
  preview: string
9
9
  children: React.ReactNode
10
- /** Per-email theme override, merged onto the default theme. */
10
+ /** Theme override, merged onto default. */
11
11
  theme?: EmailTheme | EmailThemeOverride
12
- /** Footer year. Pass explicitly to keep rendered output deterministic. */
12
+ /** Footer year. Pass explicitly for deterministic output. */
13
13
  year?: number
14
14
  }
15
15
 
@@ -14,7 +14,7 @@ export function EmailThemeProvider({ theme, children }: EmailThemeProviderProps)
14
14
  return <EmailThemeContext.Provider value={theme}>{children}</EmailThemeContext.Provider>
15
15
  }
16
16
 
17
- /** Read the active email theme. Falls back to {@link defaultTheme}. */
17
+ /** Active email theme; falls back to {@link defaultTheme}. */
18
18
  export function useEmailTheme(): EmailTheme {
19
19
  return useContext(EmailThemeContext)
20
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
@@ -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
+ }
@@ -14,7 +14,7 @@ interface TextFieldProps extends NativeInputProps {
14
14
  field: AnyFieldApi
15
15
  label: string
16
16
  description?: string
17
- /** Optional leading adornment (e.g. a lucide icon). */
17
+ /** Leading adornment (e.g. lucide icon). */
18
18
  icon?: ReactNode
19
19
  }
20
20
 
@@ -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
  }
@@ -5,9 +5,9 @@ import { createEmailTheme, type EmailTheme, type EmailThemeOverride } from './th
5
5
  export interface EmailLayoutProps {
6
6
  preview: string
7
7
  children: React.ReactNode
8
- /** Per-email theme override, merged onto the default theme. */
8
+ /** Per-email theme override, merged onto default. */
9
9
  theme?: EmailTheme | EmailThemeOverride
10
- /** Footer year. Pass explicitly to keep rendered output deterministic. */
10
+ /** Footer year; pass explicitly for deterministic output. */
11
11
  year?: number
12
12
  }
13
13
 
@@ -12,7 +12,7 @@ export function EmailThemeProvider({ theme, children }: EmailThemeProviderProps)
12
12
  return <EmailThemeContext.Provider value={theme}>{children}</EmailThemeContext.Provider>
13
13
  }
14
14
 
15
- /** Read the active email theme. Falls back to {@link defaultTheme}. */
15
+ /** Active email theme; falls back to {@link defaultTheme}. */
16
16
  export function useEmailTheme(): EmailTheme {
17
17
  return useContext(EmailThemeContext)
18
18
  }