@alfredmouelle/create-stack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/_stack/apps/next-base/.dockerignore +10 -0
- package/_stack/apps/next-base/Dockerfile +34 -0
- package/_stack/apps/next-base/README.md +32 -0
- package/_stack/apps/next-base/components.json +25 -0
- package/_stack/apps/next-base/drizzle.config.ts +16 -0
- package/_stack/apps/next-base/next.config.ts +8 -0
- package/_stack/apps/next-base/package.json +70 -0
- package/_stack/apps/next-base/postcss.config.mjs +7 -0
- package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
- package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
- package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
- package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
- package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
- package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
- package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
- package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
- package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
- package/_stack/apps/next-base/src/app/globals.css +171 -0
- package/_stack/apps/next-base/src/app/layout.tsx +23 -0
- package/_stack/apps/next-base/src/app/page.tsx +15 -0
- package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
- package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
- package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
- package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
- package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
- package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
- package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/next-base/src/env.ts +41 -0
- package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
- package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/next-base/src/lib/date.ts +4 -0
- package/_stack/apps/next-base/src/lib/utils.ts +6 -0
- package/_stack/apps/next-base/src/server/api/root.ts +10 -0
- package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
- package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
- package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
- package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
- package/_stack/apps/next-base/src/server/db/index.ts +6 -0
- package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/next-base/src/server/email/index.ts +36 -0
- package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
- package/_stack/apps/next-base/src/trpc/server.ts +23 -0
- package/_stack/apps/next-base/tsconfig.json +37 -0
- package/_stack/apps/tanstack-base/.dockerignore +13 -0
- package/_stack/apps/tanstack-base/Dockerfile +28 -0
- package/_stack/apps/tanstack-base/README.md +31 -0
- package/_stack/apps/tanstack-base/components.json +25 -0
- package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
- package/_stack/apps/tanstack-base/package.json +85 -0
- package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
- package/_stack/apps/tanstack-base/public/logo192.png +0 -0
- package/_stack/apps/tanstack-base/public/logo512.png +0 -0
- package/_stack/apps/tanstack-base/public/manifest.json +25 -0
- package/_stack/apps/tanstack-base/public/robots.txt +3 -0
- package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
- package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
- package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
- package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
- package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
- package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/tanstack-base/src/env.ts +41 -0
- package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
- package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
- package/_stack/apps/tanstack-base/src/router.tsx +40 -0
- package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
- package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
- package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
- package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
- package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
- package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
- package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
- package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
- package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
- package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
- package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
- package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
- package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
- package/_stack/apps/tanstack-base/src/styles.css +171 -0
- package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
- package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
- package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
- package/_stack/apps/tanstack-base/tsconfig.json +27 -0
- package/_stack/apps/tanstack-base/tsr.config.json +3 -0
- package/_stack/apps/tanstack-base/vite.config.ts +15 -0
- package/_stack/packages/analytics/capability.json +26 -0
- package/_stack/packages/cache/capability.json +21 -0
- package/_stack/packages/error-tracking/capability.json +21 -0
- package/_stack/packages/jobs/capability.json +26 -0
- package/_stack/packages/logger/capability.json +21 -0
- package/_stack/packages/mailer/capability.json +28 -0
- package/_stack/packages/mailer/package.json +37 -0
- package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
- package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
- package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
- package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
- package/_stack/packages/storage/capability.json +32 -0
- package/_stack/patterns/README.md +58 -0
- package/_stack/patterns/_baseline/README-author.md +10 -0
- package/_stack/patterns/_baseline/biome.jsonc +119 -0
- package/_stack/patterns/_baseline/env.ts +31 -0
- package/_stack/patterns/_baseline/tsconfig.json +27 -0
- package/_stack/patterns/better-auth/pattern.json +73 -0
- package/_stack/patterns/better-auth-next/pattern.json +76 -0
- package/_stack/patterns/data-table/pattern.json +43 -0
- package/_stack/patterns/drizzle/pattern.json +61 -0
- package/_stack/patterns/trpc/pattern.json +61 -0
- package/_stack/patterns/trpc-next/pattern.json +64 -0
- package/index.mjs +216 -0
- package/lib/build.mjs +64 -0
- package/lib/env.mjs +56 -0
- package/lib/identity.mjs +33 -0
- package/lib/mailer.mjs +95 -0
- package/lib/manifests.mjs +61 -0
- package/lib/scaffold.mjs +49 -0
- package/lib/strip.mjs +132 -0
- package/lib/util.mjs +82 -0
- package/package.json +51 -0
- package/templates/next/layout.no-trpc.tsx +22 -0
- package/templates/tanstack/__root.no-trpc.tsx +63 -0
- package/templates/tanstack/router.no-trpc.tsx +24 -0
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
|
+
}
|
package/lib/identity.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/scaffold.mjs
ADDED
|
@@ -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
|
+
}
|