@microsoft/webui 0.0.18 → 0.0.20

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 (39) hide show
  1. package/README.md +136 -16
  2. package/bin/webui +0 -0
  3. package/dist/index.d.ts +113 -30
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +135 -75
  6. package/dist/index.js.map +1 -1
  7. package/dist/projection/adapters/esbuild.d.ts +14 -0
  8. package/dist/projection/adapters/esbuild.d.ts.map +1 -0
  9. package/dist/projection/adapters/esbuild.js +501 -0
  10. package/dist/projection/adapters/esbuild.js.map +1 -0
  11. package/dist/projection/compiler.d.ts +12 -0
  12. package/dist/projection/compiler.d.ts.map +1 -0
  13. package/dist/projection/compiler.js +1216 -0
  14. package/dist/projection/compiler.js.map +1 -0
  15. package/dist/projection/diagnostics.d.ts +153 -0
  16. package/dist/projection/diagnostics.d.ts.map +1 -0
  17. package/dist/projection/diagnostics.js +210 -0
  18. package/dist/projection/diagnostics.js.map +1 -0
  19. package/dist/projection/fixtures/conformance.d.ts +40 -0
  20. package/dist/projection/fixtures/conformance.d.ts.map +1 -0
  21. package/dist/projection/fixtures/conformance.js +602 -0
  22. package/dist/projection/fixtures/conformance.js.map +1 -0
  23. package/dist/projection/graph.d.ts +158 -0
  24. package/dist/projection/graph.d.ts.map +1 -0
  25. package/dist/projection/graph.js +4 -0
  26. package/dist/projection/graph.js.map +1 -0
  27. package/dist/projection/index.d.ts +37 -0
  28. package/dist/projection/index.d.ts.map +1 -0
  29. package/dist/projection/index.js +17 -0
  30. package/dist/projection/index.js.map +1 -0
  31. package/dist/projection/loader.d.ts +10 -0
  32. package/dist/projection/loader.d.ts.map +1 -0
  33. package/dist/projection/loader.js +45 -0
  34. package/dist/projection/loader.js.map +1 -0
  35. package/dist/projection/manifest.d.ts +119 -0
  36. package/dist/projection/manifest.d.ts.map +1 -0
  37. package/dist/projection/manifest.js +396 -0
  38. package/dist/projection/manifest.js.map +1 -0
  39. package/package.json +31 -9
package/README.md CHANGED
@@ -15,14 +15,15 @@ The package automatically installs the correct platform-specific native binary f
15
15
  ## Quick start
16
16
 
17
17
  ```js
18
- import { build, render } from "@microsoft/webui";
18
+ import { build, Protocol } from "@microsoft/webui";
19
19
 
20
20
  // Build templates into a protocol
21
21
  const result = build({ appDir: "./src" });
22
22
 
23
- // Render with state data
24
- const html = render(result.protocol, { name: "World", items: ["a", "b"] });
25
- console.log(html);
23
+ // Decode and index once, then render repeatedly
24
+ const protocol = new Protocol(result.protocol, { plugin: "webui" });
25
+ const html = protocol.render({ name: "World", items: ["a", "b"] });
26
+ console.log(html.toString("utf8"));
26
27
  ```
27
28
 
28
29
  ## API
@@ -58,32 +59,151 @@ usage with a literal fallback (e.g. `var(--brand, #000)`) is exempt. Missing
58
59
  required tokens fail the build with a structured `missing-theme-token` error.
59
60
  Misspelled literal-fallback tokens are returned as non-fatal warnings.
60
61
 
61
- ### `render(protocol: Buffer, state: object | string): string`
62
+ ### Optional state projection
62
63
 
63
- Renders a compiled protocol with state data and returns the full HTML string.
64
+ The build-only `@microsoft/webui/projection.js` subpath exposes the
65
+ bundler-neutral projection compiler and the supported esbuild adapter. esbuild
66
+ and TypeScript are optional peer dependencies, so applications that do not use
67
+ projection do not install or load them:
68
+
69
+ ```bash
70
+ npm install -D esbuild typescript
71
+ ```
64
72
 
