@webjsdev/cli 0.10.28 → 0.10.30

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/bin/webjs.js CHANGED
@@ -147,6 +147,12 @@ async function main() {
147
147
  // down on exit so a watcher cannot outlive the server.
148
148
  const { readAppTasks } = await import('../lib/app-tasks.js');
149
149
  const devTasks = readAppTasks(process.cwd());
150
+ // Load `.env` BEFORE the before-steps (same as `start`, L188), so a
151
+ // `dev.before` `webjs db migrate` sees DATABASE_URL from `.env`. Without
152
+ // this a Postgres dev migrate runs with no connection string and fails
153
+ // (sqlite survives via its `?? 'db/dev.db'` config fallback). The watch
154
+ // child / inline server load `.env` again later (idempotent).
155
+ loadAppEnv(process.cwd());
150
156
  await runPhaseBeforeSteps('dev', devTasks.dev.before, process.cwd());
151
157
  const killTasks = await startDevParallelTasks(devTasks.dev.parallel, process.cwd());
152
158
  process.on('SIGINT', () => { killTasks(); process.exit(0); });
package/lib/create.js CHANGED
@@ -314,9 +314,9 @@ export async function scaffoldApp(name, cwd, opts = {}) {
314
314
  },
315
315
  scripts: {
316
316
  // No `predev` / `prestart` hooks (#550): the `webjs` block below holds
317
- // the start orchestration (`webjs db migrate`), run INSIDE `webjs start`,
318
- // so `npm run start` (a thin alias) behaves identically. Drizzle has no
319
- // codegen, so there is no dev `before` step.
317
+ // the dev + start orchestration (`webjs db migrate`), run INSIDE `webjs
318
+ // dev` / `webjs start`, so `npm run dev` / `start` (thin aliases) behave
319
+ // identically. Both apply pending migrations before serving (#725).
320
320
  //
321
321
  // Bun runtime (#541): the long-running server scripts (`dev` / `start`)
322
322
  // are prefixed `bun --bun` so the app SERVES on Bun. The `--bun` overrides
@@ -386,16 +386,16 @@ export async function scaffoldApp(name, cwd, opts = {}) {
386
386
  // into components/ui/ (they import @webjsdev/core, not the kit), and the
387
387
  // CLI resolves @webjsdev/ui from its own install.
388
388
  },
389
- // Start task orchestration (#550). `webjs start` reads `start.before` and
390
- // runs it in-process, so `npm run start` (a thin alias above) behaves
391
- // identically. Drizzle has no codegen, so there is no dev `before` step;
392
- // production applies pending migrations via `webjs db migrate`. The scaffold
389
+ // Dev + start task orchestration (#550). `webjs dev` / `webjs start` read
390
+ // `before` and run it in-process, so `npm run dev` / `start` (thin aliases
391
+ // above) behave identically. Both apply pending migrations via `webjs db
392
+ // migrate` (idempotent, a no-op when the db is current), so a freshly
393
+ // generated migration is applied without a manual step (#725). The scaffold
393
394
  // uses the Tailwind browser runtime (no CSS build step), so there is no dev
394
395
  // `parallel` watcher here; an app that adds the Tailwind CLI puts its
395
396
  // `--watch` command under `webjs.dev.parallel`.
396
397
  webjs: {
397
- // Drizzle has no codegen, so there is no dev `before` step. Production
398
- // applies pending migrations at boot via `webjs db migrate` (drizzle-kit).
398
+ dev: { before: ['webjs db migrate'] },
399
399
  start: { before: ['webjs db migrate'] },
400
400
  },
401
401
  }, null, 2) + '\n');
