create-basalt 1.11.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 +26 -7
- package/dist/bin-template.js +15 -1
- package/dist/cli.js +4 -1
- package/dist/index.js +3 -1
- package/dist/latest-versions.js +1 -0
- package/dist/project/doctor.d.ts +1 -1
- package/dist/project/doctor.js +53 -1
- package/dist/project/production.d.ts +32 -0
- package/dist/project/production.js +74 -0
- package/dist/project/seed-script.d.ts +18 -0
- package/dist/project/seed-script.js +32 -0
- package/dist/project/update.js +19 -0
- package/dist/stubs.d.ts +30 -0
- package/dist/stubs.js +123 -0
- package/dist/templates.d.ts +33 -3
- package/dist/templates.js +130 -40
- package/dist/versions.js +13 -13
- package/package.json +19 -16
package/README.md
CHANGED
|
@@ -138,9 +138,12 @@ Always:
|
|
|
138
138
|
|
|
139
139
|
```
|
|
140
140
|
my-app/
|
|
141
|
-
├── package.json # scripts: dev, start, test, typecheck, basalt (+ dev:web with --ui); create-basalt as a devDependency
|
|
141
|
+
├── package.json # scripts: dev, build, start (node dist/src/server.js), start:dev (tsx), test, typecheck, basalt (+ dev:web with --ui); create-basalt as a devDependency
|
|
142
142
|
├── .basalt/project.json # scaffold manifest: create-basalt version, options, a hash per generated file — commit it
|
|
143
143
|
├── tsconfig.json # strict TypeScript, ESM
|
|
144
|
+
├── tsconfig.build.json # what `pnpm build` compiles: src/ → dist/ (rootDir ".", so dist/src/server.js)
|
|
145
|
+
├── Dockerfile # multi-stage: build with the dev toolchain, run dist/src/server.js on plain node as USER node
|
|
146
|
+
├── .dockerignore # keeps .env, keys, node_modules and dist out of the build context
|
|
144
147
|
├── .env.example # MY_APP_PORT, MY_APP_HOST, MY_APP_LOG_LEVEL, NODE_ENV (+ MY_APP_APP_SECRET with auth) + the precedence warning — committed
|
|
145
148
|
├── .env # the same values for local development (+ a generated MY_APP_APP_SECRET with auth) — git-ignored, loaded by dev/basalt
|
|
146
149
|
├── .gitignore
|
|
@@ -150,12 +153,12 @@ my-app/
|
|
|
150
153
|
│ ├── env.ts # environment variables validated with Zod (@basaltkit/env), app-prefixed
|
|
151
154
|
│ ├── app.ts # buildApp() with the chosen plugins
|
|
152
155
|
│ ├── routes.ts # GET / (friendly index) and GET /health
|
|
153
|
-
│ ├── server.ts # startup + clean shutdown on SIGINT/SIGTERM (`pnpm start` — loads no .env)
|
|
156
|
+
│ ├── server.ts # startup + clean shutdown on SIGINT/SIGTERM (`pnpm start` runs it compiled — loads no .env)
|
|
154
157
|
│ └── dev.ts # `pnpm dev`: loads .env, defaults NODE_ENV=development, starts server.ts
|
|
155
158
|
└── tests/app.test.ts # smoke test adapted to the options
|
|
156
159
|
```
|
|
157
160
|
|
|
158
|
-
With `--prisma`, adds `prisma/schema.prisma`, `prisma.config.ts`, `src/db.ts`, `prisma/seed.ts` (with tenancy) and the `db:generate` / `db:migrate` / `db:deploy` / `db:seed` scripts. The `basalt` script is `create-basalt --project` with `--no-cli` (so `pnpm basalt update|add|doctor|info` works in every app); with `--cli` it is `tsx bin/basalt.ts`, and `bin/basalt.ts` forwards those four commands to create-basalt before booting the app. With `--ui`, adds the `web/` folder (Vite 8 + React 19 + Tailwind CSS 4 via `@tailwindcss/vite` + shadcn — Tailwind is configured in `web/src/index.css`, no `tailwind.config.js`/PostCSS — with `web/src/api.ts` built on top of `@basaltkit/sdk`; with auth on it includes a login/register screen).
|
|
161
|
+
With `--prisma`, adds `prisma/schema.prisma`, `prisma.config.ts`, `src/db.ts`, `prisma/seed.ts` (with tenancy) and the `db:generate` / `db:migrate` / `db:deploy` / `db:seed` scripts. The client is generated into `generated/prisma` (outside `src/`, so the build never copies it) and imported as `#db/client.js` through a package.json `imports` alias; `@prisma/client-runtime-utils` is a direct dependency (the generated runtime requires it by name), and `pnpm-workspace.yaml` approves the `prisma` / `@prisma/engines` build scripts. The `basalt` script is `create-basalt --project` with `--no-cli` (so `pnpm basalt update|add|doctor|info` works in every app); with `--cli` it is `tsx bin/basalt.ts`, and `bin/basalt.ts` forwards those four commands to create-basalt before booting the app. With `--ui`, adds the `web/` folder (Vite 8 + React 19 + Tailwind CSS 4 via `@tailwindcss/vite` + shadcn — Tailwind is configured in `web/src/index.css`, no `tailwind.config.js`/PostCSS — with `web/src/api.ts` built on top of `@basaltkit/sdk`; with auth on it includes a login/register screen).
|
|
159
162
|
|
|
160
163
|
### With `--ui`: running the API and frontend
|
|
161
164
|
|
|
@@ -188,10 +191,13 @@ the next restart.
|
|
|
188
191
|
pnpm create basalt my-app --prisma
|
|
189
192
|
cd my-app && pnpm install # postinstall runs `prisma generate`
|
|
190
193
|
# .env already points MY_APP_DATABASE_URL at postgres://…@localhost:5432/my_app — start PostgreSQL or edit it
|
|
191
|
-
pnpm db:migrate # prisma migrate dev — creates the tables
|
|
194
|
+
pnpm db:migrate # prisma migrate dev — creates the tables
|
|
195
|
+
pnpm db:seed # prisma db seed — the `demo` tenant
|
|
192
196
|
pnpm dev
|
|
193
197
|
```
|
|
194
198
|
|
|
199
|
+
After a schema change: `pnpm db:migrate --name <change>` writes and applies the migration (without `--name` Prisma prompts for one), then `pnpm db:generate` — Prisma 7's `migrate dev` does not regenerate the client.
|
|
200
|
+
|
|
195
201
|
**Migrations, never `prisma db push`.** The generated app boots with
|
|
196
202
|
`assertMigrated: true`, which refuses to start unless the database it reached has
|
|
197
203
|
the `_prisma_migrations` table — written by `prisma migrate dev` /
|
|
@@ -288,6 +294,13 @@ file". Files are patched **only** when they are byte-for-byte a known template
|
|
|
288
294
|
(or the manifest records them untouched); a customised one is left alone and the
|
|
289
295
|
exact snippet to paste is printed.
|
|
290
296
|
|
|
297
|
+
An app from before the production path is **offered** what it lacks:
|
|
298
|
+
`tsconfig.build.json`, a `build` script, the `Dockerfile` (pnpm apps) and
|
|
299
|
+
`.dockerignore`, and with Prisma `@prisma/client-runtime-utils`. Existing files
|
|
300
|
+
are never touched and an existing `start` script is never rewritten — the two
|
|
301
|
+
script lines to paste are printed instead. A Prisma client still generated under
|
|
302
|
+
`src/generated` gets the move instructions and no Dockerfile until it is moved.
|
|
303
|
+
|
|
291
304
|
### Adding features later
|
|
292
305
|
|
|
293
306
|
```bash
|
|
@@ -330,9 +343,15 @@ default — `DATABASE_URL` with `--prisma`) that is set neither in the environme
|
|
|
330
343
|
nor in `.env` — an error, with the fix (`cp .env.example .env`, set it, start
|
|
331
344
|
PostgreSQL); a missing `.env` next to a `.env.example` (warning); whether the Prisma client is generated and
|
|
332
345
|
migrations exist (whether they are *applied* needs a database —
|
|
333
|
-
`prisma migrate status`);
|
|
334
|
-
|
|
335
|
-
`
|
|
346
|
+
`prisma migrate status`); the `db:seed` script of create-basalt 1.8–1.11
|
|
347
|
+
(`tsx prisma/seed.ts`, which loads no `.env` — a warning with the exact
|
|
348
|
+
replacement, `"db:seed": "prisma db seed"`; `update` prints the same line and
|
|
349
|
+
never rewrites the script); `.mcp.json` when `@basaltkit/ai-mcp` is installed; dev
|
|
350
|
+
tooling declared as a runtime dependency; an outdated `bin/basalt.ts` or
|
|
351
|
+
`src/dev.ts`; and, statically, the production path (a `start` running tsx while
|
|
352
|
+
tsx is a devDependency, no `build` script, `dist/src/server.js` older than
|
|
353
|
+
`src/`, a Prisma client generated under `src/`, a missing
|
|
354
|
+
`@prisma/client-runtime-utils`). It never builds or starts the app.
|
|
336
355
|
|
|
337
356
|
`info` prints create-basalt, Node, OS, package manager, the app's features and
|
|
338
357
|
the declared/installed versions of the framework and key tools — paste it into
|
package/dist/bin-template.js
CHANGED
|
@@ -92,7 +92,11 @@ if (existsSync(envFile)) process.loadEnvFile(envFile)
|
|
|
92
92
|
* migrated. Anything else is rethrown untouched.
|
|
93
93
|
*/
|
|
94
94
|
async function explainBootFailure(error: unknown): Promise<never> {
|
|
95
|
-
const failure = (error ?? {}) as {
|
|
95
|
+
const failure = (error ?? {}) as {
|
|
96
|
+
code?: unknown; report?: unknown; message?: unknown; cause?: unknown; errors?: unknown; details?: unknown
|
|
97
|
+
}
|
|
98
|
+
// @basaltkit/prisma's describeDbError diagnosis (assertMigrated attaches it): the cause and its one-line fix.
|
|
99
|
+
const diagnosis = (failure.details as { diagnosis?: { code?: unknown; cause?: unknown; fix?: unknown } } | undefined)?.diagnosis
|
|
96
100
|
const message = String(failure.message ?? '')
|
|
97
101
|
const codes = [failure, failure.cause, ...(Array.isArray(failure.errors) ? failure.errors : [])].map((item) =>
|
|
98
102
|
String((item as { code?: unknown } | null | undefined)?.code ?? ''),
|
|
@@ -116,6 +120,16 @@ async function explainBootFailure(error: unknown): Promise<never> {
|
|
|
116
120
|
(existsSync(join(root, 'prisma')) ? ' — for DATABASE_URL, start PostgreSQL and point the URL at it' : '') +
|
|
117
121
|
'. \`pnpm basalt doctor\` checks the environment; ' + debugHint,
|
|
118
122
|
].join('\\n')
|
|
123
|
+
} else if (diagnosis && (diagnosis.code === 'DB_PERMISSION_DENIED' || diagnosis.code === 'DB_NOT_EMPTY_BASELINE')) {
|
|
124
|
+
const database = await databaseInUse()
|
|
125
|
+
explanation = [
|
|
126
|
+
diagnosis.code === 'DB_PERMISSION_DENIED'
|
|
127
|
+
? 'The app cannot start — the database refused the app database role (permission denied):'
|
|
128
|
+
: 'The app cannot start — the database has tables but no migration history:',
|
|
129
|
+
' ' + String(diagnosis.cause),
|
|
130
|
+
'Database: ' + database.target + ' (from ' + database.name + '; credentials never shown).',
|
|
131
|
+
'Fix: ' + String(diagnosis.fix) + ' ' + debugHint,
|
|
132
|
+
].join('\\n')
|
|
119
133
|
} else if (unreachable || failure.code === 'PRISMA_NOT_MIGRATED') {
|
|
120
134
|
const database = await databaseInUse()
|
|
121
135
|
const where = 'Database: ' + database.target + ' (from ' + database.name + '; credentials never shown).'
|
package/dist/cli.js
CHANGED
|
@@ -154,7 +154,10 @@ try {
|
|
|
154
154
|
// the app asserts at boot that it reached the MIGRATED database.
|
|
155
155
|
// .env is already written with a local URL (pnpm dev / pnpm basalt load it).
|
|
156
156
|
steps.push(`# start PostgreSQL, or edit ${envPrefix(result.options.name)}_DATABASE_URL in .env`);
|
|
157
|
-
steps.push(`${pm} run db:migrate
|
|
157
|
+
steps.push(`${pm} run db:migrate # creates the tables`);
|
|
158
|
+
// Prisma 7's `migrate dev` no longer seeds: the demo tenant is its own step.
|
|
159
|
+
if (flags.tenancy)
|
|
160
|
+
steps.push(`${pm} run db:seed # the demo tenant the resolvers expect`);
|
|
158
161
|
}
|
|
159
162
|
steps.push(`${pm} run dev${flags.ui ? ' # API on :3000' : ''}`);
|
|
160
163
|
// `web` is a pnpm workspace member (pnpm-workspace.yaml); `dev:web` runs its
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { appTest, appTs, dbTs, devEnvFile, devTs, dockerignore, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, prismaConfigTs, prismaSchema, prismaSeedTs, readme, routesTs, serverTs, tsconfigJson, } from './templates.js';
|
|
4
|
+
import { appTest, appTs, dbTs, devEnvFile, devTs, dockerfile, dockerignore, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, prismaConfigTs, prismaSchema, prismaSeedTs, readme, routesTs, serverTs, tsconfigBuildJson, tsconfigJson, } from './templates.js';
|
|
5
5
|
import { uiFiles } from './templates-ui.js';
|
|
6
6
|
import { INSECURE_SECRET } from './project/doctor.js';
|
|
7
7
|
import { createManifest, MANIFEST_PATH, serializeManifest } from './project/manifest.js';
|
|
@@ -43,8 +43,10 @@ export function scaffoldFiles(options) {
|
|
|
43
43
|
return {
|
|
44
44
|
'package.json': packageJson(options),
|
|
45
45
|
'tsconfig.json': tsconfigJson(options),
|
|
46
|
+
'tsconfig.build.json': tsconfigBuildJson(),
|
|
46
47
|
'.env.example': envExample(options),
|
|
47
48
|
'.gitignore': gitignore(options),
|
|
49
|
+
Dockerfile: dockerfile(),
|
|
48
50
|
'.dockerignore': dockerignore(),
|
|
49
51
|
'README.md': readme(options),
|
|
50
52
|
'pnpm-workspace.yaml': pnpmWorkspaceYaml(options),
|
package/dist/latest-versions.js
CHANGED
package/dist/project/doctor.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Colors } from './term.js';
|
|
|
9
9
|
export type Level = 'ok' | 'info' | 'warn' | 'error';
|
|
10
10
|
export interface DoctorFinding {
|
|
11
11
|
level: Level;
|
|
12
|
-
/** Short area label: node, pm, deps, env, prisma, mcp, tooling. */
|
|
12
|
+
/** Short area label: node, pm, deps, env, prisma, mcp, tooling, build. */
|
|
13
13
|
area: string;
|
|
14
14
|
message: string;
|
|
15
15
|
}
|
package/dist/project/doctor.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import { arch, platform } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { binStatus } from '../bin-template.js';
|
|
6
6
|
import { devEntryStatus } from '../dev-entry.js';
|
|
7
7
|
import { alwaysLatest, lookupLatestVersions } from '../latest-versions.js';
|
|
8
|
+
import { PRODUCTION_ENTRY } from '../stubs.js';
|
|
8
9
|
import { envPrefix } from '../templates.js';
|
|
9
10
|
import { allDependencies } from './context.js';
|
|
10
11
|
import { ownVersion } from './manifest.js';
|
|
12
|
+
import { LEGACY_PRISMA_OUTPUT, RUNS_TSX } from './production.js';
|
|
13
|
+
import { legacySeedScriptFix } from './seed-script.js';
|
|
11
14
|
import { compareVersions, parseSimpleRange, parseVersion, satisfies } from './semver.js';
|
|
12
15
|
/** Mirrors the placeholder check of `secret()` in @basaltkit/env (src/secret.ts). */
|
|
13
16
|
export const INSECURE_SECRET = /change.?me|changeme|placeholder|example|secret|password|default|test|xxxx+|0000+/i;
|
|
@@ -344,6 +347,9 @@ export async function runDoctor(ctx, options = {}) {
|
|
|
344
347
|
push('warn', 'prisma', `No migrations yet — run \`${ctx.pm === 'npm' ? 'npm run' : ctx.pm} db:migrate\` (the app boots with assertMigrated).`);
|
|
345
348
|
else
|
|
346
349
|
push('info', 'prisma', `${count} migration(s) on disk; whether they are applied needs the database: \`prisma migrate status\`.`);
|
|
350
|
+
const seedFix = legacySeedScriptFix(ctx.packageJson, await readText(join(dir, 'prisma.config.ts')));
|
|
351
|
+
if (seedFix !== undefined)
|
|
352
|
+
push('warn', 'prisma', seedFix);
|
|
347
353
|
}
|
|
348
354
|
// --- MCP -------------------------------------------------------------------------
|
|
349
355
|
if (deps['@basaltkit/ai-mcp'] !== undefined) {
|
|
@@ -389,8 +395,54 @@ export async function runDoctor(ctx, options = {}) {
|
|
|
389
395
|
push('info', 'tooling', 'src/dev.ts is customised and does not load .env — see `create-basalt update` for the snippet.');
|
|
390
396
|
if (!ctx.manifest)
|
|
391
397
|
push('info', 'tooling', 'No .basalt/project.json (scaffolded before create-basalt 1.10) — features are inferred from package.json.');
|
|
398
|
+
// --- Production path (BK-026) — static: nothing is built or started ----------
|
|
399
|
+
const scripts = ctx.packageJson.scripts ?? {};
|
|
400
|
+
const run = (script) => `${ctx.pm === 'npm' ? 'npm run' : ctx.pm} ${script}`;
|
|
401
|
+
const start = scripts['start'];
|
|
402
|
+
const tsxIsDevOnly = ctx.packageJson.dependencies?.['tsx'] === undefined && ctx.packageJson.devDependencies?.['tsx'] !== undefined;
|
|
403
|
+
if (start !== undefined && RUNS_TSX.test(start) && tsxIsDevOnly) {
|
|
404
|
+
push('warn', 'build', `\`start\` runs tsx ("${start}"), but tsx is only a devDependency — a production install (--prod) has no tsx. Build and run the compiled server: \`npx create-basalt@latest update\` prints the scripts.`);
|
|
405
|
+
}
|
|
406
|
+
if (scripts['build'] === undefined) {
|
|
407
|
+
push('warn', 'build', `No \`build\` script — there is no tested way to run the app on plain node. \`npx create-basalt@latest update\` adds tsconfig.build.json and \`build\`.`);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const built = await mtimeOf(join(dir, PRODUCTION_ENTRY));
|
|
411
|
+
const source = await newestMtime(join(dir, 'src'));
|
|
412
|
+
if (built !== undefined && source !== undefined && source > built) {
|
|
413
|
+
push('info', 'build', `${PRODUCTION_ENTRY} is older than src/ — run \`${run('build')}\` before \`${run('start')}\`.`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (schema !== undefined) {
|
|
417
|
+
if (LEGACY_PRISMA_OUTPUT.test(schema)) {
|
|
418
|
+
push('warn', 'build', 'The Prisma client is generated under src/ — `tsc` does not copy its .js files, so the built app cannot load it. `npx create-basalt@latest update` prints how to move it to ./generated.');
|
|
419
|
+
}
|
|
420
|
+
if (deps['@prisma/client'] !== undefined && deps['@prisma/client-runtime-utils'] === undefined) {
|
|
421
|
+
push('warn', 'build', 'The generated Prisma client requires @prisma/client-runtime-utils by name — declare it as a dependency (same range as @prisma/client), or plain `node` cannot load the client under pnpm.');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
392
424
|
return findings;
|
|
393
425
|
}
|
|
426
|
+
async function mtimeOf(path) {
|
|
427
|
+
try {
|
|
428
|
+
return (await stat(path)).mtimeMs;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Newest modification time of the .ts files under `dir` (recursive), or undefined. */
|
|
435
|
+
async function newestMtime(dir) {
|
|
436
|
+
let newest;
|
|
437
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
438
|
+
for (const entry of entries) {
|
|
439
|
+
const path = join(dir, entry.name);
|
|
440
|
+
const time = entry.isDirectory() ? await newestMtime(path) : entry.name.endsWith('.ts') ? await mtimeOf(path) : undefined;
|
|
441
|
+
if (time !== undefined && (newest === undefined || time > newest))
|
|
442
|
+
newest = time;
|
|
443
|
+
}
|
|
444
|
+
return newest;
|
|
445
|
+
}
|
|
394
446
|
export function renderFindings(findings, colors) {
|
|
395
447
|
const icon = {
|
|
396
448
|
ok: colors.green('✓'),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The production path (BK-026) for an app scaffolded before create-basalt
|
|
3
|
+
* shipped one: `create-basalt update` OFFERS what is missing — it is part of
|
|
4
|
+
* the printed plan, applied only on confirmation — and never rewrites what the
|
|
5
|
+
* app already has. In particular an existing `start` script is never changed:
|
|
6
|
+
* switching it from tsx to the compiled server changes how the app is
|
|
7
|
+
* deployed, so that is printed as a manual step.
|
|
8
|
+
*/
|
|
9
|
+
export interface ProductionPathPlan {
|
|
10
|
+
/** New files: path → content (only files that do not exist yet). */
|
|
11
|
+
files: Record<string, string>;
|
|
12
|
+
/** package.json after the additions (the input text when nothing changes). */
|
|
13
|
+
packageJsonText: string;
|
|
14
|
+
/** One line per change, for the plan's "Project tooling" section. */
|
|
15
|
+
notes: string[];
|
|
16
|
+
/** Steps printed for the user, never applied. */
|
|
17
|
+
manual: string[];
|
|
18
|
+
}
|
|
19
|
+
/** A Prisma generator that still writes the client under src/ — tsc does not copy its .js files to dist/. */
|
|
20
|
+
export declare const LEGACY_PRISMA_OUTPUT: RegExp;
|
|
21
|
+
/** `start` (or any script) that runs TypeScript through tsx. */
|
|
22
|
+
export declare const RUNS_TSX: RegExp;
|
|
23
|
+
export declare const MANUAL_START_SNIPPET = "In package.json, run the compiled server in production (build first: `pnpm build`):\n \"start\": \"node --enable-source-maps dist/src/server.js\",\n \"start:dev\": \"tsx src/server.ts\"";
|
|
24
|
+
export declare const MANUAL_PRISMA_LAYOUT_SNIPPET: string;
|
|
25
|
+
/**
|
|
26
|
+
* What an existing app lacks of the production path: tsconfig.build.json, a
|
|
27
|
+
* `build` script, the Dockerfile and .dockerignore, and — for a Prisma app —
|
|
28
|
+
* a direct `@prisma/client-runtime-utils` dependency. Pure apart from reading
|
|
29
|
+
* the app's files. The Dockerfile is offered to pnpm apps only, and not
|
|
30
|
+
* while the Prisma client is still generated under src/.
|
|
31
|
+
*/
|
|
32
|
+
export declare function planProductionPath(dir: string, packageJsonText: string, pm?: string): Promise<ProductionPathPlan>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DOCKERFILE, DOCKERIGNORE, PRODUCTION_ENTRY } from '../stubs.js';
|
|
4
|
+
import { BUILD_SCRIPT, PRISMA_IMPORTS, START_DEV_SCRIPT, START_SCRIPT, tsconfigBuildJson } from '../templates.js';
|
|
5
|
+
import { mergePackageJson } from './package-json.js';
|
|
6
|
+
/** A Prisma generator that still writes the client under src/ — tsc does not copy its .js files to dist/. */
|
|
7
|
+
export const LEGACY_PRISMA_OUTPUT = /generator\s+\w+\s*\{[^}]*output\s*=\s*"\.\.\/src\/[^"]*"/;
|
|
8
|
+
/** `start` (or any script) that runs TypeScript through tsx. */
|
|
9
|
+
export const RUNS_TSX = /(^|[\s;&|])(?:npx\s+|pnpm\s+(?:exec\s+)?)?tsx(\s|$)|--import[= ]tsx\b/;
|
|
10
|
+
async function readOptional(path) {
|
|
11
|
+
try {
|
|
12
|
+
return await readFile(path, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const exists = async (path) => (await readOptional(path)) !== undefined;
|
|
19
|
+
export const MANUAL_START_SNIPPET = `In package.json, run the compiled server in production (build first: \`pnpm build\`):
|
|
20
|
+
"start": "${START_SCRIPT}",
|
|
21
|
+
"start:dev": "${START_DEV_SCRIPT}"`;
|
|
22
|
+
export const MANUAL_PRISMA_LAYOUT_SNIPPET = `The Prisma client is generated under src/, where \`tsc\` does not copy its .js files — \`node ${PRODUCTION_ENTRY}\` cannot load it. Move it out of src/:
|
|
23
|
+
prisma/schema.prisma output = "../generated/prisma"
|
|
24
|
+
package.json "imports": ${JSON.stringify(PRISMA_IMPORTS)}
|
|
25
|
+
src/db.ts import { PrismaClient } from '#db/client.js'
|
|
26
|
+
.gitignore generated/ (instead of src/generated/)
|
|
27
|
+
then \`pnpm db:generate\` and delete src/generated/. The Dockerfile is offered once this is done.`;
|
|
28
|
+
/**
|
|
29
|
+
* What an existing app lacks of the production path: tsconfig.build.json, a
|
|
30
|
+
* `build` script, the Dockerfile and .dockerignore, and — for a Prisma app —
|
|
31
|
+
* a direct `@prisma/client-runtime-utils` dependency. Pure apart from reading
|
|
32
|
+
* the app's files. The Dockerfile is offered to pnpm apps only, and not
|
|
33
|
+
* while the Prisma client is still generated under src/.
|
|
34
|
+
*/
|
|
35
|
+
export async function planProductionPath(dir, packageJsonText, pm = 'pnpm') {
|
|
36
|
+
const pkg = JSON.parse(packageJsonText);
|
|
37
|
+
const files = {};
|
|
38
|
+
const notes = [];
|
|
39
|
+
const manual = [];
|
|
40
|
+
if (!(await exists(join(dir, 'tsconfig.build.json')))) {
|
|
41
|
+
files['tsconfig.build.json'] = tsconfigBuildJson();
|
|
42
|
+
notes.push('tsconfig.build.json: what `build` compiles (src/ → dist/, rootDir `.`)');
|
|
43
|
+
}
|
|
44
|
+
const schema = await readOptional(join(dir, 'prisma', 'schema.prisma'));
|
|
45
|
+
const prismaClient = pkg.dependencies?.['@prisma/client'];
|
|
46
|
+
const merged = mergePackageJson(packageJsonText, {
|
|
47
|
+
scripts: { build: BUILD_SCRIPT },
|
|
48
|
+
...(schema !== undefined && prismaClient !== undefined
|
|
49
|
+
? { dependencies: { '@prisma/client-runtime-utils': prismaClient } }
|
|
50
|
+
: {}),
|
|
51
|
+
});
|
|
52
|
+
for (const change of merged.changes) {
|
|
53
|
+
if (change.startsWith('scripts:'))
|
|
54
|
+
notes.push(`package.json: a "build" script (${BUILD_SCRIPT})`);
|
|
55
|
+
else
|
|
56
|
+
notes.push(`package.json: @prisma/client-runtime-utils ${prismaClient} — the generated client requires it by name; plain node cannot reach it as a transitive dependency`);
|
|
57
|
+
}
|
|
58
|
+
const start = pkg.scripts?.['start'];
|
|
59
|
+
if (start !== undefined && RUNS_TSX.test(start))
|
|
60
|
+
manual.push(MANUAL_START_SNIPPET);
|
|
61
|
+
const legacyPrisma = schema !== undefined && LEGACY_PRISMA_OUTPUT.test(schema);
|
|
62
|
+
if (legacyPrisma)
|
|
63
|
+
manual.push(MANUAL_PRISMA_LAYOUT_SNIPPET);
|
|
64
|
+
// The Dockerfile installs with pnpm (frozen pnpm-lock.yaml): offered to pnpm apps only.
|
|
65
|
+
else if (pm === 'pnpm' && !(await exists(join(dir, 'Dockerfile')))) {
|
|
66
|
+
files['Dockerfile'] = DOCKERFILE;
|
|
67
|
+
notes.push(`Dockerfile: build stage + plain-node runtime running ${PRODUCTION_ENTRY} (same as \`basalt publish dockerfile\`)`);
|
|
68
|
+
}
|
|
69
|
+
if (!(await exists(join(dir, '.dockerignore')))) {
|
|
70
|
+
files['.dockerignore'] = DOCKERIGNORE;
|
|
71
|
+
notes.push('.dockerignore: keeps .env, keys and node_modules out of the image');
|
|
72
|
+
}
|
|
73
|
+
return { files, packageJsonText: merged.text, notes, manual };
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PackageJson } from './package-json.js';
|
|
2
|
+
/**
|
|
3
|
+
* The `db:seed` script create-basalt 1.8.0–1.11.0 generated for `--prisma`
|
|
4
|
+
* apps: `tsx prisma/seed.ts` loads no .env, so on a fresh app it fails in
|
|
5
|
+
* src/env.ts (`ENV_INVALID`: database URL / app secret missing) unless the
|
|
6
|
+
* variables are exported. `prisma db seed` goes through prisma.config.ts,
|
|
7
|
+
* which loads .env and runs the `migrations.seed` command it declares.
|
|
8
|
+
*
|
|
9
|
+
* Only the exact old template is matched: a customised script is the app's.
|
|
10
|
+
* Nothing here rewrites package.json — `doctor` reports it and `update`
|
|
11
|
+
* prints the replacement.
|
|
12
|
+
*/
|
|
13
|
+
export declare const LEGACY_DB_SEED: RegExp;
|
|
14
|
+
/**
|
|
15
|
+
* The fix for an old `db:seed` script, as printed lines; undefined when the
|
|
16
|
+
* app does not have the old script (or no prisma.config.ts to load .env).
|
|
17
|
+
*/
|
|
18
|
+
export declare function legacySeedScriptFix(pkg: PackageJson, prismaConfig: string | undefined): string | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DB_SEED_SCRIPT } from '../templates.js';
|
|
2
|
+
/**
|
|
3
|
+
* The `db:seed` script create-basalt 1.8.0–1.11.0 generated for `--prisma`
|
|
4
|
+
* apps: `tsx prisma/seed.ts` loads no .env, so on a fresh app it fails in
|
|
5
|
+
* src/env.ts (`ENV_INVALID`: database URL / app secret missing) unless the
|
|
6
|
+
* variables are exported. `prisma db seed` goes through prisma.config.ts,
|
|
7
|
+
* which loads .env and runs the `migrations.seed` command it declares.
|
|
8
|
+
*
|
|
9
|
+
* Only the exact old template is matched: a customised script is the app's.
|
|
10
|
+
* Nothing here rewrites package.json — `doctor` reports it and `update`
|
|
11
|
+
* prints the replacement.
|
|
12
|
+
*/
|
|
13
|
+
export const LEGACY_DB_SEED = /^\s*tsx\s+(?:\.\/)?prisma\/seed\.ts\s*$/;
|
|
14
|
+
/** Whether prisma.config.ts declares a seed command (`migrations: { seed: … }`). */
|
|
15
|
+
const DECLARES_SEED = /migrations\s*:\s*\{[^}]*\bseed\s*:/;
|
|
16
|
+
/**
|
|
17
|
+
* The fix for an old `db:seed` script, as printed lines; undefined when the
|
|
18
|
+
* app does not have the old script (or no prisma.config.ts to load .env).
|
|
19
|
+
*/
|
|
20
|
+
export function legacySeedScriptFix(pkg, prismaConfig) {
|
|
21
|
+
const script = pkg.scripts?.['db:seed'];
|
|
22
|
+
if (script === undefined || prismaConfig === undefined || !LEGACY_DB_SEED.test(script))
|
|
23
|
+
return undefined;
|
|
24
|
+
const lines = [
|
|
25
|
+
`\`db:seed\` runs "${script}", which does not load .env — it fails with ENV_INVALID unless the variables are exported. Run it through Prisma (prisma.config.ts loads .env):`,
|
|
26
|
+
` package.json "db:seed": "${DB_SEED_SCRIPT}"`,
|
|
27
|
+
];
|
|
28
|
+
if (!DECLARES_SEED.test(prismaConfig)) {
|
|
29
|
+
lines.push(` prisma.config.ts migrations: { seed: '${script.trim()}' }, (inside defineConfig)`);
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
package/dist/project/update.js
CHANGED
|
@@ -4,6 +4,8 @@ import { binStatus, hasProjectCommands, manualBinSnippet, patchBasaltBin } from
|
|
|
4
4
|
import { devEntryStatus, MANUAL_DEV_SNIPPET, patchDevEntry, patchEnvExample } from '../dev-entry.js';
|
|
5
5
|
import { alwaysLatest, lookupLatestVersions, minimumReleaseAgeMinutes, } from '../latest-versions.js';
|
|
6
6
|
import { BASALT_PROJECT_SCRIPT } from '../templates.js';
|
|
7
|
+
import { planProductionPath } from './production.js';
|
|
8
|
+
import { legacySeedScriptFix } from './seed-script.js';
|
|
7
9
|
import { extendManifest, MANIFEST_PATH, serializeManifest } from './manifest.js';
|
|
8
10
|
import { DEPENDENCY_SECTIONS, mergePackageJson, setDependencyRange, } from './package-json.js';
|
|
9
11
|
import { table } from './term.js';
|
|
@@ -228,6 +230,23 @@ export async function planUpdate(ctx, options = {}) {
|
|
|
228
230
|
patched['.env.example'] = exampleAfter;
|
|
229
231
|
toolingNotes.push('.env.example: header now says dev/CLI load .env automatically and `start` does not');
|
|
230
232
|
}
|
|
233
|
+
// The production path (BK-026): tsconfig.build.json, a `build` script, the
|
|
234
|
+
// Dockerfile — offered when missing, never overwriting what the app has.
|
|
235
|
+
const production = await planProductionPath(ctx.dir, files['package.json']?.after ?? ctx.packageJsonText, ctx.pm);
|
|
236
|
+
if (production.packageJsonText !== (files['package.json']?.after ?? ctx.packageJsonText)) {
|
|
237
|
+
files['package.json'] = { before: ctx.packageJsonText, after: production.packageJsonText };
|
|
238
|
+
}
|
|
239
|
+
for (const [path, content] of Object.entries(production.files)) {
|
|
240
|
+
files[path] = { before: undefined, after: content };
|
|
241
|
+
patched[path] = content;
|
|
242
|
+
}
|
|
243
|
+
toolingNotes.push(...production.notes);
|
|
244
|
+
manual.push(...production.manual);
|
|
245
|
+
// db:seed from create-basalt 1.8–1.11 does not load .env: printed, never
|
|
246
|
+
// rewritten — package.json scripts are the app's.
|
|
247
|
+
const seedFix = legacySeedScriptFix(ctx.packageJson, await readOptional(join(ctx.dir, 'prisma.config.ts')));
|
|
248
|
+
if (seedFix !== undefined)
|
|
249
|
+
manual.push(seedFix);
|
|
231
250
|
if (ctx.manifest && Object.keys(patched).length > 0) {
|
|
232
251
|
files[MANIFEST_PATH] = {
|
|
233
252
|
before: serializeManifest(ctx.manifest),
|
package/dist/stubs.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The production stubs `basalt publish` copies into an app — the SINGLE source
|
|
3
|
+
* of truth. create-basalt ships the same files in every new scaffold and offers
|
|
4
|
+
* them from `create-basalt update`: its build copies this file verbatim to
|
|
5
|
+
* `packages/create-app/src/stubs.ts` (scripts/sync-stubs.mjs), and a test there
|
|
6
|
+
* fails if the copy drifts. Keep this module free of imports for that reason.
|
|
7
|
+
*
|
|
8
|
+
* The layout these stubs assume is the create-basalt scaffold's (BK-026):
|
|
9
|
+
* `pnpm build` runs `tsc -p tsconfig.build.json` (rootDir `.`, outDir `dist`),
|
|
10
|
+
* so `src/server.ts` compiles to {@link PRODUCTION_ENTRY}; `pnpm start` runs it
|
|
11
|
+
* with plain `node` — no tsx, no TypeScript at runtime. A Prisma client, when
|
|
12
|
+
* there is one, is generated OUTSIDE `src/` (`generated/prisma`) and imported
|
|
13
|
+
* through the package.json `imports` alias `#db/*`, so it resolves the same
|
|
14
|
+
* from `src/` (tsx) and from `dist/src/` (node).
|
|
15
|
+
*/
|
|
16
|
+
/** The compiled server entry the Dockerfile runs and the scaffold's `start` script points at. */
|
|
17
|
+
export declare const PRODUCTION_ENTRY = "dist/src/server.js";
|
|
18
|
+
/** Multi-stage production image: build with the dev toolchain, run on plain node as a non-root user. */
|
|
19
|
+
export declare const DOCKERFILE = "# syntax=docker/dockerfile:1\n# Production image for a Basalt app (the create-basalt layout): `pnpm build`\n# compiles src/ to dist/ and the image runs dist/src/server.js on plain\n# node \u2014 no tsx, no TypeScript at runtime.\n#\n# docker build -t my-app .\n# docker run -p 3000:3000 -e APP_SECRET=\u2026 -e DATABASE_URL=\u2026 my-app\n#\n# Configuration comes from the environment (nothing loads .env here, and\n# .dockerignore keeps it out of the image). Database migrations are NOT run\n# by the image: apply them before rolling out (`pnpm db:deploy` in CI).\n#\n# Installs with pnpm from pnpm-lock.yaml. An app on npm/yarn/bun replaces the\n# pnpm install / exec / run / prune lines with its own package manager's.\nARG NODE_VERSION=22\nARG PNPM_VERSION=11\n\nFROM node:${NODE_VERSION}-slim AS build\nARG PNPM_VERSION\n# Non-interactive pnpm: without it `pnpm prune` in a workspace (a --ui app's\n# web/) aborts with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.\nENV CI=true\nWORKDIR /app\nRUN npm install -g pnpm@${PNPM_VERSION}\nCOPY . .\n# Full install: the build needs the devDependencies (typescript, prisma).\nRUN pnpm install --frozen-lockfile\n# The Prisma client (when the app has a schema) is generated into ./generated.\nRUN if [ -f prisma/schema.prisma ]; then pnpm exec prisma generate; fi && mkdir -p generated\nRUN pnpm run build\n# Drop the devDependencies (tsx, typescript, prisma CLI, generators, the AI\n# bridge): none of them may ship. --ignore-scripts keeps the app's postinstall\n# (`prisma generate`, a dev tool) from running again.\nRUN pnpm prune --prod --ignore-scripts\n\nFROM node:${NODE_VERSION}-slim AS run\nENV NODE_ENV=production\nENV PORT=3000\n# Path the HEALTHCHECK probes: the scaffold's /health route by default; set\n# /readyz when the app registers healthPlugin().\nENV HEALTHCHECK_PATH=/health\nWORKDIR /app\nCOPY --from=build --chown=node:node /app/package.json ./package.json\nCOPY --from=build --chown=node:node /app/node_modules ./node_modules\nCOPY --from=build --chown=node:node /app/generated ./generated\nCOPY --from=build --chown=node:node /app/dist ./dist\nUSER node\nEXPOSE 3000\nHEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\\n CMD node -e \"fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + process.env.HEALTHCHECK_PATH).then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))\"\nCMD [\"node\", \"--enable-source-maps\", \"dist/src/server.js\"]\n";
|
|
20
|
+
/**
|
|
21
|
+
* Published with the Dockerfile: its \`COPY . .\` would otherwise bake \`.env\`,
|
|
22
|
+
* private keys and VCS metadata into an image layer. Every secret rule is
|
|
23
|
+
* \`**\/\`-prefixed: .dockerignore patterns are anchored at the context root, so
|
|
24
|
+
* a bare \`.env\` or \`*.pem\` would still let \`prisma/.env\` or
|
|
25
|
+
* \`certs/server.key\` through. \`dist\` is rebuilt inside the image.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DOCKERIGNORE = "**/.env\n**/.env.*\n!**/.env.example\n**/.npmrc\n**/.git\n**/node_modules\n**/coverage\n**/*.log\n**/*.pem\n**/*.key\n**/*.p12\n**/*.pfx\n**/.DS_Store\ndist\n";
|
|
28
|
+
/** GitHub Actions CI: install, typecheck, build (the production path), test. */
|
|
29
|
+
export declare const CI_WORKFLOW = "name: ci\non:\n push: { branches: [main] }\n pull_request:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n # Drop `with: version` if package.json declares a packageManager.\n - uses: pnpm/action-setup@v4\n with: { version: 11 }\n - uses: actions/setup-node@v4\n with: { node-version: 22, cache: pnpm }\n - run: pnpm install --frozen-lockfile\n - run: pnpm run typecheck\n - run: pnpm run build\n - run: pnpm run test\n";
|
|
30
|
+
export declare const EDITORCONFIG = "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n";
|
package/dist/stubs.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-stubs.mjs from packages/cli/src/stubs.ts (runs before every build) — do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* The production stubs `basalt publish` copies into an app — the SINGLE source
|
|
4
|
+
* of truth. create-basalt ships the same files in every new scaffold and offers
|
|
5
|
+
* them from `create-basalt update`: its build copies this file verbatim to
|
|
6
|
+
* `packages/create-app/src/stubs.ts` (scripts/sync-stubs.mjs), and a test there
|
|
7
|
+
* fails if the copy drifts. Keep this module free of imports for that reason.
|
|
8
|
+
*
|
|
9
|
+
* The layout these stubs assume is the create-basalt scaffold's (BK-026):
|
|
10
|
+
* `pnpm build` runs `tsc -p tsconfig.build.json` (rootDir `.`, outDir `dist`),
|
|
11
|
+
* so `src/server.ts` compiles to {@link PRODUCTION_ENTRY}; `pnpm start` runs it
|
|
12
|
+
* with plain `node` — no tsx, no TypeScript at runtime. A Prisma client, when
|
|
13
|
+
* there is one, is generated OUTSIDE `src/` (`generated/prisma`) and imported
|
|
14
|
+
* through the package.json `imports` alias `#db/*`, so it resolves the same
|
|
15
|
+
* from `src/` (tsx) and from `dist/src/` (node).
|
|
16
|
+
*/
|
|
17
|
+
/** The compiled server entry the Dockerfile runs and the scaffold's `start` script points at. */
|
|
18
|
+
export const PRODUCTION_ENTRY = 'dist/src/server.js';
|
|
19
|
+
/** Multi-stage production image: build with the dev toolchain, run on plain node as a non-root user. */
|
|
20
|
+
export const DOCKERFILE = `# syntax=docker/dockerfile:1
|
|
21
|
+
# Production image for a Basalt app (the create-basalt layout): \`pnpm build\`
|
|
22
|
+
# compiles src/ to dist/ and the image runs ${PRODUCTION_ENTRY} on plain
|
|
23
|
+
# node — no tsx, no TypeScript at runtime.
|
|
24
|
+
#
|
|
25
|
+
# docker build -t my-app .
|
|
26
|
+
# docker run -p 3000:3000 -e APP_SECRET=… -e DATABASE_URL=… my-app
|
|
27
|
+
#
|
|
28
|
+
# Configuration comes from the environment (nothing loads .env here, and
|
|
29
|
+
# .dockerignore keeps it out of the image). Database migrations are NOT run
|
|
30
|
+
# by the image: apply them before rolling out (\`pnpm db:deploy\` in CI).
|
|
31
|
+
#
|
|
32
|
+
# Installs with pnpm from pnpm-lock.yaml. An app on npm/yarn/bun replaces the
|
|
33
|
+
# pnpm install / exec / run / prune lines with its own package manager's.
|
|
34
|
+
ARG NODE_VERSION=22
|
|
35
|
+
ARG PNPM_VERSION=11
|
|
36
|
+
|
|
37
|
+
FROM node:\${NODE_VERSION}-slim AS build
|
|
38
|
+
ARG PNPM_VERSION
|
|
39
|
+
# Non-interactive pnpm: without it \`pnpm prune\` in a workspace (a --ui app's
|
|
40
|
+
# web/) aborts with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.
|
|
41
|
+
ENV CI=true
|
|
42
|
+
WORKDIR /app
|
|
43
|
+
RUN npm install -g pnpm@\${PNPM_VERSION}
|
|
44
|
+
COPY . .
|
|
45
|
+
# Full install: the build needs the devDependencies (typescript, prisma).
|
|
46
|
+
RUN pnpm install --frozen-lockfile
|
|
47
|
+
# The Prisma client (when the app has a schema) is generated into ./generated.
|
|
48
|
+
RUN if [ -f prisma/schema.prisma ]; then pnpm exec prisma generate; fi && mkdir -p generated
|
|
49
|
+
RUN pnpm run build
|
|
50
|
+
# Drop the devDependencies (tsx, typescript, prisma CLI, generators, the AI
|
|
51
|
+
# bridge): none of them may ship. --ignore-scripts keeps the app's postinstall
|
|
52
|
+
# (\`prisma generate\`, a dev tool) from running again.
|
|
53
|
+
RUN pnpm prune --prod --ignore-scripts
|
|
54
|
+
|
|
55
|
+
FROM node:\${NODE_VERSION}-slim AS run
|
|
56
|
+
ENV NODE_ENV=production
|
|
57
|
+
ENV PORT=3000
|
|
58
|
+
# Path the HEALTHCHECK probes: the scaffold's /health route by default; set
|
|
59
|
+
# /readyz when the app registers healthPlugin().
|
|
60
|
+
ENV HEALTHCHECK_PATH=/health
|
|
61
|
+
WORKDIR /app
|
|
62
|
+
COPY --from=build --chown=node:node /app/package.json ./package.json
|
|
63
|
+
COPY --from=build --chown=node:node /app/node_modules ./node_modules
|
|
64
|
+
COPY --from=build --chown=node:node /app/generated ./generated
|
|
65
|
+
COPY --from=build --chown=node:node /app/dist ./dist
|
|
66
|
+
USER node
|
|
67
|
+
EXPOSE 3000
|
|
68
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\
|
|
69
|
+
CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + process.env.HEALTHCHECK_PATH).then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
|
|
70
|
+
CMD ["node", "--enable-source-maps", "${PRODUCTION_ENTRY}"]
|
|
71
|
+
`;
|
|
72
|
+
/**
|
|
73
|
+
* Published with the Dockerfile: its \`COPY . .\` would otherwise bake \`.env\`,
|
|
74
|
+
* private keys and VCS metadata into an image layer. Every secret rule is
|
|
75
|
+
* \`**\/\`-prefixed: .dockerignore patterns are anchored at the context root, so
|
|
76
|
+
* a bare \`.env\` or \`*.pem\` would still let \`prisma/.env\` or
|
|
77
|
+
* \`certs/server.key\` through. \`dist\` is rebuilt inside the image.
|
|
78
|
+
*/
|
|
79
|
+
export const DOCKERIGNORE = `**/.env
|
|
80
|
+
**/.env.*
|
|
81
|
+
!**/.env.example
|
|
82
|
+
**/.npmrc
|
|
83
|
+
**/.git
|
|
84
|
+
**/node_modules
|
|
85
|
+
**/coverage
|
|
86
|
+
**/*.log
|
|
87
|
+
**/*.pem
|
|
88
|
+
**/*.key
|
|
89
|
+
**/*.p12
|
|
90
|
+
**/*.pfx
|
|
91
|
+
**/.DS_Store
|
|
92
|
+
dist
|
|
93
|
+
`;
|
|
94
|
+
/** GitHub Actions CI: install, typecheck, build (the production path), test. */
|
|
95
|
+
export const CI_WORKFLOW = `name: ci
|
|
96
|
+
on:
|
|
97
|
+
push: { branches: [main] }
|
|
98
|
+
pull_request:
|
|
99
|
+
jobs:
|
|
100
|
+
build:
|
|
101
|
+
runs-on: ubuntu-latest
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
# Drop \`with: version\` if package.json declares a packageManager.
|
|
105
|
+
- uses: pnpm/action-setup@v4
|
|
106
|
+
with: { version: 11 }
|
|
107
|
+
- uses: actions/setup-node@v4
|
|
108
|
+
with: { node-version: 22, cache: pnpm }
|
|
109
|
+
- run: pnpm install --frozen-lockfile
|
|
110
|
+
- run: pnpm run typecheck
|
|
111
|
+
- run: pnpm run build
|
|
112
|
+
- run: pnpm run test
|
|
113
|
+
`;
|
|
114
|
+
export const EDITORCONFIG = `root = true
|
|
115
|
+
|
|
116
|
+
[*]
|
|
117
|
+
charset = utf-8
|
|
118
|
+
end_of_line = lf
|
|
119
|
+
insert_final_newline = true
|
|
120
|
+
indent_style = space
|
|
121
|
+
indent_size = 2
|
|
122
|
+
trim_trailing_whitespace = true
|
|
123
|
+
`;
|
package/dist/templates.d.ts
CHANGED
|
@@ -33,6 +33,24 @@ export declare const versionOf: (pkg: string) => string;
|
|
|
33
33
|
* unknown package so every emitted range lives in that single table.
|
|
34
34
|
*/
|
|
35
35
|
export declare const thirdPartyVersionOf: (pkg: string) => string;
|
|
36
|
+
/**
|
|
37
|
+
* The production path (BK-026): `build` compiles src/ with tsconfig.build.json
|
|
38
|
+
* (rootDir `.`, outDir `dist`) and `start` runs the result on plain node — the
|
|
39
|
+
* same entry the Dockerfile's CMD runs. `start:dev` keeps the old tsx start.
|
|
40
|
+
*/
|
|
41
|
+
export declare const BUILD_SCRIPT = "tsc -p tsconfig.build.json";
|
|
42
|
+
/** The `db:seed` script of a `--prisma` scaffold with tenancy (prisma.config.ts loads .env, then runs `migrations.seed`). */
|
|
43
|
+
export declare const DB_SEED_SCRIPT = "prisma db seed";
|
|
44
|
+
export declare const START_SCRIPT = "node --enable-source-maps dist/src/server.js";
|
|
45
|
+
export declare const START_DEV_SCRIPT = "tsx src/server.ts";
|
|
46
|
+
/** The `start` script every create-basalt release before the production path wrote. */
|
|
47
|
+
export declare const LEGACY_START_SCRIPT = "tsx src/server.ts";
|
|
48
|
+
/**
|
|
49
|
+
* package.json `imports` alias for the generated Prisma client. It lives in
|
|
50
|
+
* ./generated/prisma — outside src/, so tsc never has to copy its .js files —
|
|
51
|
+
* and `#db/*` resolves to it from src/ (tsx, vitest) and dist/src/ (node) alike.
|
|
52
|
+
*/
|
|
53
|
+
export declare const PRISMA_IMPORTS: Readonly<Record<string, string>>;
|
|
36
54
|
/** The root `basalt` script of an app without `--cli`: create-basalt's project commands. */
|
|
37
55
|
export declare const BASALT_PROJECT_SCRIPT = "create-basalt --project";
|
|
38
56
|
/** The root `dev:web` script of a `--ui` app (web/ is a pnpm workspace member). */
|
|
@@ -41,6 +59,15 @@ export declare const webDevScript: (options: Pick<ProjectOptions, 'name'>) => st
|
|
|
41
59
|
export declare const enforcesMembership: (options: ProjectOptions) => boolean;
|
|
42
60
|
export declare function packageJson(options: ProjectOptions): string;
|
|
43
61
|
export declare function tsconfigJson(options: ProjectOptions): string;
|
|
62
|
+
/**
|
|
63
|
+
* `tsconfig.build.json` — what `pnpm build` compiles: src/ only (tests and
|
|
64
|
+
* bin/ stay dev-only), emitted to dist/ with rootDir `.`, so src/server.ts
|
|
65
|
+
* becomes dist/src/server.js. Extends tsconfig.json, so the compiler options
|
|
66
|
+
* (and `pnpm typecheck`) never diverge.
|
|
67
|
+
*/
|
|
68
|
+
export declare function tsconfigBuildJson(): string;
|
|
69
|
+
/** The production Dockerfile — the same file `basalt publish dockerfile` writes. */
|
|
70
|
+
export declare function dockerfile(): string;
|
|
44
71
|
export declare function envTs(options: ProjectOptions): string;
|
|
45
72
|
/**
|
|
46
73
|
* An app-specific env-var prefix derived from the project name
|
|
@@ -101,8 +128,9 @@ export declare function prismaConfigTs(options: ProjectOptions): string;
|
|
|
101
128
|
export declare function dbTs(options: ProjectOptions): string;
|
|
102
129
|
/**
|
|
103
130
|
* `prisma/seed.ts` — creates the `demo` tenant the scaffolded resolvers expect
|
|
104
|
-
* (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed`
|
|
105
|
-
* `prisma
|
|
131
|
+
* (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed`
|
|
132
|
+
* (`prisma db seed`, the command prisma.config.ts declares) — Prisma 7's
|
|
133
|
+
* `migrate dev` does not run it.
|
|
106
134
|
*/
|
|
107
135
|
export declare function prismaSeedTs(options: ProjectOptions): string;
|
|
108
136
|
export declare function appTest(options: ProjectOptions): string;
|
|
@@ -129,7 +157,9 @@ export declare function mcpJson(_options: ProjectOptions): string;
|
|
|
129
157
|
export declare function dockerignore(): string;
|
|
130
158
|
export declare function gitignore(options: ProjectOptions): string;
|
|
131
159
|
/**
|
|
132
|
-
* pnpm settings: esbuild's build script is required by tsx;
|
|
160
|
+
* pnpm settings: esbuild's build script is required by tsx; with --prisma,
|
|
161
|
+
* the Prisma CLI's build scripts (prisma, @prisma/engines) are approved too — pnpm 11 fails
|
|
162
|
+
* the install on an unapproved build;
|
|
133
163
|
* msgpackr-extract (optional native accelerator via BullMQ) is declined —
|
|
134
164
|
* msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
|
|
135
165
|
* its own dependencies resolve.
|
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'
|
|
@@ -534,7 +587,15 @@ import { env } from './env.js'
|
|
|
534
587
|
const app = await buildApp({
|
|
535
588
|
logLevel: env.LOG_LEVEL,
|
|
536
589
|
pretty: env.NODE_ENV === 'development',
|
|
537
|
-
})
|
|
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
|
+
})
|
|
538
599
|
|
|
539
600
|
const server = app.container.get(FASTIFY)
|
|
540
601
|
await server.listen({ port: env.PORT, host: env.HOST })
|
|
@@ -594,9 +655,17 @@ const PRISMA_MODELS = {
|
|
|
594
655
|
}
|
|
595
656
|
|
|
596
657
|
model TenantDomain {
|
|
597
|
-
domain
|
|
598
|
-
tenantId
|
|
599
|
-
|
|
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)
|
|
600
669
|
|
|
601
670
|
@@index([tenantId])
|
|
602
671
|
@@map("tenant_domains")
|
|
@@ -611,9 +680,11 @@ model TenantDomain {
|
|
|
611
680
|
}
|
|
612
681
|
|
|
613
682
|
model AuthSession {
|
|
614
|
-
id
|
|
615
|
-
userId
|
|
616
|
-
expiresAt
|
|
683
|
+
id String @id
|
|
684
|
+
userId String
|
|
685
|
+
expiresAt DateTime
|
|
686
|
+
/// Last use, for the idle timeout (trackSessionActivity: true).
|
|
687
|
+
lastSeenAt DateTime?
|
|
617
688
|
|
|
618
689
|
@@index([userId])
|
|
619
690
|
@@map("auth_sessions")
|
|
@@ -792,7 +863,9 @@ export function prismaSchema(options) {
|
|
|
792
863
|
|
|
793
864
|
generator client {
|
|
794
865
|
provider = "prisma-client-js"
|
|
795
|
-
|
|
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"
|
|
796
869
|
}
|
|
797
870
|
|
|
798
871
|
datasource db {
|
|
@@ -840,7 +913,7 @@ try {
|
|
|
840
913
|
export default defineConfig({
|
|
841
914
|
schema: 'prisma/schema.prisma',${options.tenancy
|
|
842
915
|
? `
|
|
843
|
-
//
|
|
916
|
+
// Run by \`prisma db seed\` (\`pnpm db:seed\`), with the .env loaded above.
|
|
844
917
|
migrations: { seed: 'tsx prisma/seed.ts' },`
|
|
845
918
|
: ''}
|
|
846
919
|
datasource: {
|
|
@@ -860,7 +933,9 @@ export default defineConfig({
|
|
|
860
933
|
*/
|
|
861
934
|
export function dbTs(options) {
|
|
862
935
|
return `import { PrismaPg } from '@prisma/adapter-pg'${options.tenancy ? `\nimport { tenancyExtension } from '@basaltkit/prisma'` : ''}
|
|
863
|
-
|
|
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'
|
|
864
939
|
import { env } from './env.js'
|
|
865
940
|
|
|
866
941
|
/**
|
|
@@ -896,8 +971,9 @@ export const db = prisma
|
|
|
896
971
|
}
|
|
897
972
|
/**
|
|
898
973
|
* `prisma/seed.ts` — creates the `demo` tenant the scaffolded resolvers expect
|
|
899
|
-
* (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed`
|
|
900
|
-
* `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.
|
|
901
977
|
*/
|
|
902
978
|
export function prismaSeedTs(options) {
|
|
903
979
|
return `import { prismaTenantSource } from '@basaltkit/tenancy-prisma'
|
|
@@ -984,7 +1060,8 @@ Included: ${features.join(' · ')}.
|
|
|
984
1060
|
pnpm install${options.prisma
|
|
985
1061
|
? `
|
|
986
1062
|
# start PostgreSQL — .env points ${envPrefix(options.name)}_DATABASE_URL at localhost:5432 — then
|
|
987
|
-
pnpm db:migrate # create the tables${options.tenancy ? `
|
|
1063
|
+
pnpm db:migrate # create the tables${options.tenancy ? `
|
|
1064
|
+
pnpm db:seed # the 'demo' tenant the resolvers expect` : ''}`
|
|
988
1065
|
: ''}
|
|
989
1066
|
pnpm dev # API on http://localhost:3000
|
|
990
1067
|
pnpm test
|
|
@@ -998,11 +1075,11 @@ the models this app needs — the reference models of the \`@basaltkit/*-prisma\
|
|
|
998
1075
|
packages it uses, plus your own \`Project\` at the end.
|
|
999
1076
|
|
|
1000
1077
|
\`\`\`bash
|
|
1001
|
-
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
|
|
1002
1079
|
? `
|
|
1003
|
-
pnpm db:seed # the 'demo' tenant the resolvers expect
|
|
1080
|
+
pnpm db:seed # prisma db seed — the 'demo' tenant the resolvers expect`
|
|
1004
1081
|
: ''}
|
|
1005
|
-
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)
|
|
1006
1083
|
pnpm db:deploy # prisma migrate deploy — apply pending migrations in production
|
|
1007
1084
|
\`\`\`
|
|
1008
1085
|
|
|
@@ -1044,7 +1121,7 @@ scaffold with \`--cli\` — \`pnpm basalt prisma:sync\` does the merge for you.)
|
|
|
1044
1121
|
\`src/env.ts\` validates \`process.env\`. For development, ${options.cli ? '`pnpm dev` and `pnpm basalt` load' : '`pnpm dev` loads'} \`.env\`
|
|
1045
1122
|
automatically — create-basalt wrote one for you: git-ignored, with local
|
|
1046
1123
|
values${options.auth ? ' and a generated secret' : ''}. \`.env.example\` is its committed template.
|
|
1047
|
-
\`pnpm start\` (production, \`
|
|
1124
|
+
\`pnpm start\` (production, \`${PRODUCTION_ENTRY}\`) loads **nothing**: there the
|
|
1048
1125
|
variables come from the real environment (or \`node --env-file=.env\`).
|
|
1049
1126
|
|
|
1050
1127
|
The variables are **app-prefixed**: \`${envPrefix(options.name)}_PORT\`,
|
|
@@ -1068,6 +1145,24 @@ To drop the fallback entirely (prefixed names only), write
|
|
|
1068
1145
|
\`prefix: { value: '${envPrefix(options.name)}', fallback: false }\`. When in doubt about what
|
|
1069
1146
|
your shell exports: \`env | grep DATABASE_URL\`.
|
|
1070
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
|
+
|
|
1071
1166
|
## Keeping it up to date
|
|
1072
1167
|
|
|
1073
1168
|
\`\`\`bash
|
|
@@ -1177,20 +1272,7 @@ export function mcpJson(_options) {
|
|
|
1177
1272
|
* `certs/server.key` through.
|
|
1178
1273
|
*/
|
|
1179
1274
|
export function dockerignore() {
|
|
1180
|
-
return
|
|
1181
|
-
**/.env.*
|
|
1182
|
-
!**/.env.example
|
|
1183
|
-
**/.npmrc
|
|
1184
|
-
**/.git
|
|
1185
|
-
**/node_modules
|
|
1186
|
-
**/coverage
|
|
1187
|
-
**/*.log
|
|
1188
|
-
**/*.pem
|
|
1189
|
-
**/*.key
|
|
1190
|
-
**/*.p12
|
|
1191
|
-
**/*.pfx
|
|
1192
|
-
**/.DS_Store
|
|
1193
|
-
`;
|
|
1275
|
+
return DOCKERIGNORE;
|
|
1194
1276
|
}
|
|
1195
1277
|
export function gitignore(options) {
|
|
1196
1278
|
return `node_modules/
|
|
@@ -1201,12 +1283,14 @@ dist/
|
|
|
1201
1283
|
*.log
|
|
1202
1284
|
${options.prisma
|
|
1203
1285
|
? `# Generated by \`prisma generate\` (runs on install) — never edit or commit it.
|
|
1204
|
-
|
|
1286
|
+
generated/
|
|
1205
1287
|
`
|
|
1206
1288
|
: ''}`;
|
|
1207
1289
|
}
|
|
1208
1290
|
/**
|
|
1209
|
-
* 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;
|
|
1210
1294
|
* msgpackr-extract (optional native accelerator via BullMQ) is declined —
|
|
1211
1295
|
* msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
|
|
1212
1296
|
* its own dependencies resolve.
|
|
@@ -1224,7 +1308,13 @@ export function pnpmWorkspaceYaml(options) {
|
|
|
1224
1308
|
`
|
|
1225
1309
|
: ''}allowBuilds:
|
|
1226
1310
|
esbuild: true
|
|
1227
|
-
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
|
+
: ''}
|
|
1228
1318
|
# @basaltkit/* releases in lockstep, often within hours — exclude the scope (and
|
|
1229
1319
|
# create-basalt, which serves \`pnpm basalt update\`) from pnpm's minimumReleaseAge
|
|
1230
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/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/auth": "^4.
|
|
34
|
-
"@basaltkit/auth-prisma": "^2.
|
|
35
|
-
"@basaltkit/
|
|
36
|
-
"@basaltkit/
|
|
37
|
-
"@basaltkit/core": "^1.5.0",
|
|
38
|
-
"@basaltkit/env": "^3.1.1",
|
|
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",
|
|
39
39
|
"@basaltkit/events": "^1.3.0",
|
|
40
|
-
"@basaltkit/fastify": "^2.
|
|
41
|
-
"@basaltkit/
|
|
40
|
+
"@basaltkit/fastify": "^2.6.0",
|
|
41
|
+
"@basaltkit/config": "^1.1.2",
|
|
42
|
+
"@basaltkit/generator": "^1.6.0",
|
|
42
43
|
"@basaltkit/logger": "^1.3.0",
|
|
43
|
-
"@basaltkit/
|
|
44
|
-
"@basaltkit/
|
|
44
|
+
"@basaltkit/env": "^3.1.1",
|
|
45
|
+
"@basaltkit/mcp": "^5.1.0",
|
|
46
|
+
"@basaltkit/prisma": "^3.1.0",
|
|
45
47
|
"@basaltkit/subscriptions": "^5.0.0",
|
|
46
48
|
"@basaltkit/subscriptions-prisma": "^3.0.0",
|
|
47
|
-
"@basaltkit/teams": "^4.
|
|
49
|
+
"@basaltkit/teams": "^4.1.0",
|
|
50
|
+
"@basaltkit/tenancy": "^3.2.0",
|
|
48
51
|
"@basaltkit/teams-prisma": "^1.1.0",
|
|
49
|
-
"@basaltkit/tenancy": "^
|
|
52
|
+
"@basaltkit/tenancy-prisma": "^2.1.0",
|
|
50
53
|
"@basaltkit/tsconfig": "^0.24.0",
|
|
51
|
-
"@basaltkit/testing": "^2.0.3"
|
|
52
|
-
"@basaltkit/tenancy-prisma": "^2.0.0"
|
|
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
|
}
|