65
73
  ```js
66
- const html = render(protocol, { title: "Hello", show: true });
74
+ import * as esbuild from "esbuild";
75
+ import { esbuildProjection } from "@microsoft/webui/projection.js";
76
+
77
+ await esbuild.build({
78
+ entryPoints: ["src/index.ts"],
79
+ outdir: "dist",
80
+ bundle: true,
81
+ splitting: true,
82
+ format: "esm",
83
+ plugins: [esbuildProjection()],
84
+ });
85
+
86
+ const result = build({
87
+ appDir: "./src",
88
+ plugin: "webui",
89
+ projectionManifests: ["./dist/webui-projection.json"],
90
+ });
67
91
  ```
68
92
 
69
- ### `renderStream(protocol: Buffer, state: object | string, onChunk: (html: string) => void): void`
93
+ esbuild runs once and emits both browser chunks and
94
+ `webui-projection.json`; WebUI then embeds the exact initial/navigation
95
+ surfaces into `protocol.bin`. The adapter uses esbuild's resolved graph and
96
+ emitted output membership, so code splitting, dynamic imports, output hashes,
97
+ and external bundles remain application-owned.
70
98
 
71
- Renders with streaming output - each HTML fragment is passed to the callback as it is produced.
99
+ Other bundler adapters can use the exported `AdapterContext`,
100
+ `compileProjection()`, and conformance fixtures without importing esbuild. The
101
+ package currently ships and supports `esbuildProjection()` as its official
102
+ adapter.
103
+
104
+ With no manifest, WebUI performs no JavaScript analysis and preserves full
105
+ state. Once any manifest is supplied, coverage is strict: every scripted
106
+ component compiled into the protocol must have exactly one entry. Shared
107
+ controls built as external bundles should emit their own manifest fragment,
108
+ then all fragments should be passed through `projectionManifests`.
109
+
110
+ Manifest keys are exact JavaScript `@observable` and `@attr` property names.
111
+ During hydration, an existing SSR host attribute wins over projected `@attr`
112
+ state. Runtime hosts never load TypeScript, esbuild, or the manifest.
113
+
114
+ ### `new Protocol(protocol: Buffer, options?: ProtocolOptions)`
115
+
116
+ Decodes and indexes a compiled protocol once. Keep this object for the server
117
+ lifetime and use it for all runtime operations.
72
118
 
73
119
  ```js
74
- renderStream(protocol, state, (chunk) => {
120
+ const protocol = new Protocol(protocolBytes, { plugin: "webui" });
121
+ ```
122
+
123
+ `Protocol` owns its decoded native state. The package does not keep a hidden
124
+ `WeakMap`, copy the source `Buffer`, or expose render functions that accept
125
+ protocol bytes on every request.
126
+
127
+ ### `protocol.render(state: object | string, options?: RenderOptions): Buffer`
128
+
129
+ Renders state into a UTF-8 Node.js `Buffer`, the canonical buffered result for
130
+ direct HTTP, file, or socket writes:
131
+
132
+ ```js
133
+ response.end(protocol.render({ title: "Hello" }));
134
+ ```
135
+
136
+ Call `.toString("utf8")` explicitly when JavaScript string operations are
137
+ required.
138
+
139
+ ### `protocol.renderStream(state, onChunk, options?): void`
140
+
141
+ Renders with streaming output. Internal handler writes are coalesced around a
142
+ 16 KiB target before the callback crosses into JavaScript. The callback runs
143
+ synchronously and its return value is ignored. In particular,
144
+ `response.write()` returning `false` does not pause native rendering, so this
145
+ API does not provide transport backpressure. A thrown callback error aborts
146
+ rendering immediately and is rethrown to the caller.
147
+
148
+ ```js
149
+ protocol.renderStream(state, (chunk) => {
75
150
  response.write(chunk);
76
151
  });
77
152
  ```
78
153
 
79
- ### `buildAndRender(options: BuildOptions, state: object | string): string`
154
+ ### `protocol.streamResponse(options?): StreamingSession`
80
155
 
