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:
parent
e7dc8b956d
commit
4af4d8fa00
6 changed files with 148 additions and 19 deletions
5
.changeset/heavy-bananas-deny.md
Normal file
5
.changeset/heavy-bananas-deny.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Prevent overcaching .astro HMR changes
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue