create-basalt 1.6.0 → 1.7.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 CHANGED
@@ -135,12 +135,12 @@ Always:
135
135
  my-app/
136
136
  ├── package.json # scripts: dev, start, test, typecheck (+ basalt with --cli)
137
137
  ├── tsconfig.json # strict TypeScript, ESM
138
- ├── .env.example # PORT, HOST, LOG_LEVEL, NODE_ENV (+ APP_SECRET with auth) + the --env-file precedence warning
138
+ ├── .env.example # MY_APP_PORT, MY_APP_HOST, MY_APP_LOG_LEVEL, NODE_ENV (+ MY_APP_APP_SECRET with auth) + the --env-file precedence warning
139
139
  ├── .gitignore
140
140
  ├── README.md # instructions adapted to the chosen options
141
141
  ├── pnpm-workspace.yaml # esbuild allowBuilds, @basaltkit/* release-age exclusion (+ "web" member with --ui) + pnpm 11 notes
142
142
  ├── src/
143
- │ ├── env.ts # environment variables validated with Zod (@basaltkit/env)
143
+ │ ├── env.ts # environment variables validated with Zod (@basaltkit/env), app-prefixed
144
144
  │ ├── app.ts # buildApp() with the chosen plugins
145
145
  │ ├── routes.ts # GET / (friendly index) and GET /health
146
146
  │ └── server.ts # startup + clean shutdown on SIGINT/SIGTERM
@@ -176,7 +176,28 @@ With pnpm 11, every `pnpm <script>` and `pnpm exec` first verifies dependencies
176
176
 
177
177
  ### Environment variables and `--env-file`
178
178
 
179
- Nothing loads `.env` for you. `node --env-file=.env` / `tsx --env-file=.env` **never override a variable already exported in the shell** — in a terminal where another project exported `DATABASE_URL` or `PORT`, the app boots against that value and only fails on the first request that touches it. The generated `.env.example` and README say so and suggest an app-specific prefix derived from the project name (`my-saas` → `MY_SAAS_DATABASE_URL`).
179
+ Nothing loads `.env` for you. `node --env-file=.env` / `tsx --env-file=.env` **never override a variable already exported in the shell** — in a terminal where another project exported `DATABASE_URL` or `PORT`, an app reading the generic name boots against that value and only fails on the first request that touches it.
180
+
181
+ The scaffold closes that trap instead of only warning about it: `src/env.ts` passes an **app-specific prefix** derived from the project name (`my-saas` → `MY_SAAS`) to `defineEnv`, and `.env.example` uses the prefixed names.
182
+
183
+ ```ts
184
+ // src/env.ts (generated)
185
+ export const env = defineEnv(
186
+ { PORT: z.coerce.number().default(3000), /* … */ },
187
+ { prefix: 'MY_SAAS' },
188
+ )
189
+ ```
190
+
191
+ ```bash
192
+ # .env.example (generated)
193
+ MY_SAAS_PORT=3000
194
+ MY_SAAS_HOST=0.0.0.0
195
+ MY_SAAS_LOG_LEVEL=info
196
+ NODE_ENV=development # never prefixed — a Node-wide convention
197
+ # MY_SAAS_APP_SECRET= # with auth
198
+ ```
199
+
200
+ Each variable is read as `MY_SAAS_<NAME>` first and **falls back** to the bare `<NAME>`, so a deployment that already exports the generic names keeps booting — while a stray `PORT` in your shell no longer wins. The rest of the app is unchanged: the keys stay bare (`env.PORT`). To require the prefixed names only, edit the generated file to `prefix: { value: 'MY_SAAS', fallback: false }`. Full rules: [`@basaltkit/env`](https://github.com/basaltkit/basalt/tree/main/packages/env#app-specific-prefix-prefix).
180
201
 
181
202
  ### Programmatic usage (Advanced)
182
203
 
@@ -267,10 +288,10 @@ Outside an interactive terminal (no TTY) the questions are skipped. Always pass
267
288
  Automatic installation failed (network, Node version, etc.). Go into the folder and run `pnpm install` (or the indicated manager) to see the real error.
268
289
 
269
290
  **I started the app and `GET /auth/login` gives a secret error.**
270
- With auth on, `src/env.ts` requires `APP_SECRET` with at least 16 characters (there's a development default `change-me-in-production--`). Copy `.env.example` to `.env` and set your own secret before going to production.
291
+ With auth on, `src/env.ts` requires `APP_SECRET` — read as `MY_SAAS_APP_SECRET`, with at least 32 characters (`secret()` supplies a development default only when `NODE_ENV` is explicitly `development`/`test`). Copy `.env.example` to `.env` and set your own secret before going to production.
271
292
 
272
293
  **My app connects to the wrong database / port.**
273
- A variable exported in your shell beats `--env-file`. Check `env | grep DATABASE_URL`, start with `env -u DATABASE_URL pnpm dev`, and prefer app-prefixed names (see *Environment variables and `--env-file`*).
294
+ A variable exported in your shell beats `--env-file`. The generated `src/env.ts` already reads app-prefixed names (`MY_SAAS_PORT`), so set those rather than the generic ones — the bare names are only a fallback. Check `env | grep DATABASE_URL`, and see *Environment variables and `--env-file`*.
274
295
 
275
296
  **`pnpm basalt …` starts by running `pnpm install` (or fails offline).**
276
297
  pnpm 11's `verifyDepsBeforeRun` (default `install`). Run `node_modules/.bin/tsx bin/basalt.ts …` instead, or set `verifyDepsBeforeRun: warn` in `pnpm-workspace.yaml`.
package/dist/templates.js CHANGED
@@ -105,25 +105,39 @@ export function tsconfigJson() {
105
105
  }, null, 2)}\n`;
106
106
  }
107
107
  export function envTs(options) {
108
+ const prefix = envPrefix(options.name);
108
109
  return `import { defineEnv${options.auth ? ', secret' : ''} } from '@basaltkit/env'
