@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 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.0",
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",
@@ -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;
@@ -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;