Skip to content

Commit a249317

Browse files
fix: prevent spurious full-reload on first browser visit after dev server start (#17283)\n\nThe invalidateDataStore function in the content virtual mod plugin was\nsending a full-reload signal to the client HMR channel during the\nbuildStart hook. Since no browser had loaded content yet at startup,\nthis queued reload was delivered to the first connecting client, causing\nan immediate page reload.\n\nAdd a notifyClient option to invalidateDataStore and pass\n{ notifyClient: false } from buildStart to skip the client reload\nduring initial startup while preserving the module invalidation needed\nto avoid the data store race condition (PR #12938)." (#17286)
Co-authored-by: Matthew Phillips <matthewphillips@cloudflare.com>
1 parent a94d4a5 commit a249317

3 files changed

Lines changed: 104 additions & 7 deletions

File tree

.changeset/free-friends-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes the first browser visit after `astro dev` starts triggering an immediate full page reload

packages/astro/src/content/vite-plugin-content-virtual-mod.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function invalidateAssetImports(viteServer: ViteDevServer, filePath: string) {
6767
}
6868
}
6969

70-
function invalidateDataStore(viteServer: ViteDevServer) {
70+
function invalidateDataStore(viteServer: ViteDevServer, { notifyClient = true } = {}) {
7171
const environment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
7272
const module = environment.moduleGraph.getModuleById(RESOLVED_DATA_STORE_VIRTUAL_ID);
7373
if (module) {
@@ -91,10 +91,16 @@ function invalidateDataStore(viteServer: ViteDevServer) {
9191
// Signal the SSR runner to clear its route cache so that getStaticPaths()
9292
// is re-evaluated with the updated content collection data.
9393
environment.hot.send('astro:content-changed', {});
94-
viteServer.environments.client.hot.send({
95-
type: 'full-reload',
96-
path: '*',
97-
});
94+
// Only notify the client to reload when data has actually changed at runtime.
95+
// During initial startup (buildStart), no client has loaded content yet, so
96+
// sending a full-reload would just cause a spurious page reload for the first
97+
// browser that connects.
98+
if (notifyClient) {
99+
viteServer.environments.client.hot.send({
100+
type: 'full-reload',
101+
path: '*',
102+
});
103+
}
98104
}
99105

100106
export function astroContentVirtualModPlugin({
@@ -126,8 +132,9 @@ export function astroContentVirtualModPlugin({
126132
// We defer adding the data store file to the watcher until the server is ready
127133
devServer.watcher.add(fileURLToPath(dataStoreFile));
128134
devServer.watcher.add(assetImportsPath);
129-
// Manually invalidate the data store to avoid a race condition in file watching
130-
invalidateDataStore(devServer);
135+
// Manually invalidate the data store to avoid a race condition in file watching.
136+
// Skip client reload since no browser has loaded content yet at startup.
137+
invalidateDataStore(devServer, { notifyClient: false });
131138
invalidateAssetImports(devServer, assetImportsPath);
132139
}
133140
},
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
import nodeFs from 'node:fs';
4+
import { astroContentVirtualModPlugin } from '../../../dist/content/vite-plugin-content-virtual-mod.js';
5+
import { createMinimalSettings, createTempDir } from './test-helpers.ts';
6+
7+
/**
8+
* Creates a minimal mock environment module graph.
9+
*/
10+
function createMockModuleGraph() {
11+
return {
12+
getModuleById: () => null,
13+
getModulesByFile: () => null,
14+
invalidateModule: () => {},
15+
};
16+
}
17+
18+
/**
19+
* Creates a minimal mock ViteDevServer with just enough structure for
20+
* the content virtual mod plugin's buildStart hook.
21+
*/
22+
function createMockViteDevServer() {
23+
const sentMessages: Array<Record<string, unknown>> = [];
24+
return {
25+
sentMessages,
26+
environments: {
27+
ssr: {
28+
moduleGraph: createMockModuleGraph(),
29+
hot: {
30+
send: (type: unknown, data: unknown) => {
31+
sentMessages.push({ channel: 'ssr', type, data });
32+
},
33+
},
34+
},
35+
client: {
36+
moduleGraph: createMockModuleGraph(),
37+
hot: {
38+
send: (payload: Record<string, unknown>) => {
39+
sentMessages.push({ channel: 'client', ...payload });
40+
},
41+
},
42+
},
43+
},
44+
watcher: {
45+
add: () => {},
46+
on: () => {},
47+
},
48+
};
49+
}
50+
51+
describe('astroContentVirtualModPlugin', () => {
52+
it('does not send full-reload to client during buildStart', () => {
53+
const root = createTempDir('content-virtual-mod-test-');
54+
const settings = createMinimalSettings(root, {
55+
config: {
56+
legacy: {},
57+
},
58+
});
59+
settings.injectedTypes = [];
60+
61+
const plugin = astroContentVirtualModPlugin({ settings, fs: nodeFs });
62+
63+
// Simulate Vite's plugin lifecycle: config → configureServer → buildStart
64+
// @ts-expect-error - mock args are sufficient for this test
65+
plugin.config?.({}, { command: 'serve' });
66+
67+
const mockServer = createMockViteDevServer();
68+
// @ts-expect-error - mock server has enough structure for this test
69+
plugin.configureServer?.(mockServer);
70+
71+
// buildStart is where the bug was: it called invalidateDataStore which sent full-reload
72+
// @ts-expect-error - calling without full Rollup context
73+
plugin.buildStart?.();
74+
75+
// Verify no full-reload was sent to the client
76+
const clientReloads = mockServer.sentMessages.filter(
77+
(msg) => msg.channel === 'client' && msg.type === 'full-reload',
78+
);
79+
assert.equal(
80+
clientReloads.length,
81+
0,
82+
'buildStart should not send full-reload to client during startup',
83+
);
84+
});
85+
});

0 commit comments

Comments
 (0)