109
110
  import { LOG_LEVELS } from '@basaltkit/logger'
110
111
  import { z } from 'zod'
111
112
 
112
- export const env = defineEnv({
113
- PORT: z.coerce.number().default(3000),
114
- HOST: z.string().default('0.0.0.0'),
115
- // Typed against the logger's LogLevel union — a free-form string here fails
116
- // \`pnpm typecheck\` where loggerPlugin({ level }) consumes it.
117
- LOG_LEVEL: z.enum(LOG_LEVELS).default('info'),
118
- // Unset counts as production (fail-closed); \`pnpm dev\` sets development.
119
- NODE_ENV: z.enum(['development', 'production', 'test']).default('production'),${options.auth
113
+ export const env = defineEnv(
114
+ {
115
+ PORT: z.coerce.number().default(3000),
116
+ HOST: z.string().default('0.0.0.0'),
117
+ // Typed against the logger's LogLevel union — a free-form string here fails
118
+ // \`pnpm typecheck\` where loggerPlugin({ level }) consumes it.
119
+ LOG_LEVEL: z.enum(LOG_LEVELS).default('info'),
120
+ // Unset counts as production (fail-closed); \`pnpm dev\` sets development.
121
+ // NODE_ENV is a Node-wide convention and is never prefixed.
122
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('production'),${options.auth
120
123
  ? `
121
- // Signs JWTs and sessions. secret() is fail-closed: required unless NODE_ENV
122
- // is explicitly development/test (no fallback when NODE_ENV is unset),
123
- // rejected if it looks like a placeholder. \`pnpm dev\` uses devDefault.
124
- APP_SECRET: secret({ minLength: 32, devDefault: 'dev-only-insecure-secret-please-change-me' }),`
124
+ // Signs JWTs and sessions. secret() is fail-closed: required unless NODE_ENV
125
+ // is explicitly development/test (no fallback when NODE_ENV is unset),
126
+ // rejected if it looks like a placeholder. \`pnpm dev\` uses devDefault.
127
+ APP_SECRET: secret({ minLength: 32, devDefault: 'dev-only-insecure-secret-please-change-me' }),`
125
128
  : ''}
126
- })
129
+ },
130
+ {
131
+ // Every variable is read as ${prefix}_<NAME> first (${prefix}_PORT before PORT),
132
+ // falling back to the bare name. \`--env-file\` never overrides a variable
133
+ // already exported in your shell, so without the prefix another project's
134
+ // exported PORT / DATABASE_URL would silently win. The keys above stay bare:
135
+ // the rest of the app still reads env.PORT.
136
+ // To require the prefixed names (no bare fallback at all), write:
137
+ // prefix: { value: '${prefix}', fallback: false }
138
+ prefix: '${prefix}',
139
+ },
140
+ )
127
141
  `;
128
142
  }
129
143
  /**
@@ -151,13 +165,24 @@ export function envExample(options) {
151
165
  return `# Nothing loads this file for you: copy it to .env and start with
152
166
  # \`node --env-file=.env\` / \`tsx --env-file=.env\`, or export the variables.
153
167
  # --env-file NEVER overrides a variable already exported in your shell: if
154
- # another project exported DATABASE_URL, the app silently uses THAT one. Give
155
- # generic names an app-specific prefix, e.g. ${prefix}_DATABASE_URL.
156
- PORT=3000
157
- HOST=0.0.0.0
158
- LOG_LEVEL=info
168
+ # another project exported PORT or DATABASE_URL, an app reading the generic
169
+ # name would silently use THAT one. That is why src/env.ts reads app-prefixed
170
+ # names — ${prefix}_PORT wins over a stray PORT. The bare names still work as a
171
+ # fallback, so an existing deployment exporting them keeps booting.
172
+ ${prefix}_PORT=3000
173
+ ${prefix}_HOST=0.0.0.0
174
+ ${prefix}_LOG_LEVEL=info
175
+ # NODE_ENV is a Node-wide convention and is never prefixed.
159
176
  NODE_ENV=development
160
- ${options.auth ? '# Required unless NODE_ENV is development/test — `pnpm start` refuses to boot\n# without it. Generate a strong one: openssl rand -base64 48\n# APP_SECRET=\n' : ''}`;
177
+ ${options.auth
178
+ ? `# Required unless NODE_ENV is development/test — \`pnpm start\` refuses to boot
179
+ # without it. Generate a strong one: openssl rand -base64 48
180
+ # ${prefix}_APP_SECRET=
181
+ `
182
+ : ''}# When you add a database, declare DATABASE_URL in src/env.ts and set it here
183
+ # under the prefixed name:
184
+ # ${prefix}_DATABASE_URL=postgres://user:pass@localhost:5432/${options.name.replace(/^@[^/]+\//, '')}
185
+ `;
161
186
  }
162
187
  export function appTs(options) {
163
188
  const imports = [
@@ -461,11 +486,21 @@ pnpm test
461
486
  \`.env.example\` to \`.env\` and launch with \`--env-file=.env\` (Node/tsx), or
462
487
  export the variables.
463
488
 
464
- **\`--env-file\` never overrides a variable that is already exported.** In a shell
465
- where another project exported \`DATABASE_URL\` (or \`PORT\`), this app boots
466
- against THAT value and only fails on the first request that touches it. Give
467
- generic names an app-specific prefix — \`${envPrefix(options.name)}_DATABASE_URL\` rather than
468
- \`DATABASE_URL\` — and check \`env | grep DATABASE_URL\` when in doubt.
489
+ The variables are **app-prefixed**: \`${envPrefix(options.name)}_PORT\`,
490
+ \`${envPrefix(options.name)}_HOST\`, \`${envPrefix(options.name)}_LOG_LEVEL\`${options.auth ? `, \`${envPrefix(options.name)}_APP_SECRET\`` : ''}. \`NODE_ENV\` is never prefixed.
491
+
492
+ Why: **\`--env-file\` never overrides a variable that is already exported.** In a
493
+ shell where another project exported \`DATABASE_URL\` (or \`PORT\`), an app reading
494
+ the generic name boots against THAT value and only fails on the first request
495
+ that touches it. \`src/env.ts\` therefore passes \`prefix: '${envPrefix(options.name)}'\` to
496
+ \`defineEnv\`: each variable is read as \`${envPrefix(options.name)}_<NAME>\` first and
497
+ **falls back** to the bare \`<NAME>\`, so a deployment that already exports the
498
+ generic names keeps booting while a stray one in your shell loses. Add a
499
+ database and \`DATABASE_URL\` is read as \`${envPrefix(options.name)}_DATABASE_URL\`.
500
+
501
+ To drop the fallback entirely (prefixed names only), write
502
+ \`prefix: { value: '${envPrefix(options.name)}', fallback: false }\`. When in doubt about what
503
+ your shell exports: \`env | grep DATABASE_URL\`.
469
504
  ${options.cli
470
505
  ? `
471
506
  ## The \`basalt\` CLI
package/dist/versions.js CHANGED
@@ -8,15 +8,15 @@ export const SCAFFOLD_VERSIONS = {
8
8
  '@basaltkit/auth': '^3.1.0',
9
9
  '@basaltkit/cli': '^1.2.0',
10
10
  '@basaltkit/config': '^1.1.0',
11
- '@basaltkit/core': '^1.3.0',
12
- '@basaltkit/env': '^3.0.0',
11
+ '@basaltkit/core': '^1.4.0',
12
+ '@basaltkit/env': '^3.1.0',
13
13
  '@basaltkit/events': '^1.3.0',
14
14
  '@basaltkit/fastify': '^2.2.0',
15
- '@basaltkit/generator': '^1.4.0',
15
+ '@basaltkit/generator': '^1.5.0',
16
16
  '@basaltkit/logger': '^1.3.0',
17
17
  '@basaltkit/mcp': '^3.0.0',
18
18
  '@basaltkit/prisma': '^2.1.0',
19
- '@basaltkit/sdk': '^2.0.0',
19
+ '@basaltkit/sdk': '^2.1.0',
20
20
  '@basaltkit/subscriptions': '^4.0.0',
21
21
  '@basaltkit/teams': '^3.0.0',
22
22
  '@basaltkit/tenancy': '^2.0.0',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-basalt",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "engines": {
5
5
  "node": ">=22.5.0"
6
6
  },
@@ -25,22 +25,22 @@
25
25
  "typescript": "^7.0.2",
26
26
  "vitest": "^5.0.1",
27
27
  "zod": "^4.6.5",
28
- "@basaltkit/auth": "^3.1.0",
29
28
  "@basaltkit/config": "^1.1.2",
30
- "@basaltkit/core": "^1.3.2",
31
- "@basaltkit/env": "^3.0.0",
29
+ "@basaltkit/auth": "^3.1.0",
32
30
  "@basaltkit/cli": "^1.2.5",
31
+ "@basaltkit/env": "^3.1.0",
32
+ "@basaltkit/core": "^1.4.0",
33
33
  "@basaltkit/events": "^1.3.0",
34
- "@basaltkit/generator": "^1.4.0",
34
+ "@basaltkit/fastify": "^2.2.0",
35
+ "@basaltkit/generator": "^1.5.0",
35
36
  "@basaltkit/logger": "^1.3.0",
36
37
  "@basaltkit/mcp": "^3.0.0",
37
- "@basaltkit/prisma": "^2.1.0",
38
+ "@basaltkit/subscriptions": "^4.0.0",
38
39
  "@basaltkit/teams": "^3.0.1",
39
- "@basaltkit/fastify": "^2.2.0",
40
40
  "@basaltkit/tenancy": "^2.0.0",
41
+ "@basaltkit/tsconfig": "^0.24.0",
41
42
  "@basaltkit/testing": "^2.0.2",
42
- "@basaltkit/subscriptions": "^4.0.0",
43
- "@basaltkit/tsconfig": "^0.24.0"
43
+ "@basaltkit/prisma": "^2.1.0"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"