@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.
Files changed (206) hide show
  1. package/README.md +56 -0
  2. package/_stack/apps/next-base/.dockerignore +10 -0
  3. package/_stack/apps/next-base/Dockerfile +34 -0
  4. package/_stack/apps/next-base/README.md +32 -0
  5. package/_stack/apps/next-base/components.json +25 -0
  6. package/_stack/apps/next-base/drizzle.config.ts +16 -0
  7. package/_stack/apps/next-base/next.config.ts +8 -0
  8. package/_stack/apps/next-base/package.json +70 -0
  9. package/_stack/apps/next-base/postcss.config.mjs +7 -0
  10. package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  11. package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
  12. package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
  13. package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
  14. package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
  15. package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
  16. package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
  17. package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
  18. package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
  19. package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
  20. package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
  21. package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
  22. package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
  23. package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
  24. package/_stack/apps/next-base/src/app/globals.css +171 -0
  25. package/_stack/apps/next-base/src/app/layout.tsx +23 -0
  26. package/_stack/apps/next-base/src/app/page.tsx +15 -0
  27. package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
  28. package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
  29. package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
  30. package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
  31. package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
  32. package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
  33. package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
  34. package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
  35. package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
  36. package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
  37. package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
  38. package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
  39. package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
  40. package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
  41. package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
  42. package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
  43. package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
  44. package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
  45. package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
  46. package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
  47. package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
  48. package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
  49. package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
  50. package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
  51. package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
  52. package/_stack/apps/next-base/src/env.ts +41 -0
  53. package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
  54. package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
  55. package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
  56. package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
  57. package/_stack/apps/next-base/src/lib/date.ts +4 -0
  58. package/_stack/apps/next-base/src/lib/utils.ts +6 -0
  59. package/_stack/apps/next-base/src/server/api/root.ts +10 -0
  60. package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
  61. package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
  62. package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
  63. package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
  64. package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
  65. package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
  66. package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
  67. package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
  68. package/_stack/apps/next-base/src/server/db/index.ts +6 -0
  69. package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
  70. package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
  71. package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
  72. package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
  73. package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
  74. package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
  75. package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
  76. package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
  77. package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
  78. package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
  79. package/_stack/apps/next-base/src/server/email/index.ts +36 -0
  80. package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
  81. package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
  82. package/_stack/apps/next-base/src/trpc/server.ts +23 -0
  83. package/_stack/apps/next-base/tsconfig.json +37 -0
  84. package/_stack/apps/tanstack-base/.dockerignore +13 -0
  85. package/_stack/apps/tanstack-base/Dockerfile +28 -0
  86. package/_stack/apps/tanstack-base/README.md +31 -0
  87. package/_stack/apps/tanstack-base/components.json +25 -0
  88. package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
  89. package/_stack/apps/tanstack-base/package.json +85 -0
  90. package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
  91. package/_stack/apps/tanstack-base/public/logo192.png +0 -0
  92. package/_stack/apps/tanstack-base/public/logo512.png +0 -0
  93. package/_stack/apps/tanstack-base/public/manifest.json +25 -0
  94. package/_stack/apps/tanstack-base/public/robots.txt +3 -0
  95. package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
  96. package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
  97. package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
  98. package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
  99. package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
  100. package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
  101. package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
  102. package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
  103. package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
  104. package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
  105. package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
  106. package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
  107. package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
  108. package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
  109. package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
  110. package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
  111. package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
  112. package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
  113. package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
  114. package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
  115. package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
  116. package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
  117. package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
  118. package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
  119. package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
  120. package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
  121. package/_stack/apps/tanstack-base/src/env.ts +41 -0
  122. package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
  123. package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
  124. package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
  125. package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
  126. package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
  127. package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
  128. package/_stack/apps/tanstack-base/src/router.tsx +40 -0
  129. package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
  130. package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
  131. package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
  132. package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
  133. package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
  134. package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
  135. package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
  136. package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
  137. package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
  138. package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
  139. package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
  140. package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
  141. package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
  142. package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
  143. package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
  144. package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
  145. package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
  146. package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
  147. package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
  148. package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
  149. package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
  150. package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
  151. package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
  152. package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
  153. package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
  154. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
  155. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
  156. package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
  157. package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
  158. package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
  159. package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
  160. package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
  161. package/_stack/apps/tanstack-base/src/styles.css +171 -0
  162. package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
  163. package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
  164. package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
  165. package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
  166. package/_stack/apps/tanstack-base/tsconfig.json +27 -0
  167. package/_stack/apps/tanstack-base/tsr.config.json +3 -0
  168. package/_stack/apps/tanstack-base/vite.config.ts +15 -0
  169. package/_stack/packages/analytics/capability.json +26 -0
  170. package/_stack/packages/cache/capability.json +21 -0
  171. package/_stack/packages/error-tracking/capability.json +21 -0
  172. package/_stack/packages/jobs/capability.json +26 -0
  173. package/_stack/packages/logger/capability.json +21 -0
  174. package/_stack/packages/mailer/capability.json +28 -0
  175. package/_stack/packages/mailer/package.json +37 -0
  176. package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
  177. package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
  178. package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
  179. package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
  180. package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
  181. package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
  182. package/_stack/packages/storage/capability.json +32 -0
  183. package/_stack/patterns/README.md +58 -0
  184. package/_stack/patterns/_baseline/README-author.md +10 -0
  185. package/_stack/patterns/_baseline/biome.jsonc +119 -0
  186. package/_stack/patterns/_baseline/env.ts +31 -0
  187. package/_stack/patterns/_baseline/tsconfig.json +27 -0
  188. package/_stack/patterns/better-auth/pattern.json +73 -0
  189. package/_stack/patterns/better-auth-next/pattern.json +76 -0
  190. package/_stack/patterns/data-table/pattern.json +43 -0
  191. package/_stack/patterns/drizzle/pattern.json +61 -0
  192. package/_stack/patterns/trpc/pattern.json +61 -0
  193. package/_stack/patterns/trpc-next/pattern.json +64 -0
  194. package/index.mjs +216 -0
  195. package/lib/build.mjs +64 -0
  196. package/lib/env.mjs +56 -0
  197. package/lib/identity.mjs +33 -0
  198. package/lib/mailer.mjs +95 -0
  199. package/lib/manifests.mjs +61 -0
  200. package/lib/scaffold.mjs +49 -0
  201. package/lib/strip.mjs +132 -0
  202. package/lib/util.mjs +82 -0
  203. package/package.json +51 -0
  204. package/templates/next/layout.no-trpc.tsx +22 -0
  205. package/templates/tanstack/__root.no-trpc.tsx +63 -0
  206. package/templates/tanstack/router.no-trpc.tsx +24 -0
