@webjsdev/cli 0.10.17 → 0.10.19

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.
@@ -14,17 +14,19 @@ const UI_REGISTRY_ROOT = resolve(
14
14
  );
15
15
 
16
16
  /**
17
- * Read a registry component and rewrite its `'../lib/utils.ts'` import for
18
- * the scaffolded app's `components/ui/<name>.ts` layout (two-up to lib/).
19
- * Mirrors the helper in `create.js`, kept private here to avoid coupling.
17
+ * Read a registry component and rewrite its `'#lib/utils.ts'` import for
18
+ * the scaffolded app's `components/ui/<name>.ts` layout (the `#lib/utils/cn.ts`
19
+ * alias). Mirrors the helper in `create.js`, kept private here to avoid coupling.
20
20
  */
21
21
  async function readUiComponent(name) {
22
22
  const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
23
23
  if (!existsSync(src)) return null;
24
24
  const raw = await readFile(src, 'utf8');
25
+ // The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
26
+ // it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
25
27
  return raw
26
- .replaceAll("'../lib/utils.ts'", "'../../lib/utils.ts'")
27
- .replaceAll('"../lib/utils.ts"', '"../../lib/utils.ts"');
28
+ .replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
29
+ .replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"');
28
30
  }
29
31
 
30
32
  /** Copy named registry components into `<appDir>/components/ui/`. */
