@webjsdev/core 0.9.0 → 0.9.1
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/index.d.ts +31 -0
- package/package.json +1 -1
- package/src/asset-url.d.ts +18 -0
- package/src/csp-nonce.d.ts +18 -0
- package/src/testing.d.ts +42 -0
- package/src/websocket-client.d.ts +24 -0
package/index.d.ts
CHANGED
|
@@ -147,6 +147,37 @@ export interface OptimisticState<State, Action> {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// Declarative Signature with custom update reducer
|
|
150
|
+
/**
|
|
151
|
+
* Show a mutation's result before the server confirms it. The declarative form
|
|
152
|
+
* keeps a queue of pending payloads on a component; `.value` is `source()` with
|
|
153
|
+
* each folded in by `update` (keep it pure: it runs on every render), and an
|
|
154
|
+
* entry drops when the promise passed to `.add()` settles. Use it in a
|
|
155
|
+
* degrade-first form: bound to the action, so it posts with JavaScript off.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* class TodoList extends WebComponent {
|
|
159
|
+
* static properties = { todos: { type: Array } };
|
|
160
|
+
* declare todos: Todo[]; // seeded by the page: .todos=${todos}
|
|
161
|
+
* pending = optimistic(this, {
|
|
162
|
+
* source: () => this.todos,
|
|
163
|
+
* update: (rows: Todo[], add: { id: string; title: string }) => [...rows, { ...add, done: false }],
|
|
164
|
+
* });
|
|
165
|
+
* async add(e: SubmitEvent) {
|
|
166
|
+
* e.preventDefault();
|
|
167
|
+
* const form = e.target as HTMLFormElement;
|
|
168
|
+
* const title = String(new FormData(form).get('title'));
|
|
169
|
+
* form.reset();
|
|
170
|
+
* const promise = createTodo({ title }); // a 'use server' action, by RPC
|
|
171
|
+
* this.pending.add({ id: crypto.randomUUID(), title }, promise);
|
|
172
|
+
* const result = await promise;
|
|
173
|
+
* if (result.success && result.data) this.todos = [...this.todos, result.data];
|
|
174
|
+
* }
|
|
175
|
+
* render() {
|
|
176
|
+
* return html`<form action=${createTodo} @submit=${(e: SubmitEvent) => this.add(e)}>...</form>
|
|
177
|
+
* <ul>${this.pending.value.map((t) => html`<li>${t.title}</li>`)}</ul>`;
|
|
178
|
+
* }
|
|
179
|
+
* }
|
|
180
|
+
*/
|
|
150
181
|
export function optimistic<State, Action>(
|
|
151
182
|
host: { requestUpdate?(): void },
|
|
152
183
|
options: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The runtime for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Ships the html and css template tags, the WebComponent base class, signals, directives, and the isomorphic renderers.",
|
|
6
6
|
"types": "./index.d.ts",
|
package/src/asset-url.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
/** Content-hashed `public/` asset urls (#1194). Server-side resolver; the browser bundle drops setAssetUrlProvider and `asset` returns its argument. */
|
|
2
2
|
export function setAssetUrlProvider(fn: (path: string) => string): void;
|
|
3
|
+
/**
|
|
4
|
+
* The url of a `public/` file with a content hash added in production
|
|
5
|
+
* (`/app.css?v=<hash>`, served `immutable` for a year), so a deploy can never
|
|
6
|
+
* leave a stale copy cached. Unchanged in dev and in the browser.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // app/(marketing)/layout.ts: a stylesheet link that leads a layout is moved into <head>
|
|
11
|
+
* import { html, asset } from '@webjsdev/core';
|
|
12
|
+
* import type { LayoutProps } from '@webjsdev/core';
|
|
13
|
+
*
|
|
14
|
+
* export default function MarketingLayout({ children }: LayoutProps) {
|
|
15
|
+
* return html`<link rel="stylesheet" href=${asset('/marketing.css')}>${children}`;
|
|
16
|
+
* }
|
|
17
|
+
* // Call it inside the render function of a page, layout or metadata route, not at
|
|
18
|
+
* // module scope or in a component (hydration would swap the hashed url for the bare path).
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
3
21
|
export function asset(path: string): string;
|
package/src/csp-nonce.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
/** Per-request CSP nonce provider (#259). Server-side; the browser bundle drops setCspNonceProvider. */
|
|
2
2
|
export function setCspNonceProvider(fn: () => string): void;
|
|
3
|
+
/**
|
|
4
|
+
* The current request's CSP nonce while rendering on the server, `''` when CSP
|
|
5
|
+
* is off and always `''` in the browser. Turn CSP on with `"webjs": { "csp": true }`
|
|
6
|
+
* in package.json; the framework already stamps its own scripts.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // app/layout.ts (the root layout): stamp it on an inline script you write yourself
|
|
11
|
+
* import { html, cspNonce } from '@webjsdev/core';
|
|
12
|
+
* import type { LayoutProps } from '@webjsdev/core';
|
|
13
|
+
*
|
|
14
|
+
* export default function RootLayout({ children }: LayoutProps) {
|
|
15
|
+
* return html`<!doctype html><html><head>
|
|
16
|
+
* <script nonce=${cspNonce()}>document.documentElement.dataset.js = 'on'</script>
|
|
17
|
+
* </head><body>${children}</body></html>`;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
3
21
|
export function cspNonce(): string;
|
package/src/testing.d.ts
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import type { TemplateResult } from './html.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Server-render a template (or an HTML string) into a container in the test
|
|
5
|
+
* page, wait two macrotasks for the element to upgrade, and resolve its first
|
|
6
|
+
* element. A quick mount; use `ssrFixture` when the hydration round trip matters.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* // modules/todos/components/browser/todo-item.test.js, run with `webjs test --browser`
|
|
11
|
+
* import { html } from '@webjsdev/core';
|
|
12
|
+
* import { fixture } from '@webjsdev/core/testing';
|
|
13
|
+
* import '../todo-item.ts';
|
|
14
|
+
*
|
|
15
|
+
* suite('<todo-item>', () => { // the runner's UI is tdd (suite / test), and a test throws to fail
|
|
16
|
+
* test('renders its title', async () => {
|
|
17
|
+
* const el = await fixture(html`<todo-item title="Buy milk"></todo-item>`);
|
|
18
|
+
* if (!el.textContent?.includes('Buy milk')) throw new Error('the title is missing');
|
|
19
|
+
* });
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
3
23
|
export function fixture(template: TemplateResult | string): Promise<Element>;
|
|
24
|
+
/**
|
|
25
|
+
* Server-render a template, hydrate it in the real browser, and resolve once
|
|
26
|
+
* the element's `updateComplete` settles, so the post-hydration DOM is what the
|
|
27
|
+
* test reads. The component module must be imported first.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```js
|
|
31
|
+
* // modules/counter/components/browser/counter-card.test.js, run with `webjs test --browser`
|
|
32
|
+
* import { html } from '@webjsdev/core';
|
|
33
|
+
* import { ssrFixture, waitForUpdate } from '@webjsdev/core/testing';
|
|
34
|
+
* import '../counter-card.ts';
|
|
35
|
+
*
|
|
36
|
+
* suite('<counter-card>', () => {
|
|
37
|
+
* test('counts a click after hydration', async () => {
|
|
38
|
+
* const el = await ssrFixture(html`<counter-card></counter-card>`);
|
|
39
|
+
* el.querySelector('button')?.click();
|
|
40
|
+
* await waitForUpdate(el);
|
|
41
|
+
* if (!el.textContent?.includes('1')) throw new Error('the count did not advance');
|
|
42
|
+
* });
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
4
46
|
export function ssrFixture(template: TemplateResult | string): Promise<Element>;
|
|
5
47
|
export function waitForUpdate(el: Element): Promise<void>;
|
|
6
48
|
export function assertNoA11yViolations(el: Element, opts?: Record<string, unknown>): Promise<void>;
|
|
@@ -19,4 +19,28 @@ export interface WSConnection {
|
|
|
19
19
|
readonly readyState: 0 | 1 | 2 | 3;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Open a managed WebSocket: a relative path is promoted to ws:// or wss://, it
|
|
24
|
+
* reconnects with backoff unless `reconnect: false`, a string message that is
|
|
25
|
+
* JSON arrives parsed, `send()` JSON-encodes an object and queues while
|
|
26
|
+
* disconnected, and `close()` stops for good.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* // modules/chat/components/chat-feed.ts, fed by app/api/chat/route.ts (its WS export)
|
|
31
|
+
* import { WebComponent, html, signal, connectWS, type WSConnection } from '@webjsdev/core';
|
|
32
|
+
*
|
|
33
|
+
* export class ChatFeed extends WebComponent {
|
|
34
|
+
* readonly #lines = signal<string[]>([]);
|
|
35
|
+
* #conn: WSConnection | null = null;
|
|
36
|
+
* connectedCallback() { // browser only, so no socket opens during SSR
|
|
37
|
+
* super.connectedCallback();
|
|
38
|
+
* this.#conn = connectWS('/api/chat', { onMessage: (msg) => this.#lines.set([...this.#lines.get(), String(msg)]) });
|
|
39
|
+
* }
|
|
40
|
+
* disconnectedCallback() { super.disconnectedCallback(); this.#conn?.close(); }
|
|
41
|
+
* render() { return html`<ul>${this.#lines.get().map((line) => html`<li>${line}</li>`)}</ul>`; }
|
|
42
|
+
* }
|
|
43
|
+
* ChatFeed.register('chat-feed');
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
22
46
|
export function connectWS(url: string, opts?: ConnectOptions): WSConnection;
|