Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-client-hmr-program-reload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes an issue where editing a client-side component (e.g. with `client:idle`, `client:load`, etc.) caused an unnecessary full program reload of the backend during development. The `astro:hmr-reload` plugin now correctly returns an empty array when all changed modules are found in the client module graph, preventing Vite's default SSR HMR propagation from triggering a full reload. This was a regression from Astro v5 where client-side component edits only triggered client-side HMR without affecting the server.
10 changes: 10 additions & 0 deletions packages/astro/src/vite-plugin-hmr-reload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ export default function hmrReload(): Plugin {
if (hasSkippedStyleModules) {
return [];
}

// If we processed modules but none were SSR-only (all were found in the
// client module graph), return an empty array to prevent Vite's default
// HMR propagation. Without this, Vite would propagate through the SSR
// module graph, find no HMR boundary (e.g. .astro files), and trigger
// a full-reload that causes unnecessary program reloads for the module
// runner. The client environment handles HMR for these modules natively.
if (modules.length > 0) {
return [];
}
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,43 @@ describe('astro:hmr-reload', () => {
};
}

/**
* Creates a mock environment (server-side) with a name and moduleGraph.
*/
function createMockEnvironment(name: string, moduleIds: string[] = []) {
const idToModuleMap = new Map<string, any>();
for (const id of moduleIds) {
idToModuleMap.set(id, createMockModule(id));
}
return {
name,
moduleGraph: {
getModuleById(id: string) {
return idToModuleMap.get(id) ?? null;
},
invalidateModule(_mod: any) {},
},
};
}

/**
* Creates a mock server with client and ssr environments.
*/
function createMockServer(clientModuleIds: string[] = []) {
const wsSent: any[] = [];
return {
environments: {
client: createMockEnvironment('client', clientModuleIds),
},
ws: {
send(payload: any) {
wsSent.push(payload);
},
},
_wsSent: wsSent,
};
}

function getHotUpdateHandler() {
const plugin = hmrReload();
// The hotUpdate hook is an object with order and handler
Expand Down Expand Up @@ -209,4 +246,20 @@ describe('astro:hmr-reload', () => {
assert.equal(ctx.invalidated.length, 1, 'should only invalidate SSR-only module');
assert.equal(ctx.invalidated[0], ssrMod);
});

it('returns [] for modules that exist in the client module graph (prevents unnecessary program reload)', () => {
const handler = getHotUpdateHandler();
const moduleId = '/src/components/Foo.tsx';
const modules = [createMockModule(moduleId)];
const server = createMockServer([moduleId]); // module IS in client graph
const env = createMockEnvironment('ssr');

const result = handler.call(
{ environment: env },
{ modules, server, timestamp: Date.now() },
);

assert.deepEqual(result, [], 'Should return empty array to prevent Vite default HMR propagation');
assert.equal(server._wsSent.length, 0, 'Should NOT send full-reload via WebSocket');
});
});
Loading