@servlyadmin/runtime-react 0.1.7 → 0.1.9

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/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # @servlyadmin/runtime-react
2
+
3
+ React wrapper for Servly runtime renderer. Render Servly components in your React applications with full support for props, slots, and event handling.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @servlyadmin/runtime-react @servlyadmin/runtime-core
9
+ # or
10
+ yarn add @servlyadmin/runtime-react @servlyadmin/runtime-core
11
+ # or
12
+ pnpm add @servlyadmin/runtime-react @servlyadmin/runtime-core
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```tsx
18
+ import { ServlyComponent } from '@servlyadmin/runtime-react';
19
+
20
+ function App() {
21
+ return (
22
+ <ServlyComponent
23
+ id="my-component"
24
+ version="latest"
25
+ props={{ title: 'Hello World' }}
26
+ />
27
+ );
28
+ }
29
+ ```
30
+
31
+ ## Props
32
+
33
+ | Prop | Type | Default | Description |
34
+ |------|------|---------|-------------|
35
+ | `id` | `string` | required | Component ID from the registry |
36
+ | `version` | `string` | `'latest'` | Version specifier |
37
+ | `props` | `object` | `{}` | Props to pass to the component |
38
+ | `slots` | `Record<string, ReactNode>` | - | Slot content |
39
+ | `fallback` | `ReactNode` | - | Loading/error fallback |
40
+ | `onError` | `(error: Error) => void` | - | Error callback |
41
+ | `onLoad` | `() => void` | - | Load complete callback |
42
+ | `className` | `string` | - | Wrapper class name |
43
+ | `style` | `CSSProperties` | - | Wrapper styles |
44
+ | `showSkeleton` | `boolean` | `true` | Show loading skeleton |
45
+ | `cacheStrategy` | `CacheStrategy` | `'memory'` | Cache strategy |
46
+ | `eventHandlers` | `object` | - | Event handlers by element ID |
47
+ | `children` | `ReactNode` | - | Default slot content |
48
+
49
+ ## Usage Examples
50
+
51
+ ### Basic Usage
52
+
53
+ ```tsx
54
+ import { ServlyComponent } from '@servlyadmin/runtime-react';
55
+
56
+ function MyPage() {
57
+ return (
58
+ <ServlyComponent
59
+ id="hero-section"
60
+ props={{
61
+ title: 'Welcome',
62
+ subtitle: 'Get started today',
63
+ }}
64
+ />
65
+ );
66
+ }
67
+ ```
68
+
69
+ ### With Slots
70
+
71
+ ```tsx
72
+ function CardExample() {
73
+ return (
74
+ <ServlyComponent
75
+ id="card-component"
76
+ slots={{
77
+ header: <h2>Card Title</h2>,
78
+ footer: <button>Learn More</button>,
79
+ }}
80
+ >
81
+ {/* Children go to default slot */}
82
+ <p>This is the card content.</p>
83
+ </ServlyComponent>
84
+ );
85
+ }
86
+ ```
87
+
88
+ ### With Event Handlers
89
+
90
+ ```tsx
91
+ function InteractiveExample() {
92
+ const [count, setCount] = useState(0);
93
+
94
+ return (
95
+ <ServlyComponent
96
+ id="counter-component"
97
+ props={{ count }}
98
+ eventHandlers={{
99
+ 'increment-btn': {
100
+ click: () => setCount(c => c + 1),
101
+ },
102
+ 'decrement-btn': {
103
+ click: () => setCount(c => c - 1),
104
+ },
105
+ }}
106
+ />
107
+ );
108
+ }
109
+ ```
110
+
111
+ ### Loading States
112
+
113
+ ```tsx
114
+ function WithLoadingState() {
115
+ return (
116
+ <ServlyComponent
117
+ id="data-component"
118
+ fallback={<div>Loading...</div>}
119
+ onLoad={() => console.log('Component loaded!')}
120
+ onError={(error) => console.error('Failed to load:', error)}
121
+ />
122
+ );
123
+ }
124
+ ```
125
+
126
+ ### Custom Loading Skeleton
127
+
128
+ ```tsx
129
+ function WithCustomSkeleton() {
130
+ return (
131
+ <ServlyComponent
132
+ id="profile-card"
133
+ showSkeleton={false}
134
+ fallback={
135
+ <div className="animate-pulse">
136
+ <div className="h-32 bg-gray-200 rounded" />
137
+ <div className="h-4 bg-gray-200 rounded mt-4 w-3/4" />
138
+ </div>
139
+ }
140
+ />
141
+ );
142
+ }
143
+ ```
144
+
145
+ ### Version Pinning
146
+
147
+ ```tsx
148
+ function VersionedComponent() {
149
+ return (
150
+ <>
151
+ {/* Exact version */}
152
+ <ServlyComponent id="my-component" version="1.2.3" />
153
+
154
+ {/* Version range */}
155
+ <ServlyComponent id="my-component" version="^1.0.0" />
156
+
157
+ {/* Latest */}
158
+ <ServlyComponent id="my-component" version="latest" />
159
+ </>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ### Cache Control
165
+
166
+ ```tsx
167
+ function CacheExample() {
168
+ return (
169
+ <>
170
+ {/* Memory cache (default) */}
171
+ <ServlyComponent id="comp1" cacheStrategy="memory" />
172
+
173
+ {/* Persist to localStorage */}
174
+ <ServlyComponent id="comp2" cacheStrategy="localStorage" />
175
+
176
+ {/* No caching */}
177
+ <ServlyComponent id="comp3" cacheStrategy="none" />
178
+ </>
179
+ );
180
+ }
181
+ ```
182
+
183
+ ### Dynamic Props
184
+
185
+ ```tsx
186
+ function DynamicPropsExample() {
187
+ const [theme, setTheme] = useState('light');
188
+ const [user, setUser] = useState({ name: 'Guest' });
189
+
190
+ return (
191
+ <ServlyComponent
192
+ id="themed-component"
193
+ props={{
194
+ theme,
195
+ userName: user.name,
196
+ timestamp: Date.now(),
197
+ }}
198
+ />
199
+ );
200
+ }
201
+ ```
202
+
203
+ ### Error Boundary Integration
204
+
205
+ ```tsx
206
+ import { ErrorBoundary } from 'react-error-boundary';
207
+
208
+ function SafeComponent() {
209
+ return (
210
+ <ErrorBoundary fallback={<div>Something went wrong</div>}>
211
+ <ServlyComponent
212
+ id="risky-component"
213
+ onError={(error) => {
214
+ // Log to error tracking service
215
+ logError(error);
216
+ }}
217
+ />
218
+ </ErrorBoundary>
219
+ );
220
+ }
221
+ ```
222
+
223
+ ## TypeScript
224
+
225
+ Full TypeScript support:
226
+
227
+ ```tsx
228
+ import { ServlyComponent, type ServlyComponentProps } from '@servlyadmin/runtime-react';
229
+
230
+ interface MyComponentProps {
231
+ title: string;
232
+ count: number;
233
+ }
234
+
235
+ function TypedExample() {
236
+ return (
237
+ <ServlyComponent<MyComponentProps>
238
+ id="typed-component"
239
+ props={{
240
+ title: 'Hello',
241
+ count: 42,
242
+ }}
243
+ />
244
+ );
245
+ }
246
+ ```
247
+
248
+ ## Server-Side Rendering
249
+
250
+ The component handles SSR gracefully by rendering the fallback on the server and hydrating on the client.
251
+
252
+ ```tsx
253
+ // Works with Next.js, Remix, etc.
254
+ function SSRPage() {
255
+ return (
256
+ <ServlyComponent
257
+ id="ssr-component"
258
+ fallback={<div>Loading component...</div>}
259
+ />
260
+ );
261
+ }
262
+ ```
263
+
264
+ ## Performance Tips
265
+
266
+ 1. **Use version pinning** in production to leverage caching
267
+ 2. **Prefetch components** that will be needed soon
268
+ 3. **Use `cacheStrategy="localStorage"`** for components that rarely change
269
+ 4. **Memoize event handlers** to prevent unnecessary re-renders
270
+
271
+ ```tsx
272
+ import { useMemo, useCallback } from 'react';
273
+
274
+ function OptimizedExample() {
275
+ const eventHandlers = useMemo(() => ({
276
+ 'btn': {
277
+ click: () => console.log('clicked'),
278
+ },
279
+ }), []);
280
+
281
+ return (
282
+ <ServlyComponent
283
+ id="optimized"
284
+ eventHandlers={eventHandlers}
285
+ />
286
+ );
287
+ }
288
+ ```
289
+
290
+ ## License
291
+
292
+ MIT
package/dist/index.js CHANGED
@@ -1,11 +1,46 @@
1
- // src/ServlyComponent.tsx
2
- import { useRef, useEffect, useState, useCallback } from "react";
3
- import {
4
- render,
5
- fetchComponent
6
- } from "@servlyadmin/runtime-core";
7
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
- var LoadingSkeleton = ({ className }) => /* @__PURE__ */ jsx(
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // packages/runtime-react/src/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ ServlyComponent: () => ServlyComponent,
23
+ clearAllCaches: () => import_runtime_core2.clearAllCaches,
24
+ compareVersions: () => import_runtime_core2.compareVersions,
25
+ default: () => ServlyComponent_default,
26
+ fetchComponent: () => import_runtime_core2.fetchComponent,
27
+ getRegistryUrl: () => import_runtime_core2.getRegistryUrl,
28
+ invalidateCache: () => import_runtime_core2.invalidateCache,
29
+ isComponentAvailable: () => import_runtime_core2.isComponentAvailable,
30
+ parseVersion: () => import_runtime_core2.parseVersion,
31
+ prefetchComponents: () => import_runtime_core2.prefetchComponents,
32
+ resolveVersion: () => import_runtime_core2.resolveVersion,
33
+ satisfiesVersion: () => import_runtime_core2.satisfiesVersion,
34
+ setRegistryUrl: () => import_runtime_core2.setRegistryUrl
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // packages/runtime-react/src/ServlyComponent.tsx
39
+ var import_react = require("react");
40
+ var import_react_dom = require("react-dom");
41
+ var import_runtime_core = require("@servlyadmin/runtime-core");
42
+ var import_jsx_runtime = require("react/jsx-runtime");
43
+ var LoadingSkeleton = ({ className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9
44
  "div",
10
45
  {
11
46
  className: `servly-skeleton ${className || ""}`,
@@ -15,7 +50,7 @@ var LoadingSkeleton = ({ className }) => /* @__PURE__ */ jsx(
15
50
  minHeight: "100px",
16
51
  animation: "servly-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite"
17
52
  },
18
- children: /* @__PURE__ */ jsx("style", { children: `
53
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
19
54
  @keyframes servly-pulse {
20
55
  0%, 100% { opacity: 1; }
21
56
  50% { opacity: 0.5; }
@@ -23,7 +58,7 @@ var LoadingSkeleton = ({ className }) => /* @__PURE__ */ jsx(
23
58
  ` })
24
59
  }
25
60
  );
26
- var ErrorDisplay = ({ error, onRetry, className, style }) => /* @__PURE__ */ jsxs(
61
+ var ErrorDisplay = ({ error, onRetry, className, style }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
27
62
  "div",
28
63
  {
29
64
  className: `servly-error ${className || ""}`,
@@ -35,9 +70,9 @@ var ErrorDisplay = ({ error, onRetry, className, style }) => /* @__PURE__ */ jsx
35
70
  ...style
36
71
  },
37
72
  children: [
38
- /* @__PURE__ */ jsx("p", { style: { margin: 0, fontWeight: 500 }, children: "Failed to load component" }),
39
- /* @__PURE__ */ jsx("p", { style: { margin: "8px 0 0", fontSize: "14px", color: "#991b1b" }, children: error.message }),
40
- onRetry && /* @__PURE__ */ jsx(
73
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: "Failed to load component" }),
74
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "8px 0 0", fontSize: "14px", color: "#991b1b" }, children: error.message }),
75
+ onRetry && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
41
76
  "button",
42
77
  {
43
78
  onClick: onRetry,
@@ -57,10 +92,30 @@ var ErrorDisplay = ({ error, onRetry, className, style }) => /* @__PURE__ */ jsx
57
92
  ]
58
93
  }
59
94
  );
95
+ function useSlotElements(containerRef, isRendered) {
96
+ const [slotElements, setSlotElements] = (0, import_react.useState)({});
97
+ (0, import_react.useEffect)(() => {
98
+ if (!isRendered || !containerRef.current) {
99
+ setSlotElements({});
100
+ return;
101
+ }
102
+ const found = {};
103
+ const slots = containerRef.current.querySelectorAll("[data-slot]");
104
+ slots.forEach((el) => {
105
+ const slotName = el.getAttribute("data-slot");
106
+ if (slotName) {
107
+ found[slotName] = el;
108
+ }
109
+ });
110
+ setSlotElements(found);
111
+ }, [isRendered, containerRef]);
112
+ return slotElements;
113
+ }
60
114
  function ServlyComponent({
61
115
  id,
62
116
  version = "latest",
63
117
  props = {},
118
+ slots,
64
119
  fallback,
65
120
  onError,
66
121
  onLoad,
@@ -69,22 +124,33 @@ function ServlyComponent({
69
124
  showSkeleton = true,
70
125
  cacheStrategy = "memory",
71
126
  retryConfig,
72
- eventHandlers
127
+ eventHandlers,
128
+ children
73
129
  }) {
74
- const containerRef = useRef(null);
75
- const renderResultRef = useRef(null);
76
- const abortControllerRef = useRef(null);
77
- const [state, setState] = useState({
130
+ const containerRef = (0, import_react.useRef)(null);
131
+ const renderResultRef = (0, import_react.useRef)(null);
132
+ const abortControllerRef = (0, import_react.useRef)(null);
133
+ const [isRendered, setIsRendered] = (0, import_react.useState)(false);
134
+ const [state, setState] = (0, import_react.useState)({
78
135
  loading: true,
79
136
  error: null,
80
137
  data: null
81
138
  });
82
- const loadComponent = useCallback(async () => {
139
+ const slotElements = useSlotElements(containerRef, isRendered);
140
+ const effectiveSlots = (0, import_react.useMemo)(() => {
141
+ const result = { ...slots };
142
+ if (children && !result.default) {
143
+ result.default = children;
144
+ }
145
+ return result;
146
+ }, [slots, children]);
147
+ const loadComponent = (0, import_react.useCallback)(async () => {
83
148
  if (abortControllerRef.current) {
84
149
  abortControllerRef.current.abort();
85
150
  }
86
151
  abortControllerRef.current = new AbortController();
87
152
  setState((prev) => ({ ...prev, loading: true, error: null }));
153
+ setIsRendered(false);
88
154
  const fetchOptions = {
89
155
  version,
90
156
  cacheStrategy,
@@ -92,11 +158,13 @@ function ServlyComponent({
92
158
  signal: abortControllerRef.current.signal
93
159
  };
94
160
  try {
95
- const result = await fetchComponent(id, fetchOptions);
161
+ const result = await (0, import_runtime_core.fetchComponent)(id, fetchOptions);
96
162
  setState({
97
163
  loading: false,
98
164
  error: null,
99
- data: result.data
165
+ data: result.data,
166
+ views: result.views,
167
+ registry: result.registry
100
168
  });
101
169
  onLoad?.();
102
170
  } catch (error) {
@@ -105,12 +173,14 @@ function ServlyComponent({
105
173
  setState({
106
174
  loading: false,
107
175
  error: err,
108
- data: null
176
+ data: null,
177
+ views: void 0,
178
+ registry: void 0
109
179
  });
110
180
  onError?.(err);
111
181
  }
112
182
  }, [id, version, cacheStrategy, retryConfig, onLoad, onError]);
113
- useEffect(() => {
183
+ (0, import_react.useEffect)(() => {
114
184
  loadComponent();
115
185
  return () => {
116
186
  if (abortControllerRef.current) {
@@ -118,7 +188,7 @@ function ServlyComponent({
118
188
  }
119
189
  };
120
190
  }, [loadComponent]);
121
- useEffect(() => {
191
+ (0, import_react.useEffect)(() => {
122
192
  if (!state.data || !containerRef.current) return;
123
193
  const context = {
124
194
  props,
@@ -129,20 +199,24 @@ function ServlyComponent({
129
199
  renderResultRef.current.update(context);
130
200
  return;
131
201
  }
132
- renderResultRef.current = render({
202
+ renderResultRef.current = (0, import_runtime_core.render)({
133
203
  container: containerRef.current,
134
204
  elements: state.data.layout,
135
205
  context,
136
- eventHandlers
206
+ eventHandlers,
207
+ views: state.views,
208
+ componentRegistry: state.registry
137
209
  });
210
+ setIsRendered(true);
138
211
  return () => {
139
212
  if (renderResultRef.current) {
140
213
  renderResultRef.current.destroy();
141
214
  renderResultRef.current = null;
215
+ setIsRendered(false);
142
216
  }
143
217
  };
144
- }, [state.data, props, eventHandlers]);
145
- useEffect(() => {
218
+ }, [state.data, eventHandlers]);
219
+ (0, import_react.useEffect)(() => {
146
220
  if (!renderResultRef.current || !state.data) return;
147
221
  const context = {
148
222
  props,
@@ -152,13 +226,13 @@ function ServlyComponent({
152
226
  renderResultRef.current.update(context);
153
227
  }, [props, state.data]);
154
228
  if (state.loading) {
155
- if (fallback) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
156
- if (showSkeleton) return /* @__PURE__ */ jsx(LoadingSkeleton, { className });
229
+ if (fallback) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
230
+ if (showSkeleton) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingSkeleton, { className });
157
231
  return null;
158
232
  }
159
233
  if (state.error) {
160
- if (fallback) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
161
- return /* @__PURE__ */ jsx(
234
+ if (fallback) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
235
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
162
236
  ErrorDisplay,
163
237
  {
164
238
  error: state.error,
@@ -168,39 +242,34 @@ function ServlyComponent({
168
242
  }
169
243
  );
170
244
  }
171
- return /* @__PURE__ */ jsx(
172
- "div",
173
- {
174
- ref: containerRef,
175
- className: `servly-component ${className || ""}`,
176
- style,
177
- "data-servly-id": id,
178
- "data-servly-version": state.data?.version
179
- }
180
- );
245
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
247
+ "div",
248
+ {
249
+ ref: containerRef,
250
+ className: `servly-component ${className || ""}`,
251
+ style,
252
+ "data-servly-id": id,
253
+ "data-servly-version": state.data?.version
254
+ }
255
+ ),
256
+ Object.entries(effectiveSlots).map(([slotName, content]) => {
257
+ const slotEl = slotElements[slotName];
258
+ if (!slotEl || !content) return null;
259
+ return (0, import_react_dom.createPortal)(content, slotEl, `slot-${slotName}`);
260
+ })
261
+ ] });
181
262
  }
182
263
  var ServlyComponent_default = ServlyComponent;
183
264
 
184
- // src/index.ts
185
- import {
186
- fetchComponent as fetchComponent2,
187
- prefetchComponents,
188
- isComponentAvailable,
189
- setRegistryUrl,
190
- getRegistryUrl,
191
- invalidateCache,
192
- clearAllCaches,
193
- parseVersion,
194
- compareVersions,
195
- satisfiesVersion,
196
- resolveVersion
197
- } from "@servlyadmin/runtime-core";
198
- export {
265
+ // packages/runtime-react/src/index.ts
266
+ var import_runtime_core2 = require("@servlyadmin/runtime-core");
267
+ // Annotate the CommonJS export names for ESM import in node:
268
+ 0 && (module.exports = {
199
269
  ServlyComponent,
200
270
  clearAllCaches,
201
271
  compareVersions,
202
- ServlyComponent_default as default,
203
- fetchComponent2 as fetchComponent,
272
+ fetchComponent,
204
273
  getRegistryUrl,
205
274
  invalidateCache,
206
275
  isComponentAvailable,
@@ -209,4 +278,4 @@ export {
209
278
  resolveVersion,
210
279
  satisfiesVersion,
211
280
  setRegistryUrl
212
- };
281
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,259 @@
1
+ // packages/runtime-react/src/ServlyComponent.tsx
2
+ import { useRef, useEffect, useState, useCallback, useMemo } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import {
5
+ render,
6
+ fetchComponent
7
+ } from "@servlyadmin/runtime-core";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ var LoadingSkeleton = ({ className }) => /* @__PURE__ */ jsx(
10
+ "div",
11
+ {
12
+ className: `servly-skeleton ${className || ""}`,
13
+ style: {
14
+ backgroundColor: "#f3f4f6",
15
+ borderRadius: "8px",
16
+ minHeight: "100px",
17
+ animation: "servly-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite"
18
+ },
19
+ children: /* @__PURE__ */ jsx("style", { children: `
20
+ @keyframes servly-pulse {
21
+ 0%, 100% { opacity: 1; }
22
+ 50% { opacity: 0.5; }
23
+ }
24
+ ` })
25
+ }
26
+ );
27
+ var ErrorDisplay = ({ error, onRetry, className, style }) => /* @__PURE__ */ jsxs(
28
+ "div",
29
+ {
30
+ className: `servly-error ${className || ""}`,
31
+ style: {
32
+ padding: "16px",
33
+ color: "#ef4444",
34
+ backgroundColor: "#fef2f2",
35
+ borderRadius: "8px",
36
+ ...style
37
+ },
38
+ children: [
39
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontWeight: 500 }, children: "Failed to load component" }),
40
+ /* @__PURE__ */ jsx("p", { style: { margin: "8px 0 0", fontSize: "14px", color: "#991b1b" }, children: error.message }),
41
+ onRetry && /* @__PURE__ */ jsx(
42
+ "button",
43
+ {
44
+ onClick: onRetry,
45
+ style: {
46
+ marginTop: "12px",
47
+ padding: "8px 16px",
48
+ backgroundColor: "#ef4444",
49
+ color: "white",
50
+ border: "none",
51
+ borderRadius: "4px",
52
+ cursor: "pointer",
53
+ fontSize: "14px"
54
+ },
55
+ children: "Retry"
56
+ }
57
+ )
58
+ ]
59
+ }
60
+ );
61
+ function useSlotElements(containerRef, isRendered) {
62
+ const [slotElements, setSlotElements] = useState({});
63
+ useEffect(() => {
64
+ if (!isRendered || !containerRef.current) {
65
+ setSlotElements({});
66
+ return;
67
+ }
68
+ const found = {};
69
+ const slots = containerRef.current.querySelectorAll("[data-slot]");
70
+ slots.forEach((el) => {
71
+ const slotName = el.getAttribute("data-slot");
72
+ if (slotName) {
73
+ found[slotName] = el;
74
+ }
75
+ });
76
+ setSlotElements(found);
77
+ }, [isRendered, containerRef]);
78
+ return slotElements;
79
+ }
80
+ function ServlyComponent({
81
+ id,
82
+ version = "latest",
83
+ props = {},
84
+ slots,
85
+ fallback,
86
+ onError,
87
+ onLoad,
88
+ className,
89
+ style,
90
+ showSkeleton = true,
91
+ cacheStrategy = "memory",
92
+ retryConfig,
93
+ eventHandlers,
94
+ children
95
+ }) {
96
+ const containerRef = useRef(null);
97
+ const renderResultRef = useRef(null);
98
+ const abortControllerRef = useRef(null);
99
+ const [isRendered, setIsRendered] = useState(false);
100
+ const [state, setState] = useState({
101
+ loading: true,
102
+ error: null,
103
+ data: null
104
+ });
105
+ const slotElements = useSlotElements(containerRef, isRendered);
106
+ const effectiveSlots = useMemo(() => {
107
+ const result = { ...slots };
108
+ if (children && !result.default) {
109
+ result.default = children;
110
+ }
111
+ return result;
112
+ }, [slots, children]);
113
+ const loadComponent = useCallback(async () => {
114
+ if (abortControllerRef.current) {
115
+ abortControllerRef.current.abort();
116
+ }
117
+ abortControllerRef.current = new AbortController();
118
+ setState((prev) => ({ ...prev, loading: true, error: null }));
119
+ setIsRendered(false);
120
+ const fetchOptions = {
121
+ version,
122
+ cacheStrategy,
123
+ retryConfig,
124
+ signal: abortControllerRef.current.signal
125
+ };
126
+ try {
127
+ const result = await fetchComponent(id, fetchOptions);
128
+ setState({
129
+ loading: false,
130
+ error: null,
131
+ data: result.data,
132
+ views: result.views,
133
+ registry: result.registry
134
+ });
135
+ onLoad?.();
136
+ } catch (error) {
137
+ const err = error instanceof Error ? error : new Error(String(error));
138
+ if (err.message === "Fetch aborted") return;
139
+ setState({
140
+ loading: false,
141
+ error: err,
142
+ data: null,
143
+ views: void 0,
144
+ registry: void 0
145
+ });
146
+ onError?.(err);
147
+ }
148
+ }, [id, version, cacheStrategy, retryConfig, onLoad, onError]);
149
+ useEffect(() => {
150
+ loadComponent();
151
+ return () => {
152
+ if (abortControllerRef.current) {
153
+ abortControllerRef.current.abort();
154
+ }
155
+ };
156
+ }, [loadComponent]);
157
+ useEffect(() => {
158
+ if (!state.data || !containerRef.current) return;
159
+ const context = {
160
+ props,
161
+ state: {},
162
+ context: {}
163
+ };
164
+ if (renderResultRef.current) {
165
+ renderResultRef.current.update(context);
166
+ return;
167
+ }
168
+ renderResultRef.current = render({
169
+ container: containerRef.current,
170
+ elements: state.data.layout,
171
+ context,
172
+ eventHandlers,
173
+ views: state.views,
174
+ componentRegistry: state.registry
175
+ });
176
+ setIsRendered(true);
177
+ return () => {
178
+ if (renderResultRef.current) {
179
+ renderResultRef.current.destroy();
180
+ renderResultRef.current = null;
181
+ setIsRendered(false);
182
+ }
183
+ };
184
+ }, [state.data, eventHandlers]);
185
+ useEffect(() => {
186
+ if (!renderResultRef.current || !state.data) return;
187
+ const context = {
188
+ props,
189
+ state: {},
190
+ context: {}
191
+ };
192
+ renderResultRef.current.update(context);
193
+ }, [props, state.data]);
194
+ if (state.loading) {
195
+ if (fallback) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
196
+ if (showSkeleton) return /* @__PURE__ */ jsx(LoadingSkeleton, { className });
197
+ return null;
198
+ }
199
+ if (state.error) {
200
+ if (fallback) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
201
+ return /* @__PURE__ */ jsx(
202
+ ErrorDisplay,
203
+ {
204
+ error: state.error,
205
+ onRetry: loadComponent,
206
+ className,
207
+ style
208
+ }
209
+ );
210
+ }
211
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
212
+ /* @__PURE__ */ jsx(
213
+ "div",
214
+ {
215
+ ref: containerRef,
216
+ className: `servly-component ${className || ""}`,
217
+ style,
218
+ "data-servly-id": id,
219
+ "data-servly-version": state.data?.version
220
+ }
221
+ ),
222
+ Object.entries(effectiveSlots).map(([slotName, content]) => {
223
+ const slotEl = slotElements[slotName];
224
+ if (!slotEl || !content) return null;
225
+ return createPortal(content, slotEl, `slot-${slotName}`);
226
+ })
227
+ ] });
228
+ }
229
+ var ServlyComponent_default = ServlyComponent;
230
+
231
+ // packages/runtime-react/src/index.ts
232
+ import {
233
+ fetchComponent as fetchComponent2,
234
+ prefetchComponents,
235
+ isComponentAvailable,
236
+ setRegistryUrl,
237
+ getRegistryUrl,
238
+ invalidateCache,
239
+ clearAllCaches,
240
+ parseVersion,
241
+ compareVersions,
242
+ satisfiesVersion,
243
+ resolveVersion
244
+ } from "@servlyadmin/runtime-core";
245
+ export {
246
+ ServlyComponent,
247
+ clearAllCaches,
248
+ compareVersions,
249
+ ServlyComponent_default as default,
250
+ fetchComponent2 as fetchComponent,
251
+ getRegistryUrl,
252
+ invalidateCache,
253
+ isComponentAvailable,
254
+ parseVersion,
255
+ prefetchComponents,
256
+ resolveVersion,
257
+ satisfiesVersion,
258
+ setRegistryUrl
259
+ };
package/package.json CHANGED
@@ -1,16 +1,14 @@
1
1
  {
2
2
  "name": "@servlyadmin/runtime-react",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "React wrapper for Servly runtime renderer",
5
5
  "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
9
8
  "exports": {
10
9
  ".": {
11
- "import": "./dist/index.js",
12
- "require": "./dist/index.cjs",
13
- "types": "./dist/index.d.ts"
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
14
12
  }
15
13
  },
16
14
  "files": [
@@ -34,8 +32,7 @@
34
32
  "react-dom": ">=17.0.0"
35
33
  },
36
34
  "dependencies": {
37
- "@servlyadmin/runtime-core": "^0.1.7",
38
- "@servlyadmin/runtime-react": "^0.1.6"
35
+ "@servlyadmin/runtime-core": "^0.1.9"
39
36
  },
40
37
  "devDependencies": {
41
38
  "@types/react": "^18.2.0",
package/dist/index.cjs DELETED
@@ -1,235 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- ServlyComponent: () => ServlyComponent,
24
- clearAllCaches: () => import_runtime_core2.clearAllCaches,
25
- compareVersions: () => import_runtime_core2.compareVersions,
26
- default: () => ServlyComponent_default,
27
- fetchComponent: () => import_runtime_core2.fetchComponent,
28
- getRegistryUrl: () => import_runtime_core2.getRegistryUrl,
29
- invalidateCache: () => import_runtime_core2.invalidateCache,
30
- isComponentAvailable: () => import_runtime_core2.isComponentAvailable,
31
- parseVersion: () => import_runtime_core2.parseVersion,
32
- prefetchComponents: () => import_runtime_core2.prefetchComponents,
33
- resolveVersion: () => import_runtime_core2.resolveVersion,
34
- satisfiesVersion: () => import_runtime_core2.satisfiesVersion,
35
- setRegistryUrl: () => import_runtime_core2.setRegistryUrl
36
- });
37
- module.exports = __toCommonJS(index_exports);
38
-
39
- // src/ServlyComponent.tsx
40
- var import_react = require("react");
41
- var import_runtime_core = require("@servlyadmin/runtime-core");
42
- var import_jsx_runtime = require("react/jsx-runtime");
43
- var LoadingSkeleton = ({ className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
44
- "div",
45
- {
46
- className: `servly-skeleton ${className || ""}`,
47
- style: {
48
- backgroundColor: "#f3f4f6",
49
- borderRadius: "8px",
50
- minHeight: "100px",
51
- animation: "servly-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite"
52
- },
53
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
54
- @keyframes servly-pulse {
55
- 0%, 100% { opacity: 1; }
56
- 50% { opacity: 0.5; }
57
- }
58
- ` })
59
- }
60
- );
61
- var ErrorDisplay = ({ error, onRetry, className, style }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
62
- "div",
63
- {
64
- className: `servly-error ${className || ""}`,
65
- style: {
66
- padding: "16px",
67
- color: "#ef4444",
68
- backgroundColor: "#fef2f2",
69
- borderRadius: "8px",
70
- ...style
71
- },
72
- children: [
73
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: "Failed to load component" }),
74
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: "8px 0 0", fontSize: "14px", color: "#991b1b" }, children: error.message }),
75
- onRetry && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
76
- "button",
77
- {
78
- onClick: onRetry,
79
- style: {
80
- marginTop: "12px",
81
- padding: "8px 16px",
82
- backgroundColor: "#ef4444",
83
- color: "white",
84
- border: "none",
85
- borderRadius: "4px",
86
- cursor: "pointer",
87
- fontSize: "14px"
88
- },
89
- children: "Retry"
90
- }
91
- )
92
- ]
93
- }
94
- );
95
- function ServlyComponent({
96
- id,
97
- version = "latest",
98
- props = {},
99
- fallback,
100
- onError,
101
- onLoad,
102
- className,
103
- style,
104
- showSkeleton = true,
105
- cacheStrategy = "memory",
106
- retryConfig,
107
- eventHandlers
108
- }) {
109
- const containerRef = (0, import_react.useRef)(null);
110
- const renderResultRef = (0, import_react.useRef)(null);
111
- const abortControllerRef = (0, import_react.useRef)(null);
112
- const [state, setState] = (0, import_react.useState)({
113
- loading: true,
114
- error: null,
115
- data: null
116
- });
117
- const loadComponent = (0, import_react.useCallback)(async () => {
118
- if (abortControllerRef.current) {
119
- abortControllerRef.current.abort();
120
- }
121
- abortControllerRef.current = new AbortController();
122
- setState((prev) => ({ ...prev, loading: true, error: null }));
123
- const fetchOptions = {
124
- version,
125
- cacheStrategy,
126
- retryConfig,
127
- signal: abortControllerRef.current.signal
128
- };
129
- try {
130
- const result = await (0, import_runtime_core.fetchComponent)(id, fetchOptions);
131
- setState({
132
- loading: false,
133
- error: null,
134
- data: result.data
135
- });
136
- onLoad?.();
137
- } catch (error) {
138
- const err = error instanceof Error ? error : new Error(String(error));
139
- if (err.message === "Fetch aborted") return;
140
- setState({
141
- loading: false,
142
- error: err,
143
- data: null
144
- });
145
- onError?.(err);
146
- }
147
- }, [id, version, cacheStrategy, retryConfig, onLoad, onError]);
148
- (0, import_react.useEffect)(() => {
149
- loadComponent();
150
- return () => {
151
- if (abortControllerRef.current) {
152
- abortControllerRef.current.abort();
153
- }
154
- };
155
- }, [loadComponent]);
156
- (0, import_react.useEffect)(() => {
157
- if (!state.data || !containerRef.current) return;
158
- const context = {
159
- props,
160
- state: {},
161
- context: {}
162
- };
163
- if (renderResultRef.current) {
164
- renderResultRef.current.update(context);
165
- return;
166
- }
167
- renderResultRef.current = (0, import_runtime_core.render)({
168
- container: containerRef.current,
169
- elements: state.data.layout,
170
- context,
171
- eventHandlers
172
- });
173
- return () => {
174
- if (renderResultRef.current) {
175
- renderResultRef.current.destroy();
176
- renderResultRef.current = null;
177
- }
178
- };
179
- }, [state.data, props, eventHandlers]);
180
- (0, import_react.useEffect)(() => {
181
- if (!renderResultRef.current || !state.data) return;
182
- const context = {
183
- props,
184
- state: {},
185
- context: {}
186
- };
187
- renderResultRef.current.update(context);
188
- }, [props, state.data]);
189
- if (state.loading) {
190
- if (fallback) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
191
- if (showSkeleton) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingSkeleton, { className });
192
- return null;
193
- }
194
- if (state.error) {
195
- if (fallback) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
196
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
197
- ErrorDisplay,
198
- {
199
- error: state.error,
200
- onRetry: loadComponent,
201
- className,
202
- style
203
- }
204
- );
205
- }
206
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
207
- "div",
208
- {
209
- ref: containerRef,
210
- className: `servly-component ${className || ""}`,
211
- style,
212
- "data-servly-id": id,
213
- "data-servly-version": state.data?.version
214
- }
215
- );
216
- }
217
- var ServlyComponent_default = ServlyComponent;
218
-
219
- // src/index.ts
220
- var import_runtime_core2 = require("@servlyadmin/runtime-core");
221
- // Annotate the CommonJS export names for ESM import in node:
222
- 0 && (module.exports = {
223
- ServlyComponent,
224
- clearAllCaches,
225
- compareVersions,
226
- fetchComponent,
227
- getRegistryUrl,
228
- invalidateCache,
229
- isComponentAvailable,
230
- parseVersion,
231
- prefetchComponents,
232
- resolveVersion,
233
- satisfiesVersion,
234
- setRegistryUrl
235
- });
package/dist/index.d.cts DELETED
@@ -1,44 +0,0 @@
1
- import React from 'react';
2
- import { CacheStrategy, RetryConfig } from '@servlyadmin/runtime-core';
3
- export { BindingContext, CacheStrategy, ComponentData, FetchOptions, LayoutElement, PropDefinition, RetryConfig, clearAllCaches, compareVersions, fetchComponent, getRegistryUrl, invalidateCache, isComponentAvailable, parseVersion, prefetchComponents, resolveVersion, satisfiesVersion, setRegistryUrl } from '@servlyadmin/runtime-core';
4
-
5
- /**
6
- * ServlyComponent
7
- * React wrapper for Servly runtime renderer
8
- */
9
-
10
- /**
11
- * Props for ServlyComponent
12
- */
13
- interface ServlyComponentProps<P = Record<string, any>> {
14
- /** Component ID from the registry */
15
- id: string;
16
- /** Version specifier (exact, range, or "latest") */
17
- version?: string;
18
- /** Props to pass to the component */
19
- props?: P;
20
- /** Fallback UI while loading or on error */
21
- fallback?: React.ReactNode;
22
- /** Error callback */
23
- onError?: (error: Error) => void;
24
- /** Load complete callback */
25
- onLoad?: () => void;
26
- /** Custom className for wrapper */
27
- className?: string;
28
- /** Custom styles for wrapper */
29
- style?: React.CSSProperties;
30
- /** Show loading skeleton */
31
- showSkeleton?: boolean;
32
- /** Cache strategy */
33
- cacheStrategy?: CacheStrategy;
34
- /** Retry configuration */
35
- retryConfig?: Partial<RetryConfig>;
36
- /** Event handlers keyed by element ID then event name */
37
- eventHandlers?: Record<string, Record<string, (e: Event) => void>>;
38
- }
39
- /**
40
- * ServlyComponent - React wrapper for Servly runtime
41
- */
42
- declare function ServlyComponent<P = Record<string, any>>({ id, version, props, fallback, onError, onLoad, className, style, showSkeleton, cacheStrategy, retryConfig, eventHandlers, }: ServlyComponentProps<P>): React.ReactElement | null;
43
-
44
- export { ServlyComponent, type ServlyComponentProps, ServlyComponent as default };
package/dist/index.d.ts DELETED
@@ -1,44 +0,0 @@
1
- import React from 'react';
2
- import { CacheStrategy, RetryConfig } from '@servlyadmin/runtime-core';
3
- export { BindingContext, CacheStrategy, ComponentData, FetchOptions, LayoutElement, PropDefinition, RetryConfig, clearAllCaches, compareVersions, fetchComponent, getRegistryUrl, invalidateCache, isComponentAvailable, parseVersion, prefetchComponents, resolveVersion, satisfiesVersion, setRegistryUrl } from '@servlyadmin/runtime-core';
4
-
5
- /**
6
- * ServlyComponent
7
- * React wrapper for Servly runtime renderer
8
- */
9
-
10
- /**
11
- * Props for ServlyComponent
12
- */
13
- interface ServlyComponentProps<P = Record<string, any>> {
14
- /** Component ID from the registry */
15
- id: string;
16
- /** Version specifier (exact, range, or "latest") */
17
- version?: string;
18
- /** Props to pass to the component */
19
- props?: P;
20
- /** Fallback UI while loading or on error */
21
- fallback?: React.ReactNode;
22
- /** Error callback */
23
- onError?: (error: Error) => void;
24
- /** Load complete callback */
25
- onLoad?: () => void;
26
- /** Custom className for wrapper */
27
- className?: string;
28
- /** Custom styles for wrapper */
29
- style?: React.CSSProperties;
30
- /** Show loading skeleton */
31
- showSkeleton?: boolean;
32
- /** Cache strategy */
33
- cacheStrategy?: CacheStrategy;
34
- /** Retry configuration */
35
- retryConfig?: Partial<RetryConfig>;
36
- /** Event handlers keyed by element ID then event name */
37
- eventHandlers?: Record<string, Record<string, (e: Event) => void>>;
38
- }
39
- /**
40
- * ServlyComponent - React wrapper for Servly runtime
41
- */
42
- declare function ServlyComponent<P = Record<string, any>>({ id, version, props, fallback, onError, onLoad, className, style, showSkeleton, cacheStrategy, retryConfig, eventHandlers, }: ServlyComponentProps<P>): React.ReactElement | null;
43
-
44
- export { ServlyComponent, type ServlyComponentProps, ServlyComponent as default };