81
- Convenience function that builds and renders in a single call.
156
+ Opens a progressive streaming session for an entry that declares `<boundary>`
157
+ directives. Unlike `renderStream`, the session **returns** each chunk, so your
158
+ server keeps the socket, the write order, and the backpressure contract.
82
159
 
83
160
  ```js
84
- const html = buildAndRender({ appDir: "./src" }, { name: "WebUI" });
161
+ import { once } from 'node:events';
162
+
163
+ const session = protocol.streamResponse({ entry: 'index.html', requestPath: '/' });
164
+ const status = session.boundary('job-status'); // resolve names once
165
+
166
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
167
+ await write(res, session.writeShell(baseState));
168
+ await write(res, session.writeBoundary(status, statusState, 'updatable'));
169
+
170
+ // Patches an already-hydrated island on this same response.
171
+ await write(res, session.update(status, { jobState: 'succeeded' }));
172
+ res.end(session.finish({}));
173
+
174
+ async function write(res, chunk) {
175
+ if (res.write(chunk)) return;
176
+ // An aborted client never emits 'drain', and surfaces as 'close', not
177
+ // 'error' — so waiting on 'drain' alone would hang forever.
178
+ await new Promise((ok, fail) => {
179
+ const done = (error) => {
180
+ res.off('drain', onDrain);
181
+ res.off('close', onClose);
182
+ if (error) fail(error);
183
+ else ok();
184
+ };
185
+ const onDrain = () => done();
186
+ const onClose = () => done(new Error('client disconnected'));
187
+ res.once('drain', onDrain);
188
+ res.once('close', onClose);
189
+ });
190
+ }
85
191
  ```
86
192
 
193
+ | Member | Returns | Description |
194
+ |--------|---------|-------------|
195
+ | `boundary(name)` | `number` | Integer handle for an authored boundary name |
196
+ | `boundaryCount` | `number` | Boundaries declared by the entry |
197
+ | `finished` | `boolean` | Whether `finish()` has been called |
198
+ | `writeShell(state)` | `Buffer` | Document prefix through the first semantic flush |
199
+ | `writeBoundary(id, state, mode?)` | `Buffer` | One boundary's markup and checkpoint (`'final'` \| `'updatable'`) |
200
+ | `update(id, state)` | `Buffer` | Projected state patch to an updatable boundary |
201
+ | `finish(state)` | `Buffer` | Tail checkpoint, terminal record, and document suffix |
202
+
203
+ Ordering is enforced. A rejected call throws and leaves the session usable, so
204
+ invalid state does not cost you the response. Sessions are independent — hold
205
+ one per in-flight request.
206
+
87
207
  ### `inspect(protocol: Buffer): string`
88
208
 
89
209
  Returns a JSON representation of the protocol for debugging.
@@ -93,16 +213,16 @@ const json = inspect(protocol);
93
213
  console.log(JSON.parse(json));
94
214
  ```
95
215
 
96
- ### `renderPartial(protocol: Buffer, stateJson: string, entryId: string, requestPath: string, inventoryHex: string): string`
216
+ ### `protocol.renderPartial(state, entryId, requestPath, inventoryHex): string`
97
217
 
98
218
  Produces a JSON partial response for client-side navigation, including state, template metadata, condition closures, and route chain.
99
219
 
100
- ### `renderComponentTemplates(protocol: Buffer, componentTags: string[], inventoryHex: string): string`
220
+ ### `protocol.renderComponentTemplates(componentTags, inventoryHex): string`
101
221
 
102
222
  Renders templates and styles for on-demand component loading (used by `Router.ensureLoaded()`). Returns a JSON string with `templateStyles`, `templates`, `templateFunctions`, and `inventory`. Uses the same inventory bitfield as partial navigation to avoid sending duplicates.
103
223
 
104
224
  ```js
105
- const json = renderComponentTemplates(protocol, ["settings-dialog"], inventoryHex);
225
+ const json = protocol.renderComponentTemplates(["settings-dialog"], inventoryHex);
106
226
  const { templates, templateFunctions, templateStyles, inventory } = JSON.parse(json);
