@valentinkolb/ssr 0.11.0 → 0.11.1
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 +8 -2
- package/package.json +1 -1
- package/src/adapter/bun.ts +3 -2
- package/src/adapter/client.js +233 -25
- package/src/adapter/elysia.ts +5 -2
- package/src/adapter/hono.ts +3 -3
- package/src/adapter/utils.ts +43 -8
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ Current source size in this repo (`packages/ssr-core/src`):
|
|
|
21
21
|
| Component | Lines | Raw | Gzipped |
|
|
22
22
|
| --- | ---: | ---: | ---: |
|
|
23
23
|
| Core (`index`, `transform`, `build`, island ID + resolver) | ~689 | 23.8 KB | 7.2 KB |
|
|
24
|
-
| Dev client (overlay + reload, dev only) | ~
|
|
25
|
-
| Adapters (`bun`, `hono`, `elysia`, shared utils) | ~
|
|
24
|
+
| Dev client (overlay + reload, dev only) | ~421 | 11.9 KB | 3.5 KB |
|
|
25
|
+
| Adapters (`bun`, `hono`, `elysia`, shared utils) | ~465 | 14.4 KB | 4.8 KB |
|
|
26
26
|
|
|
27
27
|
Important: these sizes describe framework source code that runs at build time and on the server.
|
|
28
28
|
The browser receives only:
|
|
@@ -358,6 +358,12 @@ It can:
|
|
|
358
358
|
- highlight island/client boundaries
|
|
359
359
|
- show source filenames for wrapped components
|
|
360
360
|
|
|
361
|
+
In browsers with Web Locks support, auto-reload elects one visible tab per
|
|
362
|
+
origin and SSR path to hold the SSE connection. Hidden tabs suspend reload work,
|
|
363
|
+
leadership transfers automatically, and cached pages resume safely after a
|
|
364
|
+
back-forward cache restore. Browsers without Web Locks retain visibility-scoped
|
|
365
|
+
per-tab connections as a compatibility fallback.
|
|
366
|
+
|
|
361
367
|
## Limitations
|
|
362
368
|
|
|
363
369
|
- islands must use default export
|
package/package.json
CHANGED
package/src/adapter/bun.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { SsrConfig } from "../index";
|
|
6
6
|
import {
|
|
7
7
|
createAssetResponse,
|
|
8
|
+
createPingResponse,
|
|
8
9
|
getSsrDir,
|
|
9
10
|
createReloadResponse,
|
|
10
11
|
} from "./utils";
|
|
@@ -32,8 +33,8 @@ export const routes = (config: SsrConfig): Routes => {
|
|
|
32
33
|
|
|
33
34
|
const devRoutes: Routes = dev
|
|
34
35
|
? {
|
|
35
|
-
[`${ssrPath}/_reload`]: () => createReloadResponse(),
|
|
36
|
-
[`${ssrPath}/_ping`]: () =>
|
|
36
|
+
[`${ssrPath}/_reload`]: (req) => createReloadResponse(req.signal),
|
|
37
|
+
[`${ssrPath}/_ping`]: () => createPingResponse(),
|
|
37
38
|
}
|
|
38
39
|
: {};
|
|
39
40
|
|
package/src/adapter/client.js
CHANGED
|
@@ -14,11 +14,31 @@ if (!window.__ssr_reload) {
|
|
|
14
14
|
position: "bl",
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const readStorage = (key) => {
|
|
18
|
+
try {
|
|
19
|
+
return localStorage.getItem(key);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const writeStorage = (key, value) => {
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(key, value);
|
|
27
|
+
} catch {
|
|
28
|
+
// Dev tools still work when storage is unavailable.
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const load = () => {
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
...defaults,
|
|
35
|
+
...JSON.parse(readStorage(STORAGE_KEY) || "{}"),
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return { ...defaults };
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const save = (s) => writeStorage(STORAGE_KEY, JSON.stringify(s));
|
|
22
42
|
let settings = load();
|
|
23
43
|
|
|
24
44
|
// Highlight Styles
|
|
@@ -152,10 +172,23 @@ if (!window.__ssr_reload) {
|
|
|
152
172
|
bind("_ssr_pos", "position", applyPosition);
|
|
153
173
|
|
|
154
174
|
// Live Reload (SSE)
|
|
155
|
-
|
|
175
|
+
const reloadId = globalThis.__SSR_CONFIG?.reloadId;
|
|
176
|
+
const reloadIdKey = `${STORAGE_KEY}:reload-id`;
|
|
177
|
+
const reloadLockName = `${STORAGE_KEY}:reload-lock`;
|
|
178
|
+
let es, retryTimer, retryController, animationInterval;
|
|
179
|
+
let lockController, releaseLock;
|
|
180
|
+
let retryAttempt = 0;
|
|
181
|
+
let retryGeneration = 0;
|
|
182
|
+
let ownsLock = false;
|
|
183
|
+
let pageActive = true;
|
|
184
|
+
let reloadRequested = false;
|
|
156
185
|
const spinFrames = ["[ / ]", "[ – ]", "[ \\ ]", "[ | ]"];
|
|
157
186
|
let spinIndex = 0;
|
|
158
187
|
|
|
188
|
+
// A fresh document already represents this server generation. Publishing
|
|
189
|
+
// it first wakes stale tabs without making the fresh document reload.
|
|
190
|
+
if (reloadId) writeStorage(reloadIdKey, reloadId);
|
|
191
|
+
|
|
159
192
|
const stopAnimation = () => {
|
|
160
193
|
clearInterval(animationInterval);
|
|
161
194
|
animationInterval = null;
|
|
@@ -169,45 +202,220 @@ if (!window.__ssr_reload) {
|
|
|
169
202
|
}, 150);
|
|
170
203
|
};
|
|
171
204
|
|
|
172
|
-
const
|
|
173
|
-
|
|
205
|
+
const shouldParticipate = () =>
|
|
206
|
+
pageActive &&
|
|
207
|
+
!reloadRequested &&
|
|
208
|
+
settings.autoReload &&
|
|
209
|
+
document.visibilityState === "visible";
|
|
210
|
+
const canConnect = () => shouldParticipate() && ownsLock;
|
|
211
|
+
|
|
212
|
+
const closeSource = () => {
|
|
213
|
+
const source = es;
|
|
174
214
|
es = null;
|
|
175
|
-
|
|
176
|
-
|
|
215
|
+
source?.close();
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const cancelRetry = () => {
|
|
219
|
+
retryGeneration += 1;
|
|
220
|
+
clearTimeout(retryTimer);
|
|
221
|
+
retryTimer = null;
|
|
222
|
+
const controller = retryController;
|
|
223
|
+
retryController = null;
|
|
224
|
+
controller?.abort();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const stopConnection = () => {
|
|
228
|
+
closeSource();
|
|
229
|
+
cancelRetry();
|
|
230
|
+
retryAttempt = 0;
|
|
177
231
|
stopAnimation();
|
|
232
|
+
badge.innerText = "[ssr]";
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const releaseLeadership = () => {
|
|
236
|
+
const pendingLock = lockController;
|
|
237
|
+
lockController = null;
|
|
238
|
+
pendingLock?.abort();
|
|
239
|
+
|
|
240
|
+
const release = releaseLock;
|
|
241
|
+
releaseLock = null;
|
|
242
|
+
ownsLock = false;
|
|
243
|
+
release?.();
|
|
244
|
+
stopConnection();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const requestReload = () => {
|
|
248
|
+
if (reloadRequested) return;
|
|
249
|
+
reloadRequested = true;
|
|
250
|
+
releaseLeadership();
|
|
251
|
+
location.reload();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const acceptReloadId = (nextReloadId) => {
|
|
255
|
+
if (!nextReloadId) return;
|
|
256
|
+
writeStorage(reloadIdKey, nextReloadId);
|
|
257
|
+
if (reloadId && nextReloadId !== reloadId) requestReload();
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const retryDelay = () =>
|
|
261
|
+
Math.min(2_000, 300 * 2 ** Math.min(retryAttempt, 3));
|
|
262
|
+
|
|
263
|
+
const scheduleRetry = () => {
|
|
264
|
+
if (!canConnect() || retryTimer || retryController) return;
|
|
265
|
+
|
|
266
|
+
const generation = retryGeneration;
|
|
267
|
+
retryTimer = setTimeout(async () => {
|
|
268
|
+
retryTimer = null;
|
|
269
|
+
if (generation !== retryGeneration || !canConnect()) return;
|
|
270
|
+
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
retryController = controller;
|
|
273
|
+
try {
|
|
274
|
+
const response = await fetch(`${ssrPath}/_ping`, {
|
|
275
|
+
cache: "no-store",
|
|
276
|
+
signal: controller.signal,
|
|
277
|
+
});
|
|
278
|
+
if (response.ok && canConnect()) {
|
|
279
|
+
acceptReloadId(response.headers.get("X-SSR-Reload-ID"));
|
|
280
|
+
if (!reloadRequested) requestReload();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// A stopped or unavailable dev server is expected during reload.
|
|
285
|
+
} finally {
|
|
286
|
+
if (retryController === controller) retryController = null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (generation !== retryGeneration || !canConnect()) return;
|
|
290
|
+
retryAttempt += 1;
|
|
291
|
+
scheduleRetry();
|
|
292
|
+
}, retryDelay());
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const startRetry = () => {
|
|
296
|
+
if (!canConnect()) return;
|
|
297
|
+
startAnimation();
|
|
298
|
+
scheduleRetry();
|
|
178
299
|
};
|
|
179
300
|
|
|
180
301
|
const start = () => {
|
|
181
|
-
if (es) return;
|
|
302
|
+
if (!canConnect() || es || retryTimer || retryController) return;
|
|
303
|
+
|
|
304
|
+
let source;
|
|
182
305
|
try {
|
|
183
|
-
|
|
306
|
+
source = new EventSource(`${ssrPath}/_reload`);
|
|
307
|
+
es = source;
|
|
184
308
|
stopAnimation();
|
|
185
309
|
badge.innerText = "[ssr]";
|
|
186
310
|
} catch {
|
|
311
|
+
startRetry();
|
|
187
312
|
return;
|
|
188
313
|
}
|
|
189
314
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
reconnectInterval = setInterval(() => {
|
|
196
|
-
fetch(`${ssrPath}/_ping`)
|
|
197
|
-
.then((r) => r.ok && location.reload())
|
|
198
|
-
.catch(() => {});
|
|
199
|
-
}, 300);
|
|
315
|
+
source.onopen = () => {
|
|
316
|
+
if (es !== source) return;
|
|
317
|
+
retryAttempt = 0;
|
|
318
|
+
stopAnimation();
|
|
319
|
+
badge.innerText = "[ssr]";
|
|
200
320
|
};
|
|
321
|
+
|
|
322
|
+
source.onmessage = (event) => {
|
|
323
|
+
if (es !== source) return;
|
|
324
|
+
acceptReloadId(event.data);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
source.onerror = (event) => {
|
|
328
|
+
if (es !== source) return;
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
closeSource();
|
|
331
|
+
retryAttempt = 0;
|
|
332
|
+
startRetry();
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const requestLeadership = () => {
|
|
337
|
+
if (!shouldParticipate()) return;
|
|
338
|
+
if (ownsLock) {
|
|
339
|
+
start();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (lockController) return;
|
|
343
|
+
|
|
344
|
+
if (!navigator.locks?.request) {
|
|
345
|
+
ownsLock = true;
|
|
346
|
+
start();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const controller = new AbortController();
|
|
351
|
+
lockController = controller;
|
|
352
|
+
void navigator.locks
|
|
353
|
+
.request(
|
|
354
|
+
reloadLockName,
|
|
355
|
+
{ mode: "exclusive", signal: controller.signal },
|
|
356
|
+
async () => {
|
|
357
|
+
if (lockController === controller) lockController = null;
|
|
358
|
+
if (controller.signal.aborted || !shouldParticipate()) return;
|
|
359
|
+
|
|
360
|
+
ownsLock = true;
|
|
361
|
+
start();
|
|
362
|
+
await new Promise((resolve) => {
|
|
363
|
+
releaseLock = resolve;
|
|
364
|
+
});
|
|
365
|
+
releaseLock = null;
|
|
366
|
+
ownsLock = false;
|
|
367
|
+
stopConnection();
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
.catch((error) => {
|
|
371
|
+
if (lockController === controller) lockController = null;
|
|
372
|
+
if (error?.name === "AbortError" || !shouldParticipate()) return;
|
|
373
|
+
|
|
374
|
+
// If Web Locks is unavailable at runtime, retain per-tab behavior.
|
|
375
|
+
ownsLock = true;
|
|
376
|
+
start();
|
|
377
|
+
});
|
|
201
378
|
};
|
|
202
379
|
|
|
203
|
-
|
|
380
|
+
const syncConnection = () => {
|
|
381
|
+
const knownReloadId = readStorage(reloadIdKey);
|
|
382
|
+
if (
|
|
383
|
+
shouldParticipate() &&
|
|
384
|
+
reloadId &&
|
|
385
|
+
knownReloadId &&
|
|
386
|
+
knownReloadId !== reloadId
|
|
387
|
+
) {
|
|
388
|
+
requestReload();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (shouldParticipate()) requestLeadership();
|
|
393
|
+
else releaseLeadership();
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
syncConnection();
|
|
204
397
|
|
|
205
398
|
panel.querySelector("#_ssr_reload").onchange = (e) => {
|
|
206
399
|
settings.autoReload = e.target.checked;
|
|
207
400
|
save(settings);
|
|
208
|
-
|
|
401
|
+
syncConnection();
|
|
209
402
|
};
|
|
210
403
|
|
|
211
|
-
|
|
404
|
+
document.addEventListener("visibilitychange", syncConnection);
|
|
405
|
+
window.addEventListener("storage", (event) => {
|
|
406
|
+
if (event.key !== reloadIdKey || !event.newValue) return;
|
|
407
|
+
if (reloadId && event.newValue !== reloadId && shouldParticipate()) {
|
|
408
|
+
requestReload();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
window.addEventListener("pagehide", () => {
|
|
412
|
+
pageActive = false;
|
|
413
|
+
releaseLeadership();
|
|
414
|
+
});
|
|
415
|
+
window.addEventListener("pageshow", () => {
|
|
416
|
+
pageActive = true;
|
|
417
|
+
reloadRequested = false;
|
|
418
|
+
syncConnection();
|
|
419
|
+
});
|
|
212
420
|
})();
|
|
213
421
|
}
|
package/src/adapter/elysia.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Elysia } from "elysia";
|
|
|
6
6
|
import type { SsrConfig } from "../index";
|
|
7
7
|
import {
|
|
8
8
|
createAssetResponse,
|
|
9
|
+
createPingResponse,
|
|
9
10
|
getSsrDir,
|
|
10
11
|
createReloadResponse,
|
|
11
12
|
notFound,
|
|
@@ -28,8 +29,10 @@ export const routes = (config: SsrConfig) => {
|
|
|
28
29
|
const ssrDir = getSsrDir(config);
|
|
29
30
|
|
|
30
31
|
return new Elysia({ name: "ssr" })
|
|
31
|
-
.get(`${ssrPath}/_reload`, (
|
|
32
|
-
|
|
32
|
+
.get(`${ssrPath}/_reload`, ({ request }) =>
|
|
33
|
+
dev ? createReloadResponse(request.signal) : notFound(),
|
|
34
|
+
)
|
|
35
|
+
.get(`${ssrPath}/_ping`, () => (dev ? createPingResponse() : notFound()))
|
|
33
36
|
.get(`${ssrPath}/*`, ({ request, params }) =>
|
|
34
37
|
createAssetResponse(request, ssrDir, params["*"], dev),
|
|
35
38
|
);
|
package/src/adapter/hono.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Hono } from "hono";
|
|
|
6
6
|
import { createFactory } from "hono/factory";
|
|
7
7
|
import type { Context, Env, Handler, MiddlewareHandler, TypedResponse } from "hono";
|
|
8
8
|
import type { SsrConfig, HtmlFn, RenderFn } from "../index";
|
|
9
|
-
import { createAssetResponse, getSsrDir, createReloadResponse } from "./utils";
|
|
9
|
+
import { createAssetResponse, createPingResponse, getSsrDir, createReloadResponse } from "./utils";
|
|
10
10
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Types
|
|
@@ -172,8 +172,8 @@ export const routes = (config: SsrConfig) => {
|
|
|
172
172
|
|
|
173
173
|
// Dev mode endpoints
|
|
174
174
|
if (dev) {
|
|
175
|
-
app.get("/_reload", () => createReloadResponse());
|
|
176
|
-
app.get("/_ping", (
|
|
175
|
+
app.get("/_reload", (c) => createReloadResponse(c.req.raw.signal));
|
|
176
|
+
app.get("/_ping", () => createPingResponse());
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
const serveAsset = (c: Context) => createAssetResponse(c.req.raw, ssrDir, c.req.param("filename"), dev);
|
package/src/adapter/utils.ts
CHANGED
|
@@ -116,28 +116,63 @@ export const SSE_HEADERS = {
|
|
|
116
116
|
Connection: "keep-alive",
|
|
117
117
|
} as const;
|
|
118
118
|
|
|
119
|
+
const reloadId = crypto.randomUUID();
|
|
120
|
+
const reloadConnected = new TextEncoder().encode(`: connected\ndata: ${reloadId}\n\n`);
|
|
121
|
+
const reloadPing = new TextEncoder().encode(": ping\n\n");
|
|
122
|
+
|
|
123
|
+
export const getReloadId = (): string => reloadId;
|
|
124
|
+
|
|
125
|
+
export const createPingResponse = (): Response =>
|
|
126
|
+
new Response("ok", { headers: { "X-SSR-Reload-ID": reloadId } });
|
|
127
|
+
|
|
119
128
|
/**
|
|
120
129
|
* Creates a Server-Sent Events stream for live reload
|
|
121
130
|
*/
|
|
122
|
-
export const createReloadStream = (): ReadableStream =>
|
|
123
|
-
|
|
131
|
+
export const createReloadStream = (signal?: AbortSignal): ReadableStream<Uint8Array> => {
|
|
132
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
133
|
+
let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
134
|
+
const handleAbort = () => {
|
|
135
|
+
stopHeartbeat();
|
|
136
|
+
try {
|
|
137
|
+
streamController?.close();
|
|
138
|
+
} catch {
|
|
139
|
+
// The consumer may already have canceled the stream.
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const stopHeartbeat = () => {
|
|
143
|
+
if (interval === undefined) return;
|
|
144
|
+
clearInterval(interval);
|
|
145
|
+
interval = undefined;
|
|
146
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return new ReadableStream<Uint8Array>({
|
|
124
150
|
start(controller) {
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
streamController = controller;
|
|
152
|
+
if (signal?.aborted) {
|
|
153
|
+
controller.close();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
158
|
+
controller.enqueue(reloadConnected);
|
|
159
|
+
interval = setInterval(() => {
|
|
127
160
|
try {
|
|
128
|
-
controller.enqueue(
|
|
161
|
+
controller.enqueue(reloadPing);
|
|
129
162
|
} catch {
|
|
130
|
-
|
|
163
|
+
stopHeartbeat();
|
|
131
164
|
}
|
|
132
165
|
}, 5000);
|
|
133
166
|
},
|
|
167
|
+
cancel: stopHeartbeat,
|
|
134
168
|
});
|
|
169
|
+
};
|
|
135
170
|
|
|
136
171
|
/**
|
|
137
172
|
* Creates a Response for the SSE reload endpoint
|
|
138
173
|
*/
|
|
139
|
-
export const createReloadResponse = (): Response =>
|
|
140
|
-
new Response(createReloadStream(), { headers: SSE_HEADERS });
|
|
174
|
+
export const createReloadResponse = (signal?: AbortSignal): Response =>
|
|
175
|
+
new Response(createReloadStream(signal), { headers: SSE_HEADERS });
|
|
141
176
|
|
|
142
177
|
/**
|
|
143
178
|
* 404 Response
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { transform } from "./transform";
|
|
|
12
12
|
import { buildIslands, type DevSourcemap } from "./build";
|
|
13
13
|
import { join, dirname, resolve } from "path";
|
|
14
14
|
import { resolveIslandImport } from "./island-resolve";
|
|
15
|
-
import { normalizeBasePath, toSsrPath } from "./adapter/utils";
|
|
15
|
+
import { getReloadId, normalizeBasePath, toSsrPath } from "./adapter/utils";
|
|
16
16
|
// @ts-ignore - Bun text import
|
|
17
17
|
import devClientCode from "./adapter/client.js" with { type: "text" };
|
|
18
18
|
|
|
@@ -160,7 +160,7 @@ export const createConfig = <T extends object = object>(options: SsrOptions<T> =
|
|
|
160
160
|
|
|
161
161
|
// Hydration script - dynamically loads island/client bundles based on DOM
|
|
162
162
|
const hydrationScript = `<script type="module">const p=${JSON.stringify(ssrPath)};const v=${JSON.stringify(buildVersion)};document.querySelectorAll('solid-island,solid-client').forEach(e=>import(p+'/'+e.dataset.id+'.js'+(v?'?v='+v:'')));</script>`;
|
|
163
|
-
const devConfigScript = `<script>globalThis.__SSR_CONFIG=${JSON.stringify({ ssrPath })}</script>`;
|
|
163
|
+
const devConfigScript = `<script>globalThis.__SSR_CONFIG=${JSON.stringify({ ssrPath, reloadId: getReloadId() })}</script>`;
|
|
164
164
|
|
|
165
165
|
// HTML renderer
|
|
166
166
|
const html: HtmlFn<T> = async (render, opts = {} as T) => {
|