package/lib/env.mjs ADDED
@@ -0,0 +1,56 @@
1
+ // Rebuilds src/env.ts `server` + `runtimeEnv` blocks from the final key set,
2
+ // and generates .env.example + .env. Adds keys (e.g. a swapped mailer provider)
3
+ // and prunes keys of stripped foundations/capabilities — deterministically.
4
+
5
+ import { editFile, join, write } from './util.mjs'
6
+
7
+ /** valibot schema text per known env key. */
8
+ const SCHEMAS = {
9
+ DATABASE_URL: 'v.pipe(v.string(), v.url())',
10
+ BETTER_AUTH_URL: 'v.pipe(v.string(), v.url())',
11
+ BETTER_AUTH_SECRET: 'v.pipe(v.string(), v.minLength(1))',
12
+ BETTER_AUTH_GOOGLE_CLIENT_ID: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
13
+ BETTER_AUTH_GOOGLE_CLIENT_SECRET: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
14
+ EMAIL_FROM: "v.optional(v.pipe(v.string(), v.email()), 'no-reply@example.com')",
15
+ RESEND_API_KEY: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
16
+ BREVO_API_KEY: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
17
+ AWS_REGION: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
18
+ AWS_ACCESS_KEY_ID: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
19
+ AWS_SECRET_ACCESS_KEY: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
20
+ }
21
+
22
+ /** Placeholder values for the generated .env files. */
23
+ const PLACEHOLDERS = {
24
+ DATABASE_URL: 'postgres://postgres:postgres@localhost:5432/app',
25
+ BETTER_AUTH_URL: 'http://localhost:3000',
26
+ BETTER_AUTH_SECRET: 'change-me-with-a-long-random-string',
27
+ EMAIL_FROM: 'no-reply@example.com',
28
+ }
29
+
30
+ const indent = (s) => ` ${s}`
31
+
32
+ /** Write the final env.ts and .env files. `keys` is an ordered string[]. */
33
+ export function writeEnv(projectDir, keys) {
34
+ const seen = new Set()
35
+ const ordered = keys.filter((k) => SCHEMAS[k] && !seen.has(k) && seen.add(k))
36
+
37
+ const serverBody = ordered.map((k) => indent(`${k}: ${SCHEMAS[k]},`)).join('\n')
38
+ const runtimeBody = [
39
+ indent('NODE_ENV: process.env.NODE_ENV,'),
40
+ ...ordered.map((k) => indent(`${k}: process.env.${k},`)),
41
+ ].join('\n')
42
+
43
+ editFile(join(projectDir, 'src/env.ts'), (src) => {
44
+ let out = src.replace(/ {2}server: \{[\s\S]*?\n {2}\},/, ` server: {\n${serverBody}\n },`)
45
+ out = out.replace(
46
+ / {2}runtimeEnv: \{[\s\S]*?\n {2}\},/,
47
+ ` runtimeEnv: {\n${runtimeBody}\n },`,
48
+ )
49
+ return out
50
+ })
51
+
52
+ const lines = ordered.map((k) => `${k}=${PLACEHOLDERS[k] ?? ''}`)
53
+ const body = `${lines.join('\n')}\n`
54
+ write(join(projectDir, '.env.example'), body)
55
+ write(join(projectDir, '.env'), body)
56
+ }
@@ -0,0 +1,33 @@
1
+ // Step A4 — stamp the project identity: title/meta + a README that ends with
2
+ // the stack's `# Author` footer verbatim.
3
+
4
+ import { STACK_ROOT } from './manifests.mjs'
5
+ import { editFile, join, read, write } from './util.mjs'
6
+
7
+ const titleFiles = {
8
+ next: ['src/app/layout.tsx'],
9
+ tanstack: ['src/routes/__root.tsx'],
10
+ }
11
+
12
+ export function stampIdentity(projectDir, projectName, framework) {
13
+ // Swap the placeholder title 'App' in the root document/metadata.
14
+ for (const rel of titleFiles[framework === 'next' ? 'next' : 'tanstack']) {
15
+ editFile(join(projectDir, rel), (c) => c.replaceAll("title: 'App'", `title: '${projectName}'`))
16
+ }
17
+
18
+ const footer = read(join(STACK_ROOT, 'patterns/_baseline/README-author.md'))
19
+ const readme = `# ${projectName}
20
+
21
+ Bootstrapped from the personal reference stack.
22
+
23
+ ## Getting started
24
+
25
+ \`\`\`bash
26
+ pnpm install
27
+ cp .env.example .env # fill in the values
28
+ pnpm dev
29
+ \`\`\`
30
+
31
+ ${footer}`
32
+ write(join(projectDir, 'README.md'), readme)
33
+ }
package/lib/mailer.mjs ADDED
@@ -0,0 +1,95 @@
1
+ // Mailer provider swap. The base inlines the Resend adapter; if the user picks
2
+ // another provider we swap the adapter files + the composition root (email/index.ts)
3
+ // and return the dep/env deltas. Mirrors the mailer capability manifest.
4
+
5
+ import { STACK_ROOT } from './manifests.mjs'
6
+ import { copy, join, readJSON, remove, write } from './util.mjs'
7
+
8
+ const EMAIL_DIR = 'src/server/email'
9
+
10
+ /** getMailer() body per provider (composition root in email/index.ts). */
11
+ const FACTORY = {
12
+ brevo: {
13
+ import: "import { brevoAdapter } from './adapters/brevo/index'",
14
+ adapter: "brevoAdapter({ apiKey: required(env.BREVO_API_KEY, 'BREVO_API_KEY') })",
15
+ envKeys: ['EMAIL_FROM', 'BREVO_API_KEY'],
16
+ pkgDep: '@getbrevo/brevo',
17
+ },
18
+ ses: {
19
+ import: "import { sesAdapter } from './adapters/ses/index'",
20
+ // SESv2 SDK reads AWS_REGION / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from env.
21
+ adapter: 'sesAdapter()',
22
+ envKeys: ['EMAIL_FROM', 'AWS_REGION', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'],
23
+ pkgDep: '@aws-sdk/client-sesv2',
24
+ },
25
+ }
26
+
27
+ const REQUIRED_HELPER = `
28
+ function required(value: string | undefined, name: string): string {
29
+ if (!value) throw new Error(\`\${name} is required to send email\`)
30
+ return value
31
+ }
32
+ `
33
+
34
+ const INDEX_TS = (cfg) => `import type { ReactElement } from 'react'
35
+ import { env } from '~/env'
36
+ ${cfg.import}
37
+ import type { MailAddress, Mailer } from './core/port'
38
+ import { createMailer } from './factory'
39
+
40
+ export type EmailRecipient = MailAddress
41
+ ${cfg.adapter.includes('required(') ? REQUIRED_HELPER : ''}
42
+ let mailer: Mailer | null = null
43
+ function getMailer(): Mailer {
44
+ if (!mailer) {
45
+ mailer = createMailer({
46
+ from: env.EMAIL_FROM,
47
+ adapter: ${cfg.adapter},
48
+ })
49
+ }
50
+ return mailer
51
+ }
52
+
53
+ export async function sendEmail(params: {
54
+ to: EmailRecipient
55
+ subject: string
56
+ template: ReactElement
57
+ }) {
58
+ return getMailer().send({
59
+ to: params.to,
60
+ subject: params.subject,
61
+ react: params.template,
62
+ })
63
+ }
64
+ `
65
+
66
+ /**
67
+ * Swap the inlined mailer to `provider`. Returns { addDeps, removeDeps, envKeys }.
68
+ * provider === 'resend' is a no-op (the base default).
69
+ */
70
+ export function swapMailer(projectDir, provider) {
71
+ if (provider === 'resend') {
72
+ return { addDeps: {}, removeDeps: [], envKeys: ['EMAIL_FROM', 'RESEND_API_KEY'] }
73
+ }
74
+ const cfg = FACTORY[provider]
75
+ if (!cfg) throw new Error(`Unknown mailer provider: ${provider}`)
76
+
77
+ // Swap adapter files: drop resend, copy the chosen adapter from the package.
78
+ remove(join(projectDir, EMAIL_DIR, 'adapters/resend'))
79
+ copy(
80
+ join(STACK_ROOT, 'packages/mailer/src/adapters', provider),
81
+ join(projectDir, EMAIL_DIR, 'adapters', provider),
82
+ )
83
+
84
+ // Rewrite the composition root.
85
+ write(join(projectDir, EMAIL_DIR, 'index.ts'), INDEX_TS(cfg))
86
+
87
+ // Dep delta — pull the provider's range from the mailer package manifest.
88
+ const mailerPkg = readJSON(join(STACK_ROOT, 'packages/mailer/package.json'))
89
+ const range = mailerPkg.dependencies?.[cfg.pkgDep] ?? 'latest'
90
+ return {
91
+ addDeps: { [cfg.pkgDep]: range },
92
+ removeDeps: ['resend'],
93
+ envKeys: cfg.envKeys,
94
+ }
95
+ }
@@ -0,0 +1,61 @@
1
+ // Loads the stack's pattern + capability manifests so the CLI stays
2
+ // data-driven (file lists, deps, env come from the manifests, not hardcoded).
3
+ // Only the few code "seams" (trpc/auth wiring) are hardcoded in strip.mjs.
4
+
5
+ import { existsSync, readdirSync } from 'node:fs'
6
+ import { dirname, join, resolve } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { readJSON } from './util.mjs'
9
+
10
+ const here = dirname(fileURLToPath(import.meta.url))
11
+ // Published: assets are bundled into cli/_stack (see scripts/bundle.mjs).
12
+ // Dev (inside the monorepo): read them straight from the repo root.
13
+ const bundled = resolve(here, '..', '_stack')
14
+ export const STACK_ROOT = existsSync(bundled) ? bundled : resolve(here, '..', '..')
15
+
16
+ export const loadPatterns = () => {
17
+ const dir = join(STACK_ROOT, 'patterns')
18
+ const out = {}
19
+ for (const name of readdirSync(dir)) {
20
+ if (name.startsWith('_') || name === 'README.md') continue
21
+ try {
22
+ out[name] = readJSON(join(dir, name, 'pattern.json'))
23
+ } catch {
24
+ // not a pattern dir
25
+ }
26
+ }
27
+ return out
28
+ }
29
+
30
+ export const loadCapabilities = () => {
31
+ const dir = join(STACK_ROOT, 'packages')
32
+ const out = {}
33
+ for (const name of readdirSync(dir)) {
34
+ try {
35
+ out[name] = readJSON(join(dir, name, 'capability.json'))
36
+ } catch {
37
+ // not a capability package
38
+ }
39
+ }
40
+ return out
41
+ }
42
+
43
+ /**
44
+ * Logical foundations the wizard offers, mapped to the concrete manifest name
45
+ * per framework. The base apps always contain ALL of these — selection = keep.
46
+ */
47
+ export const foundationManifest = (logical, framework) => {
48
+ const next = framework === 'next'
49
+ switch (logical) {
50
+ case 'drizzle':
51
+ return 'drizzle'
52
+ case 'data-table':
53
+ return 'data-table'
54
+ case 'trpc':
55
+ return next ? 'trpc-next' : 'trpc'
56
+ case 'better-auth':
57
+ return next ? 'better-auth-next' : 'better-auth'
58
+ default:
59
+ return logical
60
+ }
61
+ }
@@ -0,0 +1,49 @@
1
+ // Step A2 — fork a base app into the target dir and make it standalone
2
+ // (its own Biome config, pnpm workspace + build allowlist, project name).
3
+
4
+ import { STACK_ROOT } from './manifests.mjs'
5
+ import { copy, exists, join, readJSON, run, write, writeJSON } from './util.mjs'
6
+
7
+ const RSYNC_EXCLUDES = [
8
+ 'node_modules',
9
+ '.next',
10
+ '.output',
11
+ '.nitro',
12
+ '.tanstack',
13
+ 'dist',
14
+ 'src/routeTree.gen.ts',
15
+ '.env',
16
+ '.env.local',
17
+ ]
18
+
19
+ const PNPM_WORKSPACE = `allowBuilds:
20
+ esbuild: true
21
+ sharp: true
22
+ lightningcss: true
23
+ `
24
+
25
+ /** Copy the base app into projectDir, minus build output & generated files. */
26
+ export function forkBase(framework, projectDir) {
27
+ const base = join(STACK_ROOT, 'apps', framework === 'next' ? 'next-base' : 'tanstack-base')
28
+ if (!exists(base)) throw new Error(`Base app not found: ${base}`)
29
+ const args = ['-a']
30
+ for (const ex of RSYNC_EXCLUDES) args.push('--exclude', ex)
31
+ args.push(`${base}/.`, `${projectDir}/`)
32
+ if (!run('rsync', args)) throw new Error('rsync failed while forking the base app')
33
+ }
34
+
35
+ /** Make the fork standalone (Biome, pnpm workspace, package.json identity). */
36
+ export function makeStandalone(projectDir, projectName) {
37
+ // A fork needs its own Biome config (the base inherits the monorepo root's).
38
+ copy(join(STACK_ROOT, 'patterns/_baseline/biome.jsonc'), join(projectDir, 'biome.jsonc'))
39
+
40
+ // Avoid ERR_PNPM_IGNORED_BUILDS on a fresh install (native build scripts).
41
+ write(join(projectDir, 'pnpm-workspace.yaml'), PNPM_WORKSPACE)
42
+
43
+ const pkgPath = join(projectDir, 'package.json')
44
+ const pkg = readJSON(pkgPath)
45
+ pkg.name = projectName
46
+ delete pkg.private // a leaf project; let the user decide
47
+ pkg.private = true
48
+ writeJSON(pkgPath, pkg)
49
+ }
package/lib/strip.mjs ADDED
@@ -0,0 +1,132 @@
1
+ // Step A3 — reverse-strip the foundations the user did NOT select.
2
+ // Whole-directory deletes (robust against orphans) + the few code "seams"
3
+ // that need surgery (trpc/auth wiring) via shipped reduced variants.
4
+
5
+ import { dirname } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { foundationManifest } from './manifests.mjs'
8
+ import { copy, editFile, join, remove } from './util.mjs'
9
+
10
+ const here = dirname(fileURLToPath(import.meta.url))
11
+ const tpl = (rel) => join(here, '..', 'templates', rel)
12
+
13
+ const ALWAYS_KEEP = new Set(['valibot'])
14
+
15
+ const manifestDeps = (m) => [...(m?.deps ?? []), ...(m?.devDeps ?? [])]
16
+
17
+ /** Logical foundations always present in a base app. */
18
+ export const FOUNDATIONS = ['drizzle', 'trpc', 'better-auth', 'data-table']
19
+
20
+ /**
21
+ * Strip the unselected foundations from the fork.
22
+ * @returns {{ removeDeps: string[], removeScripts: string[] }}
23
+ */
24
+ export function stripFoundations({ projectDir, framework, kept, keptMailer, patterns }) {
25
+ const next = framework === 'next'
26
+ const src = (p) => join(projectDir, 'src', p)
27
+ const dropped = FOUNDATIONS.filter((f) => !kept.has(f))
28
+
29
+ // Dep diff: remove a dropped foundation's deps unless a kept one still needs it.
30
+ const keptDeps = new Set(
31
+ [...kept].flatMap((f) => manifestDeps(patterns[foundationManifest(f, framework)])),
32
+ )
33
+ const removeDeps = new Set()
34
+ const removeScripts = new Set()
35
+ for (const f of dropped) {
36
+ const m = patterns[foundationManifest(f, framework)]
37
+ for (const d of manifestDeps(m)) {
38
+ if (!keptDeps.has(d) && !ALWAYS_KEEP.has(d)) removeDeps.add(d)
39
+ }
40
+ for (const s of Object.keys(m?.scripts ?? {})) removeScripts.add(s)
41
+ }
42
+
43
+ // --- data-table ---
44
+ if (dropped.includes('data-table')) {
45
+ for (const f of ['data-table.tsx', 'infinite-data-table.tsx', 'sortable-header.tsx']) {
46
+ remove(src(join('components', f)))
47
+ }
48
+ }
49
+
50
+ // --- trpc (delete dirs + swap root wiring) ---
51
+ if (dropped.includes('trpc')) {
52
+ remove(src('trpc'))
53
+ remove(src('server/api'))
54
+ if (next) {
55
+ remove(src('app/api/trpc'))
56
+ copy(tpl('next/layout.no-trpc.tsx'), src('app/layout.tsx'))
57
+ } else {
58
+ remove(src('routes/api.trpc.$.tsx'))
59
+ copy(tpl('tanstack/router.no-trpc.tsx'), src('router.tsx'))
60
+ copy(tpl('tanstack/__root.no-trpc.tsx'), src('routes/__root.tsx'))
61
+ // extra TanStack wiring deps that only make sense with trpc/react-query
62
+ for (const d of [
63
+ '@tanstack/react-query',
64
+ '@tanstack/react-query-devtools',
65
+ '@tanstack/react-router-ssr-query',
66
+ 'superjson',
67
+ ]) {
68
+ if (!keptDeps.has(d)) removeDeps.add(d)
69
+ }
70
+ }
71
+ }
72
+
73
+ // --- better-auth ---
74
+ if (dropped.includes('better-auth')) {
75
+ remove(src('server/better-auth'))
76
+ remove(src('server/db/schemas/auth.schema.ts'))
77
+ if (next) {
78
+ remove(src('app/auth'))
79
+ remove(src('app/api/auth'))
80
+ remove(src('app/dashboard'))
81
+ remove(src('server/auth'))
82
+ remove(src('features/auth'))
83
+ } else {
84
+ remove(src('routes/auth.tsx'))
85
+ remove(src('routes/auth'))
86
+ remove(src('routes/_authed.tsx'))
87
+ remove(src('routes/_authed'))
88
+ remove(src('routes/api/auth'))
89
+ remove(src('features/auth'))
90
+ }
91
+ // drop the auth.schema barrel line (kept drizzle owns the barrel)
92
+ editFile(src('server/db/schemas/index.ts'), (c) => {
93
+ const out = c
94
+ .split('\n')
95
+ .filter((l) => !l.includes("'./auth.schema'"))
96
+ .join('\n')
97
+ // keep it a module even when empty, so `import * as schema` still resolves
98
+ return /^export /m.test(out) ? out : `${out.trimEnd()}\nexport {}\n`
99
+ })
100
+ // if trpc survives, strip auth out of its context
101
+ if (kept.has('trpc')) editFile(src('server/api/trpc.ts'), stripAuthFromTrpc)
102
+ }
103
+
104
+ // --- mailer / email-kit (inlined in the base; needed only by better-auth) ---
105
+ if (!keptMailer) {
106
+ remove(src('server/email'))
107
+ remove(src('emails'))
108
+ for (const d of ['resend', 'react-email']) removeDeps.add(d)
109
+ removeScripts.add('email:dev')
110
+ }
111
+
112
+ // --- drizzle (only droppable when nothing depends on it) ---
113
+ if (dropped.includes('drizzle')) {
114
+ remove(src('server/db'))
115
+ remove(join(projectDir, 'drizzle.config.ts'))
116
+ }
117
+
118
+ return { removeDeps: [...removeDeps], removeScripts: [...removeScripts] }
119
+ }
120
+
121
+ /** Remove better-auth coupling from a tRPC context file (both frameworks). */
122
+ function stripAuthFromTrpc(src) {
123
+ return src
124
+ .replace(
125
+ "import { initTRPC, TRPCError } from '@trpc/server'",
126
+ "import { initTRPC } from '@trpc/server'",
127
+ )
128
+ .replace("import { auth } from '~/server/better-auth'\n", '')
129
+ .replace(' const session = await auth.api.getSession({ headers: opts.headers })\n', '')
130
+ .replace('return { db, session, ...opts }', 'return { db, ...opts }')
131
+ .replace(/\n+export const protectedProcedure = t\.procedure[\s\S]*$/, '\n')
132
+ }
package/lib/util.mjs ADDED
@@ -0,0 +1,82 @@
1
+ // Small fs / exec / package.json helpers shared by the CLI modules.
2
+ // No external deps — keep the CLI lean and instantly runnable.
3
+
4
+ import { spawnSync } from 'node:child_process'
5
+ import {
6
+ cpSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from 'node:fs'
14
+ import { dirname, join } from 'node:path'
15
+
16
+ export const read = (p) => readFileSync(p, 'utf8')
17
+ export const write = (p, c) => {
18
+ mkdirSync(dirname(p), { recursive: true })
19
+ writeFileSync(p, c)
20
+ }
21
+ export const exists = (p) => existsSync(p)
22
+ export const readJSON = (p) => JSON.parse(read(p))
23
+ export const writeJSON = (p, obj) => write(p, `${JSON.stringify(obj, null, 2)}\n`)
24
+
25
+ /** Remove a file or directory if it exists (recursive, never throws on absent). */
26
+ export const remove = (p) => {
27
+ if (existsSync(p)) rmSync(p, { recursive: true, force: true })
28
+ }
29
+
30
+ /** Copy a file or directory tree. */
31
+ export const copy = (from, to) => {
32
+ mkdirSync(dirname(to), { recursive: true })
33
+ cpSync(from, to, { recursive: true })
34
+ }
35
+
36
+ /** Edit a file in place via a (content) => content transform. No-op if absent. */
37
+ export const editFile = (p, fn) => {
38
+ if (!existsSync(p)) return false
39
+ const next = fn(read(p))
40
+ if (next != null) write(p, next)
41
+ return true
42
+ }
43
+
44
+ /** Is a directory empty (or absent)? Ignores common noise files. */
45
+ export const isDirEmpty = (p) => {
46
+ if (!existsSync(p)) return true
47
+ const noise = new Set(['.git', '.DS_Store'])
48
+ return readdirSync(p).every((f) => noise.has(f))
49
+ }
50
+
51
+ /** Run a command, inheriting stdio. Returns true on exit 0. */
52
+ export const run = (cmd, args, opts = {}) => {
53
+ const res = spawnSync(cmd, args, { stdio: 'inherit', ...opts })
54
+ return res.status === 0
55
+ }
56
+
57
+ /** Run a command capturing stdout (trimmed). Returns '' on failure. */
58
+ export const runCapture = (cmd, args, opts = {}) => {
59
+ const res = spawnSync(cmd, args, { encoding: 'utf8', ...opts })
60
+ return res.status === 0 ? (res.stdout || '').trim() : ''
61
+ }
62
+
63
+ export { join }
64
+
65
+ // --- package.json helpers (operate on a parsed object, mutate in place) ---
66
+
67
+ export const pkgRemoveDeps = (pkg, names) => {
68
+ for (const field of ['dependencies', 'devDependencies']) {
69
+ if (!pkg[field]) continue
70
+ for (const name of names) delete pkg[field][name]
71
+ }
72
+ }
73
+
74
+ export const pkgRemoveScripts = (pkg, names) => {
75
+ if (!pkg.scripts) return
76
+ for (const name of names) delete pkg.scripts[name]
77
+ }
78
+
79
+ export const pkgAddDeps = (pkg, deps, field = 'dependencies') => {
80
+ pkg[field] = pkg[field] || {}
81
+ for (const [name, range] of Object.entries(deps)) pkg[field][name] = range
82
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@alfredmouelle/create-stack",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Interactive, deterministic installer for the personal reference stack — forks a base app (Next.js / TanStack Start) and strips it to your selection.",
6
+ "author": {
7
+ "name": "Alfred MOUELLE",
8
+ "email": "alfredmouelle@gmail.com",
9
+ "url": "https://alfredmouelle.com"
10
+ },
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "create",
14
+ "scaffold",
15
+ "starter",
16
+ "boilerplate",
17
+ "tanstack-start",
18
+ "nextjs",
19
+ "drizzle",
20
+ "trpc",
21
+ "better-auth"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/alfredmouelle/stack.git",
26
+ "directory": "cli"
27
+ },
28
+ "bin": {
29
+ "create-stack": "./index.mjs"
30
+ },
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "files": [
35
+ "index.mjs",
36
+ "lib",
37
+ "templates",
38
+ "_stack",
39
+ "README.md"
40
+ ],
41
+ "scripts": {
42
+ "bundle": "node scripts/bundle.mjs",
43
+ "prepack": "node scripts/bundle.mjs"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@clack/prompts": "^1.6.0"
50
+ }
51
+ }
@@ -0,0 +1,22 @@
1
+ import type { Metadata } from 'next'
2
+ import type { ReactNode } from 'react'
3
+ import { ThemeProvider } from '~/components/theme-provider'
4
+ import './globals.css'
5
+
6
+ export const metadata: Metadata = {
7
+ title: 'App',
8
+ description: 'Next.js base.',
9
+ authors: [{ name: 'Alfred MOUELLE', url: 'https://alfredmouelle.com' }],
10
+ }
11
+
12
+ export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
13
+ return (
14
+ <html lang="en" suppressHydrationWarning>
15
+ <body>
16
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
17
+ {children}
18
+ </ThemeProvider>
19
+ </body>
20
+ </html>
21
+ )
22
+ }
@@ -0,0 +1,63 @@
1
+ import { TanStackDevtools } from '@tanstack/react-devtools'
2
+ import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router'
3
+ import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
4
+
5
+ import appCss from '../styles.css?url'
6
+
7
+ // Runs before hydration to set the theme class and avoid a flash of wrong theme.
8
+ const themeScript = `(function(){try{var t=localStorage.getItem('theme')||'system';var m=window.matchMedia('(prefers-color-scheme:dark)').matches;document.documentElement.classList.toggle('dark',t==='dark'||(t==='system'&&m));}catch(e){}})();`
9
+
10
+ export const Route = createRootRoute({
11
+ head: () => ({
12
+ meta: [
13
+ {
14
+ charSet: 'utf-8',
15
+ },
16
+ {
17
+ name: 'viewport',
18
+ content: 'width=device-width, initial-scale=1',
19
+ },
20
+ {
21
+ name: 'author',
22
+ content: 'Alfred MOUELLE',
23
+ },
24
+ {
25
+ title: 'App',
26
+ },
27
+ ],
28
+ links: [
29
+ {
30
+ rel: 'stylesheet',
31
+ href: appCss,
32
+ },
33
+ ],
34
+ }),
35
+ shellComponent: RootDocument,
36
+ })
37
+
38
+ function RootDocument({ children }: { children: React.ReactNode }) {
39
+ return (
40
+ <html lang="en">
41
+ <head>
42
+ {/** biome-ignore lint/security/noDangerouslySetInnerHtml: anti-FOUC theme script */}
43
+ <script dangerouslySetInnerHTML={{ __html: themeScript }} />
44
+ <HeadContent />
45
+ </head>
46
+ <body>
47
+ {children}
48
+ <TanStackDevtools
49
+ config={{
50
+ position: 'bottom-right',
51
+ }}
52
+ plugins={[
53
+ {
54
+ name: 'Tanstack Router',
55
+ render: <TanStackRouterDevtoolsPanel />,
56
+ },
57
+ ]}
58
+ />
59
+ <Scripts />
60
+ </body>
61
+ </html>
62
+ )
63
+ }
@@ -0,0 +1,24 @@
1
+ import { createRouter as createTanStackRouter } from '@tanstack/react-router'
2
+ import type { ReactNode } from 'react'
3
+ import { ThemeProvider } from './components/theme-provider'
4
+ import { routeTree } from './routeTree.gen'
5
+
6
+ export function getRouter() {
7
+ const router = createTanStackRouter({
8
+ routeTree,
9
+ scrollRestoration: true,
10
+ defaultPreload: 'intent',
11
+
12
+ Wrap: (props: { children: ReactNode }) => (
13
+ <ThemeProvider defaultTheme="system">{props.children}</ThemeProvider>
14
+ ),
15
+ })
16
+
17
+ return router
18
+ }
19
+
20
+ declare module '@tanstack/react-router' {
21
+ interface Register {
22
+ router: ReturnType<typeof getRouter>
23
+ }
24
+ }