107
227
  ```
108
228
 
package/bin/webui CHANGED
Binary file
package/dist/index.d.ts CHANGED
@@ -20,6 +20,13 @@ export interface BuildOptions {
20
20
  outDir?: string;
21
21
  /** Design token theme: a JSON file path or npm package name. */
22
22
  theme?: string;
23
+ /** Projection manifest file paths, merged in order. */
24
+ projectionManifests?: string[];
25
+ /** Inline manifests with logical paths anchoring root/stale validation. */
26
+ projectionManifestObjects?: Array<{
27
+ path: string;
28
+ manifest: unknown;
29
+ }>;
23
30
  }
24
31
  /** Build statistics. */
25
32
  export interface BuildStats {
@@ -55,9 +62,33 @@ export interface RenderOptions {
55
62
  entry?: string;
56
63
  /** URL path to match routes against (default: "/"). */
57
64
  requestPath?: string;
65
+ }
66
+ /** Options fixed for the lifetime of a loaded protocol. */
67
+ export interface ProtocolOptions {
58
68
  /** Handler plugin name. */
59
69
  plugin?: string;
60
70
  }
71
+ /**
72
+ * Whether a committed boundary may receive later state updates.
73
+ *
74
+ * `final` releases every boundary-local reference once the island hydrates.
75
+ * `updatable` retains the roots and projection so `update()` can patch them,
76
+ * so use it only for boundaries you actually intend to patch.
77
+ */
78
+ export type BoundaryMode = "final" | "updatable";
79
+ /** Per-response settings for a host-driven streaming session. */
80
+ export interface StreamOptions {
81
+ /** Fragment ID to start rendering from (default: "index.html"). */
82
+ entry?: string;
83
+ /** URL path to match routes against (default: "/"). */
84
+ requestPath?: string;
85
+ /** CSP nonce applied to generated inline `<script>` tags. */
86
+ nonce?: string;
87
+ /** HTML injected at the structural `head_end` boundary. */
88
+ headInject?: string;
89
+ /** HTML injected at the structural `body_end` boundary. */
90
+ bodyInject?: string;
91
+ }
61
92
  /** Response from `renderComponentTemplates()` for on-demand component loading. */
62
93
  export interface ComponentTemplatesResponse {
63
94
  /** Module CSS `<style>` strings for the requested components. */
@@ -89,45 +120,97 @@ export interface PartialResponse {
89
120
  exact?: boolean;
90
121
  }>;
91
122
  }
123
+ interface NativeStreamingSession {
124
+ readonly boundaryCount: number;
125
+ readonly finished: boolean;
126
+ boundary(name: string): number;
127
+ writeShell(stateJson: string): Buffer;
128
+ writeBoundary(boundary: number, stateJson: string, mode?: BoundaryMode): Buffer;
129
+ update(boundary: number, stateJson: string): Buffer;
130
+ finish(stateJson: string): Buffer;
131
+ }
92
132
  /** Build a WebUI application from an app directory. */
93
133
  export declare function build(options: BuildOptions): BuildResult;
94
134
  /**
95
- * Render a pre-compiled protocol with state data.
96
- * Uses native addon when available, WASM fallback otherwise.
97
- */
98
- export declare function render(protocol: Buffer, state: object | string, options?: RenderOptions): string;
99
- /**
100
- * Render a protocol with streaming output.
101
- * Each HTML fragment is passed to the onChunk callback as it is produced.
135
+ * A decoded protocol with reusable indices for all runtime operations.
136
+ *
137
+ * Create one instance when the server loads `protocol.bin` and share it
138
+ * across requests. Construction decodes and indexes the protocol once.
102
139
  */
103
- export declare function renderStream(protocol: Buffer, state: object | string, onChunk: (html: string) => void, options?: RenderOptions): void;
104
- /** Build and render in a single call. */
105
- export declare function buildAndRender(options: BuildOptions, state: object | string, renderOpts?: RenderOptions): string;
106
- /** Inspect protocol bytes and return JSON representation. */
107
- export declare function inspect(protocolData: Buffer): string;
140
+ export declare class Protocol {
141
+ #private;
142
+ constructor(protocolData: Buffer, options?: ProtocolOptions);
143
+ /** Render a complete HTML response as a UTF-8 Node.js buffer. */
144
+ render(state: object | string, options?: RenderOptions): Buffer;
145
+ /** Stream a complete HTML response in chunks around 16 KiB. */
146
+ renderStream(state: object | string, onChunk: (html: string) => void, options?: RenderOptions): void;
147
+ /** Produce a complete JSON partial-navigation response. */
148
+ renderPartial(state: object | string, entryId: string, requestPath: string, inventoryHex: string): string;
149
+ /** Render component templates and styles for on-demand loading. */
150
+ renderComponentTemplates(componentTags: string[], inventoryHex: string): string;
151
+ /** Return CSS token names in build order. */
152
+ tokens(): string[];
153
+ /**
154
+ * Open a host-driven progressive response.
155
+ *
156
+ * Unlike {@link renderStream}, which pushes every chunk during one
157
+ * synchronous call, the returned session hands each chunk back so this
158
+ * server owns the socket, the write order, and backpressure.
159
+ */
160
+ streamResponse(options?: StreamOptions): StreamingSession;
161
+ }
108
162
  /**
109
- * Produce a complete JSON partial response for client-side navigation.
163
+ * A progressive HTML response written one chunk at a time.
110
164
  *
111
- * Returns a JSON string with `state`, `templates`, `inventory`, `path`, and `chain`.
112
- * Pipe directly to the HTTP response — no post-processing needed.
165
+ * Every method returns the bytes it produced instead of writing them, so the
166
+ * caller decides when they reach the socket and can await `drain` between
167
+ * chunks. The session holds no transport and never blocks on one.
113
168
  *
114
- * If you need to inspect the response, parse it with the exported `PartialResponse` type:
115
- * ```ts
116
- * const partial: PartialResponse = JSON.parse(renderPartial(...));
117
- * ```
118
- */
119
- export declare function renderPartial(protocolData: Buffer, stateJson: string, entryId: string, requestPath: string, inventoryHex: string): string;
120
- /**
121
- * Render component templates and styles for on-demand loading.
169
+ * Ordering is enforced: the shell first, then each boundary exactly once in
170
+ * declaration order, `update()` only after its boundary commits as
171
+ * `updatable`, and `finish()` last. A violation throws before any byte is
172
+ * produced.
173
+ *
174
+ * ```js
175
+ * const session = protocol.streamResponse({ requestPath: req.url });
176
+ * const weather = session.boundary("weather-shell");
177
+ *
178
+ * res.write(session.writeShell(shellState));
179
+ * res.write(session.writeBoundary(weather, weatherShell, "updatable"));
122
180
  *
