@cloudwerk/ui 0.4.0 → 0.6.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 +153 -2
- package/dist/index.js +307 -10
- package/dist/react-PVIKZSJC.js +104 -0
- package/package.json +21 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { HydrationManifest } from '@cloudwerk/core';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @cloudwerk/ui - Type Definitions
|
|
3
5
|
*
|
|
@@ -131,6 +133,135 @@ interface PropsWithChildren<_P = unknown> {
|
|
|
131
133
|
children?: unknown;
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @cloudwerk/ui - Hydration Utilities
|
|
138
|
+
*
|
|
139
|
+
* Server-side helpers for wrapping client components with hydration metadata
|
|
140
|
+
* and generating the client-side hydration bootstrap script.
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Options for wrapping a component for hydration.
|
|
145
|
+
*/
|
|
146
|
+
interface WrapForHydrationOptions {
|
|
147
|
+
/** Unique component ID */
|
|
148
|
+
componentId: string;
|
|
149
|
+
/** Props to serialize for client-side hydration */
|
|
150
|
+
props: Record<string, unknown>;
|
|
151
|
+
/** Custom wrapper element tag (default: 'div') */
|
|
152
|
+
wrapperTag?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Options for generating hydration script.
|
|
156
|
+
*/
|
|
157
|
+
interface HydrationScriptOptions {
|
|
158
|
+
/** Whether to include source maps in development */
|
|
159
|
+
includeSourceMaps?: boolean;
|
|
160
|
+
/** Custom hydration endpoint path */
|
|
161
|
+
hydrationEndpoint?: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Wrap a server-rendered element with hydration metadata.
|
|
165
|
+
*
|
|
166
|
+
* This wraps the component's HTML output with a container element
|
|
167
|
+
* that includes data attributes for client-side hydration:
|
|
168
|
+
* - `data-hydrate-id`: The component ID for looking up the client bundle
|
|
169
|
+
* - `data-hydrate-props`: JSON-serialized props to pass during hydration
|
|
170
|
+
*
|
|
171
|
+
* @param html - Server-rendered HTML string
|
|
172
|
+
* @param options - Hydration options
|
|
173
|
+
* @returns HTML string with hydration wrapper
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* const html = wrapForHydration('<button>Count: 0</button>', {
|
|
178
|
+
* componentId: 'components_Counter',
|
|
179
|
+
* props: { initialCount: 0 },
|
|
180
|
+
* })
|
|
181
|
+
* // Returns:
|
|
182
|
+
* // <div data-hydrate-id="components_Counter" data-hydrate-props='{"initialCount":0}'>
|
|
183
|
+
* // <button>Count: 0</button>
|
|
184
|
+
* // </div>
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
declare function wrapForHydration(html: string, options: WrapForHydrationOptions): string;
|
|
188
|
+
/**
|
|
189
|
+
* Generate the hydration bootstrap script to include in the HTML response.
|
|
190
|
+
*
|
|
191
|
+
* This script:
|
|
192
|
+
* 1. Finds all elements with `data-hydrate-id` attributes
|
|
193
|
+
* 2. Loads the corresponding client bundles
|
|
194
|
+
* 3. Hydrates each component with its serialized props
|
|
195
|
+
*
|
|
196
|
+
* @param manifest - Hydration manifest with component metadata
|
|
197
|
+
* @param options - Script generation options
|
|
198
|
+
* @returns Script tags for hydration
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const script = generateHydrationScript(manifest, {
|
|
203
|
+
* hydrationEndpoint: '/__cloudwerk',
|
|
204
|
+
* })
|
|
205
|
+
* // Insert at the end of the <body> tag
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
declare function generateHydrationScript(manifest: HydrationManifest, options?: HydrationScriptOptions): string;
|
|
209
|
+
/**
|
|
210
|
+
* Generate script tags for preloading client bundles.
|
|
211
|
+
*
|
|
212
|
+
* This adds modulepreload hints for better performance.
|
|
213
|
+
*
|
|
214
|
+
* @param manifest - Hydration manifest with component metadata
|
|
215
|
+
* @returns Link tags for modulepreload
|
|
216
|
+
*/
|
|
217
|
+
declare function generatePreloadHints(manifest: HydrationManifest): string;
|
|
218
|
+
/**
|
|
219
|
+
* Generate the hydration runtime module for Hono JSX.
|
|
220
|
+
*
|
|
221
|
+
* This is served at `/__cloudwerk/runtime.js` and provides the render
|
|
222
|
+
* function that uses hono/jsx/dom for client-side hydration.
|
|
223
|
+
*
|
|
224
|
+
* The render function uses hono/jsx/dom's built-in virtual DOM diffing
|
|
225
|
+
* to safely update the DOM without raw HTML insertion.
|
|
226
|
+
*
|
|
227
|
+
* @returns JavaScript module source code
|
|
228
|
+
*/
|
|
229
|
+
declare function generateHydrationRuntime(): string;
|
|
230
|
+
/**
|
|
231
|
+
* Generate the React hydration runtime module.
|
|
232
|
+
*
|
|
233
|
+
* This is served at `/__cloudwerk/react-runtime.js` and provides the
|
|
234
|
+
* hydrateRoot function from react-dom/client for client-side hydration.
|
|
235
|
+
*
|
|
236
|
+
* The runtime exports:
|
|
237
|
+
* - hydrateRoot from react-dom/client for hydration
|
|
238
|
+
* - React and all React hooks for client components
|
|
239
|
+
*
|
|
240
|
+
* @returns JavaScript module source code
|
|
241
|
+
*/
|
|
242
|
+
declare function generateReactHydrationRuntime(): string;
|
|
243
|
+
/**
|
|
244
|
+
* Generate the React hydration bootstrap script to include in the HTML response.
|
|
245
|
+
*
|
|
246
|
+
* This script:
|
|
247
|
+
* 1. Finds all elements with `data-hydrate-id` attributes
|
|
248
|
+
* 2. Loads the corresponding client bundles
|
|
249
|
+
* 3. Hydrates each component with React's hydrateRoot
|
|
250
|
+
*
|
|
251
|
+
* @param manifest - Hydration manifest with component metadata
|
|
252
|
+
* @param options - Script generation options
|
|
253
|
+
* @returns Script tags for hydration
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* const script = generateReactHydrationScript(manifest, {
|
|
258
|
+
* hydrationEndpoint: '/__cloudwerk',
|
|
259
|
+
* })
|
|
260
|
+
* // Insert at the end of the <body> tag
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
declare function generateReactHydrationScript(manifest: HydrationManifest, options?: HydrationScriptOptions): string;
|
|
264
|
+
|
|
134
265
|
/**
|
|
135
266
|
* @cloudwerk/ui - Renderer Selection
|
|
136
267
|
*
|
|
@@ -156,12 +287,32 @@ declare function getActiveRenderer(): Renderer;
|
|
|
156
287
|
* console.log(`Using ${getActiveRendererName()} renderer`)
|
|
157
288
|
*/
|
|
158
289
|
declare function getActiveRendererName(): string;
|
|
290
|
+
/**
|
|
291
|
+
* Initialize and register the React renderer.
|
|
292
|
+
*
|
|
293
|
+
* This must be called before using setActiveRenderer('react').
|
|
294
|
+
* Requires react and react-dom packages to be installed.
|
|
295
|
+
*
|
|
296
|
+
* @throws Error if React packages are not installed
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* import { initReactRenderer, setActiveRenderer } from '@cloudwerk/ui'
|
|
300
|
+
*
|
|
301
|
+
* // Initialize React renderer (requires react and react-dom)
|
|
302
|
+
* await initReactRenderer()
|
|
303
|
+
*
|
|
304
|
+
* // Now you can use React
|
|
305
|
+
* setActiveRenderer('react')
|
|
306
|
+
*/
|
|
307
|
+
declare function initReactRenderer(): Promise<void>;
|
|
159
308
|
/**
|
|
160
309
|
* Set the active renderer by name.
|
|
161
310
|
*
|
|
162
311
|
* Called during app initialization based on the `ui.renderer` config option.
|
|
163
312
|
* The renderer must be registered (either built-in or via registerRenderer).
|
|
164
313
|
*
|
|
314
|
+
* For the React renderer, you must call initReactRenderer() first.
|
|
315
|
+
*
|
|
165
316
|
* @param name - Renderer name from config (e.g., 'hono-jsx', 'react')
|
|
166
317
|
* @throws Error if renderer is not found
|
|
167
318
|
*
|
|
@@ -197,7 +348,7 @@ declare function registerRenderer(name: string, renderer: Renderer): void;
|
|
|
197
348
|
*
|
|
198
349
|
* @example
|
|
199
350
|
* const available = getAvailableRenderers()
|
|
200
|
-
* // ['hono-jsx']
|
|
351
|
+
* // ['hono-jsx'] (or ['hono-jsx', 'react'] if initReactRenderer() was called)
|
|
201
352
|
*/
|
|
202
353
|
declare function getAvailableRenderers(): string[];
|
|
203
354
|
/**
|
|
@@ -379,4 +530,4 @@ declare function html(content: string, options?: HtmlOptions): Response;
|
|
|
379
530
|
*/
|
|
380
531
|
declare function hydrate(element: unknown, root: Element): void;
|
|
381
532
|
|
|
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 };
|
|
533
|
+
export { type HtmlOptions, type HydrationScriptOptions, type PropsWithChildren, type RenderOptions, type RenderToStreamOptions, type Renderer, type StreamRenderOptions, type WrapForHydrationOptions, _resetRenderers, generateHydrationRuntime, generateHydrationScript, generatePreloadHints, generateReactHydrationRuntime, generateReactHydrationScript, getActiveRenderer, getActiveRendererName, getAvailableRenderers, honoJsxRenderer, html, hydrate, initReactRenderer, registerRenderer, render, renderStream, renderToStream, setActiveRenderer, wrapForHydration };
|
package/dist/index.js
CHANGED
|
@@ -46,17 +46,28 @@ var honoJsxRenderer = {
|
|
|
46
46
|
/**
|
|
47
47
|
* Hydrate a JSX element on the client.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Uses hono/jsx/dom render function to attach event handlers and state
|
|
50
|
+
* to server-rendered HTML. This is called by the client-side hydration
|
|
51
|
+
* bootstrap script for each Client Component.
|
|
51
52
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Note: This method is primarily used by the client-side hydration runtime.
|
|
54
|
+
* In server-side code, it will throw an error since the DOM is not available.
|
|
55
|
+
*
|
|
56
|
+
* @param element - JSX element to hydrate
|
|
57
|
+
* @param root - DOM element to hydrate into
|
|
58
|
+
* @throws Error if called in a non-browser environment
|
|
55
59
|
*/
|
|
56
|
-
hydrate(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
hydrate(element, root) {
|
|
61
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"hydrate() can only be called in a browser environment. For server-side rendering, use render() instead."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
import("hono/jsx/dom").then(({ render: render2 }) => {
|
|
67
|
+
render2(element, root);
|
|
68
|
+
}).catch((error) => {
|
|
69
|
+
console.error("[Cloudwerk] Failed to hydrate component:", error);
|
|
70
|
+
});
|
|
60
71
|
}
|
|
61
72
|
};
|
|
62
73
|
function renderStream(loadingElement, contentPromise, options = {}) {
|
|
@@ -144,10 +155,29 @@ function getActiveRenderer() {
|
|
|
144
155
|
function getActiveRendererName() {
|
|
145
156
|
return activeRendererName;
|
|
146
157
|
}
|
|
158
|
+
async function initReactRenderer() {
|
|
159
|
+
if (renderers["react"]) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const { reactRenderer } = await import("./react-PVIKZSJC.js");
|
|
164
|
+
renderers["react"] = reactRenderer;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Failed to initialize React renderer. Make sure react and react-dom are installed: npm install react react-dom
|
|
168
|
+
Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
147
172
|
function setActiveRenderer(name) {
|
|
148
173
|
const renderer = renderers[name];
|
|
149
174
|
if (!renderer) {
|
|
150
175
|
const available = Object.keys(renderers).join(", ");
|
|
176
|
+
if (name === "react") {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`React renderer is not initialized. Call initReactRenderer() first, or install react and react-dom packages.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
151
181
|
throw new Error(`Unknown renderer "${name}". Available renderers: ${available}`);
|
|
152
182
|
}
|
|
153
183
|
activeRenderer = renderer;
|
|
@@ -174,6 +204,266 @@ function _resetRenderers() {
|
|
|
174
204
|
activeRendererName = "hono-jsx";
|
|
175
205
|
}
|
|
176
206
|
|
|
207
|
+
// src/hydration.ts
|
|
208
|
+
import { serializeProps } from "@cloudwerk/core";
|
|
209
|
+
function wrapForHydration(html2, options) {
|
|
210
|
+
const { componentId, props, wrapperTag = "div" } = options;
|
|
211
|
+
const serializedProps = serializeProps(props);
|
|
212
|
+
const escapedProps = escapeHtmlAttribute(serializedProps);
|
|
213
|
+
return `<${wrapperTag} data-hydrate-id="${componentId}" data-hydrate-props="${escapedProps}">${html2}</${wrapperTag}>`;
|
|
214
|
+
}
|
|
215
|
+
function escapeHtmlAttribute(str) {
|
|
216
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
217
|
+
}
|
|
218
|
+
function generateHydrationScript(manifest, options = {}) {
|
|
219
|
+
const { hydrationEndpoint = "/__cloudwerk" } = options;
|
|
220
|
+
if (manifest.components.size === 0) {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
const bundleMap = {};
|
|
224
|
+
for (const [id, meta] of manifest.components) {
|
|
225
|
+
bundleMap[id] = meta.bundlePath;
|
|
226
|
+
}
|
|
227
|
+
const script = `
|
|
228
|
+
<script type="module">
|
|
229
|
+
(async function() {
|
|
230
|
+
// Bundle map for component lookups
|
|
231
|
+
const bundles = ${JSON.stringify(bundleMap)};
|
|
232
|
+
|
|
233
|
+
// Find all elements that need hydration
|
|
234
|
+
const elements = document.querySelectorAll('[data-hydrate-id]');
|
|
235
|
+
if (elements.length === 0) return;
|
|
236
|
+
|
|
237
|
+
// Cache for loaded modules
|
|
238
|
+
const moduleCache = new Map();
|
|
239
|
+
|
|
240
|
+
// Load a component module
|
|
241
|
+
async function loadComponent(bundlePath) {
|
|
242
|
+
if (moduleCache.has(bundlePath)) {
|
|
243
|
+
return moduleCache.get(bundlePath);
|
|
244
|
+
}
|
|
245
|
+
const module = await import(bundlePath);
|
|
246
|
+
moduleCache.set(bundlePath, module);
|
|
247
|
+
return module;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Hydrate each element
|
|
251
|
+
for (const el of elements) {
|
|
252
|
+
const componentId = el.getAttribute('data-hydrate-id');
|
|
253
|
+
const propsJson = el.getAttribute('data-hydrate-props');
|
|
254
|
+
|
|
255
|
+
if (!componentId || !bundles[componentId]) {
|
|
256
|
+
console.warn('[Cloudwerk] Unknown client component:', componentId);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Parse props
|
|
262
|
+
const props = propsJson ? JSON.parse(propsJson) : {};
|
|
263
|
+
|
|
264
|
+
// Load the component module
|
|
265
|
+
const bundlePath = bundles[componentId];
|
|
266
|
+
const module = await loadComponent(bundlePath);
|
|
267
|
+
const Component = module.default;
|
|
268
|
+
|
|
269
|
+
if (!Component) {
|
|
270
|
+
console.error('[Cloudwerk] No default export in component:', componentId);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Import hono/jsx/dom for hydration
|
|
275
|
+
const { render } = await import('${hydrationEndpoint}/runtime.js');
|
|
276
|
+
|
|
277
|
+
// Hydrate the component using hono/jsx/dom render
|
|
278
|
+
// This safely replaces content using virtual DOM diffing
|
|
279
|
+
render(Component(props), el);
|
|
280
|
+
|
|
281
|
+
// Remove hydration attributes after successful hydration
|
|
282
|
+
el.removeAttribute('data-hydrate-id');
|
|
283
|
+
el.removeAttribute('data-hydrate-props');
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('[Cloudwerk] Failed to hydrate component:', componentId, error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
</script>
|
|
290
|
+
`.trim();
|
|
291
|
+
return script;
|
|
292
|
+
}
|
|
293
|
+
function generatePreloadHints(manifest) {
|
|
294
|
+
if (manifest.components.size === 0) {
|
|
295
|
+
return "";
|
|
296
|
+
}
|
|
297
|
+
const hints = [];
|
|
298
|
+
for (const meta of manifest.components.values()) {
|
|
299
|
+
hints.push(`<link rel="modulepreload" href="${meta.bundlePath}">`);
|
|
300
|
+
}
|
|
301
|
+
return hints.join("\n");
|
|
302
|
+
}
|
|
303
|
+
function generateHydrationRuntime() {
|
|
304
|
+
return `
|
|
305
|
+
// Cloudwerk Hydration Runtime
|
|
306
|
+
// Uses hono/jsx/dom for client-side rendering with virtual DOM diffing
|
|
307
|
+
import { render as honoRender } from 'hono/jsx/dom';
|
|
308
|
+
|
|
309
|
+
export function render(element, container) {
|
|
310
|
+
// Use hono/jsx/dom render which safely updates DOM via virtual DOM diffing
|
|
311
|
+
// This replaces the server-rendered content with the interactive version
|
|
312
|
+
honoRender(element, container);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Re-export hooks for client components
|
|
316
|
+
export {
|
|
317
|
+
useState,
|
|
318
|
+
useEffect,
|
|
319
|
+
useRef,
|
|
320
|
+
useCallback,
|
|
321
|
+
useMemo,
|
|
322
|
+
useReducer,
|
|
323
|
+
useSyncExternalStore,
|
|
324
|
+
useTransition,
|
|
325
|
+
useDeferredValue,
|
|
326
|
+
useId,
|
|
327
|
+
} from 'hono/jsx/dom';
|
|
328
|
+
`.trim();
|
|
329
|
+
}
|
|
330
|
+
function generateReactHydrationRuntime() {
|
|
331
|
+
return `
|
|
332
|
+
// Cloudwerk React Hydration Runtime
|
|
333
|
+
// Uses react-dom/client for client-side hydration
|
|
334
|
+
import React from 'react';
|
|
335
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
336
|
+
import {
|
|
337
|
+
useState,
|
|
338
|
+
useEffect,
|
|
339
|
+
useRef,
|
|
340
|
+
useCallback,
|
|
341
|
+
useMemo,
|
|
342
|
+
useReducer,
|
|
343
|
+
useContext,
|
|
344
|
+
useLayoutEffect,
|
|
345
|
+
useImperativeHandle,
|
|
346
|
+
useDebugValue,
|
|
347
|
+
useSyncExternalStore,
|
|
348
|
+
useTransition,
|
|
349
|
+
useDeferredValue,
|
|
350
|
+
useId,
|
|
351
|
+
useInsertionEffect,
|
|
352
|
+
useOptimistic,
|
|
353
|
+
useActionState,
|
|
354
|
+
use,
|
|
355
|
+
} from 'react';
|
|
356
|
+
|
|
357
|
+
// Re-export React for component rendering
|
|
358
|
+
export { React };
|
|
359
|
+
|
|
360
|
+
// Re-export hydrateRoot for hydration
|
|
361
|
+
export { hydrateRoot };
|
|
362
|
+
|
|
363
|
+
// Hydrate function that wraps hydrateRoot for Cloudwerk usage
|
|
364
|
+
export function hydrate(Component, props, container) {
|
|
365
|
+
return hydrateRoot(container, React.createElement(Component, props));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Re-export all hooks for client components
|
|
369
|
+
export {
|
|
370
|
+
useState,
|
|
371
|
+
useEffect,
|
|
372
|
+
useRef,
|
|
373
|
+
useCallback,
|
|
374
|
+
useMemo,
|
|
375
|
+
useReducer,
|
|
376
|
+
useContext,
|
|
377
|
+
useLayoutEffect,
|
|
378
|
+
useImperativeHandle,
|
|
379
|
+
useDebugValue,
|
|
380
|
+
useSyncExternalStore,
|
|
381
|
+
useTransition,
|
|
382
|
+
useDeferredValue,
|
|
383
|
+
useId,
|
|
384
|
+
useInsertionEffect,
|
|
385
|
+
useOptimistic,
|
|
386
|
+
useActionState,
|
|
387
|
+
use,
|
|
388
|
+
};
|
|
389
|
+
`.trim();
|
|
390
|
+
}
|
|
391
|
+
function generateReactHydrationScript(manifest, options = {}) {
|
|
392
|
+
const { hydrationEndpoint = "/__cloudwerk" } = options;
|
|
393
|
+
if (manifest.components.size === 0) {
|
|
394
|
+
return "";
|
|
395
|
+
}
|
|
396
|
+
const bundleMap = {};
|
|
397
|
+
for (const [id, meta] of manifest.components) {
|
|
398
|
+
bundleMap[id] = meta.bundlePath;
|
|
399
|
+
}
|
|
400
|
+
const script = `
|
|
401
|
+
<script type="module">
|
|
402
|
+
(async function() {
|
|
403
|
+
// Bundle map for component lookups
|
|
404
|
+
const bundles = ${JSON.stringify(bundleMap)};
|
|
405
|
+
|
|
406
|
+
// Find all elements that need hydration
|
|
407
|
+
const elements = document.querySelectorAll('[data-hydrate-id]');
|
|
408
|
+
if (elements.length === 0) return;
|
|
409
|
+
|
|
410
|
+
// Cache for loaded modules
|
|
411
|
+
const moduleCache = new Map();
|
|
412
|
+
|
|
413
|
+
// Load a component module
|
|
414
|
+
async function loadComponent(bundlePath) {
|
|
415
|
+
if (moduleCache.has(bundlePath)) {
|
|
416
|
+
return moduleCache.get(bundlePath);
|
|
417
|
+
}
|
|
418
|
+
const module = await import(bundlePath);
|
|
419
|
+
moduleCache.set(bundlePath, module);
|
|
420
|
+
return module;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Import React and hydrateRoot from the runtime
|
|
424
|
+
const { React, hydrateRoot } = await import('${hydrationEndpoint}/react-runtime.js');
|
|
425
|
+
|
|
426
|
+
// Hydrate each element
|
|
427
|
+
for (const el of elements) {
|
|
428
|
+
const componentId = el.getAttribute('data-hydrate-id');
|
|
429
|
+
const propsJson = el.getAttribute('data-hydrate-props');
|
|
430
|
+
|
|
431
|
+
if (!componentId || !bundles[componentId]) {
|
|
432
|
+
console.warn('[Cloudwerk] Unknown client component:', componentId);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
// Parse props
|
|
438
|
+
const props = propsJson ? JSON.parse(propsJson) : {};
|
|
439
|
+
|
|
440
|
+
// Load the component module
|
|
441
|
+
const bundlePath = bundles[componentId];
|
|
442
|
+
const module = await loadComponent(bundlePath);
|
|
443
|
+
const Component = module.default;
|
|
444
|
+
|
|
445
|
+
if (!Component) {
|
|
446
|
+
console.error('[Cloudwerk] No default export in component:', componentId);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Hydrate the component using React's hydrateRoot
|
|
451
|
+
// This attaches event handlers to the server-rendered HTML
|
|
452
|
+
hydrateRoot(el, React.createElement(Component, props));
|
|
453
|
+
|
|
454
|
+
// Remove hydration attributes after successful hydration
|
|
455
|
+
el.removeAttribute('data-hydrate-id');
|
|
456
|
+
el.removeAttribute('data-hydrate-props');
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error('[Cloudwerk] Failed to hydrate component:', componentId, error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
})();
|
|
462
|
+
</script>
|
|
463
|
+
`.trim();
|
|
464
|
+
return script;
|
|
465
|
+
}
|
|
466
|
+
|
|
177
467
|
// src/index.ts
|
|
178
468
|
function render(element, options) {
|
|
179
469
|
return getActiveRenderer().render(element, options);
|
|
@@ -186,15 +476,22 @@ function hydrate(element, root) {
|
|
|
186
476
|
}
|
|
187
477
|
export {
|
|
188
478
|
_resetRenderers,
|
|
479
|
+
generateHydrationRuntime,
|
|
480
|
+
generateHydrationScript,
|
|
481
|
+
generatePreloadHints,
|
|
482
|
+
generateReactHydrationRuntime,
|
|
483
|
+
generateReactHydrationScript,
|
|
189
484
|
getActiveRenderer,
|
|
190
485
|
getActiveRendererName,
|
|
191
486
|
getAvailableRenderers,
|
|
192
487
|
honoJsxRenderer,
|
|
193
488
|
html,
|
|
194
489
|
hydrate,
|
|
490
|
+
initReactRenderer,
|
|
195
491
|
registerRenderer,
|
|
196
492
|
render,
|
|
197
493
|
renderStream,
|
|
198
494
|
renderToStream,
|
|
199
|
-
setActiveRenderer
|
|
495
|
+
setActiveRenderer,
|
|
496
|
+
wrapForHydration
|
|
200
497
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/renderers/react.ts
|
|
2
|
+
import { renderToString, renderToReadableStream } from "react-dom/server";
|
|
3
|
+
var reactRenderer = {
|
|
4
|
+
/**
|
|
5
|
+
* Render a React element to an HTML Response.
|
|
6
|
+
*
|
|
7
|
+
* Uses React's renderToString() for synchronous server-side rendering.
|
|
8
|
+
* This method wraps the output in a proper Response object with headers.
|
|
9
|
+
*
|
|
10
|
+
* @param element - React element (React.ReactElement)
|
|
11
|
+
* @param options - Render options
|
|
12
|
+
* @returns Response object with HTML content
|
|
13
|
+
*/
|
|
14
|
+
render(element, options = {}) {
|
|
15
|
+
const { status = 200, headers = {}, doctype = true } = options;
|
|
16
|
+
const htmlString = renderToString(element);
|
|
17
|
+
const body = doctype ? `<!DOCTYPE html>${htmlString}` : htmlString;
|
|
18
|
+
return new Response(body, {
|
|
19
|
+
status,
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
22
|
+
...headers
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* Create an HTML Response from a raw string.
|
|
28
|
+
*
|
|
29
|
+
* Useful for static HTML, templates, or pre-rendered content
|
|
30
|
+
* that doesn't need to go through JSX rendering.
|
|
31
|
+
*
|
|
32
|
+
* @param content - Raw HTML string
|
|
33
|
+
* @param options - HTML response options
|
|
34
|
+
* @returns Response object with HTML content
|
|
35
|
+
*/
|
|
36
|
+
html(content, options = {}) {
|
|
37
|
+
const { status = 200, headers = {} } = options;
|
|
38
|
+
return new Response(content, {
|
|
39
|
+
status,
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
42
|
+
...headers
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Hydrate a React element on the client.
|
|
48
|
+
*
|
|
49
|
+
* Uses react-dom/client's hydrateRoot to attach event handlers and state
|
|
50
|
+
* to server-rendered HTML. This is called by the client-side hydration
|
|
51
|
+
* runtime for each Client Component.
|
|
52
|
+
*
|
|
53
|
+
* Note: This method is primarily used by the client-side hydration runtime.
|
|
54
|
+
* In server-side code, it will throw an error since the DOM is not available.
|
|
55
|
+
*
|
|
56
|
+
* @param element - React element to hydrate
|
|
57
|
+
* @param root - DOM element to hydrate into
|
|
58
|
+
* @throws Error if called in a non-browser environment
|
|
59
|
+
*/
|
|
60
|
+
hydrate(element, root) {
|
|
61
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"hydrate() can only be called in a browser environment. For server-side rendering, use render() instead."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
import("react-dom/client").then(({ hydrateRoot }) => {
|
|
67
|
+
hydrateRoot(root, element);
|
|
68
|
+
}).catch((error) => {
|
|
69
|
+
console.error("[Cloudwerk] Failed to hydrate React component:", error);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function prependDoctype(stream) {
|
|
74
|
+
const encoder = new TextEncoder();
|
|
75
|
+
const doctypeBytes = encoder.encode("<!DOCTYPE html>");
|
|
76
|
+
let doctypeSent = false;
|
|
77
|
+
return stream.pipeThrough(
|
|
78
|
+
new TransformStream({
|
|
79
|
+
transform(chunk, controller) {
|
|
80
|
+
if (!doctypeSent) {
|
|
81
|
+
controller.enqueue(doctypeBytes);
|
|
82
|
+
doctypeSent = true;
|
|
83
|
+
}
|
|
84
|
+
controller.enqueue(chunk);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
async function reactRenderToStream(element, options = {}) {
|
|
90
|
+
const { status = 200, headers = {}, doctype = true } = options;
|
|
91
|
+
const contentStream = await renderToReadableStream(element);
|
|
92
|
+
const stream = doctype ? prependDoctype(contentStream) : contentStream;
|
|
93
|
+
return new Response(stream, {
|
|
94
|
+
status,
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
97
|
+
...headers
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
reactRenderToStream,
|
|
103
|
+
reactRenderer
|
|
104
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudwerk/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "UI rendering abstraction for Cloudwerk",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,20 +17,36 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
|
-
"dependencies": {
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@cloudwerk/core": "0.6.0"
|
|
22
|
+
},
|
|
21
23
|
"peerDependencies": {
|
|
22
|
-
"hono": "^4.0.0"
|
|
24
|
+
"hono": "^4.0.0",
|
|
25
|
+
"react": "^19.0.0",
|
|
26
|
+
"react-dom": "^19.0.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"react": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"react-dom": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
23
35
|
},
|
|
24
36
|
"devDependencies": {
|
|
37
|
+
"@types/react": "^19.0.0",
|
|
38
|
+
"@types/react-dom": "^19.0.0",
|
|
25
39
|
"@vitest/coverage-v8": "^1.0.0",
|
|
26
40
|
"hono": "^4.7.4",
|
|
41
|
+
"react": "^19.0.0",
|
|
42
|
+
"react-dom": "^19.0.0",
|
|
27
43
|
"tsup": "^8.0.0",
|
|
28
44
|
"typescript": "^5.0.0",
|
|
29
45
|
"vitest": "^1.0.0"
|
|
30
46
|
},
|
|
31
47
|
"scripts": {
|
|
32
|
-
"build": "tsup
|
|
33
|
-
"dev": "tsup
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"dev": "tsup --watch",
|
|
34
50
|
"test": "vitest --run",
|
|
35
51
|
"test:watch": "vitest",
|
|
36
52
|
"test:coverage": "vitest --run --coverage",
|