@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 +108 -2
- package/dist/index.js +75 -0
- package/package.json +1 -1
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
|
-
*
|
|
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
|
};
|