@webjsdev/cli 0.10.29 → 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/lib/create.js +1 -0
- 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 +5 -1
- package/templates/CONVENTIONS.md +1 -1
- 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/lib/create.js
CHANGED
|
@@ -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 });
|
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
|
@@ -1166,7 +1166,11 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1166
1166
|
`lib/utils/cn.ts` with `cn`, design-
|
|
1167
1167
|
system helpers). Server-only `lib/*` files must only be imported
|
|
1168
1168
|
from `.server.ts`/`route.ts`/`middleware.ts`; browser-safe `lib/*`
|
|
1169
|
-
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.
|
|
1170
1174
|
3. Event / property / boolean holes in `` html`` `` are unquoted:
|
|
1171
1175
|
`@click=${fn}`, not `@click="${fn}"`.
|
|
1172
1176
|
4. Component state lives in signals. Import `signal` from
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -401,7 +401,7 @@ modules/
|
|
|
401
401
|
- One exported function per server action/query file
|
|
402
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`).
|
|
403
403
|
- Components must call `Class.register('tag')`
|
|
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."
|
|
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.
|
|
405
405
|
- Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
|
|
406
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).
|
|
407
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',
|