Skip to content

Commit 072d092

Browse files
fibibotbartlomieju
andauthored
fix: inject client entry on islands-free pages for HMR (#3810)
Fixes #3806 --------- Co-authored-by: fibibot <fibibot@users.noreply.github.com> Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
1 parent 1fcf2d9 commit 072d092

4 files changed

Lines changed: 41 additions & 0 deletions

File tree

packages/fresh/src/build_cache.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export interface FileSnapshot {
1717
export interface BuildSnapshot<State> {
1818
version: string;
1919
clientEntry: string;
20+
/**
21+
* When defined, forces the boot script to be emitted in dev so HMR
22+
* listeners attach even on island-free pages. The value itself is
23+
* currently unused — only its presence matters. Undefined outside of dev.
24+
*/
25+
hmrClientEntry?: string;
2026
fsRoutes: FsRouteFile<State>[];
2127
staticFiles: Map<string, FileSnapshot>;
2228
islands: ServerIslandRegistry;
@@ -51,13 +57,15 @@ export class ProdBuildCache<State> implements BuildCache<State> {
5157
#snapshot: BuildSnapshot<State>;
5258
islandRegistry: ServerIslandRegistry;
5359
clientEntry: string;
60+
hmrClientEntry: string | undefined;
5461
features = { errorOverlay: false };
5562

5663
constructor(public root: string, snapshot: BuildSnapshot<State>) {
5764
setBuildId(snapshot.version);
5865
this.#snapshot = snapshot;
5966
this.islandRegistry = snapshot.islands;
6067
this.clientEntry = snapshot.clientEntry;
68+
this.hmrClientEntry = snapshot.hmrClientEntry;
6169
}
6270

6371
getEntryAssets(): string[] {

packages/fresh/src/dev/dev_build_cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ export async function generateSnapshotServer(
458458
outDir: string;
459459
buildId: string;
460460
clientEntry: string;
461+
hmrClientEntry?: string;
461462
islands: IslandModChunk[];
462463
// deno-lint-ignore no-explicit-any
463464
fsRoutesFiles: FsRouteFileNoMod<any>[];
@@ -524,12 +525,17 @@ export async function generateSnapshotServer(
524525
const entryAssets = options.entryAssets.map((url) => JSON.stringify(url))
525526
.join(",\n");
526527

528+
const hmrClientEntryDecl = options.hmrClientEntry !== undefined
529+
? `export const hmrClientEntry = ${JSON.stringify(options.hmrClientEntry)}`
530+
: "";
531+
527532
return `${EDIT_WARNING}
528533
import { IslandPreparer } from "fresh/internal";
529534
${islandImports}
530535
${fsRouteImports}
531536
532537
export const clientEntry = ${JSON.stringify(options.clientEntry)}
538+
${hmrClientEntryDecl}
533539
export const version = ${JSON.stringify(options.buildId)}
534540
535541
export const islands = new Map();

packages/plugin-vite/src/plugins/server_snapshot.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,19 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] {
165165
const staticFiles: PendingStaticFile[] = [];
166166
let islandMods: IslandModChunk[] = [];
167167
let clientEntry = "/@id/fresh:client-entry";
168+
let hmrClientEntry: string | undefined;
168169
let buildId = "";
169170
const entryAssets: string[] = [];
170171

171172
if (isDev && server !== undefined) {
173+
// Set hmrClientEntry so the SSR runtime always emits a boot script
174+
// in dev, even when a page has zero islands. Without this, edits
175+
// to island-free routes never trigger a browser reload because the
176+
// `fresh:reload` WebSocket listener is never attached. The value
177+
// is used as a marker only — its presence is what matters, not
178+
// what it points to.
179+
hmrClientEntry = clientEntry;
180+
172181
for (const id of islands.keys()) {
173182
const mod = server.environments.client.moduleGraph.getModuleById(
174183
id,
@@ -380,6 +389,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] {
380389
staticFiles,
381390
buildId,
382391
clientEntry,
392+
hmrClientEntry,
383393
entryAssets,
384394
fsRoutesFiles: result.routes,
385395
islands: islandMods,

packages/plugin-vite/tests/dev_server_test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,23 @@ integrationTest("vite dev - starts without islands/ dir", async () => {
9393
});
9494
});
9595

96+
// Issue: https://github.com/denoland/fresh/issues/3806
97+
// Pages without islands must still load the client entry in dev so the
98+
// HMR `fresh:reload` listener attaches and route edits trigger a refresh.
99+
integrationTest(
100+
"vite dev - injects client entry on islands-free pages for HMR",
101+
async () => {
102+
const fixture = path.join(FIXTURE_DIR, "no_islands");
103+
await withDevServer(fixture, async (address) => {
104+
const res = await fetch(`${address}/`);
105+
const text = await res.text();
106+
expect(text).toContain("ok");
107+
expect(text).toContain("/@id/fresh:client-entry");
108+
expect(text).toMatch(/import\s*\{\s*boot\s*\}/);
109+
});
110+
},
111+
);
112+
96113
integrationTest("vite dev - starts without routes/ dir", async () => {
97114
const fixture = path.join(FIXTURE_DIR, "no_routes");
98115
await withDevServer(fixture, async (address) => {

0 commit comments

Comments
 (0)