@webjsdev/cli 0.10.17 → 0.10.18
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 +3 -3
- package/bin/webjs.js +90 -22
- package/lib/app-tasks.js +62 -0
- package/lib/create.js +202 -62
- package/lib/run-tasks.js +100 -0
- package/lib/saas-template.js +39 -49
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +5 -5
- package/templates/.env.example +2 -2
- package/templates/.github/copilot-instructions.md +4 -4
- package/templates/.github/workflows/ci.yml +4 -7
- package/templates/AGENTS.md +80 -64
- package/templates/CONVENTIONS.md +45 -35
- package/templates/Dockerfile +10 -7
- package/templates/compose.yaml +3 -3
- package/lib/prisma-preflight.js +0 -168
package/templates/CONVENTIONS.md
CHANGED
|
@@ -32,18 +32,18 @@ enforced by `webjs check`; follow them by judgment.
|
|
|
32
32
|
|
|
33
33
|
- **Server actions and queries live in `modules/<feature>/actions/` and
|
|
34
34
|
`modules/<feature>/queries/`** (`*.server.{js,ts}`), not loose in the
|
|
35
|
-
app root.
|
|
36
|
-
session helpers, auth config) lives in `lib/`.
|
|
35
|
+
app root. The DB connection lives in `db/connection.server.ts`; other
|
|
36
|
+
cross-cutting server infrastructure (session helpers, auth config) lives in `lib/`.
|
|
37
37
|
- **One exported function per action/query file.** Name the file after
|
|
38
38
|
the function (`create-post.server.ts` exports `createPost`). It keeps
|
|
39
39
|
the action surface greppable.
|
|
40
40
|
- **Every feature has tests.** A `modules/<feature>/` directory should
|
|
41
41
|
have matching test files under `test/<feature>/`. A unit test for
|
|
42
42
|
logic, a browser/e2e test for user-facing behaviour.
|
|
43
|
-
- **Persist data with
|
|
44
|
-
wires up `
|
|
43
|
+
- **Persist data with Drizzle + SQLite, never JSON files.** The scaffold
|
|
44
|
+
wires up `db/schema.server.ts` and `db/connection.server.ts`. A
|
|
45
45
|
`data/todos.json` or `db.json` used as a database resets on reload and
|
|
46
|
-
cannot scale; define a
|
|
46
|
+
cannot scale; define a Drizzle table instead.
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
@@ -254,40 +254,45 @@ docs". That is the agent's default behavior in a webjs project.
|
|
|
254
254
|
|
|
255
255
|
---
|
|
256
256
|
|
|
257
|
-
## Data persistence:
|
|
257
|
+
## Data persistence: Drizzle + SQLite, never JSON files
|
|
258
258
|
|
|
259
259
|
<!-- OVERRIDE -->
|
|
260
260
|
|
|
261
|
-
Every webjs app uses **
|
|
262
|
-
scaffold ships `
|
|
263
|
-
`
|
|
264
|
-
|
|
261
|
+
Every webjs app uses **Drizzle + SQLite** for persistence by default. The
|
|
262
|
+
scaffold ships the `db/` folder (`schema.server.ts`, `columns.server.ts`,
|
|
263
|
+
`connection.server.ts`), the `webjs.start.before` step that runs
|
|
264
|
+
`webjs db migrate` inside `webjs start` (#550), and the
|
|
265
|
+
`npm run db:generate` / `db:migrate` / `db:push` / `db:studio` / `db:seed`
|
|
266
|
+
scripts (which route through `webjs db` to drizzle-kit). Drizzle has no
|
|
267
|
+
codegen, so there is no dev `before` step.
|
|
265
268
|
|
|
266
269
|
**AI agents: these rules are absolute.**
|
|
267
270
|
|
|
268
271
|
1. For ANY data the app stores (todos, posts, messages, products,
|
|
269
|
-
comments, users…), define a
|
|
272
|
+
comments, users…), define a Drizzle table in `db/schema.server.ts`
|
|
270
273
|
and persist there.
|
|
271
274
|
2. **NEVER** create JSON files under `data/`, `db.json`, `posts.json`,
|
|
272
275
|
`todos.json`, etc. as a fake database. It resets on reload and cannot
|
|
273
276
|
scale; this is a project convention (see the conventions section above).
|
|
274
277
|
3. **NEVER** use module-scope arrays or `Map`s as a "store". They
|
|
275
278
|
reset on every dev-server reload and can't scale beyond one process.
|
|
276
|
-
4. **NEVER** use `localStorage` / `sessionStorage` to persist app data
|
|
279
|
+
4. **NEVER** use `localStorage` / `sessionStorage` to persist app data,
|
|
277
280
|
it's per-browser and never reaches the server. Use it only for UI
|
|
278
281
|
preferences (theme, sidebar collapsed, etc.).
|
|
279
|
-
5. To add a model: edit `
|
|
280
|
-
|
|
281
|
-
'../../../
|
|
282
|
-
`
|
|
283
|
-
Components, pages, and
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
282
|
+
5. To add a model: edit `db/schema.server.ts`, then `npm run db:generate`
|
|
283
|
+
and `npm run db:migrate`. Access via `import { db } from
|
|
284
|
+
'../../../db/connection.server.ts'` (and the tables from
|
|
285
|
+
`db/schema.server.ts`) **only inside `.server.{js,ts}` files,
|
|
286
|
+
`route.ts` handlers, or `middleware.ts`**. Components, pages, and
|
|
287
|
+
layouts call into the wrapped server query instead; the framework
|
|
288
|
+
rewrites that import to an RPC stub on the browser side, so the DB
|
|
289
|
+
driver never reaches the client.
|
|
290
|
+
|
|
291
|
+
To switch to Postgres: scaffold with `--db postgres`, or swap
|
|
292
|
+
`db/columns.server.ts` + `db/connection.server.ts` for the Postgres
|
|
293
|
+
variants and point `DATABASE_URL` at Postgres. The schema, queries, and
|
|
294
|
+
actions are unchanged. SQLite is the right default for dev and small
|
|
295
|
+
production workloads.
|
|
291
296
|
|
|
292
297
|
---
|
|
293
298
|
|
|
@@ -302,9 +307,9 @@ saas templates) is a **starting point**.
|
|
|
302
307
|
|
|
303
308
|
When the user asks the agent to build their actual app:
|
|
304
309
|
|
|
305
|
-
1. **Replace the example `User` model** in `
|
|
306
|
-
the real domain models the app needs (e.g. `Todo`, `Post`, `Message`)
|
|
307
|
-
|
|
310
|
+
1. **Replace the example `User` model** in `db/schema.server.ts` with
|
|
311
|
+
the real domain models the app needs (e.g. `Todo`, `Post`, `Message`),
|
|
312
|
+
unless the app actually has users.
|
|
308
313
|
2. **Replace `app/page.ts`** with the app's real homepage. Don't ship
|
|
309
314
|
"Hello from …" as the deliverable.
|
|
310
315
|
3. **Delete or replace `components/theme-toggle.ts`** if the app doesn't
|
|
@@ -319,10 +324,11 @@ When the user asks the agent to build their actual app:
|
|
|
319
324
|
dashboard, or board, or a wide layout overflows into an unnecessary
|
|
320
325
|
horizontal scrollbar. Keep the design tokens and theme setup, those
|
|
321
326
|
are infrastructure.
|
|
322
|
-
6. **Keep:** the
|
|
327
|
+
6. **Keep:** the Drizzle setup, the test config, the agent config files
|
|
323
328
|
(`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
|
|
324
|
-
`
|
|
325
|
-
`app/layout.ts`. These are the
|
|
329
|
+
`db/connection.server.ts` + `db/columns.server.ts`, the directory
|
|
330
|
+
conventions, the design tokens in `app/layout.ts`. These are the
|
|
331
|
+
infrastructure, not the example app.
|
|
326
332
|
|
|
327
333
|
This is enforced, not just advised. The example `app/page.ts` and
|
|
328
334
|
`app/layout.ts` carry a `webjs-scaffold-placeholder` marker comment, and
|
|
@@ -334,7 +340,7 @@ line. So the delivered app contains only what the user asked for, never
|
|
|
334
340
|
leftover scaffold code.
|
|
335
341
|
|
|
336
342
|
The scaffold exists so the agent doesn't reinvent the directory layout,
|
|
337
|
-
the
|
|
343
|
+
the Drizzle wiring, the test runner config, or the convention files. It
|
|
338
344
|
does NOT exist so the agent ships the example homepage.
|
|
339
345
|
|
|
340
346
|
---
|
|
@@ -385,7 +391,7 @@ modules/
|
|
|
385
391
|
- One exported function per server action/query file
|
|
386
392
|
- Server actions need BOTH the `.server.{js,ts}` extension AND a `'use server'` directive at the top. Extension alone marks a server-only utility (source-protected, not RPC-callable). Directive alone is a lint violation (`use-server-needs-extension`).
|
|
387
393
|
- Components must call `Class.register('tag')`
|
|
388
|
-
- **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of
|
|
394
|
+
- **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of a DB driver (`better-sqlite3` / `pg`) or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. The DB lives in `db/*.server.ts`; `lib/` holds other server-only infra and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files."
|
|
389
395
|
- Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
|
|
390
396
|
- **Fetch server data in the component that needs it, with an `async render()`, not by prop-drilling.** A leaf component can write `const u = await getUser(this.uid)` directly in `render()`; SSR awaits it so the data is in the first paint, and the client uses stale-while-revalidate on a re-fetch. Reach for `renderFallback()` only to show a re-fetch loading state, and `Task` / signals only for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). Do not put `await getData()` in a page / layout when a leaf component can own it (page fetches run sequentially, a route-level waterfall).
|
|
391
397
|
|
|
@@ -919,8 +925,11 @@ route imports and calls it.
|
|
|
919
925
|
```ts
|
|
920
926
|
// modules/posts/actions/create-post.server.ts
|
|
921
927
|
'use server';
|
|
928
|
+
import { db } from '../../../db/connection.server.ts';
|
|
929
|
+
import { posts } from '../../../db/schema.server.ts';
|
|
922
930
|
export async function createPost({ title, body }) {
|
|
923
|
-
|
|
931
|
+
const [post] = await db.insert(posts).values({ title, body }).returning();
|
|
932
|
+
return post;
|
|
924
933
|
}
|
|
925
934
|
```
|
|
926
935
|
|
|
@@ -1030,7 +1039,8 @@ Where the data lives, where to read it:
|
|
|
1030
1039
|
```ts
|
|
1031
1040
|
// modules/posts/actions/create-post.server.ts
|
|
1032
1041
|
'use server';
|
|
1033
|
-
import {
|
|
1042
|
+
import { db } from '../../../db/connection.server.ts';
|
|
1043
|
+
import { posts } from '../../../db/schema.server.ts';
|
|
1034
1044
|
import type { ActionResult } from '../types.ts';
|
|
1035
1045
|
|
|
1036
1046
|
export async function createPost(input: {
|
|
@@ -1148,7 +1158,7 @@ Create new projects with `webjs create`:
|
|
|
1148
1158
|
```sh
|
|
1149
1159
|
webjs create <name> # full-stack (default)
|
|
1150
1160
|
webjs create <name> --template api # backend-only API
|
|
1151
|
-
webjs create <name> --template saas # auth + dashboard +
|
|
1161
|
+
webjs create <name> --template saas # auth + dashboard + Drizzle User model
|
|
1152
1162
|
```
|
|
1153
1163
|
|
|
1154
1164
|
**Route-wrapping pattern (especially for `--template api` apps):**
|
package/templates/Dockerfile
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
# framework AGENTS.md "Secure response headers" section.
|
|
22
22
|
FROM node:24-alpine
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
24
|
+
# ca-certificates for outbound TLS (e.g. a managed Postgres). better-sqlite3
|
|
25
|
+
# is a prebuilt native module, so no build toolchain is needed here.
|
|
26
|
+
RUN apk add --no-cache ca-certificates
|
|
26
27
|
|
|
27
28
|
WORKDIR /app
|
|
28
29
|
|
|
@@ -35,9 +36,9 @@ RUN npm install --no-audit --no-fund
|
|
|
35
36
|
# App source. node_modules and local state are excluded via .dockerignore.
|
|
36
37
|
COPY . .
|
|
37
38
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
39
|
+
# Drizzle has no client-codegen step, so there is nothing to build here. The
|
|
40
|
+
# database is migrated at boot via `webjs start` (the `webjs.start.before`
|
|
41
|
+
# step runs `webjs db migrate`). See the CMD note below.
|
|
41
42
|
|
|
42
43
|
ENV NODE_ENV=production
|
|
43
44
|
# webjs start reads $PORT (default 8080). compose / uncloud / Railway set it.
|
|
@@ -55,6 +56,8 @@ EXPOSE 8080
|
|
|
55
56
|
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=5 \
|
|
56
57
|
CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/__webjs/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
57
58
|
|
|
58
|
-
# `npm start`
|
|
59
|
-
#
|
|
59
|
+
# `npm start` is a thin alias for `webjs start` (#550). `webjs start` runs the
|
|
60
|
+
# `webjs.start.before` step (`webjs db migrate`, idempotent / a no-op with no
|
|
61
|
+
# pending migrations) IN-PROCESS, then serves on $PORT. `CMD ["webjs", "start"]`
|
|
62
|
+
# is now equivalent: the migrate no longer depends on an npm `prestart` hook.
|
|
60
63
|
CMD ["npm", "start"]
|
package/templates/compose.yaml
CHANGED
|
@@ -12,9 +12,9 @@ services:
|
|
|
12
12
|
- "8080:8080"
|
|
13
13
|
environment:
|
|
14
14
|
PORT: 8080
|
|
15
|
-
# SQLite on a volume for local dev. For production,
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# SQLite on a volume for local dev. For production, scaffold with
|
|
16
|
+
# --db postgres (or swap db/columns.server.ts + db/connection.server.ts
|
|
17
|
+
# for the pg variant) and point DATABASE_URL at your managed Postgres.
|
|
18
18
|
DATABASE_URL: file:/data/dev.db
|
|
19
19
|
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
20
20
|
AUTH_SECRET: ${AUTH_SECRET:-change-me-please-at-least-32-characters!}
|
package/lib/prisma-preflight.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prisma-client preflight for `webjs dev` (#452).
|
|
3
|
-
*
|
|
4
|
-
* The scaffold's `dev` npm script is `webjs dev`, and `npm run dev` runs the
|
|
5
|
-
* `predev` hook (`prisma generate`) FIRST. Invoking the `webjs dev` binary
|
|
6
|
-
* directly (easy to do, and tempting for an AI/CLI) skips `predev`, so the dev
|
|
7
|
-
* server boots against an ungenerated `@prisma/client` and crashes with a raw
|
|
8
|
-
* "did not initialize yet" error and no hint that the canonical command is
|
|
9
|
-
* `npm run dev`. This turns that crash into a one-line, actionable message.
|
|
10
|
-
*
|
|
11
|
-
* Scope is deliberately narrow: it only fires for an app that actually uses
|
|
12
|
-
* Prisma (a `prisma/schema.prisma` OR an `@prisma/client` dependency), and it
|
|
13
|
-
* only HINTS. It never auto-runs an arbitrary `predev` script and never shells
|
|
14
|
-
* out to `prisma generate` on its own, keeping the no-build promise intact.
|
|
15
|
-
*
|
|
16
|
-
* Detection (verified against a real Prisma 6 install): the GENERATED
|
|
17
|
-
* `.prisma/client` target is resolved through standard Node resolution from the
|
|
18
|
-
* app (so a hoisted monorepo, where the client lives at a PARENT `node_modules`,
|
|
19
|
-
* resolves correctly), then read. An ABSENT target, or a present-but-stub target
|
|
20
|
-
* (the ungenerated client whose `PrismaClient` constructor throws the init
|
|
21
|
-
* error), is "ungenerated". A real generated target older than the schema is
|
|
22
|
-
* "stale". We do NOT grep the static `@prisma/client` re-export shim: it is
|
|
23
|
-
* present in both states and never carries the init-error string itself.
|
|
24
|
-
*/
|
|
25
|
-
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
26
|
-
import { join, dirname } from 'node:path';
|
|
27
|
-
import { createRequire } from 'node:module';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Does this app use Prisma? True if a schema is checked in OR `@prisma/client`
|
|
31
|
-
* is a declared dependency. Either alone is enough; a non-Prisma app has
|
|
32
|
-
* neither and gets no warning.
|
|
33
|
-
*
|
|
34
|
-
* @param {string} cwd
|
|
35
|
-
* @returns {boolean}
|
|
36
|
-
*/
|
|
37
|
-
export function usesPrisma(cwd) {
|
|
38
|
-
if (existsSync(join(cwd, 'prisma', 'schema.prisma'))) return true;
|
|
39
|
-
try {
|
|
40
|
-
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
41
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
42
|
-
return Boolean(deps && deps['@prisma/client']);
|
|
43
|
-
} catch {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Marker the ungenerated `prisma-client-js` stub embeds in its generated target
|
|
49
|
-
// (`node_modules/.prisma/client/index.js`). Verified against a real Prisma 6
|
|
50
|
-
// install: after `npm i @prisma/client` but before `prisma generate`, the
|
|
51
|
-
// generated `.prisma/client` entry IS present but its `PrismaClient` constructor
|
|
52
|
-
// throws `@prisma/client did not initialize yet. Please run "prisma generate"`.
|
|
53
|
-
// A real `prisma generate` replaces that stub with the generated client, which
|
|
54
|
-
// does NOT contain this string. So the marker, read from the GENERATED target
|
|
55
|
-
// (not the static `@prisma/client` shim), is the reliable ungenerated signal.
|
|
56
|
-
const UNGENERATED_MARKER = 'did not initialize yet';
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Resolve the GENERATED Prisma client entry (`.prisma/client/index.js`) for an
|
|
60
|
-
* app, following standard Node resolution so a hoisted monorepo layout (the
|
|
61
|
-
* generated client at a PARENT `node_modules`, the app under `apps/<x>`) still
|
|
62
|
-
* resolves. Returns a discriminated result so the caller can tell the three
|
|
63
|
-
* cases apart:
|
|
64
|
-
* - `{ kind: 'unresolved' }` - `@prisma/client` itself is not resolvable.
|
|
65
|
-
* - `{ kind: 'no-target' }` - the package resolves but `.prisma/client`
|
|
66
|
-
* does not (a custom `output`, ambiguous).
|
|
67
|
-
* - `{ kind: 'target', path }` - the generated target resolves.
|
|
68
|
-
*
|
|
69
|
-
* @param {string} cwd
|
|
70
|
-
* @returns {{ kind: 'unresolved' } | { kind: 'no-target' } | { kind: 'target', path: string }}
|
|
71
|
-
*/
|
|
72
|
-
function resolveGeneratedClient(cwd) {
|
|
73
|
-
let clientDir;
|
|
74
|
-
try {
|
|
75
|
-
// Resolve @prisma/client AS THE APP would (hoisting-aware), then locate its
|
|
76
|
-
// package dir. The shim itself loads `.prisma/client/default` relative to
|
|
77
|
-
// here, so resolving from this dir follows the same (possibly hoisted) path.
|
|
78
|
-
const appRequire = createRequire(join(cwd, 'noop.js'));
|
|
79
|
-
clientDir = dirname(appRequire.resolve('@prisma/client'));
|
|
80
|
-
} catch {
|
|
81
|
-
return { kind: 'unresolved' };
|
|
82
|
-
}
|
|
83
|
-
const shimRequire = createRequire(join(clientDir, 'noop.js'));
|
|
84
|
-
for (const entry of ['.prisma/client/index.js', '.prisma/client/default.js']) {
|
|
85
|
-
try {
|
|
86
|
-
return { kind: 'target', path: shimRequire.resolve(entry) };
|
|
87
|
-
} catch { /* try the next entry */ }
|
|
88
|
-
}
|
|
89
|
-
return { kind: 'no-target' };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Inspect the generated Prisma client state for a Prisma app.
|
|
94
|
-
*
|
|
95
|
-
* Returns one of:
|
|
96
|
-
* - `{ status: 'ok' }` - client generated and not older than the schema.
|
|
97
|
-
* - `{ status: 'missing' }` - schema/dep present but no generated client.
|
|
98
|
-
* - `{ status: 'stale' }` - client exists but the schema is newer than it.
|
|
99
|
-
*
|
|
100
|
-
* Detection resolves the GENERATED `.prisma/client` target through standard Node
|
|
101
|
-
* resolution (so hoisted monorepos are handled) and reads it: an absent target,
|
|
102
|
-
* or a present-but-stub target (the ungenerated `PrismaClient` that throws on
|
|
103
|
-
* construction), is `missing`. A real generated client that is older than the
|
|
104
|
-
* schema is `stale`. A custom-`output` generator whose target Node cannot
|
|
105
|
-
* resolve falls back to `ok` rather than nag a working app (false positives are
|
|
106
|
-
* worse than a missed hint here).
|
|
107
|
-
*
|
|
108
|
-
* @param {string} cwd
|
|
109
|
-
* @returns {{ status: 'ok' | 'missing' | 'stale' }}
|
|
110
|
-
*/
|
|
111
|
-
export function prismaClientState(cwd) {
|
|
112
|
-
const resolved = resolveGeneratedClient(cwd);
|
|
113
|
-
|
|
114
|
-
// @prisma/client not resolvable: the app declared the dep (usesPrisma gated
|
|
115
|
-
// us here) but it is not installed/generated. That is the boot-crash case.
|
|
116
|
-
if (resolved.kind === 'unresolved') return { status: 'missing' };
|
|
117
|
-
|
|
118
|
-
// The package resolves but the default `.prisma/client` target does not: a
|
|
119
|
-
// custom `output` whose location we cannot cheaply verify. Fall back to `ok`
|
|
120
|
-
// rather than nag a working app (false positives are worse than a missed hint).
|
|
121
|
-
if (resolved.kind === 'no-target') return { status: 'ok' };
|
|
122
|
-
|
|
123
|
-
const generatedIndex = resolved.path;
|
|
124
|
-
|
|
125
|
-
// The generated target exists. Is it still the ungenerated stub (its
|
|
126
|
-
// PrismaClient constructor throws the init error)?
|
|
127
|
-
try {
|
|
128
|
-
const body = readFileSync(generatedIndex, 'utf8');
|
|
129
|
-
if (body.includes(UNGENERATED_MARKER)) return { status: 'missing' };
|
|
130
|
-
} catch { /* unreadable: fall through to the stale check, then ok */ }
|
|
131
|
-
|
|
132
|
-
// Generated for real. Is it older than the schema (a stale client)?
|
|
133
|
-
const schema = join(cwd, 'prisma', 'schema.prisma');
|
|
134
|
-
try {
|
|
135
|
-
if (existsSync(schema)) {
|
|
136
|
-
const schemaMtime = statSync(schema).mtimeMs;
|
|
137
|
-
const clientMtime = statSync(generatedIndex).mtimeMs;
|
|
138
|
-
if (schemaMtime > clientMtime) return { status: 'stale' };
|
|
139
|
-
}
|
|
140
|
-
} catch { /* if we can't stat, treat as ok */ }
|
|
141
|
-
|
|
142
|
-
return { status: 'ok' };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Build the actionable hint for an ungenerated/stale client, or `null` when the
|
|
147
|
-
* app is fine or does not use Prisma. The caller prints it (a warning, not a
|
|
148
|
-
* hard exit) before booting the dev server.
|
|
149
|
-
*
|
|
150
|
-
* @param {string} cwd
|
|
151
|
-
* @returns {string | null}
|
|
152
|
-
*/
|
|
153
|
-
export function prismaDevHint(cwd) {
|
|
154
|
-
if (!usesPrisma(cwd)) return null;
|
|
155
|
-
const { status } = prismaClientState(cwd);
|
|
156
|
-
if (status === 'ok') return null;
|
|
157
|
-
|
|
158
|
-
const reason =
|
|
159
|
-
status === 'stale'
|
|
160
|
-
? 'Your Prisma client looks stale (the schema changed since it was generated).'
|
|
161
|
-
: 'Your Prisma client is not generated yet.';
|
|
162
|
-
return (
|
|
163
|
-
`webjs: ${reason}\n` +
|
|
164
|
-
` The dev server will crash on an ungenerated client. Fix it with either:\n` +
|
|
165
|
-
` npm run dev # canonical: runs \`prisma generate\` (predev) first\n` +
|
|
166
|
-
` webjs db generate # just regenerate the client, then re-run\n`
|
|
167
|
-
);
|
|
168
|
-
}
|