Prevent overcaching of astro components for HMR (#5293)

* Prevent overcaching of astro components for HMR

* Get tests working in windows

* Use the right drive

* Adding a changeset
This commit is contained in:
Matthew Phillips 2022-11-03 16:38:22 -04:00 committed by GitHub
parent e7dc8b956d
commit 4af4d8fa00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 19 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Prevent overcaching .astro HMR changes

View file

@ -96,6 +96,7 @@ export async function createVite(
}, },
plugins: [ plugins: [
configAliasVitePlugin({ settings }), configAliasVitePlugin({ settings }),
astroLoadFallbackPlugin({ fs, settings }),
astroVitePlugin({ settings, logging }), astroVitePlugin({ settings, logging }),
astroScriptsPlugin({ settings }), astroScriptsPlugin({ settings }),
// The server plugin is for dev only and having it run during the build causes // The server plugin is for dev only and having it run during the build causes
@ -110,7 +111,6 @@ export async function createVite(
astroPostprocessVitePlugin({ settings }), astroPostprocessVitePlugin({ settings }),
astroIntegrationsContainerPlugin({ settings, logging }), astroIntegrationsContainerPlugin({ settings, logging }),
astroScriptsPageSSRPlugin({ settings }), astroScriptsPageSSRPlugin({ settings }),
astroLoadFallbackPlugin({ fs }),
], ],
publicDir: fileURLToPath(settings.config.publicDir), publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root), root: fileURLToPath(settings.config.root),

View file

@ -24,7 +24,7 @@ export async function handleHotUpdate(
{ config, logging, compile }: HandleHotUpdateOptions { config, logging, compile }: HandleHotUpdateOptions
) { ) {
let isStyleOnlyChange = false; let isStyleOnlyChange = false;
if (ctx.file.endsWith('.astro')) { if (ctx.file.endsWith('.astro') && isCached(config, ctx.file)) {
// Get the compiled result from the cache // Get the compiled result from the cache
const oldResult = await compile(); const oldResult = await compile();
// But we also need a fresh, uncached result to compare it to // But we also need a fresh, uncached result to compare it to

View file

@ -1,34 +1,65 @@
import nodeFs from 'fs'; import type { AstroSettings } from '../@types/astro';
import type * as vite from 'vite'; import type * as vite from 'vite';
import nodeFs from 'fs';
import npath from 'path';
type NodeFileSystemModule = typeof nodeFs; type NodeFileSystemModule = typeof nodeFs;
export interface LoadFallbackPluginParams { export interface LoadFallbackPluginParams {
fs?: NodeFileSystemModule; fs?: NodeFileSystemModule;
settings: AstroSettings;
} }
export default function loadFallbackPlugin({ fs }: LoadFallbackPluginParams): vite.Plugin | false { export default function loadFallbackPlugin({ fs, settings }: LoadFallbackPluginParams): vite.Plugin[] | false {
// Only add this plugin if a custom fs implementation is provided. // Only add this plugin if a custom fs implementation is provided.
if (!fs || fs === nodeFs) { if (!fs || fs === nodeFs) {
return false; return false;
} }
return { const tryLoadModule = async (id: string) => {
name: 'astro:load-fallback', try {
enforce: 'post', // await is necessary for the catch
async load(id) { return await fs.promises.readFile(cleanUrl(id), 'utf-8');
} catch (e) {
try { try {
// await is necessary for the catch return await fs.promises.readFile(id, 'utf-8');
return await fs.promises.readFile(cleanUrl(id), 'utf-8'); } catch (e2) {
} catch (e) {
try { try {
return await fs.promises.readFile(id, 'utf-8'); const fullpath = new URL('.' + id, settings.config.root);
} catch (e2) { return await fs.promises.readFile(fullpath, 'utf-8');
} catch (e3) {
// Let fall through to the next // Let fall through to the next
} }
} }
}, }
}; };
return [{
name: 'astro:load-fallback',
enforce: 'post',
resolveId(id, parent) {
if(id.startsWith('.') && parent && fs.existsSync(parent)) {
return npath.posix.join(npath.posix.dirname(parent), id);
}
},
async load(id) {
const source = await tryLoadModule(id);
return source;
}
}, {
name: 'astro:load-fallback-hmr',
enforce: 'pre',
handleHotUpdate(context) {
// Wrap context.read so it checks our filesystem first.
const read = context.read;
context.read = async () => {
const source = await tryLoadModule(context.file);
if(source) return source;
return read.call(context);
};
}
}];
} }
const queryRE = /\?.*$/s; const queryRE = /\?.*$/s;

View file

@ -2,7 +2,7 @@ import { expect } from 'chai';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { runInContainer } from '../../../dist/core/dev/index.js'; import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFs, createRequestAndResponse } from '../test-utils.js'; import { createFs, createRequestAndResponse, triggerFSEvent } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url); const root = new URL('../../fixtures/alias/', import.meta.url);
@ -37,4 +37,67 @@ describe('dev container', () => {
expect($('h1')).to.have.a.lengthOf(1); expect($('h1')).to.have.a.lengthOf(1);
}); });
}); });
it('HMR only short circuits on previously cached modules', async () => {
const fs = createFs(
{
'/src/components/Header.astro': `
<h1>{Astro.props.title}</h1>
`,
'/src/pages/index.astro': `
---
import Header from '../components/Header.astro';
const name = 'Testing';
---
<html>
<head><title>{name}</title></head>
<body class="one">
<Header title={name} />
</body>
</html>
`,
},
root
);
await runInContainer({ fs, root }, async (container) => {
let r = createRequestAndResponse({
method: 'GET',
url: '/',
});
container.handle(r.req, r.res);
let html = await r.text();
let $ = cheerio.load(html);
expect($('body.one')).to.have.a.lengthOf(1);
fs.writeFileFromRootSync('/src/components/Header.astro', `
<h1>{Astro.props.title}</h1>
`);
triggerFSEvent(container, fs, '/src/components/Header.astro', 'change');
fs.writeFileFromRootSync('/src/pages/index.astro', `
---
import Header from '../components/Header.astro';
const name = 'Testing';
---
<html>
<head><title>{name}</title></head>
<body class="two">
<Header title={name} />
</body>
</html>
`);
triggerFSEvent(container, fs, '/src/pages/index.astro', 'change');
r = createRequestAndResponse({
method: 'GET',
url: '/',
});
container.handle(r.req, r.res);
html = await r.text();
$ = cheerio.load(html);
expect($('body.one')).to.have.a.lengthOf(0);
expect($('body.two')).to.have.a.lengthOf(1);
});
});
}); });

