@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 +6 -0
- package/lib/create.js +25 -15
- package/lib/saas-template.js +8 -8
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +3 -1
- package/templates/.cursorrules +1 -1
- package/templates/.github/copilot-instructions.md +1 -1
- package/templates/AGENTS.md +13 -6
- package/templates/CONVENTIONS.md +6 -5
- package/templates/test/hello/browser/hello.test.js +12 -6
- package/templates/test/hello/e2e/hello.test.ts +11 -9
- package/templates/test/hello/hello.test.ts +4 -6
- package/templates/web-test-runner.config.js +84 -9
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
|
|
318
|
-
// so `npm run start` (
|
|
319
|
-
//
|
|
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
|
-
//
|
|
390
|
-
//
|
|
391
|
-
// identically.
|
|
392
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
`);
|
package/lib/saas-template.js
CHANGED
|
@@ -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
|
|
190
|
-
//
|
|
191
|
-
//
|
|
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`
|
|
209
|
-
"//
|
|
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
|
|
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
|
@@ -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
|
package/templates/.cursorrules
CHANGED
|
@@ -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'
|
package/templates/AGENTS.md
CHANGED
|
@@ -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);
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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`
|
|
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
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -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.
|
|
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).
|
|
275
|
-
|
|
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
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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',
|