@live-react-islands/vite-plugin-ssr 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/README.md +455 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +76 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +154 -0
package/README.md
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# Live React Islands
|
|
2
|
+
|
|
3
|
+
**React-powered interactive islands** inside Phoenix LiveView. Harness the NPM ecosystem with server-driven state, real-time streams and zero-lag forms + SSR.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Why Live React Islands?
|
|
8
|
+
|
|
9
|
+
**The best of both worlds!** ✨
|
|
10
|
+
|
|
11
|
+
Phoenix LiveView is excellent for server-driven UIs, but sometimes you need the rich interactivity of React for specific components. Live React Islands lets you:
|
|
12
|
+
|
|
13
|
+
- Use React components as "islands" within your LiveView templates
|
|
14
|
+
- Maintain server-side state in Elixir while rendering in React
|
|
15
|
+
- Send events from React to Elixir and push props back
|
|
16
|
+
- Use forms and streams in React with dedicated hooks
|
|
17
|
+
- Share global state across multiple islands
|
|
18
|
+
- Optionally server-side render for faster loads, SEO and no flicker
|
|
19
|
+
|
|
20
|
+
## Comparison
|
|
21
|
+
|
|
22
|
+
Choose Live React Islands when you need rich, interactive React components without giving up LiveView’s server-driven simplicity.
|
|
23
|
+
|
|
24
|
+
| Feature | LiveView Only | LiveView + Alpine | Live React Islands (this) | Pure SPA (Next.js/Vite) |
|
|
25
|
+
| :--------------------- | :----------------------------------------- | :------------------- | :------------------------------------- | :---------------------- |
|
|
26
|
+
| **UI Ecosystem** | Limited (HEEX/Custom) | Small (Alpine kits) | **Infinite (NPM/React)** | **Infinite (NPM)** |
|
|
27
|
+
| **Interactivity** | Server-Roundtrip (JS hooks for edge cases) | Simple Client-side | **High-Fidelity / Fluid** | High-Fidelity / Fluid |
|
|
28
|
+
| **State Management** | Single (Server) | Fragmented | **Single (Server-Led)** | Dual (API + Client) |
|
|
29
|
+
| **Initial Load / SEO** | Instant | Instant | **Instant (SSR-enabled)** | Slow / Complex SSR |
|
|
30
|
+
| **JS Bundle Size** | ~0kb (Core only) | Small (+15kb) | **Large (React: ~100–150 kB gzipped)** | Large |
|
|
31
|
+
| **Developer Speed** | Very High | High (until complex) | **High (Asset Reuse)** | Low (API Plumbing) |
|
|
32
|
+
| **Component Logic** | Elixir Only | Mixed (Strings) | **JSX (Encapsulated)** | JSX |
|
|
33
|
+
| **Complexity Ceiling** | Struggles with app-like complexity | Hits wall on "State" | **High** | Very High |
|
|
34
|
+
|
|
35
|
+
## When NOT to Use Live React Islands
|
|
36
|
+
|
|
37
|
+
- If your UI is mostly static or CRUD-heavy, plain LiveView is simpler and faster.
|
|
38
|
+
- If you only need light client-side behavior (toggles, dropdowns), LiveView + Alpine may be sufficient.
|
|
39
|
+
- If your application requires full offline support or heavy client-side state, a traditional SPA may be a better fit.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
### Elixir
|
|
44
|
+
|
|
45
|
+
Add to your `mix.exs`:
|
|
46
|
+
|
|
47
|
+
```elixir
|
|
48
|
+
def deps do
|
|
49
|
+
[
|
|
50
|
+
{:live_react_islands, "~> 0.1.0"},
|
|
51
|
+
# For development SSR (optional):
|
|
52
|
+
{:live_react_islands_ssr_vite, "~> 0.1.0", only: :dev},
|
|
53
|
+
# For production SSR (optional):
|
|
54
|
+
{:live_react_islands_ssr_deno, "~> 0.1.0", only: :prod}
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### JavaScript
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @live-react-islands/core
|
|
63
|
+
# For development SSR (optional):
|
|
64
|
+
npm install --save-dev @live-react-islands/vite-plugin-ssr
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### 1. Create a React Component
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
// src/islands/Counter.jsx
|
|
73
|
+
const Counter = ({ count, title, pushEvent }) => {
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<h2>{title}</h2>
|
|
77
|
+
<p>Count: {count}</p>
|
|
78
|
+
<button onClick={() => pushEvent("increment", {})}>+1</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default Counter;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Set Up the LiveView Hooks
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
// src/islands/index.js
|
|
90
|
+
export default { Counter: () => import("./Counter") };
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Islands can be lazy loaded to only load the JS used on the page.
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
// src/main.jsx
|
|
97
|
+
import { createHooks } from "@live-react-islands/core";
|
|
98
|
+
import islands from "./islands";
|
|
99
|
+
|
|
100
|
+
const islandHooks = createHooks({ islands });
|
|
101
|
+
|
|
102
|
+
// Add to your LiveSocket
|
|
103
|
+
let liveSocket = new LiveSocket("/live", Socket, {
|
|
104
|
+
hooks: { ...islandHooks },
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Create an Elixir Component
|
|
109
|
+
|
|
110
|
+
```elixir
|
|
111
|
+
defmodule MyAppWeb.Components.CounterIsland do
|
|
112
|
+
use LiveReactIslands.Component,
|
|
113
|
+
component: "Counter",
|
|
114
|
+
props: %{count: 0, title: "My Counter"}
|
|
115
|
+
|
|
116
|
+
def handle_event("increment", _params, socket) do
|
|
117
|
+
new_count = socket.assigns.count + 1
|
|
118
|
+
{:noreply, update_prop(socket, :count, new_count)}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Use in Your LiveView
|
|
124
|
+
|
|
125
|
+
```elixir
|
|
126
|
+
defmodule MyAppWeb.CounterLive do
|
|
127
|
+
use MyAppWeb, :live_view
|
|
128
|
+
use LiveReactIslands.LiveView
|
|
129
|
+
|
|
130
|
+
def render(assigns) do
|
|
131
|
+
~H"""
|
|
132
|
+
<.live_component module={MyAppWeb.Components.CounterIsland} id="counter-1" />
|
|
133
|
+
"""
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How It Works
|
|
139
|
+
|
|
140
|
+
**React components receive these props automatically:**
|
|
141
|
+
|
|
142
|
+
| Prop | Description |
|
|
143
|
+
| -------------------- | --------------------------------- |
|
|
144
|
+
| `id` | The island's unique identifier |
|
|
145
|
+
| `pushEvent` | Function to send events to Elixir |
|
|
146
|
+
| All defined props | Current values from Elixir |
|
|
147
|
+
| All consumed globals | Current global state values |
|
|
148
|
+
|
|
149
|
+
## Features
|
|
150
|
+
|
|
151
|
+
### Props
|
|
152
|
+
|
|
153
|
+
Define props with default values. Props can be set from the template or updated from event handlers:
|
|
154
|
+
|
|
155
|
+
```elixir
|
|
156
|
+
use LiveReactIslands.Component,
|
|
157
|
+
component: "Counter",
|
|
158
|
+
props: %{count: 0, title: "Default Title"}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Elixir components can override `init/2` for dynamic initialization:**
|
|
162
|
+
|
|
163
|
+
```elixir
|
|
164
|
+
def init(assigns, socket) do
|
|
165
|
+
# Called once on mount, before SSR and first render
|
|
166
|
+
socket
|
|
167
|
+
|> update_prop(:computed, compute_value(assigns))
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Updating props from Elixir:**
|
|
172
|
+
|
|
173
|
+
```elixir
|
|
174
|
+
def handle_event("increment", _, socket) do
|
|
175
|
+
{:noreply, update_prop(socket, :count, socket.assigns.count + 1)}
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Passing props from templates:**
|
|
180
|
+
|
|
181
|
+
```heex
|
|
182
|
+
<.live_component module={CounterIsland} id="counter-1" title="Custom Title" />
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Once a prop is set from outside the component any `update_prop` call on it will raise an error to prevent a nasty set of bugs. To just initialize the component use `init_[prop]` to set the value once and then the component takes over.
|
|
186
|
+
|
|
187
|
+
```heex
|
|
188
|
+
<.live_component module={CounterIsland} id="counter-1" init_count={5} />
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Events
|
|
192
|
+
|
|
193
|
+
Send events from React to Elixir using `pushEvent`:
|
|
194
|
+
|
|
195
|
+
```jsx
|
|
196
|
+
// React
|
|
197
|
+
<button onClick={() => pushEvent("save", { data: formData })}>Save</button>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```elixir
|
|
201
|
+
# Elixir
|
|
202
|
+
def handle_event("save", %{"data" => data}, socket) do
|
|
203
|
+
# Handle the event
|
|
204
|
+
{:noreply, socket}
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Global State
|
|
209
|
+
|
|
210
|
+
Share state across multiple islands. When a global changes, all islands that use it automatically rerender.
|
|
211
|
+
|
|
212
|
+
**Set up in your LiveView:**
|
|
213
|
+
|
|
214
|
+
```elixir
|
|
215
|
+
defmodule MyAppWeb.DashboardLive do
|
|
216
|
+
use MyAppWeb, :live_view
|
|
217
|
+
use LiveReactIslands.LiveView, expose_globals: [:user, :theme]
|
|
218
|
+
|
|
219
|
+
def mount(_params, session, socket) do
|
|
220
|
+
{:ok, assign(socket, user: get_user(session), theme: "light")}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Consume in your island:**
|
|
226
|
+
|
|
227
|
+
```elixir
|
|
228
|
+
use LiveReactIslands.Component,
|
|
229
|
+
component: "Header",
|
|
230
|
+
props: %{},
|
|
231
|
+
globals: [:user, :theme]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Optional globals** (won't error if not set):
|
|
235
|
+
|
|
236
|
+
```elixir
|
|
237
|
+
globals: [:user?] # The ? suffix makes it optional
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The globals are passed as props to your React component:
|
|
241
|
+
|
|
242
|
+
```jsx
|
|
243
|
+
const Header = ({ user, theme }) => (
|
|
244
|
+
<header className={theme}>Welcome, {user.name}</header>
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Forms with Server Validation
|
|
249
|
+
|
|
250
|
+
Build forms with React UI and Elixir/Ecto validation. Input is collected client side with zero typing latency and send to Elixir for validation. Errors from the changeset get pushed back to React.
|
|
251
|
+
|
|
252
|
+
The `useForm` hook implements a "Validation Lock" pattern: Updates are versioned and `isValid` will only be true until the server confirms the current form state is valid.
|
|
253
|
+
|
|
254
|
+
**Elixir component:**
|
|
255
|
+
|
|
256
|
+
```elixir
|
|
257
|
+
defmodule MyAppWeb.Components.ContactFormIsland do
|
|
258
|
+
use LiveReactIslands.Component,
|
|
259
|
+
component: "ContactForm",
|
|
260
|
+
props: %{form: %{}}
|
|
261
|
+
|
|
262
|
+
alias MyApp.Contact
|
|
263
|
+
|
|
264
|
+
def init(_assigns, socket) do
|
|
265
|
+
changeset = Contact.changeset(%Contact{}, %{})
|
|
266
|
+
socket |> init_form(:form, changeset)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def handle_form(:validate, :form, attrs, socket) do
|
|
270
|
+
changeset = Contact.changeset(%Contact{}, attrs)
|
|
271
|
+
{:noreply, update_form(socket, :form, changeset)}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def handle_form(:submit, :form, attrs, socket) do
|
|
275
|
+
case Contact.create(attrs) do
|
|
276
|
+
{:ok, _contact} ->
|
|
277
|
+
{:noreply, init_form(socket, :form, Contact.changeset(%Contact{}, %{}))}
|
|
278
|
+
{:error, changeset} ->
|
|
279
|
+
{:noreply, update_form(socket, :form, changeset)}
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**React component:**
|
|
286
|
+
|
|
287
|
+
```jsx
|
|
288
|
+
import { useForm } from "@live-react-islands/core";
|
|
289
|
+
|
|
290
|
+
const ContactForm = ({ form, pushEvent }) => {
|
|
291
|
+
const {
|
|
292
|
+
getFieldProps,
|
|
293
|
+
getError,
|
|
294
|
+
isRequired,
|
|
295
|
+
isTouched,
|
|
296
|
+
handleSubmit,
|
|
297
|
+
isValid,
|
|
298
|
+
} = useForm(form, pushEvent);
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<form onSubmit={handleSubmit}>
|
|
302
|
+
<input {...getFieldProps("name")} />
|
|
303
|
+
{isTouched("name") && getError("name") && (
|
|
304
|
+
<span className="error">{getError("name")}</span>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
<input {...getFieldProps("email")} type="email" />
|
|
308
|
+
{isTouched("email") && getError("email") && (
|
|
309
|
+
<span className="error">{getError("email")}</span>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<button type="submit" disabled={!isValid}>
|
|
313
|
+
Submit
|
|
314
|
+
</button>
|
|
315
|
+
</form>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**`useForm` returns:**
|
|
321
|
+
|
|
322
|
+
| Property | Description |
|
|
323
|
+
| ----------------------- | ----------------------------------------------------- |
|
|
324
|
+
| `values` | Current form values |
|
|
325
|
+
| `errors` | Validation errors by field |
|
|
326
|
+
| `touched` | Fields the user has interacted with |
|
|
327
|
+
| `getFieldProps(name)` | Props to spread on inputs (`value`, `onChange`, etc.) |
|
|
328
|
+
| `getError(name)` | First error message for a field |
|
|
329
|
+
| `isRequired(name)` | Whether a field is required |
|
|
330
|
+
| `isTouched(name)` | Whether user has modified this field |
|
|
331
|
+
| `setField(name, value)` | Programmatically set a field value |
|
|
332
|
+
| `handleSubmit` | Form submit handler |
|
|
333
|
+
| `reset()` | Reset form to server values |
|
|
334
|
+
| `isSyncing` | True while waiting for server validation |
|
|
335
|
+
| `isValid` | True only when synced AND server says valid |
|
|
336
|
+
|
|
337
|
+
### Streams
|
|
338
|
+
|
|
339
|
+
Stream data to React components for real-time updates like feeds, chat, or infinite scrolling:
|
|
340
|
+
|
|
341
|
+
**Define a stream prop:**
|
|
342
|
+
|
|
343
|
+
```elixir
|
|
344
|
+
use LiveReactIslands.Component,
|
|
345
|
+
component: "MessageList",
|
|
346
|
+
props: %{
|
|
347
|
+
messages: {:stream, default: []}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Push stream events from Elixir:**
|
|
352
|
+
|
|
353
|
+
```elixir
|
|
354
|
+
# Insert new item (prepends by default)
|
|
355
|
+
socket |> stream_insert(:messages, %{id: 1, text: "Hello"})
|
|
356
|
+
|
|
357
|
+
# Update existing item
|
|
358
|
+
socket |> stream_update(:messages, %{id: 1, text: "Hello, edited"})
|
|
359
|
+
|
|
360
|
+
# Delete an item
|
|
361
|
+
socket |> stream_delete(:messages, 1)
|
|
362
|
+
|
|
363
|
+
# Reset the entire stream
|
|
364
|
+
socket |> stream_reset(:messages)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Consume in React:**
|
|
368
|
+
|
|
369
|
+
```jsx
|
|
370
|
+
import { useStream } from "@live-react-islands/core";
|
|
371
|
+
|
|
372
|
+
const MessageList = ({ messages: messagesHandle }) => {
|
|
373
|
+
const messages = useStream(messagesHandle, { limit: 100 });
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<ul>
|
|
377
|
+
{messages.map((msg) => (
|
|
378
|
+
<li key={msg.id}>{msg.text}</li>
|
|
379
|
+
))}
|
|
380
|
+
</ul>
|
|
381
|
+
);
|
|
382
|
+
};
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Shared Context
|
|
386
|
+
|
|
387
|
+
Islands using `:none` (default) or `:overwrite` SSR strategies render into a shared React root via portals. This enables powerful patterns like drag-and-drop between islands, shared state managers, or animation libraries that need to coordinate across components.
|
|
388
|
+
|
|
389
|
+
**Wrap all islands in a shared context:**
|
|
390
|
+
|
|
391
|
+
```jsx
|
|
392
|
+
// src/main.jsx
|
|
393
|
+
import { createHooks } from "@live-react-islands/core";
|
|
394
|
+
import { DndProvider } from "react-beautiful-dnd";
|
|
395
|
+
import islands from "./islands";
|
|
396
|
+
|
|
397
|
+
const SharedContextProvider = ({ children }) => (
|
|
398
|
+
<DndProvider backend={HTML5Backend}>{children}</DndProvider>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const islandHooks = createHooks({
|
|
402
|
+
islands,
|
|
403
|
+
SharedContextProvider,
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Now all your islands can participate in drag-and-drop with each other, even though they're scattered across your LiveView template.
|
|
408
|
+
|
|
409
|
+
> **Note:** Islands using `:hydrate_root` SSR strategy have their own isolated React root and do not participate in the shared context. Use `:overwrite` or `:none` if you need context sharing between islands.
|
|
410
|
+
|
|
411
|
+
### Server-Side Rendering (SSR)
|
|
412
|
+
|
|
413
|
+
SSR improves initial page load performance by rendering React components on the server.
|
|
414
|
+
|
|
415
|
+
```elixir
|
|
416
|
+
use LiveReactIslands.Component,
|
|
417
|
+
component: "Counter",
|
|
418
|
+
props: %{count: 0},
|
|
419
|
+
ssr_strategy: :overwrite # or :hydrate_root or :none (default)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
| Strategy | Shared Root | Best For |
|
|
423
|
+
| --------------- | ----------- | ----------------------------------------------------------------------- |
|
|
424
|
+
| `:none` | Yes | Interactive components where initial render doesn't matter |
|
|
425
|
+
| `:overwrite` | Yes | Most islands, especially when you need cross-island context (e.g., DnD) |
|
|
426
|
+
| `:hydrate_root` | No | Large islands where you want to avoid the overwrite flash |
|
|
427
|
+
|
|
428
|
+
> ⚠️ SSR is optional. Many islands work perfectly without it. Enable SSR when initial paint, SEO, or perceived performance matter.
|
|
429
|
+
|
|
430
|
+
See the **[SSR Guide](https://github.com/dcza/live-react-islands/blob/main/docs/SSR.md)** for complete setup instructions, caching strategies, and custom renderer implementation.
|
|
431
|
+
|
|
432
|
+
## Requirements
|
|
433
|
+
|
|
434
|
+
- Elixir >= 1.14
|
|
435
|
+
- Phoenix LiveView >= 1.0
|
|
436
|
+
- React 18 or 19
|
|
437
|
+
- Any JavaScript bundler (built-in SSR plugin for Vite)
|
|
438
|
+
|
|
439
|
+
### Running Examples
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
cd examples/vite-example
|
|
443
|
+
mix deps.get
|
|
444
|
+
yarn install
|
|
445
|
+
yarn dev
|
|
446
|
+
mix phx.server # in another terminal
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## Contributing
|
|
450
|
+
|
|
451
|
+
See [CONTRIBUTING.md](https://github.com/dcza/live-react-islands/blob/main/CONTRIBUTING.md) for development setup and guidelines.
|
|
452
|
+
|
|
453
|
+
## License
|
|
454
|
+
|
|
455
|
+
MIT License - see [LICENSE](https://github.com/dcza/live-react-islands/blob/main/LICENSE) for details.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface LiveReactIslandsSSROptions {
|
|
4
|
+
/**
|
|
5
|
+
* Path to the SSR entry file (relative to project root)
|
|
6
|
+
* This file should call exposeSSR() from @live-react-islands/core/ssr
|
|
7
|
+
* @default "./src/ssr.js"
|
|
8
|
+
*/
|
|
9
|
+
ssrEntry?: string;
|
|
10
|
+
/**
|
|
11
|
+
* SSR endpoint path
|
|
12
|
+
* @default "/__ssr"
|
|
13
|
+
*/
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Vite plugin for LiveReactIslands SSR development server
|
|
18
|
+
*
|
|
19
|
+
* Adds a middleware endpoint that handles SSR requests from Elixir's ViteRenderer.
|
|
20
|
+
* Uses Vite's HMR-enabled module loading for instant feedback during development.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // vite.config.js
|
|
25
|
+
* import { defineConfig } from "vite";
|
|
26
|
+
* import react from "@vitejs/plugin-react";
|
|
27
|
+
* import liveReactIslandsSSR from "@live-react-islands/vite-plugin-ssr";
|
|
28
|
+
*
|
|
29
|
+
* export default defineConfig({
|
|
30
|
+
* plugins: [
|
|
31
|
+
* react(),
|
|
32
|
+
* liveReactIslandsSSR({
|
|
33
|
+
* ssrEntry: "./src/ssr.js",
|
|
34
|
+
* }),
|
|
35
|
+
* ],
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function liveReactIslandsSSR(options?: LiveReactIslandsSSROptions): Plugin;
|
|
40
|
+
|
|
41
|
+
export { type LiveReactIslandsSSROptions, liveReactIslandsSSR as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface LiveReactIslandsSSROptions {
|
|
4
|
+
/**
|
|
5
|
+
* Path to the SSR entry file (relative to project root)
|
|
6
|
+
* This file should call exposeSSR() from @live-react-islands/core/ssr
|
|
7
|
+
* @default "./src/ssr.js"
|
|
8
|
+
*/
|
|
9
|
+
ssrEntry?: string;
|
|
10
|
+
/**
|
|
11
|
+
* SSR endpoint path
|
|
12
|
+
* @default "/__ssr"
|
|
13
|
+
*/
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Vite plugin for LiveReactIslands SSR development server
|
|
18
|
+
*
|
|
19
|
+
* Adds a middleware endpoint that handles SSR requests from Elixir's ViteRenderer.
|
|
20
|
+
* Uses Vite's HMR-enabled module loading for instant feedback during development.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // vite.config.js
|
|
25
|
+
* import { defineConfig } from "vite";
|
|
26
|
+
* import react from "@vitejs/plugin-react";
|
|
27
|
+
* import liveReactIslandsSSR from "@live-react-islands/vite-plugin-ssr";
|
|
28
|
+
*
|
|
29
|
+
* export default defineConfig({
|
|
30
|
+
* plugins: [
|
|
31
|
+
* react(),
|
|
32
|
+
* liveReactIslandsSSR({
|
|
33
|
+
* ssrEntry: "./src/ssr.js",
|
|
34
|
+
* }),
|
|
35
|
+
* ],
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function liveReactIslandsSSR(options?: LiveReactIslandsSSROptions): Plugin;
|
|
40
|
+
|
|
41
|
+
export { type LiveReactIslandsSSROptions, liveReactIslandsSSR as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
default: () => liveReactIslandsSSR
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
function liveReactIslandsSSR(options = {}) {
|
|
27
|
+
const ssrEntry = options.ssrEntry || "./src/ssr.js";
|
|
28
|
+
const endpoint = options.endpoint || "/__ssr";
|
|
29
|
+
return {
|
|
30
|
+
name: "live-react-islands-ssr",
|
|
31
|
+
configureServer(server) {
|
|
32
|
+
server.middlewares.use(async (req, res, next) => {
|
|
33
|
+
if (req.method !== "POST" || req.url !== endpoint) {
|
|
34
|
+
return next();
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
for await (const chunk of req) {
|
|
39
|
+
chunks.push(Buffer.from(chunk));
|
|
40
|
+
}
|
|
41
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
42
|
+
const { component, id, props, globals, strategy } = body;
|
|
43
|
+
if (!component || !id) {
|
|
44
|
+
res.statusCode = 400;
|
|
45
|
+
res.setHeader("Content-Type", "application/json");
|
|
46
|
+
res.end(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
error: "Missing required fields: component and id"
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await server.ssrLoadModule(ssrEntry);
|
|
54
|
+
const ssrModule = globalThis.SSR_MODULE;
|
|
55
|
+
if (!ssrModule || !ssrModule.renderSSRIsland) {
|
|
56
|
+
res.statusCode = 500;
|
|
57
|
+
res.setHeader("Content-Type", "application/json");
|
|
58
|
+
res.end(
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
error: `SSR module not found. Make sure ${ssrEntry} calls exposeSSR() from @live-react-islands/core/ssr`
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const html = ssrModule.renderSSRIsland(
|
|
66
|
+
component,
|
|
67
|
+
id,
|
|
68
|
+
props || {},
|
|
69
|
+
globals || {},
|
|
70
|
+
strategy || "overwrite"
|
|
71
|
+
);
|
|
72
|
+
console.log(
|
|
73
|
+
`[live-react-islands-ssr] Rendered ${component} (id: ${id}, strategy: ${strategy})`,
|
|
74
|
+
{ props, globals }
|
|
75
|
+
);
|
|
76
|
+
res.statusCode = 200;
|
|
77
|
+
res.setHeader("Content-Type", "application/json");
|
|
78
|
+
res.end(JSON.stringify({ html }));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("[live-react-islands-ssr] Error rendering component:", error);
|
|
81
|
+
res.statusCode = 500;
|
|
82
|
+
res.setHeader("Content-Type", "application/json");
|
|
83
|
+
res.end(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
error: error.message || "Unknown error during SSR",
|
|
86
|
+
stack: error.stack
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
console.log(
|
|
92
|
+
`[live-react-islands-ssr] SSR endpoint available at ${endpoint}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin, ViteDevServer } from \"vite\";\n\nexport interface LiveReactIslandsSSROptions {\n /**\n * Path to the SSR entry file (relative to project root)\n * This file should call exposeSSR() from @live-react-islands/core/ssr\n * @default \"./src/ssr.js\"\n */\n ssrEntry?: string;\n\n /**\n * SSR endpoint path\n * @default \"/__ssr\"\n */\n endpoint?: string;\n}\n\ninterface SSRRequest {\n component: string;\n id: string;\n props: Record<string, any>;\n globals: Record<string, any>;\n strategy?: \"hydrate_root\" | \"overwrite\";\n}\n\ninterface SSRModule {\n renderSSRIsland: (\n component: string,\n id: string,\n props: Record<string, any>,\n globals: Record<string, any>,\n strategy: \"hydrate_root\" | \"overwrite\"\n ) => string;\n}\n\n/**\n * Vite plugin for LiveReactIslands SSR development server\n *\n * Adds a middleware endpoint that handles SSR requests from Elixir's ViteRenderer.\n * Uses Vite's HMR-enabled module loading for instant feedback during development.\n *\n * @example\n * ```ts\n * // vite.config.js\n * import { defineConfig } from \"vite\";\n * import react from \"@vitejs/plugin-react\";\n * import liveReactIslandsSSR from \"@live-react-islands/vite-plugin-ssr\";\n *\n * export default defineConfig({\n * plugins: [\n * react(),\n * liveReactIslandsSSR({\n * ssrEntry: \"./src/ssr.js\",\n * }),\n * ],\n * });\n * ```\n */\nexport default function liveReactIslandsSSR(\n options: LiveReactIslandsSSROptions = {}\n): Plugin {\n const ssrEntry = options.ssrEntry || \"./src/ssr.js\";\n const endpoint = options.endpoint || \"/__ssr\";\n\n return {\n name: \"live-react-islands-ssr\",\n\n configureServer(server: ViteDevServer) {\n server.middlewares.use(async (req, res, next) => {\n // Only handle POST requests to our SSR endpoint\n if (req.method !== \"POST\" || req.url !== endpoint) {\n return next();\n }\n\n try {\n // Parse request body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk));\n }\n const body = JSON.parse(Buffer.concat(chunks).toString());\n\n const { component, id, props, globals, strategy } = body as SSRRequest;\n\n // Validate request\n if (!component || !id) {\n res.statusCode = 400;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: \"Missing required fields: component and id\",\n })\n );\n return;\n }\n\n // Load the SSR module using Vite's HMR-enabled loader\n // This ensures we always get the latest version during development\n await server.ssrLoadModule(ssrEntry);\n\n // Access the global SSR_MODULE exposed by exposeSSR()\n const ssrModule = (globalThis as any).SSR_MODULE as\n | SSRModule\n | undefined;\n\n if (!ssrModule || !ssrModule.renderSSRIsland) {\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: `SSR module not found. Make sure ${ssrEntry} calls exposeSSR() from @live-react-islands/core/ssr`,\n })\n );\n return;\n }\n\n // Render the component using strategy-aware function\n const html = ssrModule.renderSSRIsland(\n component,\n id,\n props || {},\n globals || {},\n strategy || \"overwrite\"\n );\n\n console.log(\n `[live-react-islands-ssr] Rendered ${component} (id: ${id}, strategy: ${strategy})`,\n { props, globals }\n );\n\n // Return the rendered HTML\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ html }));\n } catch (error: any) {\n console.error(\"[live-react-islands-ssr] Error rendering component:\", error);\n\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: error.message || \"Unknown error during SSR\",\n stack: error.stack,\n })\n );\n }\n });\n\n console.log(\n `[live-react-islands-ssr] SSR endpoint available at ${endpoint}`\n );\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0De,SAAR,oBACL,UAAsC,CAAC,GAC/B;AACR,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,WAAW,QAAQ,YAAY;AAErC,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,gBAAgB,QAAuB;AACrC,aAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;AAE/C,YAAI,IAAI,WAAW,UAAU,IAAI,QAAQ,UAAU;AACjD,iBAAO,KAAK;AAAA,QACd;AAEA,YAAI;AAEF,gBAAM,SAAmB,CAAC;AAC1B,2BAAiB,SAAS,KAAK;AAC7B,mBAAO,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,UAChC;AACA,gBAAM,OAAO,KAAK,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,CAAC;AAExD,gBAAM,EAAE,WAAW,IAAI,OAAO,SAAS,SAAS,IAAI;AAGpD,cAAI,CAAC,aAAa,CAAC,IAAI;AACrB,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,kBAAkB;AAChD,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,OAAO;AAAA,cACT,CAAC;AAAA,YACH;AACA;AAAA,UACF;AAIA,gBAAM,OAAO,cAAc,QAAQ;AAGnC,gBAAM,YAAa,WAAmB;AAItC,cAAI,CAAC,aAAa,CAAC,UAAU,iBAAiB;AAC5C,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,kBAAkB;AAChD,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,OAAO,mCAAmC,QAAQ;AAAA,cACpD,CAAC;AAAA,YACH;AACA;AAAA,UACF;AAGA,gBAAM,OAAO,UAAU;AAAA,YACrB;AAAA,YACA;AAAA,YACA,SAAS,CAAC;AAAA,YACV,WAAW,CAAC;AAAA,YACZ,YAAY;AAAA,UACd;AAEA,kBAAQ;AAAA,YACN,qCAAqC,SAAS,SAAS,EAAE,eAAe,QAAQ;AAAA,YAChF,EAAE,OAAO,QAAQ;AAAA,UACnB;AAGA,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI,IAAI,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC;AAAA,QAClC,SAAS,OAAY;AACnB,kBAAQ,MAAM,uDAAuD,KAAK;AAE1E,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI;AAAA,YACF,KAAK,UAAU;AAAA,cACb,OAAO,MAAM,WAAW;AAAA,cACxB,OAAO,MAAM;AAAA,YACf,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAED,cAAQ;AAAA,QACN,sDAAsD,QAAQ;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function liveReactIslandsSSR(options = {}) {
|
|
3
|
+
const ssrEntry = options.ssrEntry || "./src/ssr.js";
|
|
4
|
+
const endpoint = options.endpoint || "/__ssr";
|
|
5
|
+
return {
|
|
6
|
+
name: "live-react-islands-ssr",
|
|
7
|
+
configureServer(server) {
|
|
8
|
+
server.middlewares.use(async (req, res, next) => {
|
|
9
|
+
if (req.method !== "POST" || req.url !== endpoint) {
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of req) {
|
|
15
|
+
chunks.push(Buffer.from(chunk));
|
|
16
|
+
}
|
|
17
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
18
|
+
const { component, id, props, globals, strategy } = body;
|
|
19
|
+
if (!component || !id) {
|
|
20
|
+
res.statusCode = 400;
|
|
21
|
+
res.setHeader("Content-Type", "application/json");
|
|
22
|
+
res.end(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
error: "Missing required fields: component and id"
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await server.ssrLoadModule(ssrEntry);
|
|
30
|
+
const ssrModule = globalThis.SSR_MODULE;
|
|
31
|
+
if (!ssrModule || !ssrModule.renderSSRIsland) {
|
|
32
|
+
res.statusCode = 500;
|
|
33
|
+
res.setHeader("Content-Type", "application/json");
|
|
34
|
+
res.end(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
error: `SSR module not found. Make sure ${ssrEntry} calls exposeSSR() from @live-react-islands/core/ssr`
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const html = ssrModule.renderSSRIsland(
|
|
42
|
+
component,
|
|
43
|
+
id,
|
|
44
|
+
props || {},
|
|
45
|
+
globals || {},
|
|
46
|
+
strategy || "overwrite"
|
|
47
|
+
);
|
|
48
|
+
console.log(
|
|
49
|
+
`[live-react-islands-ssr] Rendered ${component} (id: ${id}, strategy: ${strategy})`,
|
|
50
|
+
{ props, globals }
|
|
51
|
+
);
|
|
52
|
+
res.statusCode = 200;
|
|
53
|
+
res.setHeader("Content-Type", "application/json");
|
|
54
|
+
res.end(JSON.stringify({ html }));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("[live-react-islands-ssr] Error rendering component:", error);
|
|
57
|
+
res.statusCode = 500;
|
|
58
|
+
res.setHeader("Content-Type", "application/json");
|
|
59
|
+
res.end(
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
error: error.message || "Unknown error during SSR",
|
|
62
|
+
stack: error.stack
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
console.log(
|
|
68
|
+
`[live-react-islands-ssr] SSR endpoint available at ${endpoint}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
liveReactIslandsSSR as default
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin, ViteDevServer } from \"vite\";\n\nexport interface LiveReactIslandsSSROptions {\n /**\n * Path to the SSR entry file (relative to project root)\n * This file should call exposeSSR() from @live-react-islands/core/ssr\n * @default \"./src/ssr.js\"\n */\n ssrEntry?: string;\n\n /**\n * SSR endpoint path\n * @default \"/__ssr\"\n */\n endpoint?: string;\n}\n\ninterface SSRRequest {\n component: string;\n id: string;\n props: Record<string, any>;\n globals: Record<string, any>;\n strategy?: \"hydrate_root\" | \"overwrite\";\n}\n\ninterface SSRModule {\n renderSSRIsland: (\n component: string,\n id: string,\n props: Record<string, any>,\n globals: Record<string, any>,\n strategy: \"hydrate_root\" | \"overwrite\"\n ) => string;\n}\n\n/**\n * Vite plugin for LiveReactIslands SSR development server\n *\n * Adds a middleware endpoint that handles SSR requests from Elixir's ViteRenderer.\n * Uses Vite's HMR-enabled module loading for instant feedback during development.\n *\n * @example\n * ```ts\n * // vite.config.js\n * import { defineConfig } from \"vite\";\n * import react from \"@vitejs/plugin-react\";\n * import liveReactIslandsSSR from \"@live-react-islands/vite-plugin-ssr\";\n *\n * export default defineConfig({\n * plugins: [\n * react(),\n * liveReactIslandsSSR({\n * ssrEntry: \"./src/ssr.js\",\n * }),\n * ],\n * });\n * ```\n */\nexport default function liveReactIslandsSSR(\n options: LiveReactIslandsSSROptions = {}\n): Plugin {\n const ssrEntry = options.ssrEntry || \"./src/ssr.js\";\n const endpoint = options.endpoint || \"/__ssr\";\n\n return {\n name: \"live-react-islands-ssr\",\n\n configureServer(server: ViteDevServer) {\n server.middlewares.use(async (req, res, next) => {\n // Only handle POST requests to our SSR endpoint\n if (req.method !== \"POST\" || req.url !== endpoint) {\n return next();\n }\n\n try {\n // Parse request body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk));\n }\n const body = JSON.parse(Buffer.concat(chunks).toString());\n\n const { component, id, props, globals, strategy } = body as SSRRequest;\n\n // Validate request\n if (!component || !id) {\n res.statusCode = 400;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: \"Missing required fields: component and id\",\n })\n );\n return;\n }\n\n // Load the SSR module using Vite's HMR-enabled loader\n // This ensures we always get the latest version during development\n await server.ssrLoadModule(ssrEntry);\n\n // Access the global SSR_MODULE exposed by exposeSSR()\n const ssrModule = (globalThis as any).SSR_MODULE as\n | SSRModule\n | undefined;\n\n if (!ssrModule || !ssrModule.renderSSRIsland) {\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: `SSR module not found. Make sure ${ssrEntry} calls exposeSSR() from @live-react-islands/core/ssr`,\n })\n );\n return;\n }\n\n // Render the component using strategy-aware function\n const html = ssrModule.renderSSRIsland(\n component,\n id,\n props || {},\n globals || {},\n strategy || \"overwrite\"\n );\n\n console.log(\n `[live-react-islands-ssr] Rendered ${component} (id: ${id}, strategy: ${strategy})`,\n { props, globals }\n );\n\n // Return the rendered HTML\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ html }));\n } catch (error: any) {\n console.error(\"[live-react-islands-ssr] Error rendering component:\", error);\n\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n error: error.message || \"Unknown error during SSR\",\n stack: error.stack,\n })\n );\n }\n });\n\n console.log(\n `[live-react-islands-ssr] SSR endpoint available at ${endpoint}`\n );\n },\n };\n}\n"],"mappings":";AA0De,SAAR,oBACL,UAAsC,CAAC,GAC/B;AACR,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,WAAW,QAAQ,YAAY;AAErC,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,gBAAgB,QAAuB;AACrC,aAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;AAE/C,YAAI,IAAI,WAAW,UAAU,IAAI,QAAQ,UAAU;AACjD,iBAAO,KAAK;AAAA,QACd;AAEA,YAAI;AAEF,gBAAM,SAAmB,CAAC;AAC1B,2BAAiB,SAAS,KAAK;AAC7B,mBAAO,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,UAChC;AACA,gBAAM,OAAO,KAAK,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,CAAC;AAExD,gBAAM,EAAE,WAAW,IAAI,OAAO,SAAS,SAAS,IAAI;AAGpD,cAAI,CAAC,aAAa,CAAC,IAAI;AACrB,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,kBAAkB;AAChD,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,OAAO;AAAA,cACT,CAAC;AAAA,YACH;AACA;AAAA,UACF;AAIA,gBAAM,OAAO,cAAc,QAAQ;AAGnC,gBAAM,YAAa,WAAmB;AAItC,cAAI,CAAC,aAAa,CAAC,UAAU,iBAAiB;AAC5C,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,kBAAkB;AAChD,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,OAAO,mCAAmC,QAAQ;AAAA,cACpD,CAAC;AAAA,YACH;AACA;AAAA,UACF;AAGA,gBAAM,OAAO,UAAU;AAAA,YACrB;AAAA,YACA;AAAA,YACA,SAAS,CAAC;AAAA,YACV,WAAW,CAAC;AAAA,YACZ,YAAY;AAAA,UACd;AAEA,kBAAQ;AAAA,YACN,qCAAqC,SAAS,SAAS,EAAE,eAAe,QAAQ;AAAA,YAChF,EAAE,OAAO,QAAQ;AAAA,UACnB;AAGA,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI,IAAI,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC;AAAA,QAClC,SAAS,OAAY;AACnB,kBAAQ,MAAM,uDAAuD,KAAK;AAE1E,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI;AAAA,YACF,KAAK,UAAU;AAAA,cACb,OAAO,MAAM,WAAW;AAAA,cACxB,OAAO,MAAM;AAAA,YACf,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAED,cAAQ;AAAA,QACN,sDAAsD,QAAQ;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@live-react-islands/vite-plugin-ssr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vite plugin for LiveReactIslands SSR development server",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"clean": "rm -rf dist",
|
|
23
|
+
"prepublishOnly": "cp ../../../README.md ./"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"vite",
|
|
27
|
+
"vite-plugin",
|
|
28
|
+
"ssr",
|
|
29
|
+
"liveview",
|
|
30
|
+
"react",
|
|
31
|
+
"islands"
|
|
32
|
+
],
|
|
33
|
+
"author": "David Czaplinski",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/dcza/live-react-islands.git",
|
|
38
|
+
"directory": "packages/js/vite-plugin-ssr"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"vite": "^5.0.0 || ^6.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.0.0",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.6.3",
|
|
47
|
+
"vite": "^6.0.7"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { Plugin, ViteDevServer } from "vite";
|
|
2
|
+
|
|
3
|
+
export interface LiveReactIslandsSSROptions {
|
|
4
|
+
/**
|
|
5
|
+
* Path to the SSR entry file (relative to project root)
|
|
6
|
+
* This file should call exposeSSR() from @live-react-islands/core/ssr
|
|
7
|
+
* @default "./src/ssr.js"
|
|
8
|
+
*/
|
|
9
|
+
ssrEntry?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SSR endpoint path
|
|
13
|
+
* @default "/__ssr"
|
|
14
|
+
*/
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SSRRequest {
|
|
19
|
+
component: string;
|
|
20
|
+
id: string;
|
|
21
|
+
props: Record<string, any>;
|
|
22
|
+
globals: Record<string, any>;
|
|
23
|
+
strategy?: "hydrate_root" | "overwrite";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SSRModule {
|
|
27
|
+
renderSSRIsland: (
|
|
28
|
+
component: string,
|
|
29
|
+
id: string,
|
|
30
|
+
props: Record<string, any>,
|
|
31
|
+
globals: Record<string, any>,
|
|
32
|
+
strategy: "hydrate_root" | "overwrite"
|
|
33
|
+
) => string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Vite plugin for LiveReactIslands SSR development server
|
|
38
|
+
*
|
|
39
|
+
* Adds a middleware endpoint that handles SSR requests from Elixir's ViteRenderer.
|
|
40
|
+
* Uses Vite's HMR-enabled module loading for instant feedback during development.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* // vite.config.js
|
|
45
|
+
* import { defineConfig } from "vite";
|
|
46
|
+
* import react from "@vitejs/plugin-react";
|
|
47
|
+
* import liveReactIslandsSSR from "@live-react-islands/vite-plugin-ssr";
|
|
48
|
+
*
|
|
49
|
+
* export default defineConfig({
|
|
50
|
+
* plugins: [
|
|
51
|
+
* react(),
|
|
52
|
+
* liveReactIslandsSSR({
|
|
53
|
+
* ssrEntry: "./src/ssr.js",
|
|
54
|
+
* }),
|
|
55
|
+
* ],
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export default function liveReactIslandsSSR(
|
|
60
|
+
options: LiveReactIslandsSSROptions = {}
|
|
61
|
+
): Plugin {
|
|
62
|
+
const ssrEntry = options.ssrEntry || "./src/ssr.js";
|
|
63
|
+
const endpoint = options.endpoint || "/__ssr";
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
name: "live-react-islands-ssr",
|
|
67
|
+
|
|
68
|
+
configureServer(server: ViteDevServer) {
|
|
69
|
+
server.middlewares.use(async (req, res, next) => {
|
|
70
|
+
// Only handle POST requests to our SSR endpoint
|
|
71
|
+
if (req.method !== "POST" || req.url !== endpoint) {
|
|
72
|
+
return next();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Parse request body
|
|
77
|
+
const chunks: Buffer[] = [];
|
|
78
|
+
for await (const chunk of req) {
|
|
79
|
+
chunks.push(Buffer.from(chunk));
|
|
80
|
+
}
|
|
81
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
82
|
+
|
|
83
|
+
const { component, id, props, globals, strategy } = body as SSRRequest;
|
|
84
|
+
|
|
85
|
+
// Validate request
|
|
86
|
+
if (!component || !id) {
|
|
87
|
+
res.statusCode = 400;
|
|
88
|
+
res.setHeader("Content-Type", "application/json");
|
|
89
|
+
res.end(
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
error: "Missing required fields: component and id",
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Load the SSR module using Vite's HMR-enabled loader
|
|
98
|
+
// This ensures we always get the latest version during development
|
|
99
|
+
await server.ssrLoadModule(ssrEntry);
|
|
100
|
+
|
|
101
|
+
// Access the global SSR_MODULE exposed by exposeSSR()
|
|
102
|
+
const ssrModule = (globalThis as any).SSR_MODULE as
|
|
103
|
+
| SSRModule
|
|
104
|
+
| undefined;
|
|
105
|
+
|
|
106
|
+
if (!ssrModule || !ssrModule.renderSSRIsland) {
|
|
107
|
+
res.statusCode = 500;
|
|
108
|
+
res.setHeader("Content-Type", "application/json");
|
|
109
|
+
res.end(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
error: `SSR module not found. Make sure ${ssrEntry} calls exposeSSR() from @live-react-islands/core/ssr`,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Render the component using strategy-aware function
|
|
118
|
+
const html = ssrModule.renderSSRIsland(
|
|
119
|
+
component,
|
|
120
|
+
id,
|
|
121
|
+
props || {},
|
|
122
|
+
globals || {},
|
|
123
|
+
strategy || "overwrite"
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
console.log(
|
|
127
|
+
`[live-react-islands-ssr] Rendered ${component} (id: ${id}, strategy: ${strategy})`,
|
|
128
|
+
{ props, globals }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Return the rendered HTML
|
|
132
|
+
res.statusCode = 200;
|
|
133
|
+
res.setHeader("Content-Type", "application/json");
|
|
134
|
+
res.end(JSON.stringify({ html }));
|
|
135
|
+
} catch (error: any) {
|
|
136
|
+
console.error("[live-react-islands-ssr] Error rendering component:", error);
|
|
137
|
+
|
|
138
|
+
res.statusCode = 500;
|
|
139
|
+
res.setHeader("Content-Type", "application/json");
|
|
140
|
+
res.end(
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
error: error.message || "Unknown error during SSR",
|
|
143
|
+
stack: error.stack,
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
console.log(
|
|
150
|
+
`[live-react-islands-ssr] SSR endpoint available at ${endpoint}`
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|