@cloudwerk/ui 0.1.1 → 0.4.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.
package/dist/index.d.ts CHANGED
@@ -74,6 +74,44 @@ interface HtmlOptions {
74
74
  */
75
75
  headers?: Record<string, string>;
76
76
  }
77
+ /**
78
+ * Options for streaming render (loading-swap pattern).
79
+ */
80
+ interface StreamRenderOptions {
81
+ /**
82
+ * HTTP status code for the response.
83
+ * @default 200
84
+ */
85
+ status?: number;
86
+ /**
87
+ * Additional response headers.
88
+ * These are merged with the default Content-Type and Transfer-Encoding headers.
89
+ */
90
+ headers?: Record<string, string>;
91
+ }
92
+ /**
93
+ * Options for native progressive streaming render with Suspense boundaries.
94
+ *
95
+ * This is used by renderToStream() which uses Hono's renderToReadableStream
96
+ * for native progressive streaming with Suspense support.
97
+ */
98
+ interface RenderToStreamOptions {
99
+ /**
100
+ * HTTP status code for the response.
101
+ * @default 200
102
+ */
103
+ status?: number;
104
+ /**
105
+ * Additional response headers.
106
+ * These are merged with the default Content-Type header.
107
+ */
108
+ headers?: Record<string, string>;
109
+ /**
110
+ * Whether to include the <!DOCTYPE html> declaration.
111
+ * @default true
112
+ */
113
+ doctype?: boolean;
114
+ }
77
115
  /**
78
116
  * Props for components that receive children.
79
117
  *
@@ -187,9 +225,77 @@ declare function _resetRenderers(): void;
187
225
  * - Automatic doctype handling
188
226
  * - Proper Content-Type headers
189
227
  *
190
- * Note: Streaming support via renderToReadableStream will be added in issue #38.
228
+ * Native progressive streaming is available via renderToStream() using Hono's
229
+ * renderToReadableStream for Suspense boundary support.
191
230
  */
192
231
  declare const honoJsxRenderer: Renderer;
232
+ /**
233
+ * Create a streaming HTML Response that sends loading UI immediately,
234
+ * then streams the final content when the content promise resolves.
235
+ *
236
+ * This uses a chunked transfer encoding to send HTML in two parts:
237
+ * 1. Loading UI (sent immediately)
238
+ * 2. Final content with script to replace loading UI (sent when ready)
239
+ *
240
+ * Note: The innerHTML assignment in the client script is safe because we only
241
+ * use server-rendered content that we control. No user input is directly
242
+ * inserted into the HTML.
243
+ *
244
+ * @param loadingElement - Loading UI to show immediately (JSX element)
245
+ * @param contentPromise - Promise that resolves to final content (JSX element)
246
+ * @param options - Streaming render options
247
+ * @returns Response object with streaming HTML content
248
+ *
249
+ * @example
250
+ * const loadingElement = <Loading params={{}} searchParams={{}} pathname="/dashboard" />
251
+ * const contentPromise = (async () => {
252
+ * const data = await loader()
253
+ * return <Page {...data} />
254
+ * })()
255
+ *
256
+ * return renderStream(loadingElement, contentPromise)
257
+ */
258
+ declare function renderStream(loadingElement: unknown, contentPromise: Promise<unknown>, options?: StreamRenderOptions): Response;
259
+ /**
260
+ * Render a JSX element to a streaming Response using native progressive streaming.
261
+ *
262
+ * This uses Hono's renderToReadableStream for native support of React-style
263
+ * Suspense boundaries. Content inside Suspense components will be progressively
264
+ * streamed as their async content resolves.
265
+ *
266
+ * Unlike renderStream() which uses a loading-swap pattern, this function
267
+ * provides true progressive streaming where:
268
+ * - The initial shell is sent immediately
269
+ * - Suspense fallbacks are shown while async content loads
270
+ * - Async content is streamed in-place as it resolves
271
+ * - No JavaScript is required for the initial render
272
+ *
273
+ * @param element - Hono JSX element to render (may contain Suspense boundaries)
274
+ * @param options - Render options
275
+ * @returns Promise resolving to Response with streaming HTML content
276
+ *
277
+ * @example
278
+ * // In a route handler
279
+ * import { Suspense } from 'hono/jsx/streaming'
280
+ *
281
+ * function Page() {
282
+ * return (
283
+ * <html>
284
+ * <body>
285
+ * <h1>Dashboard</h1>
286
+ * <Suspense fallback={<p>Loading stats...</p>}>
287
+ * <AsyncStats />
288
+ * </Suspense>
289
+ * </body>
290
+ * </html>
291
+ * )
292
+ * }
293
+ *
294
+ * export function GET() {
295
+ * return renderToStream(<Page />)
296
+ * }
297
+ */
298
+ declare function renderToStream(element: unknown, options?: RenderToStreamOptions): Promise<Response>;
193
299
 