123
- * Used by `Router.ensureLoaded()` to fetch templates for components that
124
- * are not part of the route tree (e.g., dialogs, popovers). Uses the same
125
- * inventory bitfield as partial navigation to avoid sending duplicates.
181
+ * const forecast = await forecastReady;
182
+ * if (!res.write(session.update(weather, forecast))) {
183
+ * await once(res, "drain");
184
+ * }
126
185
  *
127
- * Returns a JSON string. Parse with the exported `ComponentTemplatesResponse` type:
128
- * ```ts
129
- * const resp: ComponentTemplatesResponse = JSON.parse(renderComponentTemplates(...));
186
+ * res.end(session.finish({}));
130
187
  * ```
131
188
  */
132
- export declare function renderComponentTemplates(protocolData: Buffer, componentTags: string[], inventoryHex: string): string;
189
+ export declare class StreamingSession {
190
+ #private;
191
+ /** @internal Created by {@link Protocol.streamResponse}. */
192
+ constructor(native: NativeStreamingSession);
193
+ /** Number of compile-time boundaries declared by this entry. */
194
+ get boundaryCount(): number;
195
+ /** Whether the terminal record has been written. */
196
+ get finished(): boolean;
197
+ /**
198
+ * Resolve an authored boundary name to a stable integer handle.
199
+ *
200
+ * Resolve once outside the write loop; reusing the handle costs nothing.
201
+ * An unknown name throws with the valid names and a suggestion.
202
+ */
203
+ boundary(name: string): number;
204
+ /** Render everything before the first boundary. */
205
+ writeShell(state: object | string): Buffer;
206
+ /** Render and commit the next boundary in declaration order. */
207
+ writeBoundary(boundary: number, state: object | string, mode?: BoundaryMode): Buffer;
208
+ /** Push a projected state patch to a committed `updatable` boundary. */
209
+ update(boundary: number, state: object | string): Buffer;
210
+ /** Render the document tail and emit the terminal record. */
211
+ finish(state?: object | string): Buffer;
212
+ }
213
+ /** Inspect protocol bytes and return JSON representation. */
214
+ export declare function inspect(protocolData: Buffer): string;
215
+ export {};
133
216
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -44,7 +44,14 @@ function warnFallback() {
44
44
  export function build(options) {
45
45
  const native = loadAddon();
46
46
  if (native?.build) {
47
- return native.build(options);
47
+ const { projectionManifestObjects, ...nativeOptions } = options;
48
+ return native.build({
49
+ ...nativeOptions,
50
+ projectionManifestObjects: projectionManifestObjects?.map(({ path, manifest }) => ({
51
+ path,
52
+ json: JSON.stringify(manifest),
53
+ })),
54
+ });
48
55
  }
49
56
  // Fallback: shell out to CLI binary.
50
57
  const binPath = resolve("bin");
@@ -63,6 +70,15 @@ export function build(options) {
63
70
  args.push("--components", c);
64
71
  }
65
72
  }
73
+ if (options.projectionManifests) {
74
+ for (const manifest of options.projectionManifests) {
75
+ args.push("--projection-manifest", manifest);
76
+ }
77
+ }
78
+ if (options.projectionManifestObjects &&
79
+ options.projectionManifestObjects.length > 0) {
80
+ throw new Error("[webui] Inline projection manifest objects require the native addon; write the manifest and pass projectionManifests when using the CLI fallback.");
81
+ }
66
82
  if (options.componentAssetRoots && options.componentAssetRoots.length > 0) {
67
83
  args.push("--emit-component-assets", options.componentAssetRoots.join(","));
68
84
  }
@@ -112,50 +128,131 @@ function readComponentAssetFiles(outDir) {
112
128
  }
113
129
  return files;
114
130
  }
