@live-react-islands/core 0.1.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/LICENSE +21 -0
- package/dist/IndividualIslandRenderer.d.ts +4 -0
- package/dist/IndividualIslandRenderer.d.ts.map +1 -0
- package/dist/IslandRenderer.d.ts +11 -0
- package/dist/IslandRenderer.d.ts.map +1 -0
- package/dist/PortalIslandsRenderer.d.ts +4 -0
- package/dist/PortalIslandsRenderer.d.ts.map +1 -0
- package/dist/PortalIslandsRenderer.test.d.ts +2 -0
- package/dist/PortalIslandsRenderer.test.d.ts.map +1 -0
- package/dist/client/IndividualIslandRenderer.d.ts +14 -0
- package/dist/client/IndividualIslandRenderer.d.ts.map +1 -0
- package/dist/client/PortalIslandsRenderer.d.ts +4 -0
- package/dist/client/PortalIslandsRenderer.d.ts.map +1 -0
- package/dist/client/islandsManager.d.ts +23 -0
- package/dist/client/islandsManager.d.ts.map +1 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/createHooks.d.ts +25 -0
- package/dist/createHooks.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +680 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +684 -0
- package/dist/index.js.map +1 -0
- package/dist/island-manager.d.ts +59 -0
- package/dist/island-manager.d.ts.map +1 -0
- package/dist/islandsManager.d.ts +32 -0
- package/dist/islandsManager.d.ts.map +1 -0
- package/dist/islandsManager.test.d.ts +2 -0
- package/dist/islandsManager.test.d.ts.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/setupTests.d.ts +2 -0
- package/dist/setupTests.d.ts.map +1 -0
- package/dist/ssr.d.ts +15 -0
- package/dist/ssr.d.ts.map +1 -0
- package/dist/ssr.esm.js +92 -0
- package/dist/ssr.esm.js.map +1 -0
- package/dist/ssr.js +94 -0
- package/dist/ssr.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/useForm.d.ts +60 -0
- package/dist/useForm.d.ts.map +1 -0
- package/dist/useIslandStream.d.ts +15 -0
- package/dist/useIslandStream.d.ts.map +1 -0
- package/dist/useStream.d.ts +9 -0
- package/dist/useStream.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/IndividualIslandRenderer.tsx +80 -0
- package/src/PortalIslandsRenderer.test.tsx +304 -0
- package/src/PortalIslandsRenderer.tsx +118 -0
- package/src/context.ts +9 -0
- package/src/createHooks.ts +341 -0
- package/src/index.ts +4 -0
- package/src/islandsManager.test.ts +454 -0
- package/src/islandsManager.ts +356 -0
- package/src/setupTests.ts +1 -0
- package/src/ssr.ts +118 -0
- package/src/types.ts +91 -0
- package/src/useForm.ts +253 -0
- package/src/useStream.ts +36 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ContextProviderComponent,
|
|
6
|
+
IslandData,
|
|
7
|
+
SharedIslandsRendererComponent,
|
|
8
|
+
IndividualIslandsRendererComponent,
|
|
9
|
+
IslandStoreAccess,
|
|
10
|
+
IndividualIslandsRendererProps,
|
|
11
|
+
StreamAction,
|
|
12
|
+
StreamConfig,
|
|
13
|
+
} from "./types";
|
|
14
|
+
import { PortalIslandsRenderer } from "./PortalIslandsRenderer";
|
|
15
|
+
import { IndividualIslandRenderer } from "./IndividualIslandRenderer";
|
|
16
|
+
|
|
17
|
+
// Constant root ID - one shared root per application
|
|
18
|
+
export const SHARED_ROOT_ID = "__live_react_islands_shared_root__";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
interface ManagerState {
|
|
25
|
+
propsStore: Map<string, Record<string, any>>;
|
|
26
|
+
propsListeners: Set<() => void>;
|
|
27
|
+
globalsRef: React.MutableRefObject<Record<string, any> | null>;
|
|
28
|
+
globalsListeners: Set<() => void>;
|
|
29
|
+
sharedIslandsRef: React.MutableRefObject<Record<string, IslandData>>;
|
|
30
|
+
sharedIslandsListeners: Set<() => void>;
|
|
31
|
+
streamsStore: Map<string, any[]>;
|
|
32
|
+
streamsListeners: Map<string, Set<() => void>>;
|
|
33
|
+
streamConfigs: Map<string, StreamConfig>;
|
|
34
|
+
roots: Record<string, any>;
|
|
35
|
+
storeAccess: IslandStoreAccess;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Manager {
|
|
39
|
+
initialize: (params: {
|
|
40
|
+
SharedContextProvider?: ContextProviderComponent;
|
|
41
|
+
SharedIslandsRenderer?: SharedIslandsRendererComponent;
|
|
42
|
+
}) => ManagerState;
|
|
43
|
+
mountIsland: (
|
|
44
|
+
state: ManagerState,
|
|
45
|
+
data: IslandData,
|
|
46
|
+
IslandRenderer?: IndividualIslandsRendererComponent
|
|
47
|
+
) => ManagerState;
|
|
48
|
+
unmountIsland: (state: ManagerState, id: string) => ManagerState;
|
|
49
|
+
updateIslandProps: (
|
|
50
|
+
state: ManagerState,
|
|
51
|
+
islandId: string,
|
|
52
|
+
props: Record<string, any>
|
|
53
|
+
) => ManagerState;
|
|
54
|
+
updateGlobals: (
|
|
55
|
+
state: ManagerState,
|
|
56
|
+
globals: Record<string, any>
|
|
57
|
+
) => ManagerState;
|
|
58
|
+
resetGlobals: (state: ManagerState) => ManagerState;
|
|
59
|
+
initializeStream: (
|
|
60
|
+
state: ManagerState,
|
|
61
|
+
islandId: string,
|
|
62
|
+
streamName: string,
|
|
63
|
+
initialItems: any[]
|
|
64
|
+
) => ManagerState;
|
|
65
|
+
updateStream: (
|
|
66
|
+
state: ManagerState,
|
|
67
|
+
islandId: string,
|
|
68
|
+
streamName: string,
|
|
69
|
+
action: StreamAction,
|
|
70
|
+
data: any
|
|
71
|
+
) => ManagerState;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Helper
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
const NullContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
79
|
+
children,
|
|
80
|
+
}) => children;
|
|
81
|
+
|
|
82
|
+
const withSharedRootName = (
|
|
83
|
+
ContextProvider: ContextProviderComponent
|
|
84
|
+
): ContextProviderComponent => {
|
|
85
|
+
const originalName =
|
|
86
|
+
ContextProvider.displayName || ContextProvider.name || "ContextProvider";
|
|
87
|
+
|
|
88
|
+
function NamedWrapper({ children }: { children: React.ReactNode }) {
|
|
89
|
+
return React.createElement(ContextProvider, null, children);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
NamedWrapper.displayName = `[LiveReactIslands] SharedRoot(${originalName})`;
|
|
93
|
+
return NamedWrapper;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const withIndividualRootName = (
|
|
97
|
+
islandId: string,
|
|
98
|
+
IslandRenderer: IndividualIslandsRendererComponent
|
|
99
|
+
): IndividualIslandsRendererComponent => {
|
|
100
|
+
const originalName = IslandRenderer.displayName;
|
|
101
|
+
|
|
102
|
+
function NamedWrapper(props: IndividualIslandsRendererProps) {
|
|
103
|
+
return React.createElement(IslandRenderer, props);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
NamedWrapper.displayName = `[LiveReactIslands] HydratedRoot#${islandId}`;
|
|
107
|
+
return NamedWrapper;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getOrCreateSharedRootElement = (): HTMLElement => {
|
|
111
|
+
let rootElement = document.getElementById(SHARED_ROOT_ID);
|
|
112
|
+
if (!rootElement) {
|
|
113
|
+
rootElement = document.createElement("div");
|
|
114
|
+
rootElement.id = SHARED_ROOT_ID;
|
|
115
|
+
rootElement.style.display = "none";
|
|
116
|
+
document.body.appendChild(rootElement);
|
|
117
|
+
}
|
|
118
|
+
return rootElement;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const mountSharedIsland = (
|
|
122
|
+
state: ManagerState,
|
|
123
|
+
data: IslandData
|
|
124
|
+
): ManagerState => {
|
|
125
|
+
state.sharedIslandsRef.current = {
|
|
126
|
+
...state.sharedIslandsRef.current,
|
|
127
|
+
[data.id]: data,
|
|
128
|
+
};
|
|
129
|
+
state.sharedIslandsListeners.forEach((cb) => cb());
|
|
130
|
+
return state;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const mountIndividualIsland = (
|
|
134
|
+
state: ManagerState,
|
|
135
|
+
data: IslandData,
|
|
136
|
+
IslandRenderer: IndividualIslandsRendererComponent
|
|
137
|
+
): ManagerState => {
|
|
138
|
+
const NamedIslandRenderer = withIndividualRootName(data.id, IslandRenderer);
|
|
139
|
+
const root = ReactDOM.hydrateRoot(
|
|
140
|
+
data.el,
|
|
141
|
+
React.createElement(
|
|
142
|
+
React.StrictMode,
|
|
143
|
+
null,
|
|
144
|
+
React.createElement(NamedIslandRenderer, {
|
|
145
|
+
data,
|
|
146
|
+
storeAccess: state.storeAccess,
|
|
147
|
+
})
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...state,
|
|
153
|
+
roots: { ...state.roots, [data.id]: root },
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Island Manager (Bridge between LiveView hook lifecycles and React rendering)
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
const ManagerObj: Manager = {
|
|
162
|
+
initialize: ({
|
|
163
|
+
SharedContextProvider = NullContextProvider,
|
|
164
|
+
SharedIslandsRenderer = PortalIslandsRenderer,
|
|
165
|
+
}) => {
|
|
166
|
+
const rootElement = getOrCreateSharedRootElement();
|
|
167
|
+
const root = ReactDOM.createRoot(rootElement);
|
|
168
|
+
|
|
169
|
+
const propsStore = new Map<string, Record<string, any>>();
|
|
170
|
+
const propsListeners = new Set<() => void>();
|
|
171
|
+
const globalsRef = { current: null as Record<string, any> | null };
|
|
172
|
+
const globalsListeners = new Set<() => void>();
|
|
173
|
+
const sharedIslandsRef = { current: {} as Record<string, IslandData> };
|
|
174
|
+
const sharedIslandsListeners = new Set<() => void>();
|
|
175
|
+
const streamsStore = new Map<string, any[]>();
|
|
176
|
+
const streamsListeners = new Map<string, Set<() => void>>();
|
|
177
|
+
const streamConfigs = new Map<string, StreamConfig>();
|
|
178
|
+
|
|
179
|
+
const getStreamKey = (islandId: string, streamName: string) =>
|
|
180
|
+
`${islandId}:${streamName}`;
|
|
181
|
+
|
|
182
|
+
const storeAccess: IslandStoreAccess = {
|
|
183
|
+
getProps: (islandId: string) => propsStore.get(islandId) || null,
|
|
184
|
+
subscribeToProps: (cb: () => void) => {
|
|
185
|
+
propsListeners.add(cb);
|
|
186
|
+
return () => propsListeners.delete(cb);
|
|
187
|
+
},
|
|
188
|
+
getGlobals: () => globalsRef.current,
|
|
189
|
+
subscribeToGlobals: (cb: () => void) => {
|
|
190
|
+
globalsListeners.add(cb);
|
|
191
|
+
return () => globalsListeners.delete(cb);
|
|
192
|
+
},
|
|
193
|
+
getSharedIslands: () => sharedIslandsRef.current,
|
|
194
|
+
subscribeToSharedIslands: (cb: () => void) => {
|
|
195
|
+
sharedIslandsListeners.add(cb);
|
|
196
|
+
return () => sharedIslandsListeners.delete(cb);
|
|
197
|
+
},
|
|
198
|
+
getStreamItems: (islandId: string, streamName: string) => {
|
|
199
|
+
const key = getStreamKey(islandId, streamName);
|
|
200
|
+
return streamsStore.get(key) || [];
|
|
201
|
+
},
|
|
202
|
+
subscribeToStream: (
|
|
203
|
+
islandId: string,
|
|
204
|
+
streamName: string,
|
|
205
|
+
cb: () => void,
|
|
206
|
+
config?: StreamConfig
|
|
207
|
+
) => {
|
|
208
|
+
const key = getStreamKey(islandId, streamName);
|
|
209
|
+
if (config) {
|
|
210
|
+
streamConfigs.set(key, config);
|
|
211
|
+
}
|
|
212
|
+
if (!streamsListeners.has(key)) {
|
|
213
|
+
streamsListeners.set(key, new Set());
|
|
214
|
+
}
|
|
215
|
+
streamsListeners.get(key)!.add(cb);
|
|
216
|
+
return () => {
|
|
217
|
+
streamsListeners.get(key)?.delete(cb);
|
|
218
|
+
if (streamsListeners.get(key)?.size === 0) {
|
|
219
|
+
streamConfigs.delete(key);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const state: ManagerState = {
|
|
226
|
+
propsStore,
|
|
227
|
+
globalsRef,
|
|
228
|
+
propsListeners,
|
|
229
|
+
globalsListeners,
|
|
230
|
+
sharedIslandsRef,
|
|
231
|
+
sharedIslandsListeners,
|
|
232
|
+
streamsStore,
|
|
233
|
+
streamsListeners,
|
|
234
|
+
streamConfigs,
|
|
235
|
+
roots: { [SHARED_ROOT_ID]: root },
|
|
236
|
+
storeAccess,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const NamedSharedContextProvider = withSharedRootName(
|
|
240
|
+
SharedContextProvider
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
root.render(
|
|
244
|
+
React.createElement(
|
|
245
|
+
React.StrictMode,
|
|
246
|
+
null,
|
|
247
|
+
React.createElement(
|
|
248
|
+
NamedSharedContextProvider,
|
|
249
|
+
null,
|
|
250
|
+
React.createElement(SharedIslandsRenderer, {
|
|
251
|
+
storeAccess: state.storeAccess,
|
|
252
|
+
})
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return state;
|
|
258
|
+
},
|
|
259
|
+
mountIsland: (state, data, IslandRenderer = IndividualIslandRenderer) => {
|
|
260
|
+
switch (data.ssrStrategy) {
|
|
261
|
+
case "hydrate_root":
|
|
262
|
+
return mountIndividualIsland(state, data, IslandRenderer);
|
|
263
|
+
default:
|
|
264
|
+
return mountSharedIsland(state, data);
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
unmountIsland: (state, id) => {
|
|
268
|
+
state.propsStore.delete(id);
|
|
269
|
+
|
|
270
|
+
const streamPrefix = `${id}:`;
|
|
271
|
+
for (const key of state.streamsStore.keys()) {
|
|
272
|
+
if (key.startsWith(streamPrefix)) {
|
|
273
|
+
state.streamsStore.delete(key);
|
|
274
|
+
state.streamsListeners.delete(key);
|
|
275
|
+
state.streamConfigs.delete(key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (state.roots[id]) {
|
|
280
|
+
state.roots[id].unmount();
|
|
281
|
+
const { [id]: removedRoot, ...remainingRoots } = state.roots;
|
|
282
|
+
return {
|
|
283
|
+
...state,
|
|
284
|
+
roots: remainingRoots,
|
|
285
|
+
};
|
|
286
|
+
} else {
|
|
287
|
+
const { [id]: removed, ...remaining } = state.sharedIslandsRef.current;
|
|
288
|
+
state.sharedIslandsRef.current = remaining;
|
|
289
|
+
state.sharedIslandsListeners.forEach((cb) => cb());
|
|
290
|
+
return state;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
updateIslandProps: (state, islandId, props) => {
|
|
294
|
+
const existing = state.propsStore.get(islandId) || {};
|
|
295
|
+
state.propsStore.set(islandId, { ...existing, ...props });
|
|
296
|
+
state.propsListeners.forEach((cb) => cb());
|
|
297
|
+
return state;
|
|
298
|
+
},
|
|
299
|
+
updateGlobals: (state, globals) => {
|
|
300
|
+
const incomingVersion = globals.__version ?? -1;
|
|
301
|
+
const storeVersion = state.globalsRef.current?.__version ?? -1;
|
|
302
|
+
if (incomingVersion > storeVersion) {
|
|
303
|
+
state.globalsRef.current = {
|
|
304
|
+
...(state.globalsRef.current || {}),
|
|
305
|
+
...globals,
|
|
306
|
+
};
|
|
307
|
+
state.globalsListeners.forEach((cb) => cb());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return state;
|
|
311
|
+
},
|
|
312
|
+
resetGlobals: (state) => {
|
|
313
|
+
state.globalsRef.current = null;
|
|
314
|
+
return state;
|
|
315
|
+
},
|
|
316
|
+
initializeStream: (state, islandId, streamName, initialItems) => {
|
|
317
|
+
const key = `${islandId}:${streamName}`;
|
|
318
|
+
state.streamsStore.set(key, [...initialItems]);
|
|
319
|
+
return state;
|
|
320
|
+
},
|
|
321
|
+
updateStream: (state, islandId, streamName, action, data) => {
|
|
322
|
+
const key = `${islandId}:${streamName}`;
|
|
323
|
+
let items = state.streamsStore.get(key) || [];
|
|
324
|
+
const config = state.streamConfigs.get(key);
|
|
325
|
+
|
|
326
|
+
switch (action) {
|
|
327
|
+
case StreamAction.Insert:
|
|
328
|
+
items = [data, ...items];
|
|
329
|
+
break;
|
|
330
|
+
case StreamAction.Update:
|
|
331
|
+
const idx = items.findIndex((item) => item.id === data.id);
|
|
332
|
+
if (idx !== -1) {
|
|
333
|
+
items = [...items];
|
|
334
|
+
items[idx] = { ...items[idx], ...data };
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
case StreamAction.Delete:
|
|
338
|
+
items = items.filter((item) => item.id !== data);
|
|
339
|
+
break;
|
|
340
|
+
case StreamAction.Reset:
|
|
341
|
+
items = [];
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (config?.capper) {
|
|
346
|
+
items = config.capper(items);
|
|
347
|
+
} else if (config?.limit !== undefined && items.length > config.limit) {
|
|
348
|
+
items = items.slice(0, config.limit);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
state.streamsStore.set(key, items);
|
|
352
|
+
state.streamsListeners.get(key)?.forEach((cb) => cb());
|
|
353
|
+
return state;
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
export default ManagerObj;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
package/src/ssr.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderToString, renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import {
|
|
4
|
+
type IslandsMap,
|
|
5
|
+
type IslandComponent,
|
|
6
|
+
type ContextProviderComponent,
|
|
7
|
+
type LazyIslandFactory,
|
|
8
|
+
NullContextProvider,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
export interface ExposeSSROptions {
|
|
12
|
+
islands: IslandsMap;
|
|
13
|
+
SharedContextProvider?: ContextProviderComponent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isLazyFactory = (value: unknown): value is LazyIslandFactory => {
|
|
17
|
+
if (typeof value !== "function") return false;
|
|
18
|
+
if ((value as any).prototype?.isReactComponent) return false;
|
|
19
|
+
if ((value as any).Component) return false;
|
|
20
|
+
return value.length === 0;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Server-side rendering function for React islands.
|
|
25
|
+
*
|
|
26
|
+
* This function creates SSR renderers for your island components.
|
|
27
|
+
* It works in any JavaScript runtime: Deno, Node, Bun, etc.
|
|
28
|
+
*
|
|
29
|
+
* Supports lazy-loaded islands: () => import("./Component")
|
|
30
|
+
*/
|
|
31
|
+
export async function exposeSSR({
|
|
32
|
+
islands,
|
|
33
|
+
SharedContextProvider = NullContextProvider,
|
|
34
|
+
}: ExposeSSROptions) {
|
|
35
|
+
// Resolve all lazy imports first
|
|
36
|
+
const resolvedIslands: Record<string, IslandComponent> = {};
|
|
37
|
+
|
|
38
|
+
for (const [name, config] of Object.entries(islands)) {
|
|
39
|
+
if (isLazyFactory(config)) {
|
|
40
|
+
const mod = await config();
|
|
41
|
+
resolvedIslands[name] = mod.default;
|
|
42
|
+
} else if (typeof config === "function") {
|
|
43
|
+
resolvedIslands[name] = config;
|
|
44
|
+
} else {
|
|
45
|
+
resolvedIslands[name] = config.Component;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const makeRenderer =
|
|
49
|
+
(renderFn: typeof renderToString | typeof renderToStaticMarkup) =>
|
|
50
|
+
(IslandComponent: any) =>
|
|
51
|
+
(
|
|
52
|
+
id: string,
|
|
53
|
+
props: Record<string, any> = {},
|
|
54
|
+
globals: Record<string, any> = {}
|
|
55
|
+
) => {
|
|
56
|
+
return renderFn(
|
|
57
|
+
React.createElement(
|
|
58
|
+
SharedContextProvider,
|
|
59
|
+
null,
|
|
60
|
+
React.createElement(IslandComponent, {
|
|
61
|
+
...globals,
|
|
62
|
+
...props,
|
|
63
|
+
id,
|
|
64
|
+
pushEvent: () => {},
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Create renderers for both strategies using pre-resolved components
|
|
71
|
+
// - renderToString: For hydrate_root (includes React markers for hydration)
|
|
72
|
+
// - renderToStaticMarkup: For overwrite (clean HTML, smaller size)
|
|
73
|
+
const hydrateRenderers = Object.fromEntries(
|
|
74
|
+
Object.entries(resolvedIslands).map(([key, Component]) => [
|
|
75
|
+
key,
|
|
76
|
+
makeRenderer(renderToString)(Component),
|
|
77
|
+
])
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const overwriteRenderers = Object.fromEntries(
|
|
81
|
+
Object.entries(resolvedIslands).map(([key, Component]) => [
|
|
82
|
+
key,
|
|
83
|
+
makeRenderer(renderToStaticMarkup)(Component),
|
|
84
|
+
])
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Main render function that chooses renderer based on strategy
|
|
88
|
+
const renderSSRIsland = (
|
|
89
|
+
key: string,
|
|
90
|
+
id: string,
|
|
91
|
+
props: Record<string, any> = {},
|
|
92
|
+
globalState: any = {},
|
|
93
|
+
strategy: "hydrate_root" | "overwrite" = "overwrite"
|
|
94
|
+
): string => {
|
|
95
|
+
const renderers =
|
|
96
|
+
strategy === "hydrate_root" ? hydrateRenderers : overwriteRenderers;
|
|
97
|
+
if (!renderers[key]) {
|
|
98
|
+
throw new Error(`No SSR renderer found for Component "${key}"`);
|
|
99
|
+
}
|
|
100
|
+
return renderers[key](id, props, globalState);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getGlobal = () => {
|
|
104
|
+
if (typeof globalThis !== "undefined") return globalThis;
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
if (typeof global !== "undefined") return global;
|
|
107
|
+
if (typeof window !== "undefined") return window;
|
|
108
|
+
return {};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const universalGlobal = getGlobal();
|
|
112
|
+
//@ts-ignore
|
|
113
|
+
universalGlobal.SSR_MODULE = {
|
|
114
|
+
renderSSRIsland,
|
|
115
|
+
hydrateRenderers,
|
|
116
|
+
overwriteRenderers,
|
|
117
|
+
};
|
|
118
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Core Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export type IslandComponent = ComponentType<any>;
|
|
8
|
+
|
|
9
|
+
export type LazyIslandFactory = () => Promise<{ default: IslandComponent }>;
|
|
10
|
+
|
|
11
|
+
export type IslandsMap = Record<
|
|
12
|
+
string,
|
|
13
|
+
IslandComponent | IslandConfig | LazyIslandFactory
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export type ContextProviderComponent = ComponentType<{
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export interface IslandConfig {
|
|
21
|
+
Component: IslandComponent;
|
|
22
|
+
ContextProvider?: ContextProviderComponent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SSRStrategy = "none" | "overwrite" | "hydrate_root";
|
|
26
|
+
|
|
27
|
+
export type PushEventFn = (eventName: string, payload: any) => void;
|
|
28
|
+
|
|
29
|
+
export interface StreamConfig {
|
|
30
|
+
limit?: number;
|
|
31
|
+
capper?: (items: any[]) => any[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IslandStoreAccess {
|
|
35
|
+
getProps: (islandId: string) => Record<string, any> | null;
|
|
36
|
+
subscribeToProps: (callback: () => void) => () => void;
|
|
37
|
+
getGlobals: () => Record<string, any> | null;
|
|
38
|
+
subscribeToGlobals: (callback: () => void) => () => void;
|
|
39
|
+
getSharedIslands: () => Record<string, IslandData>;
|
|
40
|
+
subscribeToSharedIslands: (callback: () => void) => () => void;
|
|
41
|
+
getStreamItems: (islandId: string, streamName: string) => any[];
|
|
42
|
+
subscribeToStream: (
|
|
43
|
+
islandId: string,
|
|
44
|
+
streamName: string,
|
|
45
|
+
callback: () => void,
|
|
46
|
+
config?: StreamConfig
|
|
47
|
+
) => () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export enum StreamAction {
|
|
51
|
+
Insert = "i",
|
|
52
|
+
Update = "u",
|
|
53
|
+
Delete = "d",
|
|
54
|
+
Reset = "r",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StreamHandle {
|
|
58
|
+
name: string;
|
|
59
|
+
initial: any[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface IslandData {
|
|
63
|
+
id: string;
|
|
64
|
+
el: HTMLElement;
|
|
65
|
+
ssrStrategy: SSRStrategy;
|
|
66
|
+
Component: IslandComponent;
|
|
67
|
+
ContextProvider?: ContextProviderComponent;
|
|
68
|
+
hydrationData: {
|
|
69
|
+
props: Record<string, any>;
|
|
70
|
+
globals: Record<string, any>;
|
|
71
|
+
} | null;
|
|
72
|
+
pushEvent: PushEventFn;
|
|
73
|
+
globalKeys: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SharedIslandsRendererProps {
|
|
77
|
+
storeAccess: IslandStoreAccess;
|
|
78
|
+
}
|
|
79
|
+
export type SharedIslandsRendererComponent =
|
|
80
|
+
React.FC<SharedIslandsRendererProps>;
|
|
81
|
+
|
|
82
|
+
export interface IndividualIslandsRendererProps {
|
|
83
|
+
data: IslandData;
|
|
84
|
+
storeAccess: IslandStoreAccess;
|
|
85
|
+
}
|
|
86
|
+
export type IndividualIslandsRendererComponent =
|
|
87
|
+
React.FC<IndividualIslandsRendererProps>;
|
|
88
|
+
|
|
89
|
+
export const NullContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
90
|
+
children,
|
|
91
|
+
}) => children;
|