@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.
- package/README.md +8 -13
- package/_stack/apps/next-base/.turbo/turbo-typecheck.log +1 -0
- package/_stack/apps/next-base/.vscode/settings.json +35 -0
- package/_stack/apps/next-base/.zed/settings.json +45 -0
- package/_stack/apps/next-base/src/components/ui/spinner.tsx +1 -1
- package/_stack/apps/next-base/src/emails/components/components.tsx +2 -2
- package/_stack/apps/next-base/src/emails/components/context.tsx +1 -1
- package/_stack/apps/next-base/src/emails/components/theme.ts +6 -7
- package/_stack/apps/next-base/src/env.ts +2 -3
- package/_stack/apps/next-base/src/lib/date.ts +1 -1
- package/_stack/apps/next-base/src/server/auth/guards.ts +1 -1
- package/_stack/apps/next-base/src/server/better-auth/config.ts +1 -1
- package/_stack/apps/next-base/src/server/better-auth/server.ts +2 -2
- package/_stack/apps/next-base/src/server/db/schemas/index.ts +1 -1
- package/_stack/apps/next-base/src/server/db/seed.ts +2 -2
- package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +3 -3
- package/_stack/apps/next-base/src/server/email/core/address.ts +3 -3
- package/_stack/apps/next-base/src/server/email/core/port.ts +13 -20
- package/_stack/apps/next-base/src/server/email/core/render.ts +2 -2
- package/_stack/apps/next-base/src/server/email/factory.ts +7 -9
- package/_stack/apps/next-base/src/server/email/index.ts +1 -1
- package/_stack/apps/next-base/src/trpc/react.tsx +2 -2
- package/_stack/apps/next-base/src/trpc/server.ts +1 -1
- package/_stack/apps/tanstack-base/.turbo/turbo-typecheck.log +1 -0
- package/_stack/apps/tanstack-base/.vscode/settings.json +35 -0
- package/_stack/apps/tanstack-base/.zed/settings.json +45 -0
- package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +1 -1
- package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +1 -1
- package/_stack/apps/tanstack-base/src/emails/components/components.tsx +2 -2
- package/_stack/apps/tanstack-base/src/emails/components/context.tsx +1 -1
- package/_stack/apps/tanstack-base/src/emails/components/theme.ts +6 -7
- package/_stack/apps/tanstack-base/src/env.ts +2 -6
- package/_stack/apps/tanstack-base/src/lib/date.ts +1 -1
- package/_stack/apps/tanstack-base/src/routes/__root.tsx +1 -1
- package/_stack/apps/tanstack-base/src/routes/_authed.tsx +1 -4
- package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +1 -1
- package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +1 -2
- package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +1 -1
- package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +6 -7
- package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +1 -1
- package/_stack/apps/tanstack-base/src/server/db/seed.ts +2 -2
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +3 -3
- package/_stack/apps/tanstack-base/src/server/email/core/address.ts +3 -3
- package/_stack/apps/tanstack-base/src/server/email/core/port.ts +12 -22
- package/_stack/apps/tanstack-base/src/server/email/core/render.ts +1 -1
- package/_stack/apps/tanstack-base/src/server/email/factory.ts +7 -8
- package/_stack/apps/tanstack-base/src/server/email/index.ts +1 -1
- package/_stack/packages/mailer/src/adapters/brevo/index.ts +3 -3
- package/_stack/packages/mailer/src/adapters/resend/index.ts +3 -3
- package/_stack/packages/mailer/src/adapters/ses/config.ts +3 -3
- package/_stack/packages/mailer/src/adapters/ses/index.ts +4 -5
- package/index.mjs +30 -47
- package/lib/build.mjs +6 -14
- package/lib/env.mjs +5 -6
- package/lib/foundations.mjs +35 -0
- package/lib/identity.mjs +4 -5
- package/lib/mailer.mjs +9 -13
- package/lib/paths.mjs +15 -0
- package/lib/scaffold.mjs +11 -11
- package/lib/strip.mjs +9 -24
- package/lib/util.mjs +8 -9
- package/package.json +1 -1
- package/_stack/packages/analytics/capability.json +0 -26
- package/_stack/packages/cache/capability.json +0 -21
- package/_stack/packages/error-tracking/capability.json +0 -21
- package/_stack/packages/jobs/capability.json +0 -26
- package/_stack/packages/logger/capability.json +0 -21
- package/_stack/packages/mailer/capability.json +0 -28
- package/_stack/packages/storage/capability.json +0 -32
- package/_stack/patterns/README.md +0 -58
- package/_stack/patterns/_baseline/env.ts +0 -31
- package/_stack/patterns/_baseline/tsconfig.json +0 -27
- package/_stack/patterns/better-auth/pattern.json +0 -73
- package/_stack/patterns/better-auth-next/pattern.json +0 -76
- package/_stack/patterns/data-table/pattern.json +0 -43
- package/_stack/patterns/drizzle/pattern.json +0 -61
- package/_stack/patterns/trpc/pattern.json +0 -61
- package/_stack/patterns/trpc-next/pattern.json +0 -64
- package/lib/manifests.mjs +0 -61
- /package/{_stack/patterns/_baseline → templates}/README-author.md +0 -0
- /package/{_stack/patterns/_baseline → templates}/biome.jsonc +0 -0
package/lib/build.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Pure build phase (no prompts
|
|
2
|
-
//
|
|
1
|
+
// Pure build phase (no prompts/install): fork → strip → mailer → env → identity.
|
|
2
|
+
// Shared by index.mjs (post-wizard) and the test harness.
|
|
3
3
|
|
|
4
4
|
import { writeEnv } from './env.mjs'
|
|
5
5
|
import { stampIdentity } from './identity.mjs'
|
|
@@ -13,33 +13,25 @@ import { join, pkgAddDeps, pkgRemoveDeps, pkgRemoveScripts, readJSON, writeJSON
|
|
|
13
13
|
* @param {string} o.projectDir absolute target dir (must be empty)
|
|
14
14
|
* @param {string} o.projectName
|
|
15
15
|
* @param {'next'|'tanstack'} o.framework
|
|
16
|
-
* @param {Set<string>} o.kept
|
|
16
|
+
* @param {Set<string>} o.kept foundations to keep (deps pre-resolved)
|
|
17
17
|
* @param {'resend'|'brevo'|'ses'|'none'} o.mailerProvider
|
|
18
|
-
* @param {object} o.patterns loadPatterns()
|
|
19
18
|
* @returns {{ kept: string[], keptMailer: boolean, mailerProvider: string, envKeys: string[] }}
|
|
20
19
|
*/
|
|
21
|
-
export function buildProject({
|
|
22
|
-
projectDir,
|
|
23
|
-
projectName,
|
|
24
|
-
framework,
|
|
25
|
-
kept,
|
|
26
|
-
mailerProvider,
|
|
27
|
-
patterns,
|
|
28
|
-
}) {
|
|
20
|
+
export function buildProject({ projectDir, projectName, framework, kept, mailerProvider }) {
|
|
29
21
|
const authKept = kept.has('better-auth')
|
|
30
22
|
const keptMailer = mailerProvider !== 'none'
|
|
31
23
|
|
|
32
24
|
forkBase(framework, projectDir)
|
|
33
25
|
makeStandalone(projectDir, projectName, framework)
|
|
34
26
|
|
|
35
|
-
const strip = stripFoundations({ projectDir, framework, kept, keptMailer
|
|
27
|
+
const strip = stripFoundations({ projectDir, framework, kept, keptMailer })
|
|
36
28
|
const mailer = keptMailer
|
|
37
29
|
? swapMailer(projectDir, mailerProvider)
|
|
38
30
|
: { addDeps: {}, removeDeps: [], envKeys: [] }
|
|
39
31
|
|
|
40
32
|
const pkgPath = join(projectDir, 'package.json')
|
|
41
33
|
const pkg = readJSON(pkgPath)
|
|
42
|
-
pkg.description = `${projectName} —
|
|
34
|
+
pkg.description = `${projectName} — scaffolded from the personal reference stack.`
|
|
43
35
|
pkgRemoveDeps(pkg, [...strip.removeDeps, ...mailer.removeDeps])
|
|
44
36
|
pkgRemoveScripts(pkg, strip.removeScripts)
|
|
45
37
|
pkgAddDeps(pkg, mailer.addDeps)
|
package/lib/env.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// Rebuilds src/env.ts `server` + `runtimeEnv`
|
|
2
|
-
//
|
|
3
|
-
// and prunes keys of stripped foundations/capabilities — deterministically.
|
|
1
|
+
// Rebuilds src/env.ts `server` + `runtimeEnv` from the final key set; generates
|
|
2
|
+
// .env.example + .env. Adds swapped-mailer keys, prunes stripped ones, deterministically.
|
|
4
3
|
|
|
5
4
|
import { editFile, join, write } from './util.mjs'
|
|
6
5
|
|
|
7
|
-
/** valibot schema text per
|
|
6
|
+
/** valibot schema text per env key. */
|
|
8
7
|
const SCHEMAS = {
|
|
9
8
|
DATABASE_URL: 'v.pipe(v.string(), v.url())',
|
|
10
9
|
BETTER_AUTH_URL: 'v.pipe(v.string(), v.url())',
|
|
@@ -19,7 +18,7 @@ const SCHEMAS = {
|
|
|
19
18
|
AWS_SECRET_ACCESS_KEY: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
/** Placeholder values for
|
|
21
|
+
/** Placeholder values for generated .env files. */
|
|
23
22
|
const PLACEHOLDERS = {
|
|
24
23
|
DATABASE_URL: 'postgres://postgres:postgres@localhost:5432/app',
|
|
25
24
|
BETTER_AUTH_URL: 'http://localhost:3000',
|
|
@@ -29,7 +28,7 @@ const PLACEHOLDERS = {
|
|
|
29
28
|
|
|
30
29
|
const indent = (s) => ` ${s}`
|
|
31
30
|
|
|
32
|
-
/** Write
|
|
31
|
+
/** Write final env.ts + .env files. `keys`: ordered string[]. */
|
|
33
32
|
export function writeEnv(projectDir, keys) {
|
|
34
33
|
const seen = new Set()
|
|
35
34
|
const ordered = keys.filter((k) => SCHEMAS[k] && !seen.has(k) && seen.add(k))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Foundations the CLI can strip + the npm footprint each adds (deps/devDeps/scripts only).
|
|
2
|
+
// File deletes, env keys, code seams live in strip.mjs/build.mjs (need framework-specific surgery).
|
|
3
|
+
|
|
4
|
+
export const FOUNDATIONS = ['drizzle', 'trpc', 'better-auth', 'data-table']
|
|
5
|
+
|
|
6
|
+
const DATA = {
|
|
7
|
+
drizzle: {
|
|
8
|
+
deps: ['drizzle-orm', 'pg'],
|
|
9
|
+
devDeps: ['drizzle-kit', 'dotenv', 'tsx', '@types/pg', '@faker-js/faker'],
|
|
10
|
+
scripts: ['db:generate', 'db:migrate', 'db:push', 'db:studio', 'db:seed'],
|
|
11
|
+
},
|
|
12
|
+
trpc: {
|
|
13
|
+
deps: ['@trpc/server', '@trpc/client', '@tanstack/react-query', 'superjson', 'valibot'],
|
|
14
|
+
// React Query bridge differs per framework
|
|
15
|
+
perFramework: { tanstack: ['@trpc/tanstack-react-query'], next: ['@trpc/react-query'] },
|
|
16
|
+
},
|
|
17
|
+
'better-auth': {
|
|
18
|
+
deps: ['better-auth'],
|
|
19
|
+
},
|
|
20
|
+
'data-table': {
|
|
21
|
+
deps: ['@tanstack/react-table'],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Every npm dep (prod + dev) a foundation contributes, for `framework`. */
|
|
26
|
+
export function foundationDeps(name, framework) {
|
|
27
|
+
const d = DATA[name]
|
|
28
|
+
if (!d) return []
|
|
29
|
+
return [...(d.deps ?? []), ...(d.devDeps ?? []), ...(d.perFramework?.[framework] ?? [])]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** package.json script names a foundation adds. */
|
|
33
|
+
export function foundationScripts(name) {
|
|
34
|
+
return DATA[name]?.scripts ?? []
|
|
35
|
+
}
|
package/lib/identity.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
// the stack's `# Author` footer verbatim.
|
|
1
|
+
// Stamp project identity: title/meta + README ending with the `# Author` footer verbatim.
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { TEMPLATES } from './paths.mjs'
|
|
5
4
|
import { editFile, join, read, write } from './util.mjs'
|
|
6
5
|
|
|
7
6
|
const titleFiles = {
|
|
@@ -10,12 +9,12 @@ const titleFiles = {
|
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export function stampIdentity(projectDir, projectName, framework) {
|
|
13
|
-
//
|
|
12
|
+
// swap placeholder title 'App' in root document/metadata
|
|
14
13
|
for (const rel of titleFiles[framework === 'next' ? 'next' : 'tanstack']) {
|
|
15
14
|
editFile(join(projectDir, rel), (c) => c.replaceAll("title: 'App'", `title: '${projectName}'`))
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
const footer = read(join(
|
|
17
|
+
const footer = read(join(TEMPLATES, 'README-author.md'))
|
|
19
18
|
const readme = `# ${projectName}
|
|
20
19
|
|
|
21
20
|
Bootstrapped from the personal reference stack.
|
package/lib/mailer.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
// Mailer provider swap.
|
|
2
|
-
//
|
|
3
|
-
// and return the dep/env deltas. Mirrors the mailer capability manifest.
|
|
1
|
+
// Mailer provider swap. Base inlines Resend; other providers swap adapter files +
|
|
2
|
+
// composition root (email/index.ts) and return dep/env deltas. Mirrors the mailer manifest.
|
|
4
3
|
|
|
5
|
-
import { STACK_ROOT } from './
|
|
4
|
+
import { STACK_ROOT } from './paths.mjs'
|
|
6
5
|
import { copy, join, readJSON, remove, write } from './util.mjs'
|
|
7
6
|
|
|
8
7
|
const EMAIL_DIR = 'src/server/email'
|
|
9
8
|
|
|
10
|
-
/** getMailer() body per provider (composition root
|
|
9
|
+
/** getMailer() body per provider (composition root). */
|
|
11
10
|
const FACTORY = {
|
|
12
11
|
brevo: {
|
|
13
12
|
import: "import { brevoAdapter } from './adapters/brevo/index'",
|
|
@@ -17,7 +16,7 @@ const FACTORY = {
|
|
|
17
16
|
},
|
|
18
17
|
ses: {
|
|
19
18
|
import: "import { sesAdapter } from './adapters/ses/index'",
|
|
20
|
-
// SESv2 SDK reads AWS_REGION / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from env
|
|
19
|
+
// SESv2 SDK reads AWS_REGION / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from env
|
|
21
20
|
adapter: 'sesAdapter()',
|
|
22
21
|
envKeys: ['EMAIL_FROM', 'AWS_REGION', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'],
|
|
23
22
|
pkgDep: '@aws-sdk/client-sesv2',
|
|
@@ -63,10 +62,7 @@ export async function sendEmail(params: {
|
|
|
63
62
|
}
|
|
64
63
|
`
|
|
65
64
|
|
|
66
|
-
/**
|
|
67
|
-
* Swap the inlined mailer to `provider`. Returns { addDeps, removeDeps, envKeys }.
|
|
68
|
-
* provider === 'resend' is a no-op (the base default).
|
|
69
|
-
*/
|
|
65
|
+
/** Swap inlined mailer to `provider` → { addDeps, removeDeps, envKeys }. 'resend' is a no-op (base default). */
|
|
70
66
|
export function swapMailer(projectDir, provider) {
|
|
71
67
|
if (provider === 'resend') {
|
|
72
68
|
return { addDeps: {}, removeDeps: [], envKeys: ['EMAIL_FROM', 'RESEND_API_KEY'] }
|
|
@@ -74,17 +70,17 @@ export function swapMailer(projectDir, provider) {
|
|
|
74
70
|
const cfg = FACTORY[provider]
|
|
75
71
|
if (!cfg) throw new Error(`Unknown mailer provider: ${provider}`)
|
|
76
72
|
|
|
77
|
-
//
|
|
73
|
+
// swap adapter files: drop resend, copy chosen adapter from package
|
|
78
74
|
remove(join(projectDir, EMAIL_DIR, 'adapters/resend'))
|
|
79
75
|
copy(
|
|
80
76
|
join(STACK_ROOT, 'packages/mailer/src/adapters', provider),
|
|
81
77
|
join(projectDir, EMAIL_DIR, 'adapters', provider),
|
|
82
78
|
)
|
|
83
79
|
|
|
84
|
-
//
|
|
80
|
+
// rewrite composition root
|
|
85
81
|
write(join(projectDir, EMAIL_DIR, 'index.ts'), INDEX_TS(cfg))
|
|
86
82
|
|
|
87
|
-
//
|
|
83
|
+
// dep delta — pull provider's range from mailer package manifest
|
|
88
84
|
const mailerPkg = readJSON(join(STACK_ROOT, 'packages/mailer/package.json'))
|
|
89
85
|
const range = mailerPkg.dependencies?.[cfg.pkgDep] ?? 'latest'
|
|
90
86
|
return {
|
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Where the bundled stack assets live: cli/_stack when published (scripts/bundle.mjs), repo root in dev.
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const bundled = resolve(here, '..', '_stack')
|
|
9
|
+
|
|
10
|
+
// Forkable source (base apps + mailer adapters): _stack when published, monorepo root in dev.
|
|
11
|
+
export const STACK_ROOT = existsSync(bundled) ? bundled : resolve(here, '..', '..')
|
|
12
|
+
|
|
13
|
+
// CLI-owned templates injected into the fork (root-wiring variants, biome.jsonc,
|
|
14
|
+
// # Author README footer). Always shipped in the package.
|
|
15
|
+
export const TEMPLATES = resolve(here, '..', 'templates')
|
package/lib/scaffold.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
// (
|
|
1
|
+
// Fork a base app into the target dir + make it standalone
|
|
2
|
+
// (own Biome config, pnpm workspace + build allowlist, project name).
|
|
3
3
|
|
|
4
|
-
import { STACK_ROOT } from './
|
|
4
|
+
import { STACK_ROOT, TEMPLATES } from './paths.mjs'
|
|
5
5
|
import { copy, exists, join, readJSON, run, write, writeJSON } from './util.mjs'
|
|
6
6
|
|
|
7
7
|
const RSYNC_EXCLUDES = [
|
|
@@ -22,8 +22,8 @@ const PNPM_WORKSPACE = `allowBuilds:
|
|
|
22
22
|
lightningcss: true
|
|
23
23
|
`
|
|
24
24
|
|
|
25
|
-
// Generated explicitly: npm strips `.gitignore` from published tarballs, so
|
|
26
|
-
//
|
|
25
|
+
// Generated explicitly: npm strips `.gitignore` from published tarballs, so the
|
|
26
|
+
// bundled base app's copy won't survive. Both keep `.env.example`.
|
|
27
27
|
const GITIGNORE = {
|
|
28
28
|
tanstack: `node_modules
|
|
29
29
|
.DS_Store
|
|
@@ -74,7 +74,7 @@ next-env.d.ts
|
|
|
74
74
|
`,
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/** Copy
|
|
77
|
+
/** Copy base app into projectDir, minus build output & generated files. */
|
|
78
78
|
export function forkBase(framework, projectDir) {
|
|
79
79
|
const base = join(STACK_ROOT, 'apps', framework === 'next' ? 'next-base' : 'tanstack-base')
|
|
80
80
|
if (!exists(base)) throw new Error(`Base app not found: ${base}`)
|
|
@@ -86,19 +86,19 @@ export function forkBase(framework, projectDir) {
|
|
|
86
86
|
|
|
87
87
|
/** Make the fork standalone (Biome, pnpm workspace, .gitignore, identity). */
|
|
88
88
|
export function makeStandalone(projectDir, projectName, framework) {
|
|
89
|
-
//
|
|
90
|
-
copy(join(
|
|
89
|
+
// fork needs its own Biome config (base inherits the monorepo root's)
|
|
90
|
+
copy(join(TEMPLATES, 'biome.jsonc'), join(projectDir, 'biome.jsonc'))
|
|
91
91
|
|
|
92
|
-
// Biome
|
|
92
|
+
// Biome vcs.useIgnoreFile needs a .gitignore
|
|
93
93
|
write(join(projectDir, '.gitignore'), GITIGNORE[framework === 'next' ? 'next' : 'tanstack'])
|
|
94
94
|
|
|
95
|
-
//
|
|
95
|
+
// avoid ERR_PNPM_IGNORED_BUILDS on fresh install (native build scripts)
|
|
96
96
|
write(join(projectDir, 'pnpm-workspace.yaml'), PNPM_WORKSPACE)
|
|
97
97
|
|
|
98
98
|
const pkgPath = join(projectDir, 'package.json')
|
|
99
99
|
const pkg = readJSON(pkgPath)
|
|
100
100
|
pkg.name = projectName
|
|
101
|
-
delete pkg.private //
|
|
101
|
+
delete pkg.private // leaf project
|
|
102
102
|
pkg.private = true
|
|
103
103
|
writeJSON(pkgPath, pkg)
|
|
104
104
|
}
|
package/lib/strip.mjs
CHANGED
|
@@ -1,43 +1,28 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Whole-directory deletes (robust against orphans) + the few code "seams"
|
|
3
|
-
// that need surgery (trpc/auth wiring) via shipped reduced variants.
|
|
1
|
+
// Strip unselected foundations: whole-dir deletes + code-seam variants (trpc/auth).
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { foundationManifest } from './manifests.mjs'
|
|
3
|
+
import { FOUNDATIONS, foundationDeps, foundationScripts } from './foundations.mjs'
|
|
4
|
+
import { TEMPLATES } from './paths.mjs'
|
|
8
5
|
import { copy, editFile, join, remove } from './util.mjs'
|
|
9
6
|
|
|
10
|
-
const
|
|
11
|
-
const tpl = (rel) => join(here, '..', 'templates', rel)
|
|
7
|
+
const tpl = (rel) => join(TEMPLATES, rel)
|
|
12
8
|
|
|
13
9
|
const ALWAYS_KEEP = new Set(['valibot'])
|
|
14
10
|
|
|
15
|
-
|
|
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 }) {
|
|
11
|
+
/** @returns {{ removeDeps: string[], removeScripts: string[] }} */
|
|
12
|
+
export function stripFoundations({ projectDir, framework, kept, keptMailer }) {
|
|
25
13
|
const next = framework === 'next'
|
|
26
14
|
const src = (p) => join(projectDir, 'src', p)
|
|
27
15
|
const dropped = FOUNDATIONS.filter((f) => !kept.has(f))
|
|
28
16
|
|
|
29
17
|
// 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
|
-
)
|
|
18
|
+
const keptDeps = new Set([...kept].flatMap((f) => foundationDeps(f, framework)))
|
|
33
19
|
const removeDeps = new Set()
|
|
34
20
|
const removeScripts = new Set()
|
|
35
21
|
for (const f of dropped) {
|
|
36
|
-
const
|
|
37
|
-
for (const d of manifestDeps(m)) {
|
|
22
|
+
for (const d of foundationDeps(f, framework)) {
|
|
38
23
|
if (!keptDeps.has(d) && !ALWAYS_KEEP.has(d)) removeDeps.add(d)
|
|
39
24
|
}
|
|
40
|
-
for (const s of
|
|
25
|
+
for (const s of foundationScripts(f)) removeScripts.add(s)
|
|
41
26
|
}
|
|
42
27
|
|
|
43
28
|
// --- data-table ---
|
package/lib/util.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// No external deps — keep the CLI lean and instantly runnable.
|
|
1
|
+
// fs / exec / package.json helpers shared by the CLI. No external deps.
|
|
3
2
|
|
|
4
3
|
import { spawnSync } from 'node:child_process'
|
|
5
4
|
import {
|
|
@@ -22,18 +21,18 @@ export const exists = (p) => existsSync(p)
|
|
|
22
21
|
export const readJSON = (p) => JSON.parse(read(p))
|
|
23
22
|
export const writeJSON = (p, obj) => write(p, `${JSON.stringify(obj, null, 2)}\n`)
|
|
24
23
|
|
|
25
|
-
/** Remove
|
|
24
|
+
/** Remove file/dir if present (recursive, never throws on absent). */
|
|
26
25
|
export const remove = (p) => {
|
|
27
26
|
if (existsSync(p)) rmSync(p, { recursive: true, force: true })
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
/** Copy a file
|
|
29
|
+
/** Copy a file/dir tree. */
|
|
31
30
|
export const copy = (from, to) => {
|
|
32
31
|
mkdirSync(dirname(to), { recursive: true })
|
|
33
32
|
cpSync(from, to, { recursive: true })
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
/** Edit a file in place via
|
|
35
|
+
/** Edit a file in place via (content) => content. No-op if absent. */
|
|
37
36
|
export const editFile = (p, fn) => {
|
|
38
37
|
if (!existsSync(p)) return false
|
|
39
38
|
const next = fn(read(p))
|
|
@@ -41,20 +40,20 @@ export const editFile = (p, fn) => {
|
|
|
41
40
|
return true
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
/**
|
|
43
|
+
/** Dir empty (or absent)? Ignores noise files. */
|
|
45
44
|
export const isDirEmpty = (p) => {
|
|
46
45
|
if (!existsSync(p)) return true
|
|
47
46
|
const noise = new Set(['.git', '.DS_Store'])
|
|
48
47
|
return readdirSync(p).every((f) => noise.has(f))
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
/** Run a command
|
|
50
|
+
/** Run a command (inherits stdio). True on exit 0. */
|
|
52
51
|
export const run = (cmd, args, opts = {}) => {
|
|
53
52
|
const res = spawnSync(cmd, args, { stdio: 'inherit', ...opts })
|
|
54
53
|
return res.status === 0
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
/** Run a command capturing stdout
|
|
56
|
+
/** Run a command, capturing trimmed stdout. '' on failure. */
|
|
58
57
|
export const runCapture = (cmd, args, opts = {}) => {
|
|
59
58
|
const res = spawnSync(cmd, args, { encoding: 'utf8', ...opts })
|
|
60
59
|
return res.status === 0 ? (res.stdout || '').trim() : ''
|
|
@@ -62,7 +61,7 @@ export const runCapture = (cmd, args, opts = {}) => {
|
|
|
62
61
|
|
|
63
62
|
export { join }
|
|
64
63
|
|
|
65
|
-
// --- package.json helpers (
|
|
64
|
+
// --- package.json helpers (mutate parsed object in place) ---
|
|
66
65
|
|
|
67
66
|
export const pkgRemoveDeps = (pkg, names) => {
|
|
68
67
|
for (const field of ['dependencies', 'devDependencies']) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfredmouelle/create-stack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"author": {
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../capability.schema.json",
|
|
3
|
-
"name": "analytics",
|
|
4
|
-
"description": "Product analytics behind a swappable port. Capture events and identify users; flush/shutdown to drain pending events.",
|
|
5
|
-
"port": "src/core/port.ts",
|
|
6
|
-
"defaultAdapter": "posthog",
|
|
7
|
-
"adapters": {
|
|
8
|
-
"posthog": {
|
|
9
|
-
"deps": ["posthog-node"],
|
|
10
|
-
"env": ["POSTHOG_API_KEY", "POSTHOG_HOST"],
|
|
11
|
-
"files": ["src/adapters/posthog"]
|
|
12
|
-
},
|
|
13
|
-
"plausible": {
|
|
14
|
-
"deps": ["@alfredmouelle/http"],
|
|
15
|
-
"env": ["PLAUSIBLE_DOMAIN", "PLAUSIBLE_API_HOST"],
|
|
16
|
-
"files": ["src/adapters/plausible"]
|
|
17
|
-
},
|
|
18
|
-
"noop": {
|
|
19
|
-
"deps": [],
|
|
20
|
-
"env": [],
|
|
21
|
-
"files": ["src/adapters/noop"]
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"sharedDeps": ["valibot"],
|
|
25
|
-
"sharedFiles": ["src/core", "src/index.ts"]
|
|
26
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../capability.schema.json",
|
|
3
|
-
"name": "cache",
|
|
4
|
-
"description": "Key/value cache behind a swappable port. Values are serialized as JSON for remote stores.",
|
|
5
|
-
"port": "src/core/port.ts",
|
|
6
|
-
"defaultAdapter": "redis",
|
|
7
|
-
"adapters": {
|
|
8
|
-
"redis": {
|
|
9
|
-
"deps": ["ioredis"],
|
|
10
|
-
"env": ["REDIS_URL"],
|
|
11
|
-
"files": ["src/adapters/redis"]
|
|
12
|
-
},
|
|
13
|
-
"memory": {
|
|
14
|
-
"deps": [],
|
|
15
|
-
"env": [],
|
|
16
|
-
"files": ["src/adapters/memory"]
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"sharedDeps": ["valibot"],
|
|
20
|
-
"sharedFiles": ["src/core", "src/index.ts"]
|
|
21
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../capability.schema.json",
|
|
3
|
-
"name": "error-tracking",
|
|
4
|
-
"description": "Error reporting behind a swappable port. Capture exceptions, messages, breadcrumbs and user context, then ship them to a provider.",
|
|
5
|
-
"port": "src/core/port.ts",
|
|
6
|
-
"defaultAdapter": "sentry",
|
|
7
|
-
"adapters": {
|
|
8
|
-
"sentry": {
|
|
9
|
-
"deps": ["@sentry/node"],
|
|
10
|
-
"env": ["SENTRY_DSN", "SENTRY_ENVIRONMENT"],
|
|
11
|
-
"files": ["src/adapters/sentry"]
|
|
12
|
-
},
|
|
13
|
-
"console": {
|
|
14
|
-
"deps": [],
|
|
15
|
-
"env": [],
|
|
16
|
-
"files": ["src/adapters/console"]
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"sharedDeps": ["valibot"],
|
|
20
|
-
"sharedFiles": ["src/core", "src/index.ts"]
|
|
21
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../capability.schema.json",
|
|
3
|
-
"name": "jobs",
|
|
4
|
-
"description": "Background jobs / events behind a swappable port. Event-driven: define jobs against named events and trigger them; the adapter handles delivery and execution.",
|
|
5
|
-
"port": "src/core/port.ts",
|
|
6
|
-
"defaultAdapter": "inngest",
|
|
7
|
-
"adapters": {
|
|
8
|
-
"inngest": {
|
|
9
|
-
"deps": ["inngest"],
|
|
10
|
-
"env": ["INNGEST_EVENT_KEY", "INNGEST_SIGNING_KEY"],
|
|
11
|
-
"files": ["src/adapters/inngest"]
|
|
12
|
-
},
|
|
13
|
-
"trigger": {
|
|
14
|
-
"deps": ["@trigger.dev/sdk"],
|
|
15
|
-
"env": ["TRIGGER_SECRET_KEY"],
|
|
16
|
-
"files": ["src/adapters/trigger"]
|
|
17
|
-
},
|
|
18
|
-
"memory": {
|
|
19
|
-
"deps": [],
|
|
20
|
-
"env": [],
|
|
21
|
-
"files": ["src/adapters/memory"]
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"sharedDeps": ["valibot"],
|
|
25
|
-
"sharedFiles": ["src/core", "src/index.ts"]
|
|
26
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../capability.schema.json",
|
|
3
|
-
"name": "logger",
|
|
4
|
-
"description": "Structured logging behind a swappable port. Application code depends only on the Logger interface; pick an adapter (pino, console) at the composition root.",
|
|
5
|
-
"port": "src/core/port.ts",
|
|
6
|
-
"defaultAdapter": "pino",
|
|
7
|
-
"adapters": {
|
|
8
|
-
"pino": {
|
|
9
|
-
"deps": ["pino"],
|
|
10
|
-
"env": [],
|
|
11
|
-
"files": ["src/adapters/pino"]
|
|
12
|
-
},
|
|
13
|
-
"console": {
|
|
14
|
-
"deps": [],
|
|
15
|
-
"env": [],
|
|
16
|
-
"files": ["src/adapters/console"]
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"sharedDeps": ["valibot"],
|
|
20
|
-
"sharedFiles": ["src/core", "src/index.ts"]
|
|
21
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../capability.schema.json",
|
|
3
|
-
"name": "mailer",
|
|
4
|
-
"description": "Transactional email. Bodies are always React Email components, rendered to HTML + plain text.",
|
|
5
|
-
"port": "src/core/port.ts",
|
|
6
|
-
"factory": "src/factory.ts",
|
|
7
|
-
"defaultAdapter": "resend",
|
|
8
|
-
"adapters": {
|
|
9
|
-
"resend": {
|
|
10
|
-
"deps": ["resend"],
|
|
11
|
-
"env": ["RESEND_API_KEY"],
|
|
12
|
-
"files": ["src/adapters/resend"]
|
|
13
|
-
},
|
|
14
|
-
"brevo": {
|
|
15
|
-
"deps": ["@getbrevo/brevo"],
|
|
16
|
-
"env": ["BREVO_API_KEY"],
|
|
17
|
-
"files": ["src/adapters/brevo"]
|
|
18
|
-
},
|
|
19
|
-
"ses": {
|
|
20
|
-
"deps": ["@aws-sdk/client-sesv2"],
|
|
21
|
-
"env": ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
|
22
|
-
"files": ["src/adapters/ses"]
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"sharedDeps": ["valibot", "@react-email/render"],
|
|
26
|
-
"peerDeps": ["react", "react-dom"],
|
|
27
|
-
"sharedFiles": ["src/core", "src/factory.ts", "src/index.ts"]
|
|
28
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../capability.schema.json",
|
|
3
|
-
"name": "storage",
|
|
4
|
-
"description": "Object storage behind a swappable port: put/get/delete/exists plus signed URLs. Adapters for S3, Cloudflare R2, Google Cloud Storage and the local filesystem.",
|
|
5
|
-
"port": "src/core/port.ts",
|
|
6
|
-
"factory": "src/index.ts",
|
|
7
|
-
"defaultAdapter": "s3",
|
|
8
|
-
"adapters": {
|
|
9
|
-
"s3": {
|
|
10
|
-
"deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
|
|
11
|
-
"env": ["S3_BUCKET", "S3_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
|
12
|
-
"files": ["src/adapters/s3"]
|
|
13
|
-
},
|
|
14
|
-
"r2": {
|
|
15
|
-
"deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
|
|
16
|
-
"env": ["R2_BUCKET", "R2_ACCOUNT_ID", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"],
|
|
17
|
-
"files": ["src/adapters/r2", "src/adapters/s3"]
|
|
18
|
-
},
|
|
19
|
-
"gcs": {
|
|
20
|
-
"deps": ["@google-cloud/storage"],
|
|
21
|
-
"env": ["GCS_BUCKET", "GOOGLE_CLOUD_PROJECT"],
|
|
22
|
-
"files": ["src/adapters/gcs"]
|
|
23
|
-
},
|
|
24
|
-
"local": {
|
|
25
|
-
"deps": [],
|
|
26
|
-
"env": ["STORAGE_LOCAL_DIR"],
|
|
27
|
-
"files": ["src/adapters/local"]
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"sharedDeps": ["valibot"],
|
|
31
|
-
"sharedFiles": ["src/core", "src/index.ts"]
|
|
32
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# Patterns
|
|
2
|
-
|
|
3
|
-
Foundational, framework-coupled **patterns** the `bootstrap` skill vendors into a
|
|
4
|
-
freshly scaffolded app — the counterpart to `packages/` (swappable capabilities).
|
|
5
|
-
|
|
6
|
-
A *capability* is a provider behind a port (mailer, storage, …), swappable by
|
|
7
|
-
changing one line. A *pattern* is a foundation you don't swap but always set up
|
|
8
|
-
the same way: tRPC wiring, the better-auth instance, the Drizzle client. They are
|
|
9
|
-
**framework-coupled** (currently `tanstack-start`, mirrored from the reference
|
|
10
|
-
base apps) and depend on each other.
|
|
11
|
-
|
|
12
|
-
**The code lives in the base apps, not here.** `patterns/` is a pure *manifest
|
|
13
|
-
layer*: each `pattern.json` describes a foundation (how to detect it, its deps,
|
|
14
|
-
env, framework, dependencies) and lists the files that make it up — by pointing
|
|
15
|
-
**into the base apps** (`apps/tanstack-base`, `apps/next-base`), the single source
|
|
16
|
-
of truth. No code is duplicated.
|
|
17
|
-
|
|
18
|
-
Each pattern is `<name>/pattern.json` (see `../pattern.schema.json`).
|
|
19
|
-
`_baseline/` is special: real always-applied config files (Biome, tsconfig, env
|
|
20
|
-
skeleton, the `# Author` README footer) that a standalone fork needs but the base
|
|
21
|
-
apps don't carry on their own (they inherit the monorepo's Biome).
|
|
22
|
-
|
|
23
|
-
## How the skills use these
|
|
24
|
-
|
|
25
|
-
The manifests drive two flows:
|
|
26
|
-
|
|
27
|
-
- **bootstrap — create mode** (empty folder): fork a base app, then *strip* every
|
|
28
|
-
foundation/capability the user didn't pick, using each manifest's `files`/`deps`/
|
|
29
|
-
`env` to know its exact footprint.
|
|
30
|
-
- **bootstrap — existing project / add-capability**: match each manifest's `detect`
|
|
31
|
-
against the project → the opt-in set, then *vendor* the listed files (copied from
|
|
32
|
-
the base apps) + deps + env, wire `integratesWith` when both sides are opt-in, and
|
|
33
|
-
pull required `capabilities`. A pattern not referenced is never pulled.
|
|
34
|
-
|
|
35
|
-
## Available patterns
|
|
36
|
-
|
|
37
|
-
- **drizzle** — Drizzle ORM + drizzle-kit (Postgres). Client, schema barrel,
|
|
38
|
-
cursor pagination, seed harness.
|
|
39
|
-
- **better-auth** — better-auth v1 with the Drizzle adapter. Email+password,
|
|
40
|
-
verification, optional Google OAuth, rate limiting, auth tables, client +
|
|
41
|
-
session helpers, route guard. `dependsOn` drizzle; needs the mailer + email-kit
|
|
42
|
-
capabilities.
|
|
43
|
-
- **trpc** — tRPC v11 + TanStack React Query. Context, procedure tiers, error
|
|
44
|
-
formatter, client + SSR caller, fetch handler. `dependsOn` drizzle,
|
|
45
|
-
`integratesWith` better-auth.
|
|
46
|
-
- **data-table** — headless tables with TanStack Table (table + skeleton
|
|
47
|
-
primitives, DataTable, InfiniteDataTable, SortableHeader). `framework: agnostic`
|
|
48
|
-
— works in both Next and TanStack Start.
|
|
49
|
-
|
|
50
|
-
Next.js variants (App Router) of the framework-coupled patterns:
|
|
51
|
-
|
|
52
|
-
- **better-auth-next** — better-auth with `next/headers` session, `toNextJsHandler`
|
|
53
|
-
catch-all, server-component guards (`requireAuth`).
|
|
54
|
-
- **trpc-next** — tRPC with the classic `api.x.useQuery` hooks (createTRPCReact) +
|
|
55
|
-
RSC hydration. `integratesWith` better-auth-next.
|
|
56
|
-
|
|
57
|
-
bootstrap picks the variant matching the project's framework: `trpc`/`better-auth`
|
|
58
|
-
for TanStack Start, `trpc-next`/`better-auth-next` for Next.
|