115
- // ── Render API ───────────────────────────────────────────────────────
131
+ // ── Runtime protocol API ─────────────────────────────────────────────
116
132
  /**
117
- * Render a pre-compiled protocol with state data.
118
- * Uses native addon when available, WASM fallback otherwise.
133
+ * A decoded protocol with reusable indices for all runtime operations.
134
+ *
135
+ * Create one instance when the server loads `protocol.bin` and share it
136
+ * across requests. Construction decodes and indexes the protocol once.
119
137
  */
120
- export function render(protocol, state, options) {
121
- const native = loadAddon();
122
- if (native) {
123
- let result = "";
124
- const stateStr = typeof state === "string" ? state : JSON.stringify(state);
125
- const entry = options?.entry ?? "index.html";
126
- const requestPath = options?.requestPath ?? "/";
127
- native.render(protocol, stateStr, entry, requestPath, (chunk) => {
128
- result += chunk;
129
- }, options?.plugin);
130
- return result;
131
- }
132
- warnFallback();
133
- throw new Error("[webui] render() requires the native addon. WASM render fallback not yet wired.");
138
+ export class Protocol {
139
+ #native;
140
+ constructor(protocolData, options) {
141
+ const native = loadAddon();
142
+ const NativeProtocol = native?.Protocol;
143
+ if (!NativeProtocol) {
144
+ warnFallback();
145
+ throw new Error("[webui] Native addon is incompatible: Protocol is required.");
146
+ }
147
+ this.#native = new NativeProtocol(protocolData, options?.plugin);
148
+ }
149
+ /** Render a complete HTML response as a UTF-8 Node.js buffer. */
150
+ render(state, options) {
151
+ const stateJson = typeof state === "string" ? state : JSON.stringify(state);
152
+ return this.#native.render(stateJson, options?.entry ?? "index.html", options?.requestPath ?? "/");
153
+ }
154
+ /** Stream a complete HTML response in chunks around 16 KiB. */
155
+ renderStream(state, onChunk, options) {
156
+ const stateJson = typeof state === "string" ? state : JSON.stringify(state);
157
+ this.#native.renderStream(stateJson, options?.entry ?? "index.html", options?.requestPath ?? "/", onChunk);
158
+ }
159
+ /** Produce a complete JSON partial-navigation response. */
160
+ renderPartial(state, entryId, requestPath, inventoryHex) {
161
+ const stateJson = typeof state === "string" ? state : JSON.stringify(state);
162
+ return this.#native.renderPartial(stateJson, entryId, requestPath, inventoryHex);
163
+ }
164
+ /** Render component templates and styles for on-demand loading. */
165
+ renderComponentTemplates(componentTags, inventoryHex) {
166
+ return this.#native.renderComponentTemplates(componentTags, inventoryHex);
167
+ }
168
+ /** Return CSS token names in build order. */
169
+ tokens() {
170
+ return this.#native.tokens();
171
+ }
172
+ /**
173
+ * Open a host-driven progressive response.
174
+ *
175
+ * Unlike {@link renderStream}, which pushes every chunk during one
176
+ * synchronous call, the returned session hands each chunk back so this
177
+ * server owns the socket, the write order, and backpressure.
178
+ */
179
+ streamResponse(options) {
180
+ return new StreamingSession(this.#native.streamResponse(options?.entry ?? "index.html", options?.requestPath ?? "/", {
181
+ nonce: options?.nonce,
182
+ headInject: options?.headInject,
183
+ bodyInject: options?.bodyInject,
184
+ }));
185
+ }
134
186
  }
