Skip to content

Commit

Permalink
fix: dynamic-imported module can HMR if it is self-accepting (vitejs#320
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jun 2, 2020
2 parents 8d6ca75 + f6aa343 commit c096aa6
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 46 deletions.
2 changes: 1 addition & 1 deletion playground/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import { defineAsyncComponent } from 'vue'
import TestEnv from './TestEnv.vue'
import TestModuleResolve from './TestModuleResolve.vue'
import TestHmr from './TestHmr.vue'
import TestHmr from './TestHmr/TestHmr.vue'
import TestPostCss from './TestPostCss.vue'
import TestScopedCss from './TestScopedCss.vue'
import TestCssModules from './TestCssModules.vue'
Expand Down
37 changes: 0 additions & 37 deletions playground/TestHmr.vue

This file was deleted.

118 changes: 118 additions & 0 deletions playground/TestHmr/TestHmr.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<template>
<h2>Hot Module Replacement</h2>
<p>
<span>
HMR: click button and edit template part of <code>./TestHmr.vue</code>,
count should not reset
</span>
<button class="hmr-increment" @click="count++">
&gt;&gt;&gt; {{ count }} &lt;&lt;&lt;
</button>
</p>
<p>
<span>
HMR: edit the return value of <code>foo()</code> in
<code>./testHmrPropagation.js</code>, should update without reloading
page:
</span>
<span class="hmr-propagation">{{ foo() }}</span>
</p>
<p>
<span>
HMR: edit the value of <code>bar</code> in
<code>./testDynamicImportHmrPropagation.js</code>, should update without
reloading page:
</span>
<span class="hmr-propagation-dynamic">{{ barValue }}</span>
<button class="hmr-propagation-dynamic-load" @click="loadDynamic()">
load
</button>
</p>
<p>
<span>
HMR: edit the value of <code>baz</code> in
<code>./testFullDynamicImportHmrPropagation.js</code>, the app should not
update because the imported module is not self-accepting:
</span>
<span class="hmr-propagation-full-dynamic">{{ bazValue }}</span>
<button
class="hmr-propagation-full-dynamic-load"
@click="loadFullDynamic()"
>
load
</button>
</p>
<p>
<span>
HMR: edit the value of <code>__text</code> in
<code>./testFullDynamicImportHmrPropagationSelfAccepting.js</code>, the
app should update without reloading and the count state should persist,
because the imported module is self-accepting:
</span>
<span
class="hmr-propagation-full-dynamic-self-accepting"
ref="dynamicDataOutlet"
>qux not loaded</span
>
<button
class="hmr-propagation-full-dynamic-load-self-accepting"
@click="loadFullDynamicSelfAccepting()"
>
load
</button>
</p>
<p>
HMR: manual API (see console) - edit <code>./testHmrManual.js</code> and it
should log new exported value without reloading the page.
</p>
</template>

<script>
import { ref } from 'vue'
import { foo } from './testHmrPropagation'
export default {
setup() {
const barValue = ref('bar not loaded')
const bazValue = ref('baz not loaded')
const dynamicDataOutlet = ref()
return {
count: 0,
foo,
barValue,
bazValue,
dynamicDataOutlet,
loadDynamic() {
barValue.value = 'bar loading'
// This kind of dynamic import can be analyzed and rewrited by vite
import('./testHmrPropagationDynamicImport').then(({ bar }) => {
barValue.value = bar
})
},
loadFullDynamic() {
bazValue.value = 'baz loading'
// This kind of dynamic import can't be analyzed and rewrited by vite
import(dummy('./testHmrPropagationFullDynamicImport')).then(
({ baz }) => {
bazValue.value = baz
}
)
},
loadFullDynamicSelfAccepting() {
dynamicDataOutlet.value.innerHTML = 'qux loading'
// This kind of dynamic import can't be analyzed and rewrited by vite
import(
dummy('./testHmrPropagationFullDynamicImportSelfAccepting')
).then(({ render }) => {
render(dynamicDataOutlet.value)
})
}
}
}
}
// make the imported path unanalysable in build time
function dummy(value) {
return value
}
</script>
File renamed without changes.
1 change: 1 addition & 0 deletions playground/TestHmr/testHmrPropagationDynamicImport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const bar = 'bar loaded'
1 change: 1 addition & 0 deletions playground/TestHmr/testHmrPropagationFullDynamicImport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const baz = 'baz loaded'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export let __text = 'qux loaded'

let domEl

export function render(_domEl) {
domEl = _domEl
domEl.innerHTML = __text
}

if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
newModule.render(domEl)
})
}
13 changes: 11 additions & 2 deletions src/node/server/serverPluginHmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ export const hmrPlugin: ServerPlugin = ({

const publicPath = resolver.fileToRequest(filePath)
const importers = importerMap.get(publicPath)
if (importers) {
if (importers || isHmrAccepted(publicPath, publicPath)) {
const hmrBoundaries = new Set<string>()
const dirtyFiles = new Set<string>()
dirtyFiles.add(publicPath)

const hasDeadEnd = walkImportChain(
publicPath,
importers,
importers || new Set(),
hmrBoundaries,
dirtyFiles
)
Expand Down Expand Up @@ -191,6 +191,14 @@ export const hmrPlugin: ServerPlugin = ({
}
} else {
debugHmr(`no importers for ${publicPath}.`)
// bust sw cache anyway since this may be a full dynamic import.
if (config.serviceWorker) {
send({
type: 'sw-bust-cache',
path: publicPath,
timestamp
})
}
}
})

Expand Down Expand Up @@ -369,6 +377,7 @@ export function rewriteFileWithHMR(
// self accepting
// import.meta.hot.accept() OR import.meta.hot.accept(() => {})
ensureMapEntry(hmrAcceptanceMap, importer).add(importer)
debugHmr(`${importer} self accepts`)
} else {
console.error(
chalk.yellow(
Expand Down
11 changes: 10 additions & 1 deletion src/node/server/serverPluginModuleRewrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,19 @@ export const moduleRewritePlugin: ServerPlugin = ({
ctx.body = rewriteCache.get(content)
} else {
await initLexer
// dynamic import may conatin extension-less path,
// (.e.g import(runtimePathString))
// so we need to normalize importer to ensure it contains extension
// before we perform hmr analysis.
// on the other hand, static import is guaranteed to have extension
// because they must all have gone through module rewrite.
const importer = resolver.fileToRequest(
resolver.requestToFile(ctx.path)
)
ctx.body = rewriteImports(
root,
content!,
ctx.path,
importer,
resolver,
ctx.query.t
)
Expand Down
18 changes: 16 additions & 2 deletions src/sw/serviceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,25 @@ sw.addEventListener('activate', (e) => {
)
})

const extRe = /(\bindex)?\.\w+$/

sw.addEventListener('message', async (e) => {
if (e.data.type === 'bust-cache') {
// console.log(`[vite:sw] busted cache for ${e.data.path}`)
;(await caches.open(USER_CACHE_NAME)).delete(e.data.path)
;(await caches.open(DEPS_CACHE_NAME)).delete(e.data.path)
const path = e.data.path
const userCache = await caches.open(USER_CACHE_NAME)
const depsCache = await caches.open(DEPS_CACHE_NAME)
userCache.delete(path)
depsCache.delete(path)

// also bust cache for extension-less paths - this happens when the user
// has non-statically-analyzable dynamic import paths.
if (extRe.test(path)) {
const cleanPath = path.replace(extRe, '')
userCache.delete(cleanPath)
depsCache.delete(cleanPath)
}

// notify the client that cache has been busted
e.ports[0].postMessage({
busted: true
Expand Down
96 changes: 93 additions & 3 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('vite', () => {
await button.click()
expect(await getText(button)).toMatch('>>> 1 <<<')

await updateFile('TestHmr.vue', (content) =>
await updateFile('TestHmr/TestHmr.vue', (content) =>
content.replace('{{ count }}', 'count is {{ count }}')
)
// note: using the same button to ensure the component did only re-render
Expand All @@ -127,7 +127,7 @@ describe('vite', () => {
})

test('hmr (vue reload)', async () => {
await updateFile('TestHmr.vue', (content) =>
await updateFile('TestHmr/TestHmr.vue', (content) =>
content.replace('count: 0,', 'count: 1337,')
)
await expectByPolling(() => getText('.hmr-increment'), 'count is 1337')
Expand All @@ -136,12 +136,102 @@ describe('vite', () => {
test('hmr (js -> vue propagation)', async () => {
const span = await page.$('.hmr-propagation')
expect(await getText(span)).toBe('1')
await updateFile('testHmrPropagation.js', (content) =>
await updateFile('TestHmr/testHmrPropagation.js', (content) =>
content.replace('return 1', 'return 666')
)
await expectByPolling(() => getText('.hmr-propagation'), '666')
})

test('hmr (js -> vue propagation. dynamic import, static-analyzable)', async () => {
let span = await page.$('.hmr-propagation-dynamic')
expect(await getText(span)).toBe('bar not loaded')
// trigger the dynamic import
let button = await page.$('.hmr-propagation-dynamic-load')
await button.click()
expect(await getText(span)).toBe('bar loading')
await expectByPolling(() => getText(span), 'bar loaded')
// update souce code
await updateFile(
'TestHmr/testHmrPropagationDynamicImport.js',
(content) => content.replace('bar loaded', 'bar updated')
)
// the update trigger the reload of TestHmr component
// all states in it are lost
await expectByPolling(
() => getText('.hmr-propagation-dynamic'),
'bar not loaded'
)
span = await page.$('.hmr-propagation-dynamic')
button = await page.$('.hmr-propagation-dynamic-load')
await button.click()
expect(await getText(span)).toBe('bar loading')
await expectByPolling(() => getText(span), 'bar updated')
})

test('hmr (js -> vue propagation. full dynamic import, non-static-analyzable)', async () => {
let span = await page.$('.hmr-propagation-full-dynamic')
expect(await getText(span)).toBe('baz not loaded')
// trigger the dynamic import
let button = await page.$('.hmr-propagation-full-dynamic-load')
await button.click()
expect(await getText(span)).toBe('baz loading')
await expectByPolling(() => getText(span), 'baz loaded')
// update souce code
await updateFile(
'TestHmr/testHmrPropagationFullDynamicImport.js',
(content) => content.replace('baz loaded', 'baz updated')
)
// the update doesn't trigger hmr
// because it is a non-static-analyzable dynamic import
// and the imported file is not self-accepting
await timeout(200)
expect(await getText('.hmr-propagation-full-dynamic')).toBe(
'baz loaded'
)
// only if we reload the whole page, we can see the new content
await page.reload({ waitUntil: ['networkidle0', 'domcontentloaded'] })
span = await page.$('.hmr-propagation-full-dynamic')
expect(await getText(span)).toBe('baz not loaded')
// trigger the dynamic import
button = await page.$('.hmr-propagation-full-dynamic-load')
await button.click()
expect(await getText(span)).toBe('baz loading')
await expectByPolling(() => getText(span), 'baz updated')
})

test('hmr (js -> vue propagation. full dynamic import, non-static-analyzable, but self-accepting)', async () => {
// reset the sate
await page.reload({ waitUntil: ['networkidle0', 'domcontentloaded'] })
let stateIncrementButton = await page.$('.hmr-increment')
await stateIncrementButton.click()
expect(await getText(stateIncrementButton)).toMatch(
'>>> count is 1338 <<<'
)

let span = await page.$('.hmr-propagation-full-dynamic-self-accepting')
expect(await getText(span)).toBe('qux not loaded')
// trigger the dynamic import
let button = await page.$(
'.hmr-propagation-full-dynamic-load-self-accepting'
)
await button.click()
expect(await getText(span)).toBe('qux loading')
await expectByPolling(() => getText(span), 'qux loaded')
// update souce code
await updateFile(
'TestHmr/testHmrPropagationFullDynamicImportSelfAccepting.js',
(content) => content.replace('qux loaded', 'qux updated')
)
// the update is accepted by the imported file
await expectByPolling(() => getText(span), 'qux updated')
// the state should be the same
// because the TestHmr component is not reloaded
stateIncrementButton = await page.$('.hmr-increment')
expect(await getText(stateIncrementButton)).toMatch(
'>>> count is 1338 <<<'
)
})

test('hmr (manual API, self accepting)', async () => {
await updateFile('testHmrManual.js', (content) =>
content.replace('foo = 1', 'foo = 2')
Expand Down

0 comments on commit c096aa6

Please sign in to comment.