From 0554f063f6392fa49da0478fef68c80f10c391fc Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 27 May 2020 17:14:33 -0400 Subject: [PATCH] fix(hmr): avoid fetching stale modules on nested hmr updates + clear stale accept callbacks on hmr context re-creation --- src/client/client.ts | 11 ++++-- src/node/server/serverPluginHmr.ts | 41 ++++++++++++-------- src/node/server/serverPluginModuleRewrite.ts | 16 ++++---- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/client/client.ts b/src/client/client.ts index f10e5d7d38aabf..57d2fa551e3a06 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -196,10 +196,6 @@ async function updateModule( ? deps.some((dep) => modulesToUpdate.has(dep)) : modulesToUpdate.has(deps) }) - // reset callbacks on self update since they are going to be registered again - if (isSelfUpdate) { - mod.callbacks = [] - } await Promise.all( Array.from(modulesToUpdate).map(async (dep) => { @@ -247,6 +243,13 @@ export const createHotContext = (id: string) => { dataMap.set(id, {}) } + // when a file is hot updated, a new context is created + // clear its stale callbacks + const mod = hotModulesMap.get(id) + if (mod) { + mod.callbacks = [] + } + const hot = { get data() { return dataMap.get(id) diff --git a/src/node/server/serverPluginHmr.ts b/src/node/server/serverPluginHmr.ts index 45953099f1befb..1ea4d7113f947f 100644 --- a/src/node/server/serverPluginHmr.ts +++ b/src/node/server/serverPluginHmr.ts @@ -27,11 +27,17 @@ import { resolveImport } from './serverPluginModuleRewrite' import { FSWatcher } from 'chokidar' import MagicString from 'magic-string' import { parse } from '../utils/babelParse' -import { Node, StringLiteral, Statement, Expression } from '@babel/types' import { InternalResolver } from '../resolver' import LRUCache from 'lru-cache' import slash from 'slash' import { cssPreprocessLangRE } from '../utils/cssUtils' +import { + Node, + StringLiteral, + Statement, + Expression, + IfStatement +} from '@babel/types' export const debugHmr = require('debug')('vite:hmr') @@ -57,6 +63,7 @@ export const importeeMap: HMRStateMap = new Map() // files that are dirty (i.e. in the import chain between the accept boundrary // and the actual changed file) for an hmr update at a given timestamp. export const hmrDirtyFilesMap = new LRUCache>({ max: 10 }) +export const latestVersionsMap = new Map() // client and node files are placed flat in the dist folder export const hmrClientFilePath = path.resolve(__dirname, '../client.js') @@ -98,6 +105,9 @@ export const hmrPlugin: ServerPlugin = ({ ctx.status = 200 ctx.body = hmrClient } else { + if (ctx.query.t) { + latestVersionsMap.set(ctx.path, ctx.query.t) + } return next() } }) @@ -267,7 +277,8 @@ export function rewriteFileWithHMR( resolver: InternalResolver, s: MagicString ) { - let hasDeclined + let hasDeclined = false + let importMetaConditional: IfStatement | undefined const registerDep = (e: StringLiteral) => { const deps = ensureMapEntry(hmrAcceptanceMap, importer) @@ -291,7 +302,7 @@ export function rewriteFileWithHMR( if (isTopLevel) { console.warn( chalk.yellow( - `[vite warn] HMR syntax error in ${importer}: import.meta.hot.accept() ` + + `[vite] HMR syntax error in ${importer}: import.meta.hot.accept() ` + `should be wrapped in \`if (import.meta.hot) {}\` conditional ` + `blocks so that they can be tree-shaken in production.` ) @@ -384,18 +395,13 @@ export function rewriteFileWithHMR( if (node.type === 'ExpressionStatement') { // top level hot.accept() call checkHotCall(node.expression, isTopLevel, isDevBlock) - // import.meta.hot && import.meta.hot.accept() - if ( - node.expression.type === 'LogicalExpression' && - node.expression.operator === '&&' && - isMetaHot(node.expression.left) - ) { - checkHotCall(node.expression.right, false, isDevBlock) - } } // if (import.meta.hot) ... if (node.type === 'IfStatement') { const isDevBlock = isMetaHot(node.test) + if (isDevBlock) { + importMetaConditional = node + } if (node.consequent.type === 'BlockStatement') { node.consequent.body.forEach((s) => checkStatements(s, false, isDevBlock) @@ -410,11 +416,14 @@ export function rewriteFileWithHMR( const ast = parse(source) ast.forEach((s) => checkStatements(s, true, false)) - // inject import.meta.hot - s.prepend(` -import { createHotContext } from "${hmrClientPublicPath}" -import.meta.hot = createHotContext(${JSON.stringify(importer)}) - `) + if (importMetaConditional) { + // inject import.meta.hot + s.prependLeft( + importMetaConditional.start!, + `import { createHotContext } from "${hmrClientPublicPath}"; ` + + `import.meta.hot = createHotContext(${JSON.stringify(importer)}); ` + ) + } // clear decline state if (!hasDeclined) { diff --git a/src/node/server/serverPluginModuleRewrite.ts b/src/node/server/serverPluginModuleRewrite.ts index 56beecc2ed7294..79660ef86f136e 100644 --- a/src/node/server/serverPluginModuleRewrite.ts +++ b/src/node/server/serverPluginModuleRewrite.ts @@ -19,7 +19,8 @@ import { ensureMapEntry, rewriteFileWithHMR, hmrClientPublicPath, - hmrDirtyFilesMap + hmrDirtyFilesMap, + latestVersionsMap } from './serverPluginHmr' import { readBody, @@ -273,14 +274,13 @@ export const resolveImport = ( // 5. force re-fetch dirty imports by appending timestamp if (timestamp) { const dirtyFiles = hmrDirtyFilesMap.get(timestamp) - // only force re-fetch if this is a marked dirty file (in the import - // chain of the changed file) or a vue part request (made by a dirty - // vue main request) - if ( - (dirtyFiles && dirtyFiles.has(pathname)) || - /\?type=(template|style)/.test(id) - ) { + // only rewrite if: + if (dirtyFiles && dirtyFiles.has(pathname)) { + // 1. this is a marked dirty file (in the import chain of the changed file) query += `${query ? `&` : `?`}t=${timestamp}` + } else if (latestVersionsMap.has(pathname)) { + // 2. this file was previously hot-updated and has an updated version + query += `${query ? `&` : `?`}t=${latestVersionsMap.get(pathname)}` } } return pathname + query