135
187
  /**
136
- * Render a protocol with streaming output.
137
- * Each HTML fragment is passed to the onChunk callback as it is produced.
188
+ * A progressive HTML response written one chunk at a time.
189
+ *
190
+ * Every method returns the bytes it produced instead of writing them, so the
191
+ * caller decides when they reach the socket and can await `drain` between
192
+ * chunks. The session holds no transport and never blocks on one.
193
+ *
194
+ * Ordering is enforced: the shell first, then each boundary exactly once in
195
+ * declaration order, `update()` only after its boundary commits as
196
+ * `updatable`, and `finish()` last. A violation throws before any byte is
197
+ * produced.
198
+ *
199
+ * ```js
200
+ * const session = protocol.streamResponse({ requestPath: req.url });
201
+ * const weather = session.boundary("weather-shell");
202
+ *
203
+ * res.write(session.writeShell(shellState));
204
+ * res.write(session.writeBoundary(weather, weatherShell, "updatable"));
205
+ *
206
+ * const forecast = await forecastReady;
207
+ * if (!res.write(session.update(weather, forecast))) {
208
+ * await once(res, "drain");
209
+ * }
210
+ *
211
+ * res.end(session.finish({}));
212
+ * ```
138
213
  */
139
- export function renderStream(protocol, state, onChunk, options) {
140
- const native = loadAddon();
141
- if (native) {
142
- const stateStr = typeof state === "string" ? state : JSON.stringify(state);
143
- const entry = options?.entry ?? "index.html";
144
- const requestPath = options?.requestPath ?? "/";
145
- native.render(protocol, stateStr, entry, requestPath, onChunk, options?.plugin);
146
- return;
214
+ export class StreamingSession {
215
+ #native;
216
+ /** @internal Created by {@link Protocol.streamResponse}. */
217
+ constructor(native) {
218
+ this.#native = native;
219
+ }
220
+ /** Number of compile-time boundaries declared by this entry. */
221
+ get boundaryCount() {
222
+ return this.#native.boundaryCount;
223
+ }
224
+ /** Whether the terminal record has been written. */
225
+ get finished() {
226
+ return this.#native.finished;
227
+ }
228
+ /**
229
+ * Resolve an authored boundary name to a stable integer handle.
230
+ *
231
+ * Resolve once outside the write loop; reusing the handle costs nothing.
232
+ * An unknown name throws with the valid names and a suggestion.
233
+ */
234
+ boundary(name) {
235
+ return this.#native.boundary(name);
236
+ }
237
+ /** Render everything before the first boundary. */
238
+ writeShell(state) {
239
+ return this.#native.writeShell(toStateJson(state));
240
+ }
241
+ /** Render and commit the next boundary in declaration order. */
242
+ writeBoundary(boundary, state, mode = "final") {
243
+ return this.#native.writeBoundary(boundary, toStateJson(state), mode);
244
+ }
245
+ /** Push a projected state patch to a committed `updatable` boundary. */
246
+ update(boundary, state) {
247
+ return this.#native.update(boundary, toStateJson(state));
248
+ }
249
+ /** Render the document tail and emit the terminal record. */
250
+ finish(state = {}) {
251
+ return this.#native.finish(toStateJson(state));
147
252
  }
148
- warnFallback();
149
- throw new Error("[webui] renderStream() requires the native addon. WASM render fallback not yet wired.");
150
253
  }
151
- // ── Convenience ──────────────────────────────────────────────────────
152
- /** Build and render in a single call. */
153
- export function buildAndRender(options, state, renderOpts) {
154
- const result = build(options);
155
- if (!result.protocol || result.protocol.length === 0) {
156
- throw new Error("[webui] Build did not return protocol data.");
157
- }
158
- return render(result.protocol, state, renderOpts);
254
+ function toStateJson(state) {
255
+ return typeof state === "string" ? state : JSON.stringify(state);
159
256
  }
160
257
  /** Inspect protocol bytes and return JSON representation. */
161
258
  export function inspect(protocolData) {
@@ -165,43 +262,6 @@ export function inspect(protocolData) {
165
262
  }
166
263
  throw new Error("[webui] inspect() requires the native addon.");
167
264
  }
168
- /**
169
- * Produce a complete JSON partial response for client-side navigation.
170
- *
171
- * Returns a JSON string with `state`, `templates`, `inventory`, `path`, and `chain`.
172
- * Pipe directly to the HTTP response — no post-processing needed.
173
- *
174
- * If you need to inspect the response, parse it with the exported `PartialResponse` type:
175
- * ```ts
176
- * const partial: PartialResponse = JSON.parse(renderPartial(...));
177
- * ```
178
- */
179
- export function renderPartial(protocolData, stateJson, entryId, requestPath, inventoryHex) {
180
- const native = loadAddon();
181
- if (native?.renderPartial) {
182
- return native.renderPartial(protocolData, stateJson, entryId, requestPath, inventoryHex);
183
- }
184
- throw new Error("[webui] renderPartial() requires the native addon.");
185
- }
186
- /**
187
- * Render component templates and styles for on-demand loading.
188
- *
189
- * Used by `Router.ensureLoaded()` to fetch templates for components that
190
- * are not part of the route tree (e.g., dialogs, popovers). Uses the same
191
- * inventory bitfield as partial navigation to avoid sending duplicates.
192
- *
193
- * Returns a JSON string. Parse with the exported `ComponentTemplatesResponse` type:
194
- * ```ts
195
- * const resp: ComponentTemplatesResponse = JSON.parse(renderComponentTemplates(...));
196
- * ```
197
- */
198
- export function renderComponentTemplates(protocolData, componentTags, inventoryHex) {
199
- const native = loadAddon();
200
- if (native?.renderComponentTemplates) {
201
- return native.renderComponentTemplates(protocolData, JSON.stringify(componentTags), inventoryHex);
202
- }
203
- throw new Error("[webui] renderComponentTemplates() requires the native addon.");
204
- }
205
265
  // ── Helpers ──────────────────────────────────────────────────────────
206
266
  function emptyStats() {
207
267
  return {