View file

@ -12,15 +12,23 @@ class MyVolume extends Volume {
this.#root = root; this.#root = root;
} }
#forcePath(p) {
if (p instanceof URL) {
p = unixify(fileURLToPath(p));
}
return p;
}
getFullyResolvedPath(pth) { getFullyResolvedPath(pth) {
return npath.posix.join(this.#root, pth); return npath.posix.join(this.#root, pth);
} }
existsSync(p) { existsSync(p) {
if (p instanceof URL) { return super.existsSync(this.#forcePath(p));
p = fileURLToPath(p); }
}
return super.existsSync(p); readFile(p, ...args) {
return super.readFile(this.#forcePath(p), ...args);
} }
writeFileFromRootSync(pth, ...rest) { writeFileFromRootSync(pth, ...rest) {
@ -44,6 +52,28 @@ export function createFs(json, root) {
return fs; return fs;
} }
/**
*
* @param {import('../../src/core/dev/container').Container} container
* @param {typeof import('fs')} fs
* @param {string} shortPath
* @param {'change'} eventType
*/
export function triggerFSEvent(container, fs, shortPath, eventType) {
container.viteServer.watcher.emit(
eventType,
fs.getFullyResolvedPath(shortPath)
);
if(!fileURLToPath(container.settings.config.root).startsWith('/')) {
const drive = fileURLToPath(container.settings.config.root).slice(0, 2);
container.viteServer.watcher.emit(
eventType,
drive + fs.getFullyResolvedPath(shortPath)
);
}
}
export function createRequestAndResponse(reqOptions = {}) { export function createRequestAndResponse(reqOptions = {}) {
const req = httpMocks.createRequest(reqOptions); const req = httpMocks.createRequest(reqOptions);