@webjsdev/cli 0.10.29 → 0.10.31
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 +1 -1
- package/lib/api-gallery.js +229 -0
- package/lib/create.js +452 -158
- package/lib/saas-template.js +39 -15
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +70 -2
- package/templates/.claude/hooks/check-server-imports.mjs +86 -0
- package/templates/.claude/hooks/check-server-imports.sh +26 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.cursorrules +41 -2
- package/templates/.github/copilot-instructions.md +41 -2
- package/templates/AGENTS.md +160 -10
- package/templates/CONVENTIONS.md +150 -11
- package/templates/gallery/app/examples/todo/page.ts +34 -0
- package/templates/gallery/app/features/async-render/page.ts +14 -0
- package/templates/gallery/app/features/broadcast/feed/route.ts +19 -0
- package/templates/gallery/app/features/broadcast/page.ts +24 -0
- package/templates/gallery/app/features/caching/page.ts +39 -0
- package/templates/gallery/app/features/client-router/page.ts +34 -0
- package/templates/gallery/app/features/client-router/second/page.ts +20 -0
- package/templates/gallery/app/features/components/page.ts +14 -0
- package/templates/gallery/app/features/directives/page.ts +14 -0
- package/templates/gallery/app/features/env/page.ts +36 -0
- package/templates/gallery/app/features/file-storage/file/[key]/route.ts +19 -0
- package/templates/gallery/app/features/file-storage/page.ts +62 -0
- package/templates/gallery/app/features/forms/page.ts +72 -0
- package/templates/gallery/app/features/metadata/page.ts +55 -0
- package/templates/gallery/app/features/optimistic-ui/page.ts +14 -0
- package/templates/gallery/app/features/rate-limit/page.ts +29 -0
- package/templates/gallery/app/features/rate-limit/ping/middleware.ts +8 -0
- package/templates/gallery/app/features/rate-limit/ping/route.ts +7 -0
- package/templates/gallery/app/features/route-handler/data/route.ts +8 -0
- package/templates/gallery/app/features/route-handler/page.ts +13 -0
- package/templates/gallery/app/features/routing/[id]/page.ts +25 -0
- package/templates/gallery/app/features/routing/page.ts +47 -0
- package/templates/gallery/app/features/server-actions/page.ts +14 -0
- package/templates/gallery/app/features/service-worker/page.ts +35 -0
- package/templates/gallery/app/features/websockets/echo/route.ts +19 -0
- package/templates/gallery/app/features/websockets/page.ts +25 -0
- package/templates/gallery/modules/async-render/components/server-clock.ts +26 -0
- package/templates/gallery/modules/async-render/queries/server-greeting.server.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +61 -0
- package/templates/gallery/modules/components/components/counter-card.ts +35 -0
- package/templates/gallery/modules/directives/components/directive-demo.ts +53 -0
- package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +18 -0
- package/templates/gallery/modules/optimistic-ui/actions/like-post.server.ts +9 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +35 -0
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +49 -0
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +12 -0
- package/templates/gallery/modules/server-actions/components/greeter.ts +30 -0
- package/templates/gallery/modules/server-actions/utils/format.server.ts +8 -0
- package/templates/gallery/modules/todo/actions/create-todo.server.ts +16 -0
- package/templates/gallery/modules/todo/actions/delete-todo.server.ts +15 -0
- package/templates/gallery/modules/todo/actions/toggle-todo.server.ts +21 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +140 -0
- package/templates/gallery/modules/todo/queries/list-todos.server.ts +19 -0
- package/templates/gallery/modules/todo/types.ts +12 -0
- package/templates/gallery/modules/websockets/components/ws-echo.ts +62 -0
- package/templates/lib/utils/ui.ts +4 -4
- 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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A WebSocket echo client. connectWS() (from '@webjsdev/core') opens the socket,
|
|
2
|
+
// auto-reconnects with backoff, JSON-encodes/decodes, and queues sends while
|
|
3
|
+
// disconnected. The connection is opened in connectedCallback (browser-only, so
|
|
4
|
+
// it never runs during SSR) and closed in disconnectedCallback. At SSR the
|
|
5
|
+
// component renders its disconnected state, so with JS off the page still reads
|
|
6
|
+
// (a live socket has no no-JS equivalent, which is the honest fallback here).
|
|
7
|
+
import { WebComponent, signal, html, connectWS } from '@webjsdev/core';
|
|
8
|
+
|
|
9
|
+
export class WsEcho extends WebComponent {
|
|
10
|
+
private connected = signal(false);
|
|
11
|
+
private lines = signal<string[]>([]);
|
|
12
|
+
#conn: ReturnType<typeof connectWS> | null = null;
|
|
13
|
+
|
|
14
|
+
connectedCallback() {
|
|
15
|
+
super.connectedCallback();
|
|
16
|
+
this.#conn = connectWS('/features/websockets/echo', {
|
|
17
|
+
onOpen: () => this.connected.set(true),
|
|
18
|
+
onClose: () => this.connected.set(false),
|
|
19
|
+
onMessage: (msg: unknown) => this.lines.set([...this.lines.get(), String(msg)]),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
disconnectedCallback() {
|
|
24
|
+
super.disconnectedCallback();
|
|
25
|
+
this.#conn?.close();
|
|
26
|
+
this.#conn = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private send(e: SubmitEvent) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
const form = e.target as HTMLFormElement;
|
|
32
|
+
const input = form.elements.namedItem('msg') as HTMLInputElement;
|
|
33
|
+
const text = input.value.trim();
|
|
34
|
+
if (!text) return;
|
|
35
|
+
this.#conn?.send(text);
|
|
36
|
+
input.value = '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
const on = this.connected.get();
|
|
41
|
+
return html`
|
|
42
|
+
<div class="grid gap-4 max-w-[420px]">
|
|
43
|
+
<div class="flex items-center gap-2 text-sm">
|
|
44
|
+
<span class="w-2 h-2 rounded-full ${on ? 'bg-primary' : 'bg-muted-foreground/40'}"></span>
|
|
45
|
+
<span class="text-muted-foreground">${on ? 'connected' : 'connecting (live echo needs JavaScript)'}</span>
|
|
46
|
+
</div>
|
|
47
|
+
<form @submit=${(e: SubmitEvent) => this.send(e)} class="flex gap-2">
|
|
48
|
+
<input name="msg" autocomplete="off" placeholder="Say something"
|
|
49
|
+
class="flex-1 px-3 py-2 rounded-xl bg-card border border-border text-foreground text-sm outline-none focus:border-border-strong" />
|
|
50
|
+
<button type="submit"
|
|
51
|
+
class="px-3.5 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm border-0 cursor-pointer transition-all hover:bg-primary/90 active:scale-[0.97]">Send</button>
|
|
52
|
+
</form>
|
|
53
|
+
<ul class="grid gap-1.5 list-none m-0 p-0">
|
|
54
|
+
${this.lines.get().map((line) => html`
|
|
55
|
+
<li class="px-3 py-2 rounded-xl bg-card border border-border text-[15px] text-foreground font-mono">${line}</li>
|
|
56
|
+
`)}
|
|
57
|
+
</ul>
|
|
58
|
+
</div>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
WsEcho.register('ws-echo');
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { html } from '@webjsdev/core';
|
|
18
18
|
|
|
19
|
-
/** `● label` kicker: small caps,
|
|
19
|
+
/** `● label` kicker: small caps, brand colour, above headings. */
|
|
20
20
|
export function rubric(label: string, mb: 'sm' | 'md' = 'md') {
|
|
21
21
|
const mbCls = mb === 'sm' ? 'mb-3' : 'mb-4';
|
|
22
22
|
return html`
|
|
23
|
-
<span class="block font-mono text-[11px] leading-none font-semibold tracking-[0.2em] uppercase text-
|
|
23
|
+
<span class="block font-mono text-[11px] leading-none font-semibold tracking-[0.2em] uppercase text-primary ${mbCls}">● ${label}</span>
|
|
24
24
|
`;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -68,10 +68,10 @@ export function banner(content: unknown) {
|
|
|
68
68
|
`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** Inline
|
|
71
|
+
/** Inline brand link: used inside body copy and banners. */
|
|
72
72
|
export function accentLink(href: string, label: unknown) {
|
|
73
73
|
return html`
|
|
74
|
-
<a href=${href} class="text-
|
|
74
|
+
<a href=${href} class="text-primary font-semibold no-underline hover:underline hover:underline-offset-[3px]">${label}</a>
|
|
75
75
|
`;
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -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',
|