@@ -50,16 +52,10 @@ export async function writeSaasFiles(appDir) {
50
52
  // the saas auth pages use raw <form> + label/input class helpers instead.
51
53
  await copyUiComponents(appDir, ['dialog', 'switch', 'checkbox']);
52
54
 
53
- // lib/prisma.server.ts
55
+ // The db/ layer (columns/connection) is written by the full-stack scaffold
56
+ // already; this template overwrites db/schema.server.ts below to add the
57
+ // User.passwordHash column auth needs.
54
58
  await mkdir(join(appDir, 'lib'), { recursive: true });
55
- await writeFile(join(appDir, 'lib', 'prisma.server.ts'), [
56
- "import { PrismaClient } from '@prisma/client';",
57
- "",
58
- "const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };",
59
- "export const prisma = globalForPrisma.prisma || new PrismaClient();",
60
- "if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;",
61
- "",
62
- ].join('\n'));
63
59
 
64
60
  // lib/password.server.ts
65
61
  await writeFile(join(appDir, 'lib', 'password.server.ts'), [
@@ -85,14 +81,14 @@ export async function writeSaasFiles(appDir) {
85
81
  // lib/auth.server.ts
86
82
  await writeFile(join(appDir, 'lib', 'auth.server.ts'), [
87
83
  "import { createAuth, Credentials } from '@webjsdev/server';",
88
- "import { prisma } from './prisma.server.ts';",
84
+ "import { db } from '#db/connection.server.ts';",
89
85
  "import { compare } from './password.server.ts';",
90
86
  "",
91
87
  "export const { auth, signIn, signOut, handlers } = createAuth({",
92
88
  " providers: [",
93
89
  " Credentials({",
94
90
  " async authorize(credentials: { email: string; password: string }) {",
95
- " const user = await prisma.user.findUnique({ where: { email: credentials.email } });",
91
+ " const user = await db.query.users.findFirst({ where: { email: credentials.email } });",
96
92
  " if (!user || !await compare(credentials.password, user.passwordHash)) return null;",
97
93
  " return { id: String(user.id), name: user.name, email: user.email };",
98
94
  " },",
@@ -103,26 +99,24 @@ export async function writeSaasFiles(appDir) {
103
99
  "",
104
100
  ].join('\n'));
105
101
 
106
- // prisma/schema.prisma
107
- await mkdir(join(appDir, 'prisma'), { recursive: true });
108
- await writeFile(join(appDir, 'prisma', 'schema.prisma'), [
109
- 'datasource db {',
110
- ' provider = "sqlite"',
111
- ' url = env("DATABASE_URL")',
112
- '}',
113
- '',
114
- 'generator client {',
115
- ' provider = "prisma-client-js"',
116
- '}',
117
- '',
118
- 'model User {',
119
- ' id Int @id @default(autoincrement())',
120
- ' email String @unique',
121
- ' name String?',
122
- ' passwordHash String',
123
- ' createdAt DateTime @default(now())',
124
- '}',
125
- '',
102
+ // db/schema.server.ts: overwrite the full-stack scaffold's example User to
103
+ // add passwordHash (the column auth needs). Drizzle, dialect-agnostic.
104
+ await writeFile(join(appDir, 'db', 'schema.server.ts'), [
105
+ "import { defineRelations } from 'drizzle-orm';",
106
+ "import { table, pk, text, createdAt } from './columns.server.ts';",
107
+ "",
108
+ "export const users = table('users', {",
109
+ " id: pk(),",
110
+ " email: text().notNull().unique(),",
111
+ " name: text(),",
112
+ " passwordHash: text().notNull(),",
113
+ " createdAt: createdAt(),",
114
+ "});",
115
+ "",
116
+ "export const relations = defineRelations({ users }, () => ({}));",
117
+ "",
118
+ "export type User = typeof users.$inferSelect;",
119
+ "",
126
120
  ].join('\n'));
127
121
 
128
122
  // modules/auth/actions/signup.server.ts
@@ -132,15 +126,14 @@ export async function writeSaasFiles(appDir) {
132
126
  await writeFile(join(appDir, 'modules', 'auth', 'actions', 'signup.server.ts'), [
133
127
  "'use server';",
134
128
  "",
135
- "import { prisma } from '../../../lib/prisma.server.ts';",
136
- "import { hash } from '../../../lib/password.server.ts';",
129
+ "import { db } from '#db/connection.server.ts';",
130
+ "import { users } from '#db/schema.server.ts';",
131
+ "import { hash } from '#lib/password.server.ts';",
137
132
  "",
138
133
  "export async function signup(input: { name: string; email: string; password: string }) {",
139
- " const exists = await prisma.user.findUnique({ where: { email: input.email } });",
134
+ " const exists = await db.query.users.findFirst({ where: { email: input.email }, columns: { id: true } });",
140
135
  " if (exists) return { success: false as const, error: 'Email already registered', status: 409 };",
141
- " const user = await prisma.user.create({",
142
- " data: { name: input.name, email: input.email, passwordHash: await hash(input.password) },",
143
- " });",
136
+ " const [user] = await db.insert(users).values({ name: input.name, email: input.email, passwordHash: await hash(input.password) }).returning();",
144
137
  " return { success: true as const, data: { id: user.id, name: user.name, email: user.email } };",
145
138
  "}",
146
139
  "",
@@ -150,7 +143,7 @@ export async function writeSaasFiles(appDir) {
150
143
  await writeFile(join(appDir, 'modules', 'auth', 'queries', 'current-user.server.ts'), [
151
144
  "'use server';",
152
145
  "",
153
- "import { auth } from '../../../lib/auth.server.ts';",
146
+ "import { auth } from '#lib/auth.server.ts';",
154
147
  "",
155
148
  "export async function currentUser() {",
156
149
  " const session = await auth();",
@@ -183,11 +176,10 @@ export async function writeSaasFiles(appDir) {
183
176
  // - The protected-route gate (unauthenticated /dashboard -> 302 /login) runs
184
177
  // ALWAYS once the app modules import: auth() only reads a cookie, no DB
185
178
  // query. This is the headline security assertion and it is REAL.
186
- // - The signup -> login -> protected-route flow writes + reads a user, so it
187
- // needs Prisma generated AND migrated (`npm run db:generate` +
188
- // `npm run db:migrate`). When the Prisma client is not yet generated the
189
- // app modules can't import at all, so the whole suite skips with a clear
190
- // message instead of crashing. After you set up the DB it runs for real.
179
+ // The signup, login, and protected-route flow writes + reads a user, so it
180
+ // needs the DB migrated (`npm run db:generate` then `npm run db:migrate`).
181
+ // Until the users table exists those flows error, so the suite skips with a
182
+ // clear message instead of crashing. After DB setup it runs for real.
191
183
  await mkdir(join(appDir, 'test', 'auth'), { recursive: true });
192
184
  await writeFile(join(appDir, 'test', 'auth', 'auth.test.ts'), [
193
185
  "import { test } from 'node:test';",
@@ -200,20 +192,20 @@ export async function writeSaasFiles(appDir) {
200
192
  "",
201
193
  "const appDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');",
202
194
  "",
203
- "// The auth pages + dashboard middleware import lib/prisma.server.ts, which",
204
- "// imports @prisma/client. Until `npm run db:generate` (prisma generate) has",
205
- "// run, that import is missing, so a request hitting those modules 500s. We",
206
- "// detect that at the RESPONSE level (a 5xx on the dashboard) and SKIP with a",
207
- "// clear message rather than reporting a misleading failure. After you run",
195
+ "// The auth pages + dashboard middleware query the users table via Drizzle.",
196
+ "// Until `npm run db:generate` + `npm run db:migrate` have created it, a",
197
+ "// request hitting those modules 500s. We detect that at the RESPONSE level",
198
+ "// (a 5xx on the dashboard) and SKIP with a clear message rather than report",
199
+ "// a misleading failure. After you run",
208
200
  "// npm install && npm run db:generate && npm run db:migrate",
209
201
  "// every assertion below runs for real.",
210
202
  "process.env.DATABASE_URL ||= 'file:./dev.db';",
211
203
  "process.env.AUTH_SECRET ||= 'test-secret-at-least-32-characters-long!!';",
212
204
  "",
213
205
  "function makeHandler() {",
214
- " // createRequestHandler builds lazily, so it succeeds even before prisma is",
215
- " // generated; the missing dependency only surfaces when a request reaches",
216
- " // the prisma-importing module. That is why readiness is probed per-response.",
206
+ " // createRequestHandler builds lazily, so it succeeds even before the DB",
207
+ " // is migrated; the missing table only surfaces when a request reaches a",
208
+ " // module that queries it. That is why readiness is probed per-response.",
217
209
  " return createRequestHandler({ appDir, dev: true });",
218
210
  "}",
219
211
  "",
@@ -273,7 +265,7 @@ export async function writeSaasFiles(appDir) {
273
265
  // app/api/auth/[...path]/route.ts
274
266
  await mkdir(join(appDir, 'app', 'api', 'auth', '[...path]'), { recursive: true });
275
267
  await writeFile(join(appDir, 'app', 'api', 'auth', '[...path]', 'route.ts'), [
276
- "import { handlers } from '../../../../lib/auth.server.ts';",
268
+ "import { handlers } from '#lib/auth.server.ts';",
277
269
  "export const GET = handlers.GET;",
278
270
  "export const POST = handlers.POST;",
279
271
  "",
@@ -283,10 +275,10 @@ export async function writeSaasFiles(appDir) {
283
275
  await mkdir(join(appDir, 'app', 'login'), { recursive: true });
284
276
  await writeFile(join(appDir, 'app', 'login', 'page.ts'), [
285
277
  "import { html } from '@webjsdev/core';",
286
- "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '../../components/ui/card.ts';",
287
- "import { buttonClass } from '../../components/ui/button.ts';",
288
- "import { inputClass } from '../../components/ui/input.ts';",
289
- "import { labelClass } from '../../components/ui/label.ts';",
278
+ "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '#components/ui/card.ts';",
279
+ "import { buttonClass } from '#components/ui/button.ts';",
280
+ "import { inputClass } from '#components/ui/input.ts';",
281
+ "import { labelClass } from '#components/ui/label.ts';",
290
282
  "",
291
283
  "export const metadata = { title: 'Login' };",
292
284
  "",
@@ -325,11 +317,11 @@ export async function writeSaasFiles(appDir) {
325
317
  await mkdir(join(appDir, 'app', 'signup'), { recursive: true });
326
318
  await writeFile(join(appDir, 'app', 'signup', 'page.ts'), [
327
319
  "import { html } from '@webjsdev/core';",
328
- "import { signup } from '../../modules/auth/actions/signup.server.ts';",
329
- "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '../../components/ui/card.ts';",
330
- "import { buttonClass } from '../../components/ui/button.ts';",
331
- "import { inputClass } from '../../components/ui/input.ts';",
332
- "import { labelClass } from '../../components/ui/label.ts';",
320
+ "import { signup } from '#modules/auth/actions/signup.server.ts';",
321
+ "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '#components/ui/card.ts';",
322
+ "import { buttonClass } from '#components/ui/button.ts';",
323
+ "import { inputClass } from '#components/ui/input.ts';",
324
+ "import { labelClass } from '#components/ui/label.ts';",
333
325
  "",
334
326
  "export const metadata = { title: 'Sign up' };",
335
327
  "",
@@ -397,7 +389,7 @@ export async function writeSaasFiles(appDir) {
397
389
  // app/dashboard/middleware.ts
398
390
  await mkdir(join(appDir, 'app', 'dashboard', 'settings'), { recursive: true });
399
391
  await writeFile(join(appDir, 'app', 'dashboard', 'middleware.ts'), [
400
- "import { auth } from '../../lib/auth.server.ts';",
392
+ "import { auth } from '#lib/auth.server.ts';",
401
393
  "",
402
394
  "export default async function requireAuth(req: Request, next: () => Promise<Response>) {",
403
395
  " const session = await auth();",
@@ -412,10 +404,10 @@ export async function writeSaasFiles(appDir) {
412
404
  // app/dashboard/page.ts
413
405
  await writeFile(join(appDir, 'app', 'dashboard', 'page.ts'), [
414
406
  "import { html } from '@webjsdev/core';",
415
- "import { currentUser } from '../../modules/auth/queries/current-user.server.ts';",
416
- "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '../../components/ui/card.ts';",
417
- "import { buttonClass } from '../../components/ui/button.ts';",
418
- "import { badgeClass } from '../../components/ui/badge.ts';",
407
+ "import { currentUser } from '#modules/auth/queries/current-user.server.ts';",
408
+ "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '#components/ui/card.ts';",
409
+ "import { buttonClass } from '#components/ui/button.ts';",
410
+ "import { badgeClass } from '#components/ui/badge.ts';",
419
411
  "",
420
412
  "export const metadata = { title: 'Dashboard' };",
421
413
  "",
@@ -443,8 +435,8 @@ export async function writeSaasFiles(appDir) {
443
435
  // app/dashboard/settings/page.ts
444
436
  await writeFile(join(appDir, 'app', 'dashboard', 'settings', 'page.ts'), [
445
437
  "import { html } from '@webjsdev/core';",
446
- "import { currentUser } from '../../../modules/auth/queries/current-user.server.ts';",
447
- "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '../../../components/ui/card.ts';",
438
+ "import { currentUser } from '#modules/auth/queries/current-user.server.ts';",
439
+ "import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '#components/ui/card.ts';",
448
440
  "",
449
441
  "export const metadata = { title: 'Settings' };",
450
442
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.17",
3
+ "version": "0.10.19",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -7,10 +7,10 @@ cover what you need, the full hosted docs are at **https://docs.webjs.com**.
7
7
 
8
8
  ## Persistence + scaffold rules (non-negotiable)
9
9
 
10
- - **Use Prisma + SQLite for data, never JSON files.** It is already wired up
11
- (`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`). For
10
+ - **Use Drizzle + SQLite for data, never JSON files.** It is already wired up
11
+ (`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate` + `npm run db:migrate`). For
12
12
  ANY data the app stores (todos, posts, messages, products, comments), define
13
- a Prisma model. NEVER create `data/*.json`, `db.json`, or any JSON file as a
13
+ a Drizzle table. NEVER create `data/*.json`, `db.json`, or any JSON file as a
14
14
  fake database. NEVER use module-scope arrays / Maps as a substitute. NEVER
15
15
  use localStorage for app data. These are project conventions in
16
16
  CONVENTIONS.md (a JSON file used as a database resets on reload and
@@ -131,12 +131,12 @@ self-review loop.
131
131
  - Custom-element tag names are passed to `.register('tag-name')`. They are NOT
132
132
  a static field on the class.
133
133
  - One function per server action file (`*.server.ts`).
134
- - Server-only code (`@prisma/client`, `node:*`, anything that needs Node APIs)
134
+ - Server-only code (a DB driver like `better-sqlite3`/`pg`, `node:*`, anything that needs Node APIs)
135
135
  goes only in `.server.{js,ts}` files, `route.ts` handlers, or
136
136
  `middleware.ts`. Never in pages, layouts, or components. Wrap the access in
137
137
  a `.server.{js,ts}` file; the framework rewrites that import into an RPC
138
138
  stub for the browser. `lib/` holds both server-only infra
139
- (`lib/prisma.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
139
+ (the DB in `db/*.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
140
140
  `cn`); follow the same rule per file.
141
141
  - Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat`
142
142
  ship. Use plain template-literal expressions
@@ -23,5 +23,5 @@ AUTH_SECRET=
23
23
  # REDIS_URL=redis://localhost:6379
24
24
 
25
25
  # ── Database ────────────────────────────────────────────────────────
26
- # Used by Prisma. SQLite for dev, PostgreSQL/MySQL for production.
27
- DATABASE_URL=file:./dev.db
26
+ # Used by Drizzle. SQLite for dev, PostgreSQL for production (--db postgres).
27
+ DATABASE_URL=file:./db/dev.db
@@ -7,10 +7,10 @@ the full hosted docs are at **https://docs.webjs.com**.
7
7
 
8
8
  ## Persistence + scaffold rules (non-negotiable)
9
9
 
10
- - **Use Prisma + SQLite for data, never JSON files.** It's already wired up
11
- (`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`). For ANY
10
+ - **Use Drizzle + SQLite for data, never JSON files.** It's already wired up
11
+ (`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate` + `npm run db:migrate`). For ANY
12
12
  data the app stores (todos, posts, messages, products, comments…),
13
- define a Prisma model. NEVER create `data/*.json`, `db.json`, or any
13
+ define a Drizzle table. NEVER create `data/*.json`, `db.json`, or any
14
14
  JSON file as a fake database. NEVER use module-scope arrays / Maps as
15
15
  a substitute. NEVER use localStorage for app data. It resets on reload and cannot scale. This is a project convention
16
16
  (CONVENTIONS.md).
@@ -104,7 +104,7 @@ each change must include.
104
104
  - Components: extend WebComponent, declare `static properties` (and `static styles` for shadow-DOM components), call `Class.register('tag-name')` at the bottom of the file. The tag name is the argument to `.register()`, not a static field.
105
105
  - Async data in a component: prefer an `async render()` (`const u = await getUser(this.uid)`), which SSR awaits so the data is in the first paint, over prop-drilling from the page or fetching in `connectedCallback`. `renderFallback()` is the optional re-fetch loading state (never first paint); error isolation is automatic (`renderError()` customizes it); a `Task` is for genuinely client-only data.
106
106
  - Server actions: *.server.ts files with one exported async function each.
107
- - Server-only code (@prisma/client, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to an RPC stub for the browser. lib/ holds both server-only infra (lib/prisma.server.ts) and browser-safe utilities (lib/utils/cn.ts with cn); apply the same rule per file.
107
+ - Server-only code (a DB driver like better-sqlite3/pg, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to an RPC stub for the browser. The DB lives in db/*.server.ts; lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); apply the same rule per file.
108
108
  - Directives: webjs ships only `unsafeHTML`, `live`, and `repeat`. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported. Use plain template-literal expressions and lifecycle hooks instead.
109
109
  - Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
110
110
  - Task: import { Task, TaskStatus } from '@webjsdev/core/task'
@@ -45,9 +45,8 @@ jobs:
45
45
  node-version: '24'
46
46
  cache: npm
47
47
  - run: npm ci
48
- - run: npx prisma generate
49
- - name: Apply migrations to the test database
50
- run: npx prisma migrate deploy
48
+ - name: Set up the database (generate + apply migrations)
49
+ run: npm run db:generate && npm run db:migrate
51
50
  env:
52
51
  DATABASE_URL: file:./ci.db
53
52
  # --server keeps this job to node:test (the browser layer is its own
@@ -66,7 +65,6 @@ jobs:
66
65
  node-version: '24'
67
66
  cache: npm
68
67
  - run: npm ci
69
- - run: npx prisma generate
70
68
  - name: Install Playwright Chromium
71
69
  run: npx playwright install --with-deps chromium
72
70
  - run: npm run test:browser
@@ -81,9 +79,8 @@ jobs:
81
79
  node-version: '24'
82
80
  cache: npm
83
81
  - run: npm ci
84
- - run: npx prisma generate
85
- - name: Apply migrations to the test database
86
- run: npx prisma migrate deploy
82
+ - name: Set up the database (generate + apply migrations)
83
+ run: npm run db:generate && npm run db:migrate
87
84
  env:
88
85
  DATABASE_URL: file:./ci.db
89
86
  # The scaffold's e2e test (test/hello/e2e/) drives a real browser