Improve style HMR (#4125)

* feat: improve style HMR

* chore: add inline comments

* Update hmr.ts

Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
Nate Moore 2022-08-03 14:03:56 -05:00 committed by GitHub
parent 09eca9be5e
commit 5f3b3b44db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 10 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix HMR of style blocks in Astro files. Updating a style block should no longer perform a full reload of the page.

View file

@ -47,8 +47,8 @@ export function reload({ file }: { file: string }): string {
return `${green('reload'.padStart(PREFIX_PADDING))} ${file}`;
}
export function hmr({ file }: { file: string }): string {
return `${green('update'.padStart(PREFIX_PADDING))} ${file}`;
export function hmr({ file, style = false }: { file: string; style?: boolean }): string {
return `${green('update'.padStart(PREFIX_PADDING))} ${file}${style ? ` ${dim('style')}` : ''}`;
}
/** Display dev server host and startup time */

View file

@ -5,7 +5,7 @@ import type { AstroConfig } from '../@types/astro';
import type { LogOptions } from '../core/logger/core.js';
import { info } from '../core/logger/core.js';
import * as msg from '../core/messages.js';
import { invalidateCompilation, isCached } from './compile.js';
import { cachedCompilation, invalidateCompilation, isCached } from './compile.js';
interface TrackCSSDependenciesOptions {
viteDevServer: ViteDevServer | null;
@ -55,9 +55,40 @@ const isPkgFile = (id: string | null) => {
return id?.startsWith(fileURLToPath(PKG_PREFIX)) || id?.startsWith(PKG_PREFIX.pathname);
};
export async function handleHotUpdate(ctx: HmrContext, config: AstroConfig, logging: LogOptions) {
// Invalidate the compilation cache so it recompiles
export interface HandleHotUpdateOptions {
config: AstroConfig;
logging: LogOptions;
compile: () => ReturnType<typeof cachedCompilation>;
}
export async function handleHotUpdate(
ctx: HmrContext,
{ config, logging, compile }: HandleHotUpdateOptions
) {
let isStyleOnlyChange = false;
if (ctx.file.endsWith('.astro')) {
// Get the compiled result from the cache
const oldResult = await compile();
// But we also need a fresh, uncached result to compare it to
invalidateCompilation(config, ctx.file);
const newResult = await compile();
// If the hashes are identical, we assume only styles have changed
if (oldResult.scope === newResult.scope) {
isStyleOnlyChange = true;
// All styles are the same, we can skip an HMR update
const styles = new Set(newResult.css);
for (const style of oldResult.css) {
if (styles.has(style)) {
styles.delete(style);
}
}
if (styles.size === 0) {
return [];
}
}
} else {
invalidateCompilation(config, ctx.file);
}
// Skip monorepo files to avoid console spam
if (isPkgFile(ctx.file)) {
@ -91,12 +122,21 @@ export async function handleHotUpdate(ctx: HmrContext, config: AstroConfig, logg
// Invalidate happens as a separate step because a single .astro file
// produces multiple CSS modules and we want to return all of those.
for (const file of files) {
if (isStyleOnlyChange && file === ctx.file) continue;
invalidateCompilation(config, file);
}
// Bugfix: sometimes style URLs get normalized and end with `lang.css=`
// These will cause full reloads, so filter them out here
const mods = ctx.modules.filter((m) => !m.url.endsWith('='));
const file = ctx.file.replace(config.root.pathname, '/');
// If only styles are changed, remove the component file from the update list
if (isStyleOnlyChange) {
info(logging, 'astro', msg.hmr({ file, style: true }));
// remove base file and hoisted scripts
return mods.filter((mod) => mod.id !== ctx.file && !mod.id?.endsWith('.ts'));
}
// Add hoisted scripts so these get invalidated
for (const mod of mods) {
@ -106,9 +146,9 @@ export async function handleHotUpdate(ctx: HmrContext, config: AstroConfig, logg
}
}
}
const isSelfAccepting = mods.every((m) => m.isSelfAccepting || m.url.endsWith('.svelte'));
const file = ctx.file.replace(config.root.pathname, '/');
// TODO: Svelte files should be marked as `isSelfAccepting` but they don't appear to be
const isSelfAccepting = mods.every((m) => m.isSelfAccepting || m.url.endsWith('.svelte'));
if (isSelfAccepting) {
info(logging, 'astro', msg.hmr({ file }));
} else {

View file

@ -142,6 +142,11 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
return {
code,
meta: {
vite: {
isSelfAccepting: true,
},
},
};
}
case 'script': {
@ -342,9 +347,19 @@ ${source}
throw err;
}
},
async handleHotUpdate(context) {
async handleHotUpdate(this: PluginContext, context) {
if (context.server.config.isProduction) return;
return handleHotUpdate.call(this, context, config, logging);
const compileProps: CompileProps = {
config,
filename: context.file,
moduleId: context.file,
source: await context.read(),
ssr: true,
viteTransform,
pluginContext: this,
};
const compile = () => cachedCompilation(compileProps);
return handleHotUpdate.call(this, context, { config, logging, compile });
},
};
}