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/free-friends-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes the first browser visit after `astro dev` starts triggering an immediate full page reload
21 changes: 14 additions & 7 deletions packages/astro/src/content/vite-plugin-content-virtual-mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function invalidateAssetImports(viteServer: ViteDevServer, filePath: string) {
}
}

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

export function astroContentVirtualModPlugin({
Expand Down Expand Up @@ -126,8 +132,9 @@ export function astroContentVirtualModPlugin({
// We defer adding the data store file to the watcher until the server is ready
devServer.watcher.add(fileURLToPath(dataStoreFile));
devServer.watcher.add(assetImportsPath);
// Manually invalidate the data store to avoid a race condition in file watching
invalidateDataStore(devServer);
// Manually invalidate the data store to avoid a race condition in file watching.
// Skip client reload since no browser has loaded content yet at startup.
invalidateDataStore(devServer, { notifyClient: false });
invalidateAssetImports(devServer, assetImportsPath);
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import nodeFs from 'node:fs';
import { astroContentVirtualModPlugin } from '../../../dist/content/vite-plugin-content-virtual-mod.js';
import { createMinimalSettings, createTempDir } from './test-helpers.ts';

/**
* Creates a minimal mock environment module graph.
*/
function createMockModuleGraph() {
return {
getModuleById: () => null,
getModulesByFile: () => null,
invalidateModule: () => {},
};
}

/**
* Creates a minimal mock ViteDevServer with just enough structure for
* the content virtual mod plugin's buildStart hook.
*/
function createMockViteDevServer() {
const sentMessages: Array<Record<string, unknown>> = [];
return {
sentMessages,
environments: {
ssr: {
moduleGraph: createMockModuleGraph(),
hot: {
send: (type: unknown, data: unknown) => {
sentMessages.push({ channel: 'ssr', type, data });
},
},
},
client: {
moduleGraph: createMockModuleGraph(),
hot: {
send: (payload: Record<string, unknown>) => {
sentMessages.push({ channel: 'client', ...payload });
},
},
},
},
watcher: {
add: () => {},
on: () => {},
},
};
}

describe('astroContentVirtualModPlugin', () => {
it('does not send full-reload to client during buildStart', () => {
const root = createTempDir('content-virtual-mod-test-');
const settings = createMinimalSettings(root, {
config: {
legacy: {},
},
});
settings.injectedTypes = [];

const plugin = astroContentVirtualModPlugin({ settings, fs: nodeFs });

// Simulate Vite's plugin lifecycle: config → configureServer → buildStart
// @ts-expect-error - mock args are sufficient for this test
plugin.config?.({}, { command: 'serve' });

const mockServer = createMockViteDevServer();
// @ts-expect-error - mock server has enough structure for this test
plugin.configureServer?.(mockServer);

// buildStart is where the bug was: it called invalidateDataStore which sent full-reload
// @ts-expect-error - calling without full Rollup context
plugin.buildStart?.();

// Verify no full-reload was sent to the client
const clientReloads = mockServer.sentMessages.filter(
(msg) => msg.channel === 'client' && msg.type === 'full-reload',
);
assert.equal(
clientReloads.length,
0,
'buildStart should not send full-reload to client during startup',
);
});
});
Loading