redweb 0.9.0 → 0.11.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +187 -23
  3. package/docs/LIVE_HTML.md +352 -0
  4. package/examples/live-html/cards.css +36 -0
  5. package/examples/live-html/cards.html +11 -0
  6. package/examples/live-html/cards.js +91 -0
  7. package/examples/live-html/cards.ts +35 -0
  8. package/examples/live-html/chatroom.css +156 -0
  9. package/examples/live-html/chatroom.js +268 -0
  10. package/examples/live-html/chatroom.ts +217 -0
  11. package/examples/live-html/components.css +7 -0
  12. package/examples/live-html/components.js +113 -0
  13. package/examples/live-html/components.ts +41 -0
  14. package/examples/live-html/counter.css +24 -0
  15. package/examples/live-html/counter.html +10 -0
  16. package/examples/live-html/counter.js +73 -0
  17. package/examples/live-html/counter.ts +21 -0
  18. package/examples/live-html/jsx-page.js +81 -0
  19. package/examples/live-html/jsx-page.tsx +41 -0
  20. package/examples/live-html/tsconfig.json +19 -0
  21. package/index.d.ts +219 -1
  22. package/index.js +18 -1
  23. package/jsx-dev-runtime.d.ts +13 -0
  24. package/jsx-dev-runtime.js +9 -0
  25. package/jsx-runtime.d.ts +28 -0
  26. package/jsx-runtime.js +5 -0
  27. package/package.json +47 -3
  28. package/src/htmx/Html.js +137 -0
  29. package/src/htmx/HtmlRenderer.js +88 -0
  30. package/src/htmx/HtmlSyntax.js +168 -0
  31. package/src/htmx/Jsx.js +86 -0
  32. package/src/htmx/LiveHtmlServer.js +91 -0
  33. package/src/htmx/LivePage.js +232 -0
  34. package/src/htmx/PageAssetLoader.js +34 -0
  35. package/src/htmx/PageManager.js +435 -0
  36. package/src/htmx/StaticExporter.js +78 -0
  37. package/src/htmx/StaticSite.js +182 -0
  38. package/src/htmx/TemplateRenderer.js +231 -0
  39. package/src/htmx/browserRuntime.js +97 -0
  40. package/src/htmx/index.js +10 -0
  41. package/src/htmx/metadata.js +349 -0
  42. package/src/htmx/sourceRoot.js +28 -0
  43. package/src/htmx/start.js +17 -0
  44. package/src/htmx/synchronous.js +9 -0
  45. package/src/http/BaseHttpServer.js +0 -35
  46. package/src/ws/BaseSocketServer.js +4 -0
  47. package/src/htmx/HtmxRenderer.js +0 -73
  48. package/src/htmx/RedWebHtmxComponent.js +0 -11
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ - Added dependency-free server-side TSX rendering through `redweb/jsx-runtime` and `redweb/jsx-dev-runtime`, with fragments, function components, automatic text and attribute escaping, safe URL validation, boolean attributes, and direct interoperability with existing `HtmlFragment` values.
6
+ - Added a compiled TSX Live HTML example plus real HTTP, WebSocket, type-checking, and packed-consumer verification.
7
+
8
+ ## 0.10.0
9
+
10
+ - Added `defineSite()` for shared static-site CSS, metadata, caching, layouts, canonical URLs, public asset copying, and concise page decorators.
11
+ - Added synchronous function components, readonly collection typings, direct safe primitive attribute/URL interpolation, and a `codeBlock()` highlighter hook.
12
+ - Enabled a bounded heartbeat for Live HTML so chat presence and component disconnect hooks detect half-open browser connections instead of waiting for operating-system TCP timeouts.
13
+ - Rebuilt the Live HTML chatroom as a reusable component with a one-time join flow, reserved display names, online presence, bounded shared history, reconnect recovery, and a dedicated message composer.
14
+ - Added reusable `@component()` Live HTML classes with nested composition, isolated state/actions, shared request and connection lifecycle hooks, and deterministic cascading cleanup.
15
+ - Added explicit safe attribute and URL interpolation for Live HTML.
16
+ - Added nested fragment composition and reusable safe code blocks.
17
+ - Added page head metadata, non-live ETag/cache handling, and runtime-free static export with content-addressed CSS.
18
+ - Replaced the executable `.htmx` sandbox with decorator-first, declarative Live HTML pages.
19
+ - Added context-restricted safe server rendering, shallow reactive state, explicit browser actions and bindings, connection/shared page scopes, expiring page sessions, same-origin admission, optional identity binding, HTTPS/WSS, reconnect snapshots, and awaited deterministic cleanup.
20
+ - Integrated the browser runtime with `redweb-client` rather than maintaining a second WebSocket client.
21
+ - Added runnable server-counter and shared-chatroom examples with real HTTP/WebSocket, browser DOM, load, memory, and packed-artifact verification gates.
22
+ - Converted the canonical Live HTML examples to compiled TypeScript so they use `@page()`, `@state()`, and `@action()` directly.
23
+ - Added `start(PageClass)`, optional `LivePage` inheritance, inferred colocated templates, and `shared: true` to remove framework setup boilerplate from decorated pages.
24
+
3
25
  ## 0.9.0
