create-basalt 1.10.0 → 1.12.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 +64 -26
- package/dist/args.d.ts +1 -1
- package/dist/args.js +5 -2
- package/dist/bin-template.d.ts +13 -8
- package/dist/bin-template.js +178 -13
- package/dist/cli.js +6 -2
- package/dist/dev-entry.d.ts +23 -0
- package/dist/dev-entry.js +49 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +23 -2
- package/dist/latest-versions.js +1 -0
- package/dist/project/doctor.d.ts +11 -1
- package/dist/project/doctor.js +176 -14
- package/dist/project/production.d.ts +32 -0
- package/dist/project/production.js +74 -0
- package/dist/project/run.js +8 -4
- package/dist/project/seed-script.d.ts +18 -0
- package/dist/project/seed-script.js +32 -0
- package/dist/project/update.js +53 -11
- package/dist/stubs.d.ts +30 -0
- package/dist/stubs.js +123 -0
- package/dist/templates.d.ts +53 -7
- package/dist/templates.js +183 -55
- package/dist/versions.js +13 -13
- package/dist/wizard.js +3 -3
- package/package.json +20 -17
package/dist/templates.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { THIRD_PARTY_VERSIONS } from './latest-versions.js';
|
|
2
|
+
import { DOCKERFILE, DOCKERIGNORE, PRODUCTION_ENTRY } from './stubs.js';
|
|
2
3
|
import { SCAFFOLD_VERSIONS } from './versions.js';
|
|
3
4
|
export { basaltBin } from './bin-template.js';
|
|
4
5
|
export { SCAFFOLD_VERSIONS } from './versions.js';
|
|
@@ -29,6 +30,24 @@ export const thirdPartyVersionOf = (pkg) => {
|
|
|
29
30
|
}
|
|
30
31
|
return range;
|
|
31
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The production path (BK-026): `build` compiles src/ with tsconfig.build.json
|
|
35
|
+
* (rootDir `.`, outDir `dist`) and `start` runs the result on plain node — the
|
|
36
|
+
* same entry the Dockerfile's CMD runs. `start:dev` keeps the old tsx start.
|
|
37
|
+
*/
|
|
38
|
+
export const BUILD_SCRIPT = 'tsc -p tsconfig.build.json';
|
|
39
|
+
/** The `db:seed` script of a `--prisma` scaffold with tenancy (prisma.config.ts loads .env, then runs `migrations.seed`). */
|
|
40
|
+
export const DB_SEED_SCRIPT = 'prisma db seed';
|
|
41
|
+
export const START_SCRIPT = `node --enable-source-maps ${PRODUCTION_ENTRY}`;
|
|
42
|
+
export const START_DEV_SCRIPT = 'tsx src/server.ts';
|
|
43
|
+
/** The `start` script every create-basalt release before the production path wrote. */
|
|
44
|
+
export const LEGACY_START_SCRIPT = 'tsx src/server.ts';
|
|
45
|
+
/**
|
|
46
|
+
* package.json `imports` alias for the generated Prisma client. It lives in
|
|
47
|
+
* ./generated/prisma — outside src/, so tsc never has to copy its .js files —
|
|
48
|
+
* and `#db/*` resolves to it from src/ (tsx, vitest) and dist/src/ (node) alike.
|
|
49
|
+
*/
|
|
50
|
+
export const PRISMA_IMPORTS = { '#db/*': './generated/prisma/*' };
|
|
32
51
|
/** The root `basalt` script of an app without `--cli`: create-basalt's project commands. */
|
|
33
52
|
export const BASALT_PROJECT_SCRIPT = 'create-basalt --project';
|
|
34
53
|
/** The root `dev:web` script of a `--ui` app (web/ is a pnpm workspace member). */
|
|
@@ -75,7 +94,11 @@ export function packageJson(options) {
|
|
|
75
94
|
if (options.prisma) {
|
|
76
95
|
// Prisma 7 talks to PostgreSQL through a driver adapter, so `pg` is a real
|
|
77
96
|
// runtime dependency (see src/db.ts); the `prisma` CLI is dev-only.
|
|
78
|
-
|
|
97
|
+
// @prisma/client-runtime-utils is required BY NAME from the generated
|
|
98
|
+
// client's runtime/ (./generated/prisma/runtime/client.js). Under pnpm it is
|
|
99
|
+
// only a transitive dependency of @prisma/client, out of reach of plain
|
|
100
|
+
// `node` — so the app declares it directly (BK-026).
|
|
101
|
+
for (const pkg of ['@prisma/adapter-pg', '@prisma/client', '@prisma/client-runtime-utils', 'pg']) {
|
|
79
102
|
dependencies[pkg] = thirdPartyVersionOf(pkg);
|
|
80
103
|
}
|
|
81
104
|
}
|
|
@@ -108,11 +131,15 @@ export function packageJson(options) {
|
|
|
108
131
|
version: '0.1.0',
|
|
109
132
|
private: true,
|
|
110
133
|
type: 'module',
|
|
134
|
+
...(options.prisma ? { imports: PRISMA_IMPORTS } : {}),
|
|
111
135
|
scripts: {
|
|
112
136
|
// dev opts into NODE_ENV=development (src/dev.ts); start does not, so an
|
|
113
137
|
// unset NODE_ENV counts as production and secrets are required.
|
|
114
138
|
dev: 'tsx watch src/dev.ts',
|
|
115
|
-
|
|
139
|
+
// Production: compile once, run on plain node (no tsx at runtime).
|
|
140
|
+
build: BUILD_SCRIPT,
|
|
141
|
+
start: START_SCRIPT,
|
|
142
|
+
'start:dev': START_DEV_SCRIPT,
|
|
116
143
|
test: 'vitest run',
|
|
117
144
|
typecheck: 'tsc --noEmit',
|
|
118
145
|
// With --cli, bin/basalt.ts serves the generators AND hands the project
|
|
@@ -132,7 +159,11 @@ export function packageJson(options) {
|
|
|
132
159
|
// is deliberately no `db:push` script.
|
|
133
160
|
'db:migrate': 'prisma migrate dev',
|
|
134
161
|
'db:deploy': 'prisma migrate deploy',
|
|
135
|
-
|
|
162
|
+
// Through Prisma, not `tsx prisma/seed.ts`: prisma.config.ts loads
|
|
163
|
+
// .env (the seed imports src/env.ts, which needs the database URL),
|
|
164
|
+
// then runs the `migrations.seed` command it declares. Prisma 7's
|
|
165
|
+
// `migrate dev` no longer seeds on its own.
|
|
166
|
+
...(options.tenancy ? { 'db:seed': DB_SEED_SCRIPT } : {}),
|
|
136
167
|
}
|
|
137
168
|
: {}),
|
|
138
169
|
},
|
|
@@ -153,12 +184,34 @@ export function tsconfigJson(options) {
|
|
|
153
184
|
noEmit: true,
|
|
154
185
|
types: ['node'],
|
|
155
186
|
},
|
|
156
|
-
// The generated Prisma client lives
|
|
157
|
-
// re-created by `prisma generate
|
|
158
|
-
// type-checked too.
|
|
187
|
+
// The generated Prisma client lives in ./generated (git-ignored,
|
|
188
|
+
// re-created by `prisma generate`, reached through the `#db/*` import
|
|
189
|
+
// alias); the seed script under prisma/ is type-checked too.
|
|
159
190
|
include: ['src', 'tests', ...(options.prisma && options.tenancy ? ['prisma/seed.ts'] : [])],
|
|
160
191
|
}, null, 2)}\n`;
|
|
161
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* `tsconfig.build.json` — what `pnpm build` compiles: src/ only (tests and
|
|
195
|
+
* bin/ stay dev-only), emitted to dist/ with rootDir `.`, so src/server.ts
|
|
196
|
+
* becomes dist/src/server.js. Extends tsconfig.json, so the compiler options
|
|
197
|
+
* (and `pnpm typecheck`) never diverge.
|
|
198
|
+
*/
|
|
199
|
+
export function tsconfigBuildJson() {
|
|
200
|
+
return `${JSON.stringify({
|
|
201
|
+
extends: './tsconfig.json',
|
|
202
|
+
compilerOptions: {
|
|
203
|
+
noEmit: false,
|
|
204
|
+
rootDir: '.',
|
|
205
|
+
outDir: 'dist',
|
|
206
|
+
sourceMap: true,
|
|
207
|
+
},
|
|
208
|
+
include: ['src'],
|
|
209
|
+
}, null, 2)}\n`;
|
|
210
|
+
}
|
|
211
|
+
/** The production Dockerfile — the same file `basalt publish dockerfile` writes. */
|
|
212
|
+
export function dockerfile() {
|
|
213
|
+
return DOCKERFILE;
|
|
214
|
+
}
|
|
162
215
|
export function envTs(options) {
|
|
163
216
|
const prefix = envPrefix(options.name);
|
|
164
217
|
return `import { defineEnv${options.auth ? ', secret' : ''} } from '@basaltkit/env'
|
|
@@ -222,10 +275,19 @@ export function envPrefix(name) {
|
|
|
222
275
|
base = base.slice(start, end);
|
|
223
276
|
return base === '' || /^\d/.test(base) ? `APP_${base}` : base;
|
|
224
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* The opening lines of `.env.example`. Up to create-basalt 1.10 they read
|
|
280
|
+
* {@link LEGACY_ENV_EXAMPLE_LEAD} ("nothing loads this file"); `update`
|
|
281
|
+
* swaps that for this one in existing apps.
|
|
282
|
+
*/
|
|
283
|
+
export const ENV_EXAMPLE_LEAD = `# Development: copy this file to .env — \`pnpm dev\` and \`pnpm basalt\` load it
|
|
284
|
+
# automatically (a new app already has one). \`pnpm start\` (production) does
|
|
285
|
+
# NOT: there the variables come from the environment (or \`node --env-file\`).`;
|
|
286
|
+
export const LEGACY_ENV_EXAMPLE_LEAD = `# Nothing loads this file for you: copy it to .env and start with
|
|
287
|
+
# \`node --env-file=.env\` / \`tsx --env-file=.env\`, or export the variables.`;
|
|
225
288
|
export function envExample(options) {
|
|
226
289
|
const prefix = envPrefix(options.name);
|
|
227
|
-
return
|
|
228
|
-
# \`node --env-file=.env\` / \`tsx --env-file=.env\`, or export the variables.
|
|
290
|
+
return `${ENV_EXAMPLE_LEAD}
|
|
229
291
|
# --env-file NEVER overrides a variable already exported in your shell: if
|
|
230
292
|
# another project exported PORT or DATABASE_URL, an app reading the generic
|
|
231
293
|
# name would silently use THAT one. That is why src/env.ts reads app-prefixed
|
|
@@ -252,6 +314,20 @@ ${prefix}_DATABASE_URL=postgres://postgres:postgres@localhost:5432/${databaseNam
|
|
|
252
314
|
# ${prefix}_DATABASE_URL=postgres://user:pass@localhost:5432/${options.name.replace(/^@[^/]+\//, '')}
|
|
253
315
|
`}`;
|
|
254
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* The `.env` a new app starts with (git-ignored, never in the manifest): the
|
|
319
|
+
* values of `.env.example` for local development plus — with auth — a freshly
|
|
320
|
+
* generated APP_SECRET, so `pnpm dev` works without a copy step.
|
|
321
|
+
*/
|
|
322
|
+
export function devEnvFile(options, appSecret) {
|
|
323
|
+
const prefix = envPrefix(options.name);
|
|
324
|
+
const example = envExample(options);
|
|
325
|
+
const body = example.slice(example.indexOf(`${prefix}_PORT=`));
|
|
326
|
+
return `# Local development settings, generated by create-basalt from .env.example.
|
|
327
|
+
# Git-ignored: never commit it. \`pnpm dev\` and \`pnpm basalt\` load it (a
|
|
328
|
+
# variable exported in your shell wins); \`pnpm start\` (production) does not.
|
|
329
|
+
${body.replace(`# ${prefix}_APP_SECRET=\n`, `${prefix}_APP_SECRET=${appSecret}\n`)}`;
|
|
330
|
+
}
|
|
255
331
|
/** A PostgreSQL-safe database name derived from the project name. */
|
|
256
332
|
export function databaseName(name) {
|
|
257
333
|
return name.replace(/^@[^/]+\//, '').replace(/[^a-zA-Z0-9]+/g, '_').toLowerCase();
|
|
@@ -511,7 +587,15 @@ import { env } from './env.js'
|
|
|
511
587
|
const app = await buildApp({
|
|
512
588
|
logLevel: env.LOG_LEVEL,
|
|
513
589
|
pretty: env.NODE_ENV === 'development',
|
|
514
|
-
})
|
|
590
|
+
})
|
|
591
|
+
.boot()
|
|
592
|
+
.catch((error: unknown) => {
|
|
593
|
+
// A database failure diagnosed by @basaltkit/prisma (permission denied, not
|
|
594
|
+
// migrated, unreachable…) carries its one-line fix: print it above the stack.
|
|
595
|
+
const diagnosis = (error as { details?: { diagnosis?: { cause?: string; fix?: string } } } | null)?.details?.diagnosis
|
|
596
|
+
if (diagnosis) console.error('Cannot start: ' + diagnosis.cause + '\\nFix: ' + diagnosis.fix)
|
|
597
|
+
throw error
|
|
598
|
+
})
|
|
515
599
|
|
|
516
600
|
const server = app.container.get(FASTIFY)
|
|
517
601
|
await server.listen({ port: env.PORT, host: env.HOST })
|
|
@@ -526,14 +610,27 @@ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
|
526
610
|
`;
|
|
527
611
|
}
|
|
528
612
|
/**
|
|
529
|
-
* `pnpm dev` entrypoint.
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* NODE_ENV
|
|
613
|
+
* `pnpm dev` entrypoint. Loads `.env` (when present) with `node --env-file`
|
|
614
|
+
* semantics — an exported variable wins, the file only fills in what is
|
|
615
|
+
* missing — and opts into NODE_ENV=development (dev secret defaults, pretty
|
|
616
|
+
* logs) unless NODE_ENV is already set, then starts the server. Kept out of
|
|
617
|
+
* server.ts on purpose: `pnpm start` runs server.ts directly, where nothing
|
|
618
|
+
* loads `.env` (production configuration comes from the real environment) and
|
|
619
|
+
* an unset NODE_ENV counts as production, so secrets are required (fail-closed).
|
|
533
620
|
*/
|
|
534
621
|
export function devTs() {
|
|
535
622
|
return `// Development entrypoint (\`pnpm dev\`). \`pnpm start\` runs server.ts directly,
|
|
536
623
|
// where an unset NODE_ENV counts as production: secrets are then required.
|
|
624
|
+
import { existsSync } from 'node:fs'
|
|
625
|
+
import { fileURLToPath } from 'node:url'
|
|
626
|
+
|
|
627
|
+
// Development loads .env (when present) like \`node --env-file=.env\`: a variable
|
|
628
|
+
// already exported in your shell still wins, the file only fills in what is
|
|
629
|
+
// missing. \`pnpm start\` (production) never loads it — there the configuration
|
|
630
|
+
// comes from the real environment.
|
|
631
|
+
const envFile = fileURLToPath(new URL('../.env', import.meta.url))
|
|
632
|
+
if (existsSync(envFile)) process.loadEnvFile(envFile)
|
|
633
|
+
|
|
537
634
|
process.env['NODE_ENV'] ??= 'development'
|
|
538
635
|
await import('./server.js')
|
|
539
636
|
`;
|
|
@@ -558,9 +655,17 @@ const PRISMA_MODELS = {
|
|
|
558
655
|
}
|
|
559
656
|
|
|
560
657
|
model TenantDomain {
|
|
561
|
-
domain
|
|
562
|
-
tenantId
|
|
563
|
-
|
|
658
|
+
domain String @id
|
|
659
|
+
tenantId String
|
|
660
|
+
// Custom-domain verification (PrismaDomainStore). A row with a NULL token
|
|
661
|
+
// mirrors \`tenant.domains\` and is written by PrismaTenantSource; a row with a
|
|
662
|
+
// token is a CustomDomains claim, which the source never deletes, and which
|
|
663
|
+
// only resolves a request once \`verified\` is true.
|
|
664
|
+
verificationToken String?
|
|
665
|
+
verified Boolean @default(true)
|
|
666
|
+
createdAt DateTime @default(now())
|
|
667
|
+
verifiedAt DateTime?
|
|
668
|
+
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
564
669
|
|
|
565
670
|
@@index([tenantId])
|
|
566
671
|
@@map("tenant_domains")
|
|
@@ -575,9 +680,11 @@ model TenantDomain {
|
|
|
575
680
|
}
|
|
576
681
|
|
|
577
682
|
model AuthSession {
|
|
578
|
-
id
|
|
579
|
-
userId
|
|
580
|
-
expiresAt
|
|
683
|
+
id String @id
|
|
684
|
+
userId String
|
|
685
|
+
expiresAt DateTime
|
|
686
|
+
/// Last use, for the idle timeout (trackSessionActivity: true).
|
|
687
|
+
lastSeenAt DateTime?
|
|
581
688
|
|
|
582
689
|
@@index([userId])
|
|
583
690
|
@@map("auth_sessions")
|
|
@@ -756,7 +863,9 @@ export function prismaSchema(options) {
|
|
|
756
863
|
|
|
757
864
|
generator client {
|
|
758
865
|
provider = "prisma-client-js"
|
|
759
|
-
|
|
866
|
+
// Outside src/ on purpose: src/db.ts imports it as \`#db/client.js\` (the
|
|
867
|
+
// package.json "imports" alias), which resolves from src/ and dist/src/ alike.
|
|
868
|
+
output = "../generated/prisma"
|
|
760
869
|
}
|
|
761
870
|
|
|
762
871
|
datasource db {
|
|
@@ -792,9 +901,9 @@ export function prismaConfigTs(options) {
|
|
|
792
901
|
return `import { loadEnvFile } from 'node:process'
|
|
793
902
|
import { defineConfig } from 'prisma/config'
|
|
794
903
|
|
|
795
|
-
// Prisma loads no .env for you
|
|
796
|
-
// mirrors \`node --env-file=.env\`: a variable already
|
|
797
|
-
// still wins, the file only fills in what is missing.
|
|
904
|
+
// Prisma loads no .env for you. Like the app's dev entrypoints (src/dev.ts,
|
|
905
|
+
// bin/basalt.ts) this mirrors \`node --env-file=.env\`: a variable already
|
|
906
|
+
// exported in your shell still wins, the file only fills in what is missing.
|
|
798
907
|
try {
|
|
799
908
|
loadEnvFile('.env')
|
|
800
909
|
} catch {
|
|
@@ -804,7 +913,7 @@ try {
|
|
|
804
913
|
export default defineConfig({
|
|
805
914
|
schema: 'prisma/schema.prisma',${options.tenancy
|
|
806
915
|
? `
|
|
807
|
-
//
|
|
916
|
+
// Run by \`prisma db seed\` (\`pnpm db:seed\`), with the .env loaded above.
|
|
808
917
|
migrations: { seed: 'tsx prisma/seed.ts' },`
|
|
809
918
|
: ''}
|
|
810
919
|
datasource: {
|
|
@@ -824,7 +933,9 @@ export default defineConfig({
|
|
|
824
933
|
*/
|
|
825
934
|
export function dbTs(options) {
|
|
826
935
|
return `import { PrismaPg } from '@prisma/adapter-pg'${options.tenancy ? `\nimport { tenancyExtension } from '@basaltkit/prisma'` : ''}
|
|
827
|
-
|
|
936
|
+
// The generated client (./generated/prisma), through the package.json
|
|
937
|
+
// "imports" alias — the same specifier works under tsx and from dist/.
|
|
938
|
+
import { PrismaClient } from '#db/client.js'
|
|
828
939
|
import { env } from './env.js'
|
|
829
940
|
|
|
830
941
|
/**
|
|
@@ -860,8 +971,9 @@ export const db = prisma
|
|
|
860
971
|
}
|
|
861
972
|
/**
|
|
862
973
|
* `prisma/seed.ts` — creates the `demo` tenant the scaffolded resolvers expect
|
|
863
|
-
* (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed`
|
|
864
|
-
* `prisma
|
|
974
|
+
* (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed`
|
|
975
|
+
* (`prisma db seed`, the command prisma.config.ts declares) — Prisma 7's
|
|
976
|
+
* `migrate dev` does not run it.
|
|
865
977
|
*/
|
|
866
978
|
export function prismaSeedTs(options) {
|
|
867
979
|
return `import { prismaTenantSource } from '@basaltkit/tenancy-prisma'
|
|
@@ -947,8 +1059,9 @@ Included: ${features.join(' · ')}.
|
|
|
947
1059
|
\`\`\`bash
|
|
948
1060
|
pnpm install${options.prisma
|
|
949
1061
|
? `
|
|
950
|
-
#
|
|
951
|
-
pnpm db:migrate # create the tables${options.tenancy ? `
|
|
1062
|
+
# start PostgreSQL — .env points ${envPrefix(options.name)}_DATABASE_URL at localhost:5432 — then
|
|
1063
|
+
pnpm db:migrate # create the tables${options.tenancy ? `
|
|
1064
|
+
pnpm db:seed # the 'demo' tenant the resolvers expect` : ''}`
|
|
952
1065
|
: ''}
|
|
953
1066
|
pnpm dev # API on http://localhost:3000
|
|
954
1067
|
pnpm test
|
|
@@ -962,11 +1075,11 @@ the models this app needs — the reference models of the \`@basaltkit/*-prisma\
|
|
|
962
1075
|
packages it uses, plus your own \`Project\` at the end.
|
|
963
1076
|
|
|
964
1077
|
\`\`\`bash
|
|
965
|
-
pnpm db:migrate
|
|
1078
|
+
pnpm db:migrate --name <change> # prisma migrate dev — writes AND applies a migration (asks for a name without --name)${options.tenancy
|
|
966
1079
|
? `
|
|
967
|
-
pnpm db:seed # the 'demo' tenant the resolvers expect
|
|
1080
|
+
pnpm db:seed # prisma db seed — the 'demo' tenant the resolvers expect`
|
|
968
1081
|
: ''}
|
|
969
|
-
pnpm db:generate # prisma generate — refresh
|
|
1082
|
+
pnpm db:generate # prisma generate — refresh generated/prisma after a schema change (also runs on install; migrate dev does not)
|
|
970
1083
|
pnpm db:deploy # prisma migrate deploy — apply pending migrations in production
|
|
971
1084
|
\`\`\`
|
|
972
1085
|
|
|
@@ -1005,16 +1118,18 @@ scaffold with \`--cli\` — \`pnpm basalt prisma:sync\` does the merge for you.)
|
|
|
1005
1118
|
: ''}
|
|
1006
1119
|
## Environment
|
|
1007
1120
|
|
|
1008
|
-
\`src/env.ts\` validates \`process.env
|
|
1009
|
-
|
|
1010
|
-
|
|
1121
|
+
\`src/env.ts\` validates \`process.env\`. For development, ${options.cli ? '`pnpm dev` and `pnpm basalt` load' : '`pnpm dev` loads'} \`.env\`
|
|
1122
|
+
automatically — create-basalt wrote one for you: git-ignored, with local
|
|
1123
|
+
values${options.auth ? ' and a generated secret' : ''}. \`.env.example\` is its committed template.
|
|
1124
|
+
\`pnpm start\` (production, \`${PRODUCTION_ENTRY}\`) loads **nothing**: there the
|
|
1125
|
+
variables come from the real environment (or \`node --env-file=.env\`).
|
|
1011
1126
|
|
|
1012
1127
|
The variables are **app-prefixed**: \`${envPrefix(options.name)}_PORT\`,
|
|
1013
1128
|
\`${envPrefix(options.name)}_HOST\`, \`${envPrefix(options.name)}_LOG_LEVEL\`${options.auth ? `, \`${envPrefix(options.name)}_APP_SECRET\`` : ''}${options.prisma ? `, \`${envPrefix(options.name)}_DATABASE_URL\`` : ''}. \`NODE_ENV\` is never prefixed.
|
|
1014
1129
|
|
|
1015
|
-
Why:
|
|
1016
|
-
shell where another project exported \`DATABASE_URL\`
|
|
1017
|
-
the generic name boots against THAT value and only fails on the first request
|
|
1130
|
+
Why: **loading \`.env\` (like \`--env-file\`) never overrides a variable that is
|
|
1131
|
+
already exported.** In a shell where another project exported \`DATABASE_URL\`
|
|
1132
|
+
(or \`PORT\`), an app reading the generic name boots against THAT value and only fails on the first request
|
|
1018
1133
|
that touches it. \`src/env.ts\` therefore passes \`prefix: '${envPrefix(options.name)}'\` to
|
|
1019
1134
|
\`defineEnv\`: each variable is read as \`${envPrefix(options.name)}_<NAME>\` first and
|
|
1020
1135
|
**falls back** to the bare \`<NAME>\`, so a deployment that already exports the
|
|
@@ -1030,6 +1145,24 @@ To drop the fallback entirely (prefixed names only), write
|
|
|
1030
1145
|
\`prefix: { value: '${envPrefix(options.name)}', fallback: false }\`. When in doubt about what
|
|
1031
1146
|
your shell exports: \`env | grep DATABASE_URL\`.
|
|
1032
1147
|
|
|
1148
|
+
## Build & ship
|
|
1149
|
+
|
|
1150
|
+
\`\`\`bash
|
|
1151
|
+
pnpm build # tsc -p tsconfig.build.json → dist/ (src only)
|
|
1152
|
+
pnpm start # node --enable-source-maps ${PRODUCTION_ENTRY} — no tsx at runtime
|
|
1153
|
+
pnpm start:dev # the server from source with tsx, without building
|
|
1154
|
+
docker build -t ${options.name} . # the Dockerfile: build stage + plain-node runtime
|
|
1155
|
+
\`\`\`
|
|
1156
|
+
|
|
1157
|
+
\`pnpm start\` runs the compiled server, so build first. The \`Dockerfile\` (also
|
|
1158
|
+
what \`basalt publish dockerfile\` writes) installs, ${options.prisma ? 'generates the Prisma client, ' : ''}builds, drops the
|
|
1159
|
+
devDependencies and runs ${PRODUCTION_ENTRY} as the \`node\` user, with a
|
|
1160
|
+
\`HEALTHCHECK\` on \`/health\`. Pass the configuration as environment variables${options.prisma ? ', and apply migrations (\`pnpm db:deploy\`) before rolling out — the image does not run them' : ''}.${options.prisma
|
|
1161
|
+
? ` The Prisma client is generated into \`generated/prisma\` (outside \`src/\`) and
|
|
1162
|
+
imported as \`#db/client.js\` through the package.json \`imports\` alias, so the same
|
|
1163
|
+
import works under tsx and from \`dist/\`.`
|
|
1164
|
+
: ''}
|
|
1165
|
+
|
|
1033
1166
|
## Keeping it up to date
|
|
1034
1167
|
|
|
1035
1168
|
\`\`\`bash
|
|
@@ -1139,20 +1272,7 @@ export function mcpJson(_options) {
|
|
|
1139
1272
|
* `certs/server.key` through.
|
|
1140
1273
|
*/
|
|
1141
1274
|
export function dockerignore() {
|
|
1142
|
-
return
|
|
1143
|
-
**/.env.*
|
|
1144
|
-
!**/.env.example
|
|
1145
|
-
**/.npmrc
|
|
1146
|
-
**/.git
|
|
1147
|
-
**/node_modules
|
|
1148
|
-
**/coverage
|
|
1149
|
-
**/*.log
|
|
1150
|
-
**/*.pem
|
|
1151
|
-
**/*.key
|
|
1152
|
-
**/*.p12
|
|
1153
|
-
**/*.pfx
|
|
1154
|
-
**/.DS_Store
|
|
1155
|
-
`;
|
|
1275
|
+
return DOCKERIGNORE;
|
|
1156
1276
|
}
|
|
1157
1277
|
export function gitignore(options) {
|
|
1158
1278
|
return `node_modules/
|
|
@@ -1163,12 +1283,14 @@ dist/
|
|
|
1163
1283
|
*.log
|
|
1164
1284
|
${options.prisma
|
|
1165
1285
|
? `# Generated by \`prisma generate\` (runs on install) — never edit or commit it.
|
|
1166
|
-
|
|
1286
|
+
generated/
|
|
1167
1287
|
`
|
|
1168
1288
|
: ''}`;
|
|
1169
1289
|
}
|
|
1170
1290
|
/**
|
|
1171
|
-
* pnpm settings: esbuild's build script is required by tsx;
|
|
1291
|
+
* pnpm settings: esbuild's build script is required by tsx; with --prisma,
|
|
1292
|
+
* the Prisma CLI's build scripts (prisma, @prisma/engines) are approved too — pnpm 11 fails
|
|
1293
|
+
* the install on an unapproved build;
|
|
1172
1294
|
* msgpackr-extract (optional native accelerator via BullMQ) is declined —
|
|
1173
1295
|
* msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
|
|
1174
1296
|
* its own dependencies resolve.
|
|
@@ -1186,7 +1308,13 @@ export function pnpmWorkspaceYaml(options) {
|
|
|
1186
1308
|
`
|
|
1187
1309
|
: ''}allowBuilds:
|
|
1188
1310
|
esbuild: true
|
|
1189
|
-
msgpackr-extract: false
|
|
1311
|
+
msgpackr-extract: false${options.prisma
|
|
1312
|
+
? `
|
|
1313
|
+
# The Prisma CLI's install scripts fetch its engines; pnpm 11 refuses to finish
|
|
1314
|
+
# an install (ERR_PNPM_IGNORED_BUILDS) while a dependency's build is unapproved.
|
|
1315
|
+
'@prisma/engines': true
|
|
1316
|
+
prisma: true`
|
|
1317
|
+
: ''}
|
|
1190
1318
|
# @basaltkit/* releases in lockstep, often within hours — exclude the scope (and
|
|
1191
1319
|
# create-basalt, which serves \`pnpm basalt update\`) from pnpm's minimumReleaseAge
|
|
1192
1320
|
# policy so an update is never blocked on a fresh release.
|
package/dist/versions.js
CHANGED
|
@@ -4,26 +4,26 @@
|
|
|
4
4
|
export const SCAFFOLD_VERSIONS = {
|
|
5
5
|
'@basaltkit/admin': '^2.0.0',
|
|
6
6
|
'@basaltkit/admin-shadcn': '^1.1.0',
|
|
7
|
-
'@basaltkit/ai-mcp': '^0.
|
|
8
|
-
'@basaltkit/auth': '^4.
|
|
9
|
-
'@basaltkit/auth-prisma': '^2.
|
|
10
|
-
'@basaltkit/cli': '^1.
|
|
7
|
+
'@basaltkit/ai-mcp': '^0.4.0',
|
|
8
|
+
'@basaltkit/auth': '^4.2.0',
|
|
9
|
+
'@basaltkit/auth-prisma': '^2.1.0',
|
|
10
|
+
'@basaltkit/cli': '^1.4.0',
|
|
11
11
|
'@basaltkit/config': '^1.1.0',
|
|
12
|
-
'@basaltkit/core': '^1.
|
|
12
|
+
'@basaltkit/core': '^1.6.0',
|
|
13
13
|
'@basaltkit/env': '^3.1.0',
|
|
14
14
|
'@basaltkit/events': '^1.3.0',
|
|
15
|
-
'@basaltkit/fastify': '^2.
|
|
16
|
-
'@basaltkit/generator': '^1.
|
|
15
|
+
'@basaltkit/fastify': '^2.6.0',
|
|
16
|
+
'@basaltkit/generator': '^1.6.0',
|
|
17
17
|
'@basaltkit/logger': '^1.3.0',
|
|
18
|
-
'@basaltkit/mcp': '^5.
|
|
19
|
-
'@basaltkit/prisma': '^3.
|
|
18
|
+
'@basaltkit/mcp': '^5.1.0',
|
|
19
|
+
'@basaltkit/prisma': '^3.1.0',
|
|
20
20
|
'@basaltkit/sdk': '^2.1.0',
|
|
21
21
|
'@basaltkit/subscriptions': '^5.0.0',
|
|
22
22
|
'@basaltkit/subscriptions-prisma': '^3.0.0',
|
|
23
|
-
'@basaltkit/teams': '^4.
|
|
23
|
+
'@basaltkit/teams': '^4.1.0',
|
|
24
24
|
'@basaltkit/teams-prisma': '^1.1.0',
|
|
25
|
-
'@basaltkit/tenancy': '^3.
|
|
26
|
-
'@basaltkit/tenancy-prisma': '^2.
|
|
25
|
+
'@basaltkit/tenancy': '^3.2.0',
|
|
26
|
+
'@basaltkit/tenancy-prisma': '^2.1.0',
|
|
27
27
|
'@basaltkit/testing': '^2.0.0',
|
|
28
|
-
'create-basalt': '^1.
|
|
28
|
+
'create-basalt': '^1.12.0',
|
|
29
29
|
};
|
package/dist/wizard.js
CHANGED
|
@@ -11,9 +11,9 @@ export const FEATURES = [
|
|
|
11
11
|
];
|
|
12
12
|
export const PRESETS = [
|
|
13
13
|
{ value: 'saas', label: 'SaaS starter', hint: 'tenancy + auth + billing + database + CLI', features: ['tenancy', 'auth', 'billing', 'prisma', 'cli'] },
|
|
14
|
-
{ value: 'api', label: 'API only', hint: 'auth + MCP, no tenancy or UI', features: ['auth', 'mcp'] },
|
|
14
|
+
{ value: 'api', label: 'API only', hint: 'auth + MCP + CLI, no tenancy or UI', features: ['auth', 'mcp', 'cli'] },
|
|
15
15
|
{ value: 'full', label: 'Full stack', hint: 'everything + web UI', features: ['tenancy', 'auth', 'billing', 'prisma', 'ui', 'cli', 'mcp'] },
|
|
16
|
-
{ value: 'minimal', label: 'Minimal', hint: '
|
|
16
|
+
{ value: 'minimal', label: 'Minimal', hint: 'just the CLI, add the rest later', features: ['cli'] },
|
|
17
17
|
{ value: 'custom', label: 'Custom', hint: 'pick features yourself', features: null },
|
|
18
18
|
];
|
|
19
19
|
const PACKAGE_MANAGERS = [
|
|
@@ -59,7 +59,7 @@ export async function runWizard(prompter, options = {}) {
|
|
|
59
59
|
(await prompter.multiselect({
|
|
60
60
|
message: 'Select features',
|
|
61
61
|
choices: FEATURES,
|
|
62
|
-
initial: ['tenancy', 'auth'],
|
|
62
|
+
initial: ['tenancy', 'auth', 'cli'],
|
|
63
63
|
})));
|
|
64
64
|
let pm = await prompter.select({
|
|
65
65
|
message: 'Package manager',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-basalt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.5.0"
|
|
6
6
|
},
|
|
@@ -23,33 +23,35 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@prisma/adapter-pg": "^7.10.0",
|
|
25
25
|
"@prisma/client": "^7.10.0",
|
|
26
|
+
"@prisma/client-runtime-utils": "^7.10.0",
|
|
26
27
|
"@types/node": "^26.6.1",
|
|
27
28
|
"@types/pg": "^8.23.1",
|
|
28
29
|
"pg": "^8.23.0",
|
|
29
30
|
"prisma": "^7.10.0",
|
|
31
|
+
"tsx": "^4.23.13",
|
|
30
32
|
"typescript": "^7.0.2",
|
|
31
33
|
"vitest": "^5.0.1",
|
|
32
34
|
"zod": "^4.6.5",
|
|
33
|
-
"@basaltkit/
|
|
35
|
+
"@basaltkit/auth": "^4.2.0",
|
|
36
|
+
"@basaltkit/auth-prisma": "^2.1.0",
|
|
37
|
+
"@basaltkit/cli": "^1.4.0",
|
|
38
|
+
"@basaltkit/core": "^1.6.0",
|
|
34
39
|
"@basaltkit/events": "^1.3.0",
|
|
35
|
-
"@basaltkit/
|
|
36
|
-
"@basaltkit/
|
|
37
|
-
"@basaltkit/
|
|
38
|
-
"@basaltkit/auth-prisma": "^2.0.0",
|
|
40
|
+
"@basaltkit/fastify": "^2.6.0",
|
|
41
|
+
"@basaltkit/config": "^1.1.2",
|
|
42
|
+
"@basaltkit/generator": "^1.6.0",
|
|
39
43
|
"@basaltkit/logger": "^1.3.0",
|
|
40
|
-
"@basaltkit/
|
|
41
|
-
"@basaltkit/
|
|
44
|
+
"@basaltkit/env": "^3.1.1",
|
|
45
|
+
"@basaltkit/mcp": "^5.1.0",
|
|
46
|
+
"@basaltkit/prisma": "^3.1.0",
|
|
47
|
+
"@basaltkit/subscriptions": "^5.0.0",
|
|
42
48
|
"@basaltkit/subscriptions-prisma": "^3.0.0",
|
|
43
|
-
"@basaltkit/teams": "^4.
|
|
44
|
-
"@basaltkit/
|
|
45
|
-
"@basaltkit/cli": "^1.3.0",
|
|
46
|
-
"@basaltkit/tenancy": "^3.1.0",
|
|
49
|
+
"@basaltkit/teams": "^4.1.0",
|
|
50
|
+
"@basaltkit/tenancy": "^3.2.0",
|
|
47
51
|
"@basaltkit/teams-prisma": "^1.1.0",
|
|
48
|
-
"@basaltkit/tenancy-prisma": "^2.
|
|
49
|
-
"@basaltkit/testing": "^2.0.3",
|
|
50
|
-
"@basaltkit/subscriptions": "^5.0.0",
|
|
52
|
+
"@basaltkit/tenancy-prisma": "^2.1.0",
|
|
51
53
|
"@basaltkit/tsconfig": "^0.24.0",
|
|
52
|
-
"@basaltkit/
|
|
54
|
+
"@basaltkit/testing": "^2.0.3"
|
|
53
55
|
},
|
|
54
56
|
"publishConfig": {
|
|
55
57
|
"access": "public"
|
|
@@ -71,8 +73,9 @@
|
|
|
71
73
|
"boilerplate"
|
|
72
74
|
],
|
|
73
75
|
"scripts": {
|
|
74
|
-
"build": "node scripts/sync-versions.mjs && tsc -p tsconfig.build.json",
|
|
76
|
+
"build": "node scripts/sync-versions.mjs && node scripts/sync-stubs.mjs && tsc -p tsconfig.build.json",
|
|
75
77
|
"sync-versions": "node scripts/sync-versions.mjs",
|
|
78
|
+
"sync-stubs": "node scripts/sync-stubs.mjs",
|
|
76
79
|
"test": "vitest run",
|
|
77
80
|
"typecheck": "tsc --noEmit"
|
|
78
81
|
}
|