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