astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
97 lines (96 loc) • 4.75 kB
JavaScript
import { parseAstroRequest } from "./query.js";
import { frontmatterRE } from "./utils.js";
export async function handleHotUpdate(ctx, { logger, compile, astroFileToCompileMetadata }) {
// HANDLING 1: Invalidate compile metadata if CSS dependency updated
//
// If any `ctx.file` is part of a CSS dependency of any Astro file, invalidate its `astroFileToCompileMetadata`
// so the next transform of the Astro file or Astro script/style virtual module will re-generate it
for (const [astroFile, compileData] of astroFileToCompileMetadata) {
// PostCSS plugins such as `@unocss/postcss` can list the component itself as a dependency
// of its own style. Edits to `ctx.file` are handled below, which needs its metadata (#18406).
if (astroFile === ctx.file) continue;
const isUpdatedFileCssDep = compileData.css.some((css) => css.dependencies?.includes(ctx.file));
if (isUpdatedFileCssDep) {
astroFileToCompileMetadata.delete(astroFile);
}
}
// HANDLING 2: Only invalidate modules whose output includes the changed styles
//
// If only the style code has changed, e.g. editing the `color`, then we can directly invalidate
// the Astro CSS virtual modules and `?container` modules. Other Astro modules don't include the CSS
// in their generated JavaScript and don't need to be invalidated.
const oldCode = astroFileToCompileMetadata.get(ctx.file)?.originalCode;
if (oldCode == null) return;
const newCode = await ctx.read();
if (isStyleOnlyChanged(oldCode, newCode)) {
logger.debug("watch", "style-only change");
// Eagerly re-compile to update the metadata with the new CSS. This ensures
// the compile metadata stays consistent so that subsequent SSR requests
// (e.g. page refresh) can load the style virtual modules without needing
// to re-compile from disk, avoiding potential stale state.
try {
await compile(newCode, ctx.file);
} catch {
// If re-compilation fails, fall back to deleting the metadata so the
// load hook will re-compile lazily on the next request.
astroFileToCompileMetadata.delete(ctx.file);
}
return ctx.modules.filter((mod) => {
if (!mod.id) {
return false;
}
const { query } = parseAstroRequest(mod.id);
// Inline style virtual modules are SSR-only, while `?container` modules embed extracted styles.
return query.container || query.astro && query.type === "style" && !query.inline;
});
}
// When non-style content changed (markup, frontmatter, scripts), Vite's default HMR
// invalidates the affected modules in its module graph. Clear the compile metadata so
// the load hook recompiles from disk on the next request instead of serving stale CSS.
astroFileToCompileMetadata.delete(ctx.file);
}
// Disable eslint as we're not sure how to improve this regex yet
// eslint-disable-next-line regexp/no-super-linear-backtracking
const scriptRE = /<script(?:\s.*?)?>.*?<\/script>/gs;
// eslint-disable-next-line regexp/no-super-linear-backtracking
const styleRE = /<style(?:\s.*?)?>.*?<\/style>/gs;
export function isStyleOnlyChanged(oldCode, newCode) {
if (oldCode === newCode) return false;
// Before we can regex-capture style tags, we remove the frontmatter and scripts
// first as they could contain false-positive style tag matches. At the same time,
// we can also compare if they have changed and early out.
// Strip off and compare frontmatter
let oldFrontmatter = "";
let newFrontmatter = "";
oldCode = oldCode.replace(frontmatterRE, (m) => (oldFrontmatter = m, ""));
newCode = newCode.replace(frontmatterRE, (m) => (newFrontmatter = m, ""));
if (oldFrontmatter !== newFrontmatter) return false;
// Strip off and compare scripts
const oldScripts = [];
const newScripts = [];
oldCode = oldCode.replace(scriptRE, (m) => (oldScripts.push(m), ""));
newCode = newCode.replace(scriptRE, (m) => (newScripts.push(m), ""));
if (!isArrayEqual(oldScripts, newScripts)) return false;
// Finally, we can compare styles
const oldStyles = [];
const newStyles = [];
oldCode = oldCode.replace(styleRE, (m) => (oldStyles.push(m), ""));
newCode = newCode.replace(styleRE, (m) => (newStyles.push(m), ""));
// Remaining of `oldCode` and `newCode` is the markup, return false if they're different
if (oldCode !== newCode) return false;
// Finally, check if only the style changed.
// The length must also be the same for style only change. If style tags are added/removed,
// we need to regenerate the main Astro file so that its CSS imports are also added/removed
return oldStyles.length === newStyles.length && !isArrayEqual(oldStyles, newStyles);
}
function isArrayEqual(a, b) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}