@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.
- package/dist/chunk-C4MFDUV4.js +27 -0
- package/dist/client.d.ts +47 -0
- package/dist/client.js +6 -0
- package/dist/index.d.ts +157 -2
- package/dist/index.js +327 -10
- package/dist/react-PVIKZSJC.js +104 -0
- package/package.json +26 -5
|
@@ -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
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -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
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
|
-
*
|
|
50
|
-
*
|
|
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
|
|
53
|
-
* @param
|
|
54
|
-
* @throws Error
|
|
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(
|
|
57
|
-
|
|
58
|
-
|
|
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, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
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.
|
|
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
|
|
33
|
-
"dev": "tsup
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"dev": "tsup --watch",
|
|
34
55
|
"test": "vitest --run",
|
|
35
56
|
"test:watch": "vitest",
|
|
36
57
|
"test:coverage": "vitest --run --coverage",
|