astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
158 lines (157 loc) • 7.25 kB
JavaScript
import { isRunnableDevEnvironment } from "vite";
import { VIRTUAL_PAGE_RESOLVED_MODULE_ID } from "../vite-plugin-pages/const.js";
import { RESOLVED_MODULE_DEV_CSS_PREFIX } from "../vite-plugin-css/const.js";
import { getDevCssModuleNameFromPageVirtualModuleName } from "../vite-plugin-css/util.js";
import { isAstroServerEnvironment } from "../environments.js";
const STYLE_EXT_REGEX = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/i;
const STYLE_COMPONENT_EXT_REGEX = /\.(astro|svelte|vue)$/i;
const RAW_QUERY_REGEX = /(?:\?|&)raw(?:&|$)/;
function hasStyleExtension(id) {
// Style module IDs may include Vite query params such as ?used or ?direct.
return STYLE_EXT_REGEX.test(id.split("?")[0]);
}
function isComponentStyleModule(id) {
const [filename, rawQuery] = id.split("?", 2);
const extensionMatch = STYLE_COMPONENT_EXT_REGEX.exec(filename);
if (!rawQuery || !extensionMatch) return false;
const query = new URLSearchParams(rawQuery);
return query.get("type") === "style" && query.has(extensionMatch[1].toLowerCase());
}
function getStyleModuleType(mod) {
// CSS imported with ?raw is a JS string export, so SSR importers need to be invalidated
// instead of relying on Vite's client-side CSS HMR handling.
if (mod.id && RAW_QUERY_REGEX.test(mod.id) && hasStyleExtension(mod.id)) return;
if (mod.id && isComponentStyleModule(mod.id)) return "component";
if (mod.file && hasStyleExtension(mod.file)) return "file";
// CSS modules and other style files may have query params in their id (e.g. ?used, ?direct)
return mod.id && hasStyleExtension(mod.id) ? "file" : undefined;
}
function hasClientStyleModuleByFile(moduleGraph, mod, styleType) {
if (mod.file == null || moduleGraph.getModulesByFile == null) return false;
const fileModules = moduleGraph.getModulesByFile(mod.file);
if (fileModules == null) return false;
for (const fileMod of fileModules) {
if (fileMod.id == null) continue;
if (styleType === "component") {
if (isComponentStyleModule(fileMod.id)) return true;
} else if (!RAW_QUERY_REGEX.test(fileMod.id) && hasStyleExtension(fileMod.id)) {
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
* it used to unconditionally reload for us.
*/
export default function hmrReload() {
return {
name: "astro:hmr-reload",
enforce: "post",
hotUpdate: {
order: "post",
handler({ modules, server, timestamp, file }) {
if (!isAstroServerEnvironment(this.environment)) return;
let hasSsrOnlyModules = false;
let hasStyleModules = false;
let hasSkippedStyleModules = false;
const invalidatedModules = new Set();
for (const mod of modules) {
if (mod.id == null) continue;
const styleType = getStyleModuleType(mod);
const clientModule = server.environments.client.moduleGraph.getModuleById(mod.id);
if (styleType) {
hasStyleModules = true;
// No client module means nothing will apply the CSS update client-side, so force a reload.
if (clientModule == null && !hasClientStyleModuleByFile(server.environments.client.moduleGraph, mod, styleType)) {
this.environment.moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true);
hasSsrOnlyModules = true;
continue;
}
hasSkippedStyleModules = true;
continue;
}
if (clientModule != null) continue;
this.environment.moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true);
hasSsrOnlyModules = true;
}
const invalidateDevCssModules = () => {
for (const [id, mod] of this.environment.moduleGraph.idToModuleMap) {
if (id.startsWith(RESOLVED_MODULE_DEV_CSS_PREFIX)) {
this.environment.moduleGraph.invalidateModule(mod, undefined, timestamp, true);
if (isRunnableDevEnvironment(this.environment)) {
const runnerMod = this.environment.runner.evaluatedModules.getModuleById(id);
if (runnerMod) {
this.environment.runner.evaluatedModules.invalidateModule(runnerMod);
}
}
}
}
};
// If any invalidated modules are virtual modules for pages, also invalidate their
// associated dev CSS modules, if any.
for (const invalidatedModule of invalidatedModules) {
if (invalidatedModule.id?.startsWith(VIRTUAL_PAGE_RESOLVED_MODULE_ID)) {
const cssMod = this.environment.moduleGraph.getModuleById(getDevCssModuleNameFromPageVirtualModuleName(invalidatedModule.id));
if (!cssMod || cssMod.id == null) continue;
this.environment.moduleGraph.invalidateModule(cssMod, undefined, timestamp, true);
}
}
if (hasSsrOnlyModules) {
if (hasStyleModules) {
invalidateDevCssModules();
}
// Invalidate all recursively-invalidated modules (importers) in the
// runner cache, not just the directly changed files. Without this,
// barrel files like index.ts stay cached and dynamic import() calls
// return stale exports.
if (isRunnableDevEnvironment(this.environment)) {
for (const invalidated of invalidatedModules) {
if (invalidated.id == null) continue;
const runnerModule = this.environment.runner.evaluatedModules.getModuleById(invalidated.id);
if (runnerModule) {
this.environment.runner.evaluatedModules.invalidateModule(runnerModule);
}
}
}
// Tell the browser to reload the page.
server.ws.send({ type: "full-reload" });
// For non-runnable environments (e.g. Cloudflare's workerd), we can't
// directly access the module runner. Send a full-reload through the
// environment's hot channel so the remote runner clears its cache.
if (!isRunnableDevEnvironment(this.environment)) {
this.environment.hot.send({
type: "full-reload",
triggeredBy: file,
path: "*"
});
}
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) {
// Invalidate all per-route dev CSS virtual modules so the next SSR request
// re-collects CSS with updated content. Without this, the inline <style>
// tags injected for anti-FOUC would serve stale CSS after HMR updates.
invalidateDevCssModules();
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 [];
}
}
}
};
}