194
300
  /**
195
301
  * @cloudwerk/ui
@@ -273,4 +379,4 @@ declare function html(content: string, options?: HtmlOptions): Response;
273
379
  */
274
380
  declare function hydrate(element: unknown, root: Element): void;
275
381
 
276
- export { type HtmlOptions, type PropsWithChildren, type RenderOptions, type Renderer, _resetRenderers, getActiveRenderer, getActiveRendererName, getAvailableRenderers, honoJsxRenderer, html, hydrate, registerRenderer, render, setActiveRenderer };
382
+ export { type HtmlOptions, type PropsWithChildren, type RenderOptions, type RenderToStreamOptions, type Renderer, type StreamRenderOptions, _resetRenderers, getActiveRenderer, getActiveRendererName, getAvailableRenderers, honoJsxRenderer, html, hydrate, registerRenderer, render, renderStream, renderToStream, setActiveRenderer };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/renderers/hono-jsx.ts
2
+ import { renderToReadableStream } from "hono/jsx/streaming";
2
3
  var honoJsxRenderer = {
3
4
  /**
4
5
  * Render a JSX element to an HTML Response.
@@ -58,6 +59,78 @@ var honoJsxRenderer = {
58
59
  );
59
60
  }
60
61
  };
62
+ function renderStream(loadingElement, contentPromise, options = {}) {
63
+ const { status = 200, headers = {} } = options;
64
+ const stream = new ReadableStream({
65
+ async start(controller) {
66
+ const encoder = new TextEncoder();
67
+ try {
68
+ const loadingHtml = String(loadingElement);
69
+ const loadingWrapper = `<!DOCTYPE html><div id="__cloudwerk_loading">${loadingHtml}</div>`;
70
+ controller.enqueue(encoder.encode(loadingWrapper));
71
+ const finalElement = await contentPromise;
72
+ const finalHtml = String(finalElement);
73
+ const replacementScript = `
74
+ <script>
75
+ (function() {
76
+ var loading = document.getElementById('__cloudwerk_loading');
77
+ if (loading) {
78
+ var content = document.getElementById('__cloudwerk_content');
79
+ if (content) {
80
+ document.body.innerHTML = content.innerHTML;
81
+ }
82
+ }
83
+ })();
84
+ </script>
85
+ <div id="__cloudwerk_content" style="display:none">${finalHtml}</div>
86
+ `;
87
+ controller.enqueue(encoder.encode(replacementScript));
88
+ controller.close();
89
+ } catch (error) {
90
+ const errorMessage = error instanceof Error ? error.message : String(error);
91
+ const errorHtml = `<div style="color:red;padding:20px;">Error loading content: ${errorMessage}</div>`;
92
+ controller.enqueue(encoder.encode(errorHtml));
93
+ controller.close();
94
+ }
95
+ }
96
+ });
97
+ return new Response(stream, {
98
+ status,
99
+ headers: {
100
+ "Content-Type": "text/html; charset=utf-8",
101
+ "Transfer-Encoding": "chunked",
102
+ ...headers
103
+ }
104
+ });
105
+ }
106
+ function prependDoctype(stream) {
107
+ const encoder = new TextEncoder();
108
+ const doctypeBytes = encoder.encode("<!DOCTYPE html>");
109
+ let doctypeSent = false;
110
+ return stream.pipeThrough(
111
+ new TransformStream({
112
+ transform(chunk, controller) {
113
+ if (!doctypeSent) {
114
+ controller.enqueue(doctypeBytes);
115
+ doctypeSent = true;
116
+ }
117
+ controller.enqueue(chunk);
118
+ }
119
+ })
120
+ );
121
+ }
122
+ async function renderToStream(element, options = {}) {
123
+ const { status = 200, headers = {}, doctype = true } = options;
124
+ const contentStream = renderToReadableStream(element);
125
+ const stream = doctype ? prependDoctype(contentStream) : contentStream;
126
+ return new Response(stream, {
127
+ status,
128
+ headers: {
129
+ "Content-Type": "text/html; charset=utf-8",
130
+ ...headers
131
+ }
132
+ });
133
+ }
61
134
 
62
135
  // src/renderer.ts
63
136
  var renderers = {
@@ -121,5 +194,7 @@ export {
121
194
  hydrate,
122
195
  registerRenderer,
123
196
  render,
197
+ renderStream,
198
+ renderToStream,
124
199
  setActiveRenderer
125
200
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudwerk/ui",
3
- "version": "0.1.1",
3
+ "version": "0.4.0",
4
4
  "description": "UI rendering abstraction for Cloudwerk",
5
5
  "repository": {
6
6
  "type": "git",