4
26
 
5
27
  - Added opt-in pre-upgrade admission, origin validation, secure placement, bounded pending upgrades, transport limits, ordered processing, and route-level heartbeat monitoring.
package/README.md CHANGED
@@ -30,10 +30,191 @@ const {
30
30
  HTTP_OPTIONS, // Defaults for HTTP servers
31
31
  ENCODINGS, // json/urlencoded encoding names
32
32
  SOCKET_OPTIONS, // Defaults for socket servers
33
- METHODS // Express method helpers
33
+ METHODS, // Express method helpers
34
+ LiveHtmlServer, // SSR plus lifecycle-safe realtime HTML
35
+ HtmlRenderer, // Safe HTML templates, collections, and state payloads
36
+ LivePage, // Optional base for advanced page internals
37
+ page, state, action, view, // Live HTML decorators
38
+ html, start // Safe HTML plus one-call page startup
34
39
  } = require('redweb');
35
40
  ```
36
41
 
42
+ ## Live HTML
43
+
44
+ `start(PageClass)` combines server-rendered TSX or `.html` templates and Redweb WebSockets on one listener. Decorated plain classes hold the behavior. Redweb injects a small browser runtime backed by [`redweb-client`](https://www.npmjs.com/package/redweb-client), binds the HTTP render to an expiring page token, and disposes connection-owned state after disconnect.
45
+
46
+ TSX is the concise default for new pages. It renders straight to Redweb's existing `HtmlFragment`; there is no React dependency, virtual DOM, hydration pass, or client component runtime:
47
+
48
+ ```json
49
+ {
50
+ "compilerOptions": {
51
+ "jsx": "react-jsx",
52
+ "jsxImportSource": "redweb"
53
+ }
54
+ }
55
+ ```
56
+
57
+ ```tsx
58
+ import { LivePage, action, component, page, start, state } from 'redweb';
59
+ import type { Child } from 'redweb/jsx-runtime';
60
+
61
+ const Card = component((props: { title: string; children?: Child }) => (
62
+ <article class="card">
63
+ <h2>{props.title}</h2>
64
+ {props.children}
65
+ </article>
66
+ ));
67
+
68
+ @page('/', { css: 'counter.css' })
69
+ class CounterPage extends LivePage {
70
+ @state() count = 0;
71
+
72
+ @action()
73
+ increment() { this.count += 1; }
74
+
75
+ render() {
76
+ return (
77
+ <main>
78
+ <Card title="Server counter">
79
+ <button rw-click="increment">
80
+ Count <output data-rw-state="count">{this.count}</output>
81
+ </button>
82
+ </Card>
83
+ </main>
84
+ );
85
+ }
86
+ }
87
+
88
+ start(CounterPage, { port: 8181 });
89
+ ```
90
+
91
+ Text and attribute values are escaped automatically. URL attributes use Redweb's existing safe-protocol policy. `on*`, inline `style`, `srcdoc`, `srcset`, and executable `<script>` or `<style>` children are rejected; use `rw-*` server directives and external CSS or JavaScript assets. Existing `html` fragments can be nested in TSX, and TSX fragments can be nested in `html`, so migration can be incremental.
92
+
93
+ Ordinary declarative `.html` templates remain available when separating markup into a standalone file is preferable:
94
+
95
+ ```ts
96
+ import { page, start, state } from 'redweb';
97
+
98
+ @page('/', { template: 'counter.html', css: 'counter.css' })
99
+ class CounterPage {
100
+ @state()
101
+ count = 0;
102
+
103
+ private ticker?: NodeJS.Timeout;
104
+
105
+ connected() {
106
+ this.ticker = setInterval(() => this.count++, 1000);
107
+ }
108
+
109
+ disconnected() {
110
+ clearInterval(this.ticker);
111
+ }
112
+ }
113
+
114
+ start(CounterPage, { port: 8080 });
115
+ ```
116
+
117
+ `counter.html` contains no executable server code:
118
+
119
+ ```html
120
+ <h1>Server-side counter</h1>
121
+ <output aria-live="polite" data-rw-state="count"></output>
122
+ ```
123
+
124
+ Changing a `@state()` property sends only that binding's new value. State updates are shallow and assignment-driven; Redweb does not install deep proxies or rerender the document for scalar changes.
125
+
126
+ CSS is colocated with the page and needs no static-server setup. Pass one file with `css: 'counter.css'` or compose several with `css: ['base.css', 'counter.css']`. Redweb resolves the files beside the decorated class, injects `<link>` elements during SSR, and serves content-addressed stylesheets with immutable browser caching.
127
+
128
+ Browser events can call only explicitly exposed actions:
129
+
130
+ ```ts
131
+ @component()
132
+ class Chatroom {
133
+ @state()
134
+ screen = html`<form rw-submit="join"><input name="name"><button>Join</button></form>`;
135
+
136
+ @action()
137
+ join({ name }: { name: string }) {
138
+ this.screen = html`<p>Connected as ${name}</p><form rw-submit="send"><input name="message"><button>Send</button></form>`;
139
+ }
140
+ }
141
+ ```
142
+
143
+ ```ts
144
+ @page('/chat', { css: 'chatroom.css' })
145
+ class ChatroomPage {
146
+ chat = new Chatroom();
147
+ render() { return html`<main>${this.chat}</main>`; }
148
+ }
149
+ ```
150
+
151
+ Interpolations created with `html` are escaped by default and are restricted to element text—not attributes, URLs, scripts, or styles. Only `HtmlFragment` values may produce HTML patches; ordinary state uses `textContent`. Use `@state({ writable: true })` to opt a property into `rw-bind="property"` browser updates. A page is connection-scoped by default; `shared: true` deliberately shares one instance across its connected visitors. The older `scope: 'shared'` spelling remains supported.
152
+
153
+ Collections use the same model without manual concatenation. Keep the array in `@state()`, render one item with `@view('cards')`, and place it with `<section rw-each="cards"></section>`. Item views must return `html` fragments, so values remain escaped. The current protocol replaces the collection contents atomically; keyed incremental patches can be added later without changing the page API.
154
+
155
+ Documentation and content-heavy pages can compose nested fragments without a client framework:
156
+
157
+ ```ts
158
+ import { attribute, codeBlock, each, html, url } from 'redweb';
159
+
160
+ const sections = each(apiSections, section => html`
161
+ <article id="${attribute(section.id)}">
162
+ <h2>${section.name}</h2>
163
+ <a href="${url(`#${section.id}`)}">Permalink</a>
164
+ ${each(section.methods, method => html`<section><h3>${method.name}</h3></section>`)}
165
+ ${codeBlock(section.usage, { language: 'ts', label: 'TypeScript' })}
166
+ </article>
167
+ `);
168
+ ```
169
+
170
+ Primitive values may be interpolated directly into quoted attributes and safe URL attributes. Redweb escapes attributes and rejects unsafe or protocol-relative URL schemes; `attribute()` and `url()` remain available when explicit intent helps readability. Event handlers, inline styles, `srcdoc`, and `srcset` remain prohibited. `codeBlock()` escapes ordinary code and can call a server-side `highlight` function that returns an `HtmlFragment`.
171
+
172
+ For React-free documentation or marketing pages, set `live: false`. Redweb omits page tokens, browser JavaScript, and WebSockets; adds document metadata; and serves the result with an ETag:
173
+
174
+ ```ts
175
+ @page('/docs', {
176
+ template: 'docs.html',
177
+ css: 'docs.css',
178
+ live: false,
179
+ head: {
180
+ title: 'Redweb API',
181
+ description: 'Complete Redweb API reference.',
182
+ canonical: 'https://example.com/docs',
183
+ image: 'https://example.com/og.png',
184
+ },
185
+ cache: { maxAge: 300, staleWhileRevalidate: 3600 },
186
+ })
187
+ class DocsPage {}
188
+ ```
189
+
190
+ Export the same decorated page to CDN-ready files with `await exportStatic(DocsPage, { outDir: 'dist' })`. Route paths become `index.html` files, colocated stylesheets are emitted under their content-addressed URLs, and no Live HTML runtime is included. Static export requires `live: false`.
191
+
192
+ For a multi-page site, `defineSite()` removes repeated static-page configuration. It shares CSS, metadata, caching, and a safe layout; generates canonical URLs; and can copy a public asset directory during export:
193
+
194
+ ```ts
195
+ const docs = defineSite({
196
+ origin: 'https://redweb.example',
197
+ css: 'site.css',
198
+ head: { description: 'Redweb documentation' },
199
+ layout: content => html`<body><nav>Redweb</nav><main>${content}</main></body>`,
200
+ });
201
+
202
+ @docs.page('/docs', { head: { title: 'Documentation' } })
203
+ class DocsPage {
204
+ render() { return html`<h1>Documentation</h1>`; }
205
+ }
206
+
207
+ await docs.export(DocsPage, { outDir: 'dist', publicDir: 'public' });
208
+ ```
209
+
210
+ An `html` fragment returned by `render()` is final safe markup, so documentation examples containing literal `{{ bindings }}` are never parsed a second time. Return a string or use a template file when Redweb should resolve template bindings and directives.
211
+
212
+ The same API serves HTTPS/WSS when `ssl` is provided. For private pages, an optional `authenticate(request)` callback binds the page token to the same stable user identity across the HTTP render and WebSocket upgrade. Initial connections and reconnects always receive a complete authoritative state snapshot.
213
+
214
+ See the [Live HTML guide](docs/LIVE_HTML.md), runnable [TSX page](examples/live-html/jsx-page.tsx), TypeScript [server counter](examples/live-html/counter.ts), component-based [chatroom](examples/live-html/chatroom.ts), and [persistent card collection](examples/live-html/cards.ts). The chatroom separates joining from its stable message composer, tracks online members, preserves bounded history, restores identity and missed messages after reconnect, and creates an isolated room for every server. The cards page uses `shared: true`, so additions survive reloads, reconnects, and new visitors while its server is running. Run the examples with `npm run example:jsx`, `npm run example:counter`, `npm run example:chatroom`, and `npm run example:cards`. The decorated sources are compiled and exercised unchanged by mock-free HTTP/WebSocket integration tests and a real-Chromium DOM gate.
215
+
216
+ Reusable snippets can own server behavior without page-level forwarding methods. Decorate a class with `@component()`, put instances in page fields, and interpolate them directly: `` html`<main>${this.primary}${this.secondary}</main>` ``. Each instance gets isolated `@state()`, scoped `@action()` methods, nested-component support, and page-owned lifecycle cleanup. See the runnable [component counters](examples/live-html/components.ts) or run `npm run example:components`.
217
+
37
218
  ## Multiplayer in 0.9
38
219
 
39
220
  Redweb keeps each production feature independent and opt-in:
@@ -66,8 +247,7 @@ Options:
66
247
  - `encoding` (`'json' | 'urlencoded'`): body parser selection.
67
248
  - `corsOptions`: passed to `cors`.
68
249
  - `corsOptions: false`: disables the CORS middleware entirely.
69
- - `enableHtmxRendering` (boolean): render `.htmx` files with the built-in renderer.
70
- - `exposeErrors` (boolean): include HTMX rendering details in responses; defaults to `false`.
250
+ - `exposeErrors` (boolean): include WebSocket handler details in responses; defaults to `false`.
71
251
  - `logger`: an object with optional `log`, `warn`, and `error` methods. Pass `null` to disable library logging.
72
252
 
73
253
  Example:
@@ -88,26 +268,6 @@ new HttpServer({
88
268
  });
89
269
  ```
90
270
 
91
- HTMX rendering example (`enableHtmxRendering: true`):
92
-
93
- ```js
94
- new HttpServer({ publicPaths: ['./public'], enableHtmxRendering: true });
95
- ```
96
-
97
- `public/example.htmx`:
98
-
99
- ```js
100
- const name = 'RedWeb';
101
-
102
- <@>
103
- <h1>Hello, {{name}}!</h1>
104
- <@/>
105
- ```
106
-
107
- Requesting `/example.htmx` returns rendered HTML.
108
-
109
- Templates are trusted server-side code. They may load relative modules within their configured public directory, execute for at most one second by default, and interpolate raw HTML. Never render user-supplied template files.
110
-
111
271
  CORS remains permissive by default for backward compatibility. CORS is not authorization; configure `corsOptions`, add authentication middleware to `server.app`, or disable the middleware as appropriate.
112
272
 
113
273
  ## WebSocket servers
@@ -453,6 +613,10 @@ Helpers: `add`, `remove(itemOrId, byKey = 'id')`, `all()`, `count()`.
453
613
  - `ProtocolClient` is available from `redweb/client` for negotiated protocol routes without adding runtime dependencies.
454
614
  - The minimum supported Node.js version is 18.
455
615
 
616
+ ## Live HTML migration
617
+
618
+ The earlier executable `.htmx` sandbox and `enableHtmxRendering` option have been replaced. Templates are now ordinary `.html` files registered through decorated plain classes. Move template calculations and imports into the page class, mark reactive fields with `@state()`, expose browser-callable methods with `@action()`, and launch the page with `start(PageClass)`.
619
+
456
620
  ## Developing
457
621
 
458
622
  - Run tests with `npm test` (Jest). The suite includes mock-free HTTP, HTTPS, WebSocket, and secure WebSocket integration tests plus unit tests, with 100% coverage enforced for statements, branches, functions, and lines.
@@ -0,0 +1,352 @@
1
+ # Redweb Live HTML
2
+
3
+ Live HTML is Redweb's decorator-first server-rendering layer. It uses the existing `HttpServer`, `SocketRoute`, admission, protocol, ordering, backpressure, and shutdown implementations rather than maintaining a second network stack.
4
+
5
+ ## TSX rendering
6
+
7
+ New pages can return TSX directly. Configure TypeScript with `"jsx": "react-jsx"` and `"jsxImportSource": "redweb"`; Redweb supplies its own dependency-free JSX runtimes and renders immediately to `HtmlFragment` values:
8
+
9
+ ```tsx
10
+ import { LivePage, action, component, page, state } from 'redweb';
11
+ import type { Child } from 'redweb/jsx-runtime';
12
+
13
+ const Panel = component((props: { title: string; children?: Child }) => (
14
+ <section class="panel">
15
+ <h2>{props.title}</h2>
16
+ {props.children}
17
+ </section>
18
+ ));
19
+
20
+ @page('/counter', { css: 'counter.css' })
21
+ class CounterPage extends LivePage {
22
+ @state() count = 0;
23
+
24
+ @action()
25
+ increment() { this.count += 1; }
26
+
27
+ render() {
28
+ return (
29
+ <Panel title="Server counter">
30
+ <button rw-click="increment">
31
+ Count <output data-rw-state="count">{this.count}</output>
32
+ </button>
33
+ </Panel>
34
+ );
35
+ }
36
+ }
37
+ ```
38
+
39
+ Intrinsic elements, fragments (`<>...</>`), nested readonly arrays, and synchronous function components are supported. Strings, numbers, and attributes are escaped once; null, undefined, and boolean children render nothing. Safe existing `html` fragments compose in either direction.
40
+
41
+ JSX intentionally remains a server serializer rather than a React compatibility layer. It retains no tree and provides no hooks, refs, hydration, client event functions, or object-style API. Use `rw-click`, `rw-submit`, `rw-bind`, and the other Redweb directives for server actions, and use `@page({ css })` or external assets for styling and scripts. Unsafe URL protocols, `on*`, dynamic `style`, `srcdoc`, `srcset`, children on void elements, and executable `<script>` or `<style>` children are rejected.
42
+
43
+ This layer deliberately owns page concerns only: `@page`, `@state`, `@view`, and `@action`. It does not clone jax.on's `@get`/`@post` controller API. Continue using Redweb's `services` option for ordinary HTTP APIs; a unified controller decorator surface is a separate compatibility decision rather than hidden behavior in the rendering layer.
44
+
45
+ ## Page model
46
+
47
+ Every page is a plain class registered with `@page(path, options)`. Extending `LivePage` remains compatible but is not required:
48
+
49
+ ```ts
50
+ @page('/profile', { template: 'profile.html', css: 'profile.css' })
51
+ class ProfilePage {
52
+ @state()
53
+ displayName = 'Guest';
54
+ }
55
+ ```
56
+
57
+ The decorators support both TypeScript's current standard decorator emit and the legacy `experimentalDecorators` ABI.
58
+
59
+ Pages use connection scope by default: each rendered browser page receives its own instance. `shared: true` creates one instance shared by every visitor to that page class and is appropriate for intentionally shared state such as a bounded chatroom history. `scope: 'shared'` remains available as the explicit equivalent.
60
+
61
+ `start(PageClass)` creates the Live HTML server. `@page()` captures its source directory when the module is evaluated, so colocated templates and styles work for unexported classes, CommonJS, ESM, and barrel exports without module scanning. Pass `templateRoot` explicitly only when page assets live in a different directory. Template and stylesheet traversal outside that root is rejected.
62
+
63
+ ## Colocated CSS
64
+
65
+ Declare a stylesheet on the same decorator—no Express static middleware or manual `<link>` is required:
66
+
67
+ ```ts
68
+ @page('/profile', { template: 'profile.html', css: 'profile.css' })
69
+ class ProfilePage {}
70
+ ```
71
+
72
+ For composed styles, use `css: ['base.css', 'profile.css']`. Paths resolve from the same captured source directory as the template and cannot traverse outside it. Redweb reads each file once at startup, injects stylesheet links into the server-rendered document, and serves the CSS from a content-addressed URL with the correct content type and immutable caching. Remote URLs and static asset hosting remain under the application's control.
73
+
74
+ ## Declarative HTML templates
75
+
76
+ Template files use the ordinary `.html` extension and contain no executable server code:
77
+
78
+ ```html
79
+ <h1>{{ displayName }}</h1>
80
+ <input rw-bind="displayName">
81
+ ```
82
+
83
+ `{{ property }}` creates an inline text binding. For context-safe container updates, bind an existing element; this is especially useful when a value contains several list or table children:
84
+
85
+ ```html
86
+ <ul data-rw-state="messages"></ul>
87
+ ```
88
+
89
+ During SSR Redweb fills the bound element with the current property value, and subsequent assignments to a decorated `@state()` property update the same element.
90
+
91
+ Ordinary values are escaped during SSR and applied with `textContent` in the browser. The `html` tagged template returns an explicit `HtmlFragment`; its interpolations are escaped, while the resulting fragment may be applied as HTML.
92
+
93
+ ## Rendering collections
94
+
95
+ Keep collection data as an ordinary array and decorate the method that renders one item:
96
+
97
+ ```ts
98
+ @state()
99
+ cards = [{ title: 'Sword' }, { title: 'Shield' }];
100
+
101
+ @view('cards')
102
+ card(item: { title: string }) {
103
+ return html`<article class="card"><h2>${item.title}</h2></article>`;
104
+ }
105
+ ```
106
+
107
+ Place the collection in the template with `<section rw-each="cards"></section>`. Redweb server-renders every item, escapes interpolated values, and replaces the collection contents when the array is reassigned. View methods are synchronous and must return an `HtmlFragment`. Arrays of fragments also compose naturally inside `html`, such as ``html`<div>${items.map(renderItem)}</div>` ``.
108
+
109
+ For a small, auditable safety model, primitive values may be interpolated into quoted attributes. URL-bearing attributes additionally pass through Redweb's safe-URL policy. The explicit `attribute()` and `url()` wrappers remain supported when they improve intent. Interpolation in event handlers, inline styles, `srcdoc`, `srcset`, `<script>`, and `<style>` remains prohibited.
110
+
111
+ ## Reusable components
112
+
113
+ For stateless snippets, pass a render function directly to `component()`:
114
+
115
+ ```ts
116
+ const Badge = component((properties: { label: string }) =>
117
+ html`<strong class="badge">${properties.label}</strong>`
118
+ );
119
+ ```
120
+
121
+ Function components are synchronous and must return `html`. Use a decorated class when a component needs state, actions, or lifecycle hooks.
122
+
123
+ Decorate a plain class with `@component()` to give a reusable HTML snippet its own server state, actions, and lifecycle. Store component instances in page fields and interpolate them like any other safe HTML fragment:
124
+
125
+ ```ts
126
+ import { action, component, html, page, start, state } from 'redweb';
127
+
128
+ @component()
129
+ class Counter {
130
+ @state()
131
+ count = 0;
132
+
133
+ constructor(private readonly label: string) {}
134
+
135
+ @action()
136
+ increment() {
137
+ this.count += 1;
138
+ }
139
+
140
+ render() {
141
+ return html`
142
+ <article>
143
+ <h2>${this.label}</h2>
144
+ <output data-rw-state="count">${this.count}</output>
145
+ <button rw-click="increment">Increment</button>
146
+ </article>
147
+ `;
148
+ }
149
+ }
150
+
151
+ @page('/')
152
+ class Dashboard {
153
+ primary = new Counter('Primary');
154
+ secondary = new Counter('Independent');
155
+
156
+ render() {
157
+ return html`<main>${this.primary}${this.secondary}</main>`;
158
+ }
159
+ }
160
+
161
+ start(Dashboard);
162
+ ```
163
+
164
+ The field path is the component's public protocol namespace, so both counters can expose `count` and `increment` without collisions. Browser events carry that visible namespace and the server resolves it through its component registry; client-supplied object paths are never evaluated. It is routing metadata, not an authorization boundary—component actions must enforce the same application authorization as page actions. Components may contain other decorated components, and state updates retain the complete nested namespace.
165
+
166
+ Component instances are owned by exactly one construction-time page or component field. Their synchronous `render(context)` method may return a safe `HtmlFragment` or a declarative template string; request context is propagated per render, including on concurrent shared pages. Components receive the same `loading`, `connected`, `disconnected`, and `disposed` hooks as their page, including the authenticated principal and cancellation signal where applicable. Disposal starts every child and owner cleanup together and preserves every settled failure, so one broken sibling cannot starve later hooks.
167
+
168
+ Redweb scopes only elements that carry a state, binding, or action directive; it does not add layout wrappers or inline styles. Components therefore remain valid in restricted contexts such as tables and selects and work with strict `style-src` policies.
169
+
170
+ ### Safe attributes and links
171
+
172
+ Dynamic document navigation remains explicit:
173
+
174
+ ```ts
175
+ import { attribute, html, url } from 'redweb';
176
+
177
+ const section = { id: 'socket-server', name: 'SocketServer' };
178
+ const markup = html`
179
+ <article id="${attribute(section.id)}">
180
+ <a href="${url(`#${section.id}`)}">${section.name}</a>
181
+ </article>
182
+ `;
183
+ ```
184
+
185
+ `attribute()` accepts primitive values and is valid only inside a quoted non-URL attribute. `url()` explicitly brands URL-bearing attributes such as `href`, `src`, and `action`; direct string values receive the same validation. Redweb permits relative URLs plus HTTP, HTTPS, mail, and telephone URLs, while rejecting control characters, protocol-relative URLs, and executable schemes. Both wrappers are escaped when rendered and are rejected in element text.
186
+
187
+ ### Nested components and code
188
+
189
+ Plain functions returning `html` fragments are reusable server components. `each()` validates and joins arrays of those fragments, including nested lists:
190
+
191
+ ```ts
192
+ import { codeBlock, each, html } from 'redweb';
193
+
194
+ const method = (entry: Method) => html`
195
+ <section>
196
+ <h3>${entry.name}</h3>
197
+ <p>${entry.description}</p>
198
+ ${codeBlock(entry.usage, { language: 'ts', label: 'TypeScript' })}
199
+ </section>
200
+ `;
201
+
202
+ const reference = each(apiSections, section => html`
203
+ <article>
204
+ <h2>${section.name}</h2>
205
+ ${each(section.methods, method)}
206
+ </article>
207
+ `);
208
+ ```
209
+
210
+ `codeBlock()` escapes strings by default. It may also receive an explicit `HtmlFragment`, or a `highlight(source, language)` callback that returns one, allowing a server-side highlighter to compose safe token spans without accepting arbitrary HTML strings.
211
+
212
+ An `HtmlFragment` returned from `render()` is already fully composed and is never reparsed for `{{ bindings }}` or directives. This keeps code samples literal and prevents escaped documentation text from becoming executable template syntax. Return a string or use `template` when Redweb should process declarative bindings.
213
+
214
+ State observation is deliberately shallow. Assigning a new value publishes an update; mutating a nested object or array in place does not. Reassign after nested changes:
215
+
216
+ ```ts
217
+ this.players = [...this.players, player];
218
+ ```
219
+
220
+ ## Browser actions and input
221
+
222
+ Only methods decorated with `@action()` may be invoked by the browser:
223
+
224
+ ```ts
225
+ @action()
226
+ save(form: { displayName: string }) {
227
+ this.displayName = form.displayName;
228
+ }
229
+ ```
230
+
231
+ ```html
232
+ <form rw-submit="save">
233
+ <input name="displayName">
234
+ <button>Save</button>
235
+ </form>
236
+ ```
237
+
238
+ `rw-click="action"` prevents default navigation and invokes an action without arguments. `rw-submit="action"` prevents submission, passes form fields as the first argument, preserves duplicate field names as arrays, and resets only after the server acknowledges success. `rw-bind="property"` sends text values or checkbox state only when that property was declared with `@state({ writable: true })`.
239
+
240
+ When an HTML-valued component state renders new actions or bindings, Redweb automatically scopes those directives back to that component. A component can therefore replace a join form with a composer—or swap any other interactive view—without manual component IDs or browser glue.
241
+
242
+ The document emits `redweb:connection` events as transport state changes and `redweb:error` events when an interaction fails. A bounded queue covers interaction during initial connection; actions are request/response operations and are not replayed during reconnect.
243
+
244
+ Names such as `constructor`, `prototype`, and `__proto__` are rejected. Arbitrary methods and undeclared state cannot be reached through the Live HTML protocol.
245
+
246
+ ## Lifecycle
247
+
248
+ Pages can implement these optional hooks:
249
+
250
+ - `loading(context)` runs before SSR and receives the portable page request, params, query, body, and shutdown `signal`.
251
+ - `connected(context)` runs after the page's authenticated socket connects and receives the socket and cancellation signal.
252
+ - `disconnected(context)` runs when that socket closes and may be asynchronous.
253
+ - `disposed()` runs once when a connection-scoped page expires or the server shuts down and may be asynchronous.
254
+
255
+ Timers and subscriptions created by a page should be owned by that page and stopped in `disconnected()` or `disposed()`. `dispose()` is idempotent.
256
+
257
+ Shutdown aborts the render signal and waits up to `shutdownTimeoutMs` (one second by default) for active `loading()` and `render()` hooks. If a hook ignores cancellation, Redweb disposes its page, force-closes the affected HTTP connection, completes the remaining cleanup phases, and then reports the timeout.
258
+
259
+ HTTP rendering produces an unpredictable page ID. The browser presents it during a same-origin, versioned WebSocket upgrade. Pending and disconnected sessions expire, the registry is bounded by `maxSessions`, and a page ID cannot own two active sockets simultaneously.
260
+
261
+ For authenticated pages, provide `authenticate(request)`. It runs for both the HTTP render and WebSocket upgrade and must return the same stable primitive identity (commonly a user ID) for both requests. A missing, rejected, changed, or object identity is denied, preventing a copied page token from crossing authentication boundaries. The identity is available as `context.principal` in page hooks and actions.
262
+
263
+ ## Browser transport
264
+
265
+ The injected module uses the published `redweb-client` package served by the same Redweb listener. It derives `ws:` or `wss:` from the current page, negotiates protocol version `1`, uses one socket per page, delegates DOM events at the document level, and opts into bounded reconnection attempts. Every initial connection and reconnect receives an authoritative state snapshot. Supplying the normal `ssl` option runs both the page and socket over HTTPS/WSS.
266
+
267
+ ## Options
268
+
269
+ `start(PageClass, options)` accepts normal HTTP options plus the following Live HTML controls. `new LiveHtmlServer({ pages, ...options })` remains available for explicit composition:
270
+
271
+ - `pages`: non-empty array of decorated class constructors when using `LiveHtmlServer` directly.
272
+ - `templateRoot`: optional root for all `.html` templates and CSS files; when omitted, each page uses the source directory captured by its `@page()` decorator.
273
+ - `livePaths.css`: optional internal URL prefix for generated stylesheet routes; defaults to `/__redweb/css`.
274
+ - `sessionTtlMs`: pending/reconnect session lifetime; defaults to 30 seconds.
275
+ - `maxSessions`: maximum pending plus active page sessions; defaults to 1,000.
276
+ - `maxConcurrentRenders`: maximum simultaneous HTTP page renders, independent of live session occupancy; defaults to `maxSessions`.
277
+ - `shutdownTimeoutMs`: maximum render/route drain time before forced cleanup; defaults to one second.
278
+ - `heartbeat`: optional `{ intervalMs, timeoutMs }` WebSocket liveness policy. Live HTML defaults to a 15-second ping interval and 10-second pong timeout so half-open browsers are disconnected and component `disconnected()` hooks update presence promptly.
279
+ - `authenticate`: optional HTTP/WebSocket identity function for binding page sessions to an authenticated principal.
280
+ - `origins`: optional exact origin list or predicate for deployments behind a trusted proxy. Without it, Redweb requires a scheme-and-host match (`http`/WS or `https`/WSS).
281
+ - `livePaths`: optional `{ socket, client, runtime }` internal path overrides.
282
+
283
+ The internal paths and application page paths must be unique.
284
+
285
+ ## Verification examples
286
+
287
+ - `examples/live-html/counter.ts` uses `@page()`, colocated CSS, and `@state()` to prove a connection-owned server timer can update browser state and is stopped on disconnect.
288
+ - `examples/live-html/chatroom.ts` uses a connection-scoped `@component()` backed by a room service created by `createChatroomPage()`, so separate server instances cannot leak history or names. Visitors join once, receive a stable dedicated composer, see a capped presence list with the total online count, share bounded history, and recover their identity and missed messages after reconnect.
289
+ - `examples/live-html/cards.ts` uses a shared decorated page, `@view()`, and `rw-each` to prove server-rendered collection SSR, realtime replacement, and persistence across reloads and reconnects while the server is running.
290
+ - `examples/live-html/components.ts` uses two instances of one `@component()` class to prove reusable markup, isolated server state, scoped actions, and component CSS composition.
291
+ - `examples/live-html/jsx-page.tsx` uses Redweb's automatic JSX runtime, a function component, decorated state, and a server action without HTML template strings.
292
+
293
+ Run the examples immediately with `npm run example:counter`, `npm run example:chatroom`, `npm run example:cards`, `npm run example:components`, and `npm run example:jsx`. Their checked-in JavaScript artifacts are generated from the decorated TypeScript or TSX sources, and every test and package build rejects stale output. The artifacts are launched unchanged by `tests/integration/live-html.integration.test.js` over real loopback HTTP and WebSocket connections. Run the focused gate with `npm run verify:live-html`, or the complete 100% coverage suite with `npm test`.
294
+
295
+ ## Static pages and documentation export
296
+
297
+ Set `live: false` when a page needs server rendering but no realtime session:
298
+
299
+ ```ts
300
+ import { exportStatic, page } from 'redweb';
301
+
302
+ @page('/docs', {
303
+ template: 'docs.html',
304
+ css: ['base.css', 'docs.css'],
305
+ live: false,
306
+ head: {
307
+ title: 'Redweb API reference',
308
+ description: 'HTTP, WebSocket, multiplayer, and Live HTML APIs.',
309
+ canonical: 'https://example.com/docs',
310
+ image: 'https://example.com/og.png',
311
+ robots: 'index,follow',
312
+ },
313
+ cache: { maxAge: 300, staleWhileRevalidate: 3600 },
314
+ })
315
+ class DocsPage {}
316
+
317
+ await exportStatic(DocsPage, { outDir: 'dist' });
318
+ ```
319
+
320
+ Non-live pages contain no page token or browser runtime. When served by `start()`, Redweb skips its WebSocket route, emits an ETag, honors `If-None-Match`, and applies the declared public cache policy. Interactive pages are always sent with `private, no-store`.
321
+
322
+ `exportStatic()` accepts one decorated class or an array. It requires `live: false`, maps `/` to `index.html` and `/docs` to `docs/index.html`, emits content-addressed CSS beside the pages, and returns frozen lists of written files. It never deletes or cleans the output directory.
323
+
324
+ For several pages, define shared defaults once:
325
+
326
+ ```ts
327
+ import { defineSite, html } from 'redweb';
328
+
329
+ const docs = defineSite({
330
+ origin: 'https://example.com',
331
+ css: 'site.css',
332
+ head: { description: 'Redweb documentation' },
333
+ cache: { maxAge: 300 },
334
+ layout: (content, context) => html`
335
+ <body data-path="${context.request.path}">
336
+ <nav>Redweb</nav>
337
+ <main>${content}</main>
338
+ </body>
339
+ `,
340
+ });
341
+
342
+ @docs.page('/docs', { head: { title: 'Documentation' } })
343
+ class DocsPage {
344
+ render() { return html`<h1>Documentation</h1>`; }
345
+ }
346
+
347
+ await docs.export(DocsPage, { outDir: 'dist', publicDir: 'public' });
348
+ ```
349
+
350
+ `defineSite()` creates runtime-free page decorators, merges and deduplicates shared CSS, inherits head/cache/layout defaults, and derives canonical URLs from `origin`. Shared and page-local styles resolve from the modules that declare them. Layouts receive a trusted page fragment plus the normal render context, run synchronously, and must return `html`. `site.export()` stages a validated, link-free `publicDir` with the rendered pages before touching the destination, rejects case-insensitive public/generated path collisions, never cleans existing output, and includes copied files in its returned `assets` list.
351
+
352
+ The request exposed to `loading()` and `render()` is deliberately the portable `LivePageRequest` surface: `path`, `url`, `method`, `headers`, `params`, `query`, `body`, and `get(name)`. HTTP rendering supplies these from Express; static export supplies deterministic empty headers, parameters, query, and body values. Framework-specific Express request methods are not part of the page contract.
@@ -0,0 +1,36 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family: system-ui, sans-serif;
4
+ background: #111827;
5
+ color: #f9fafb;
6
+ }
7
+
8
+ main {
9
+ width: min(64rem, calc(100% - 2rem));
10
+ margin: 3rem auto;
11
+ }
12
+
13
+ .card-grid {
14
+ display: grid;
15
+ grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
16
+ gap: 1rem;
17
+ }
18
+
19
+ .card {
20
+ padding: 1rem;
21
+ border: 1px solid #374151;
22
+ border-radius: .75rem;
23
+ background: #1f2937;
24
+ }
25
+
26
+ button {
27
+ margin-top: 1rem;
28
+ padding: .7rem 1rem;
29
+ border: 0;
30
+ border-radius: .4rem;
31
+ background: #22d3ee;
32
+ color: #083344;
33
+ font: inherit;
34
+ font-weight: 700;
35
+ cursor: pointer;
36
+ }