diff --git a/.changeset/free-friends-lick.md b/.changeset/free-friends-lick.md new file mode 100644 index 000000000000..1df398644234 --- /dev/null +++ b/.changeset/free-friends-lick.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes the first browser visit after `astro dev` starts triggering an immediate full page reload diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index d2f794327276..de6fc216f330 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -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) { @@ -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({ @@ -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); } }, diff --git a/packages/astro/test/units/content-layer/content-virtual-mod.test.ts b/packages/astro/test/units/content-layer/content-virtual-mod.test.ts new file mode 100644 index 000000000000..6196ee30fffc --- /dev/null +++ b/packages/astro/test/units/content-layer/content-virtual-mod.test.ts @@ -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> = []; + 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) => { + 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', + ); + }); +});