@@ -667,6 +667,7 @@ function tune<T extends { exec(sql: string): unknown }>(client: T): T {
667
667
 
668
668
  async function open() {
669
669
  if ((globalThis as { Bun?: unknown }).Bun) {
670
+ // @ts-expect-error bun:sqlite is a Bun builtin with no Node typings
670
671
  const { Database } = await import('bun:sqlite');
671
672
  const { drizzle } = await import('drizzle-orm/bun-sqlite');
672
673
  return drizzle({ client: tune(new Database(url)), relations: schema.relations });
@@ -1388,13 +1389,22 @@ For AI agents, read this before editing scaffolded files:
1388
1389
  // Single copy-paste line so the user can move from "scaffold done"
1389
1390
  // to "dev server up" in one command. The full-stack and saas
1390
1391
  // templates ship with @webjsdev/ui already initialised; the api
1391
- // template has no UI but may add one later. Saas needs a one-time
1392
- // generate + migrate before the first run (the example User model wants
1393
- // its table to exist). Drizzle splits Prisma's `migrate dev` into
1394
- // `db:generate` (schema to SQL) then `db:migrate` (apply).
1392
+ // template has no UI but may add one later.
1395
1393
  const installSegment = installed ? '' : `${pm} install && `;
1396
- const dbSegment = isSaas ? `${pm} run db:generate && ${pm} run db:migrate && ` : '';
1394
+ // The saas example queries the users table on its first request (auth), so it
1395
+ // needs a migration authored first: `db:generate` writes it and the
1396
+ // `webjs.dev.before` migrate applies it on `run dev` (Drizzle splits Prisma's
1397
+ // `migrate dev` into generate-then-migrate). The full-stack / api examples do
1398
+ // not query the db on first paint, so they boot with just `run dev`; once you
1399
+ // add a db route, `db:generate` then `run dev` is the loop (dev auto-migrates).
1400
+ const dbSegment = isSaas ? `${pm} run db:generate && ` : '';
1397
1401
  const runCommand = `cd ${name} && ${installSegment}${dbSegment}${pm} run dev`;
1402
+ // Postgres needs a reachable DATABASE_URL before any migrate (sqlite uses a
1403
+ // local file with no .env). Point it at a running database; `dev` / `start`
1404
+ // then apply pending migrations via webjs.*.before.
1405
+ const pgNote = dialect === 'postgres'
1406
+ ? `\nPostgres: copy .env.example to .env and set DATABASE_URL to a running database before \`${pm} run dev\`.\n`
1407
+ : '';
1398
1408
  // Use `npx webjsdev ui ...` here, not `npx webjs ui ...`. The bare
1399
1409
  // `webjs` npm name is owned by an unrelated package; `npx webjs
1400
1410
  // <cmd>` would fetch THAT package instead of ours when run outside
@@ -1410,7 +1420,7 @@ For AI agents, read this before editing scaffolded files:
1410
1420
  Next steps:
1411
1421
  ${runCommand}
1412
1422
  # → http://localhost:8080
1413
-
1423
+ ${pgNote}
1414
1424
  Optional:
1415
1425
  ${uiNote}
1416
1426
  `);
@@ -186,9 +186,10 @@ export async function writeSaasFiles(appDir, opts = {}) {
186
186
  // ALWAYS once the app modules import: auth() only reads a cookie, no DB
187
187
  // query. This is the headline security assertion and it is REAL.
188
188
  // The signup, login, and protected-route flow writes + reads a user, so it
189
- // needs the DB migrated (`npm run db:generate` then `npm run db:migrate`).
190
- // Until the users table exists those flows error, so the suite skips with a
191
- // clear message instead of crashing. After DB setup it runs for real.
189
+ // needs the migrated users table (`npm run db:generate`, then `npm run dev`
190
+ // applies it via webjs.dev.before). Until then those flows error, so the
191
+ // suite probes readiness and skips with a clear message instead of crashing,
192
+ // then runs for real once the db is set up.
192
193
  await mkdir(join(appDir, 'test', 'auth'), { recursive: true });
193
194
  // The generated comments reference `npm run db:*` setup; bun-ify them so a
194
195
  // bun-flavored saas app reads `bun run db:*` (#541; db is Node tooling, so a
@@ -205,12 +206,11 @@ export async function writeSaasFiles(appDir, opts = {}) {
205
206
  "const appDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');",
206
207
  "",
207
208
  "// The auth pages + dashboard middleware query the users table via Drizzle.",
208
- "// Until `npm run db:generate` + `npm run db:migrate` have created it, a",
209
- "// request hitting those modules 500s. We detect that at the RESPONSE level",
209
+ "// Until `npm run db:generate` has authored the migration (then `npm run dev`",
210
+ "// applies it via webjs.dev.before, or `npm run db:migrate` directly), a",
211
+ "// request hitting those modules 500s; we detect that at the RESPONSE level",
210
212
  "// (a 5xx on the dashboard) and SKIP with a clear message rather than report",
211
- "// a misleading failure. After you run",
212
- "// npm install && npm run db:generate && npm run db:migrate",
213
- "// every assertion below runs for real.",
213
+ "// a misleading failure. After the db is set up every assertion runs for real.",
214
214
  "process.env.DATABASE_URL ||= 'file:./dev.db';",
215
215
  "process.env.AUTH_SECRET ||= 'test-secret-at-least-32-characters-long!!';",
216
216
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.28",
3
+ "version": "0.10.30",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -145,7 +145,9 @@ self-review loop.
145
145
  a `.server.{js,ts}` file; the framework rewrites that import into an RPC
146
146
  stub for the browser. `lib/` holds both server-only infra
147
147
  (the DB in `db/*.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
148
- `cn`); follow the same rule per file.
148
+ `cn`); follow the same rule per file. A TYPE-ONLY `import type { Todo } from
149
+ '#db/schema.server.ts'` is the exception, fine in a page or component because
150
+ the stripper erases it before it reaches the browser.
149
151
  - Keep pages and layouts as pure carriers so their modules stay out of the
150
152
  network tab. A page/layout never hydrates; the framework drops its module
151
153
  from the browser as long as its only browser job is registering the
@@ -114,7 +114,7 @@ self-review loop.
114
114
  - One function per server action file (*.server.ts)
115
115
  - Components must call customElements.define('tag', Class)
116
116
  - **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
117
- - Server-only code (the DB driver `pg`, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. The DB lives in db/*.server.ts (db/connection.server.ts); lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: if a lib/ file needs Node APIs, only import it from server-only files.
117
+ - Server-only code (the DB driver `pg`, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. The DB lives in db/*.server.ts (db/connection.server.ts); lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: if a lib/ file needs Node APIs, only import it from server-only files. A TYPE-ONLY `import type { Todo } from '#db/schema.server.ts'` is the exception, fine in a page or component because the stripper erases it before it reaches the browser.
118
118
  - Directives: webjs exports the lit directives with no clean native equivalent (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `cache`, `until`, `ref` / `createRef`, `templateContent`, `asyncAppend` / `asyncReplace`, `watch`). Lit's `classMap` / `styleMap` / `ifDefined` / `when` / `choose` are NOT exported. For those, use plain template-literal expressions (`class=${cond ? 'a' : 'b'}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead.
119
119
  - **Progressive enhancement is the default.** Pages AND every web component are SSR'd to real HTML. Write components so the first paint is the right content. Read SSR-meaningful defaults in `constructor()`. `connectedCallback` is never called on the server, so anything there only runs after hydration. Initial data for components comes from the page function (server-side fetch plus pass as attribute/property) OR from an `async render()` in the component itself (`const u = await getUser(this.uid)`, which SSR awaits so the data is in the first paint), NOT from `fetch` calls in `connectedCallback`. Prefer the co-located `async render()` over prop-drilling; `renderFallback()` is the optional re-fetch loading state (never first paint), and a `Task` is for genuinely client-only data. For write-paths, prefer `<form action=...>` plus server action over `fetch` plus click handler. The framework upgrades plain forms to partial-swap submissions automatically.
120
120
  - **Client navigation is auto-magic.** Real `<a href>` and `<form action>` get partial-swap behavior with no opt-in. Because layouts persist across navigation, put shared chrome (sidenav, header) in `layout.ts` and page-specific content in `page.ts`. For validation errors, return 4xx HTML from a `route.ts` POST handler; the router renders it in place preserving the user's input. For non-layout swap regions, wrap in `<webjs-frame id="...">`. See "Client navigation patterns" in AGENTS.md.
@@ -110,7 +110,7 @@ each change must include.
110
110
  - Components: extend the `WebComponent({ ... })` factory to declare reactive properties (e.g. `extends WebComponent({ count: Number })`; per-prop options via `prop(Number, { reflect: true })`), add `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. A hand-written `static properties` throws at construction (`no-static-properties`); set defaults in the constructor, never a class-field initializer (`reactive-props-no-class-field`). Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`), flagged by `array-prop-uses-array-type`. **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
111
111
  - 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.
112
112
  - Server actions: *.server.ts files with one exported async function each.
113
- - Server-only code (a DB driver like 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.
113
+ - Server-only code (a DB driver like 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. A TYPE-ONLY `import type { Todo } from '#db/schema.server.ts'` is the exception, fine in a page or component because the stripper erases it before it reaches the browser.
114
114
  - Directives: webjs exports the lit directives with no clean native equivalent (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `cache`, `until`, `ref` / `createRef`, `templateContent`, `asyncAppend` / `asyncReplace`, `watch`). Lit's `classMap` / `styleMap` / `ifDefined` / `when` / `choose` are NOT exported; use plain template-literal expressions instead.
115
115
  - Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
116
116
  - Task: import { Task, TaskStatus } from '@webjsdev/core/task'
@@ -329,7 +329,7 @@ db/
329
329
  columns.server.ts column helpers (dialect-specific; the only file to swap for Postgres)
330
330
  connection.server.ts opens the driver, exports the \`db\` singleton (import \`db\` from here)
331
331
  seed.server.ts optional seed (run via \`webjs db seed\`)
332
- dev.db SQLite file (gitignored); run \`npm run db:migrate\` to create
332
+ dev.db SQLite file (gitignored); created when migrations apply (\`dev\`/\`start\` run \`webjs db migrate\`)
333
333
  migrations/ generated migration SQL (committed)
334
334
  drizzle.config.ts drizzle-kit config (root; SQLite by default, --db postgres to switch)
335
335
  public/ static assets, served at /public/*
@@ -387,13 +387,16 @@ lives in the `webjs` block of `package.json` and runs INSIDE
387
387
 
388
388
  ```jsonc
389
389
  "webjs": {
390
+ "dev": { "before": ["webjs db migrate"] },
390
391
  "start": { "before": ["webjs db migrate"] }
391
392
  }
392
393
  ```
393
394
 
394
- Drizzle has no codegen, so there is no dev `before` step. An app that
395
- adds the Tailwind CLI puts its `--watch` command under
396
- `webjs.dev.parallel` and it runs alongside the server, torn down on exit.
395
+ Both `dev` and `start` apply pending migrations via `webjs db migrate`
396
+ (idempotent, a no-op when the db is current), so a freshly generated
397
+ migration is applied without a manual step. An app that adds the Tailwind
398
+ CLI puts its `--watch` command under `webjs.dev.parallel` and it runs
399
+ alongside the server, torn down on exit.
397
400
  `before` steps run to completion first; a failed `webjs db migrate`
398
401
  aborts the boot with a clear message rather than serving a stale schema.
399
402
 
@@ -457,7 +460,7 @@ Scripts (all wrap `drizzle-kit`):
457
460
  - `npm run db:push`: `webjs db push` (push the schema straight to the dev DB)
458
461
  - `npm run db:studio`: `webjs db studio` (visual DB browser)
459
462
  - `npm run db:seed`: `webjs db seed` (run `db/seed.server.ts`)
460
- - `webjs.start.before` runs `webjs db migrate` inside `webjs start` (idempotent; replaces the old `prestart` hook). No dev `before` step (no codegen).
463
+ - `webjs.dev.before` and `webjs.start.before` both run `webjs db migrate` inside `webjs dev` / `webjs start` (idempotent; replaces the old `prestart` hook), so after you `db:generate` a migration it is applied on the next boot with no manual `db:migrate` step.
461
464
 
462
465
  Always import `db` from `db/connection.server.ts` (the globalThis-cached
463
466
  singleton avoids opening a new connection on every dev-server reload), and
@@ -1163,7 +1166,11 @@ composition, so a nested shell ends up dropped by the HTML parser.
1163
1166
  `lib/utils/cn.ts` with `cn`, design-
1164
1167
  system helpers). Server-only `lib/*` files must only be imported
1165
1168
  from `.server.ts`/`route.ts`/`middleware.ts`; browser-safe `lib/*`
1166
- files (like `lib/utils/cn.ts`) can be imported anywhere.
1169
+ files (like `lib/utils/cn.ts`) can be imported anywhere. A TYPE-ONLY
1170
+ import is the exception: `import type { Todo } from
1171
+ '#db/schema.server.ts'` is fine in a page or component, because the
1172
+ TypeScript stripper erases it before it reaches the browser, so
1173
+ sharing a derived row type is safe and is not flagged.
1167
1174
  3. Event / property / boolean holes in `` html`` `` are unquoted:
1168
1175
  `@click=${fn}`, not `@click="${fn}"`.
1169
1176
  4. Component state lives in signals. Import `signal` from
@@ -268,11 +268,12 @@ docs". That is the agent's default behavior in a webjs project.
268
268
 
269
269
  Every webjs app uses **Drizzle + SQLite** for persistence by default. The
270
270
  scaffold ships the `db/` folder (`schema.server.ts`, `columns.server.ts`,
271
- `connection.server.ts`), the `webjs.start.before` step that runs
272
- `webjs db migrate` inside `webjs start` (#550), and the
271
+ `connection.server.ts`), the `webjs.dev.before` + `webjs.start.before` steps
272
+ that run `webjs db migrate` inside `webjs dev` / `webjs start` (#550), and the
273
273
  `npm run db:generate` / `db:migrate` / `db:push` / `db:studio` / `db:seed`
274
- scripts (which route through `webjs db` to drizzle-kit). Drizzle has no
275
- codegen, so there is no dev `before` step.
274
+ scripts (which route through `webjs db` to drizzle-kit). The loop after a
275
+ schema change is `db:generate` (authors the migration) then `webjs dev` (the
276
+ `dev.before` step applies it); `db:generate` is never auto-run on boot.
276
277
 
277
278
  **AI agents: these rules are absolute.**
278
279
 
@@ -400,7 +401,7 @@ modules/
400
401
  - One exported function per server action/query file
401
402
  - 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`).
402
403
  - Components must call `Class.register('tag')`
403
- - **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 (`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."
404
+ - **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 (`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." A TYPE-ONLY `import type { Todo } from '#db/schema.server.ts'` is the exception, fine in a page or component because the stripper erases it before it reaches the browser.
404
405
  - Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
405
406
  - **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).
406
407
  - **Keep pages and layouts as pure carriers, so their modules stay out of the network tab.** A page/layout never hydrates; the framework drops its module from the browser as long as its only browser-relevant job is registering the components it imports. It starts shipping its own module (invisible in tests) the moment its closure does any OTHER client work. So do not give a page/layout module-scope client work (a top-level call, a `window` / `document` / `customElements` access, a bare side-effect import, or a `@webjsdev/core/client-router` import: routing is automatic), and do not import a client-global-touching non-component util into it. Put client behaviour in a component, server-only code in `.server.{js,ts}`. Self-check: `page.ts` / `layout.ts` should not appear in the browser's network tab.
@@ -51,11 +51,17 @@ suite('Example browser tests', () => {
51
51
  await assertNoA11yViolations(el);
52
52
  });
53
53
 
54
- // Replace with your component tests:
55
- // test('my-widget renders correctly', async () => {
56
- // await import('../../components/my-widget.ts');
57
- // const el = await ssrFixture(html`<my-widget></my-widget>`);
58
- // assert.ok(el.shadowRoot ?? el.firstElementChild);
59
- // await assertNoA11yViolations(el); // opt-in a11y check
54
+ // Your REAL `.ts` app components load here. `webjs test --browser` serves
55
+ // them through the webjs dev pipeline (TypeScript stripped, any `.server.ts`
56
+ // action import rewritten to an RPC stub, `#` aliases resolved), so a
57
+ // component that talks to the server works in a real browser, not just a
58
+ // node test. Point the import at a component your app actually has:
59
+ //
60
+ // test('todo-list adds a row optimistically', async () => {
61
+ // await import('../../../components/todo-list.ts'); // imports create-todo.server.ts
62
+ // const el = await ssrFixture(html`<todo-list></todo-list>`);
63
+ // el.querySelector('button')?.click(); // fires the action RPC
64
+ // // assert on the DOM, then optionally:
65
+ // await assertNoA11yViolations(el);
60
66
  // });
61
67
  });
@@ -1,12 +1,14 @@
1
- /**
2
- * Example E2E test: replace with tests for your user flows.
3
- *
4
- * Run: WEBJS_E2E=1 webjs test
5
- * Or: WEBJS_E2E=1 node --test test/**/e2e/**/*.test.ts
6
- *
7
- * Requires: puppeteer-core + chromium installed.
8
- * npm i -D puppeteer-core
9
- */
1
+ // Example E2E test: replace with tests for your user flows.
2
+ //
3
+ // Run: WEBJS_E2E=1 webjs test
4
+ // (or point node --test at your e2e test files directly)
5
+ //
6
+ // Requires: puppeteer-core + chromium installed.
7
+ // npm i -D puppeteer-core
8
+ //
9
+ // Note: this header uses line comments on purpose. A JSDoc block comment
10
+ // here cannot contain a glob like test/**/e2e/ because the ** followed by /
11
+ // closes the block comment early and breaks TypeScript stripping.
10
12
  import { test, describe, before, after } from 'node:test';
11
13
  import assert from 'node:assert/strict';
12
14
  import { spawn } from 'node:child_process';
@@ -1,9 +1,7 @@
1
- /**
2
- * Example unit test: replace with tests for your modules.
3
- *
4
- * Run: webjs test
5
- * Or: node --test test/**/*.test.ts
6
- */
1
+ // Example unit test: replace with tests for your modules.
2
+ //
3
+ // Run: webjs test
4
+ // Or: node --test 'test/**/*.test.ts'
7
5
  import { test } from 'node:test';
8
6
  import assert from 'node:assert/strict';
9
7
  import { html } from '@webjsdev/core';
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * Web Test Runner configuration.
3
3
  *
4
- * Runs browser tests (components, directives, interactions) in real
5
- * Chromium via Playwright. Server tests (actions, queries) use node:test.
6
- *
7
- * Tests are organised by feature. Each feature folder may have a
8
- * `browser/` subfolder containing real-browser tests; the glob below
9
- * picks them up wherever they live.
4
+ * Runs browser tests (components, directives, interactions) in real Chromium
5
+ * via Playwright. Server tests (actions, queries) use node:test.
10
6
  *
11
7
  * test/<feature>/<file>.test.ts ← node tests
12
8
  * test/<feature>/browser/<file>.test.js ← this runner
@@ -15,15 +11,94 @@
15
11
  * webjs test # runs both server + browser tests
16
12
  * webjs test --browser # browser tests only
17
13
  * webjs test --server # server tests only
14
+ *
15
+ * A webjs browser test imports the REAL app: a `.ts` component that imports a
16
+ * `'use server'` action. Plain web-test-runner serves raw TypeScript with no
17
+ * transform, so that never loads. This config proxies every module request to
18
+ * the webjs dev pipeline via `createBrowserTestHandler`, so the browser gets
19
+ * the SAME output as `webjs dev`: TypeScript stripped, a `.server.ts` import
20
+ * rewritten to a typed RPC stub, `#`-alias imports resolved, `@webjsdev/core`
21
+ * served, and the importmap injected. (#806)
18
22
  */
19
23
  import { playwrightLauncher } from '@web/test-runner-playwright';
24
+ import { createBrowserTestHandler } from '@webjsdev/server/testing';
25
+ import { resolve } from 'node:path';
26
+ import { Readable } from 'node:stream';
27
+
28
+ // One webjs handler for the app, warmed once and shared. Top-level await so the
29
+ // importmap is ready before `testRunnerHtml` is called for the first test file.
30
+ const webjs = await createBrowserTestHandler(resolve('.'));
20
31
 
21
32
  export default {
33
+ // Browser tests are `.js` (web-test-runner serves them through its own test
34
+ // framework); the components + modules they import are `.ts`, served
35
+ // transformed by the webjs middleware below.
22
36
  files: ['test/**/browser/**/*.test.js'],
23
- nodeResolve: true,
24
- browsers: [
25
- playwrightLauncher({ product: 'chromium' }),
37
+ // webjs's importmap resolves `@webjsdev/core`, the `#` app aliases, and
38
+ // vendors, so web-test-runner must NOT rewrite bare specifiers to
39
+ // node_modules paths.
40
+ nodeResolve: false,
41
+ // Inject the webjs importmap so a bare / `#`-aliased import in a served module
42
+ // resolves in the browser exactly as it does under `webjs dev`.
43
+ testRunnerHtml: (testFrameworkImport) =>
44
+ `<!DOCTYPE html>
45
+ <html>
46
+ <head>${webjs.importmapHtml()}</head>
47
+ <body>
48
+ <script type="module" src="${testFrameworkImport}"></script>
49
+ </body>
50
+ </html>`,
51
+ middleware: [
52
+ async (ctx, next) => {
53
+ // web-test-runner owns: its own internals (/__web-test-runner,
54
+ // /__web-dev-server, /__wds), the TEST FILES themselves (it wraps each
55
+ // for the test framework), and the DOCUMENT navigation (the test-runner
56
+ // HTML page, `Sec-Fetch-Dest: document`). If webjs served the page, WTR's
57
+ // test bootstrap would never load and the session would time out. NOTE:
58
+ // match the WTR/WDS prefixes specifically, NOT a broad `/__web`, because
59
+ // webjs's own paths are `/__webjs/...` and MUST be proxied below.
60
+ if (
61
+ ctx.path.startsWith('/__web-test-runner') ||
62
+ ctx.path.startsWith('/__web-dev-server') ||
63
+ ctx.path.startsWith('/__wds') ||
64
+ /\.test\.(js|mjs)$/.test(ctx.path) ||
65
+ (ctx.get('sec-fetch-dest') || '') === 'document'
66
+ ) {
67
+ return next();
68
+ }
69
+ // The dev live-reload SSE has no meaning in a test run; short-circuit it
70
+ // so it neither hangs nor logs a 404.
71
+ if (ctx.path === '/__webjs/events') {
72
+ ctx.status = 204;
73
+ return;
74
+ }
75
+ // Everything else (a `.ts` component, a `.server.ts` action, the `#`
76
+ // alias, `/__webjs/core/*`, vendors) goes through the webjs dev pipeline.
77
+ // A GET/HEAD has no body; a POST (a browser test firing an action RPC)
78
+ // carries one. `ctx.req` is a Node IncomingMessage, so wrap it in a web
79
+ // ReadableStream (the same `Readable.toWeb` the server's own request
80
+ // bridge uses), NOT pass the raw Node stream.
81
+ const hasBody = ctx.method !== 'GET' && ctx.method !== 'HEAD';
82
+ const req = new Request(`http://localhost${ctx.originalUrl || ctx.url}`, {
83
+ method: ctx.method,
84
+ headers: ctx.headers,
85
+ body: hasBody ? Readable.toWeb(ctx.req) : undefined,
86
+ duplex: 'half',
87
+ });
88
+ const res = await webjs.handle(req);
89
+ // A 404 means webjs does not own this path; let web-test-runner try.
90
+ if (res.status === 404) return next();
91
+ ctx.status = res.status;
92
+ // Copy headers, but handle Set-Cookie separately: `Headers.forEach`
93
+ // comma-joins multiple Set-Cookie into one malformed value, so use
94
+ // `getSetCookie()` and append each (an action driving a multi-cookie auth
95
+ // flow would otherwise lose a cookie in the browser).
96
+ res.headers.forEach((value, key) => { if (key.toLowerCase() !== 'set-cookie') ctx.set(key, value); });
97
+ for (const cookie of res.headers.getSetCookie?.() ?? []) ctx.append('set-cookie', cookie);
98
+ ctx.body = Buffer.from(await res.arrayBuffer());
99
+ },
26
100
  ],
101
+ browsers: [playwrightLauncher({ product: 'chromium' })],
27
102
  testFramework: {
28
103
  config: {
29
104
  ui: 'tdd',