Skip to content
Merged
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/famous-heads-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes SCSS and CSS module file changes triggering a full page reload instead of hot-updating styles in place during development
6 changes: 6 additions & 0 deletions packages/astro/e2e/fixtures/hmr/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import preact from '@astrojs/preact';
import { defineConfig } from 'astro/config';

export default defineConfig({
integrations: [preact()],
});
4 changes: 4 additions & 0 deletions packages/astro/e2e/fixtures/hmr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"name": "@e2e/hmr",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/preact": "workspace:*",
"preact": "^10.28.2"
},
"devDependencies": {
"astro": "workspace:*",
"sass": "^1.98.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styles from '../styles/scss-module.module.scss';

export default function ScssModuleHeading() {
return <h1 class={styles.scssModule}>This is blue</h1>;
}
11 changes: 11 additions & 0 deletions packages/astro/e2e/fixtures/hmr/src/pages/scss-external.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<html>
<head>
<title>Test</title>
</head>
<body>
<h1 class="scss-external">This is blue</h1>
<style>
@import "../styles/scss-external.scss";
</style>
</body>
</html>
12 changes: 12 additions & 0 deletions packages/astro/e2e/fixtures/hmr/src/pages/scss-module.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import ScssModuleHeading from '../components/ScssModuleHeading.jsx';
---

<html>
<head>
<title>Test</title>
</head>
<body>
<ScssModuleHeading client:load />
</body>
</html>
3 changes: 3 additions & 0 deletions packages/astro/e2e/fixtures/hmr/src/styles/scss-external.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.scss-external {
color: blue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.scssModule {
color: blue;
}
30 changes: 30 additions & 0 deletions packages/astro/e2e/hmr.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,36 @@ test.describe('Styles', () => {
await expect(h).toHaveCSS('color', 'rgb(255, 0, 0)');
});

test('external SCSS refresh with HMR', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/scss-external'));

page.once('load', throwPageShouldNotReload);

const h = page.locator('h1');
await expect(h).toHaveCSS('color', 'rgb(0, 0, 255)');

await astro.editFile('./src/styles/scss-external.scss', (original) =>
original.replace('blue', 'red'),
);

await expect(h).toHaveCSS('color', 'rgb(255, 0, 0)');
});

test('SCSS modules refresh with HMR', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/scss-module'));

page.once('load', throwPageShouldNotReload);

const h = page.locator('h1');
await expect(h).toHaveCSS('color', 'rgb(0, 0, 255)');

await astro.editFile('./src/styles/scss-module.module.scss', (original) =>
original.replace('blue', 'red'),
);

await expect(h).toHaveCSS('color', 'rgb(255, 0, 0)');
});

test('added style tag refresh with full-reload', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/css-inline-component'));

Expand Down
28 changes: 28 additions & 0 deletions packages/astro/src/vite-plugin-hmr-reload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import { VIRTUAL_PAGE_RESOLVED_MODULE_ID } from '../vite-plugin-pages/const.js';
import { getDevCssModuleNameFromPageVirtualModuleName } from '../vite-plugin-css/util.js';
import { isAstroServerEnvironment } from '../environments.js';

const STYLE_EXT_REGEX = /\.(?:css|scss|sass|less|styl|pcss)$/i;
Comment thread
aralroca marked this conversation as resolved.

function isStyleModule(mod: EnvironmentModuleNode): boolean {
if (mod.file && STYLE_EXT_REGEX.test(mod.file)) return true;
// CSS modules and other style files may have query params in their id (e.g. ?used, ?direct)
if (mod.id) {
const idPath = mod.id.split('?')[0];
if (STYLE_EXT_REGEX.test(idPath)) return true;
}
return false;
}

/**
* The very last Vite plugin to reload the browser if any SSR-only module are updated
* which will require a full page reload. This mimics the behaviour of Vite 5 where
Expand All @@ -18,10 +30,16 @@ export default function hmrReload(): Plugin {
if (!isAstroServerEnvironment(this.environment)) return;

let hasSsrOnlyModules = false;
let hasSkippedStyleModules = false;

const invalidatedModules = new Set<EnvironmentModuleNode>();
for (const mod of modules) {
if (mod.id == null) continue;
if (isStyleModule(mod)) {
hasSkippedStyleModules = true;
continue;
}

const clientModule = server.environments.client.moduleGraph.getModuleById(mod.id);
if (clientModule != null) continue;

Expand All @@ -45,6 +63,16 @@ export default function hmrReload(): Plugin {
server.ws.send({ type: 'full-reload' });
return [];
}

// When style modules were skipped, return an empty array to prevent Vite's
// default SSR HMR propagation. Without this, Vite would propagate through the
// module graph to .astro importers, find no HMR acceptor, and trigger a
// full page reload. The client environment handles CSS HMR natively via
// Vite's built-in style update mechanism, which works for all pages
// (with or without framework components).
if (hasSkippedStyleModules) {
return [];
}
},
},
};
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading