Skip to content
This repository was archived by the owner on Jan 18, 2022. It is now read-only.

Commit 62e340f

Browse files
committed
feat: hmr support for vite 2.0
1 parent 048936b commit 62e340f

File tree

8 files changed

+190
-7
lines changed

8 files changed

+190
-7
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"dependencies": {
2121
"debug": "^4.1.1",
2222
"hash-sum": "^2.0.0",
23-
"rollup-pluginutils": "^2.8.2"
23+
"@rollup/pluginutils": "^4.1.0"
2424
},
2525
"peerDependencies": {
2626
"@vue/compiler-sfc": "*"

src/handleHotUpdate.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import fs from 'fs'
2+
import { parse, SFCBlock } from '@vue/compiler-sfc'
3+
import { getDescriptor, setDescriptor } from './utils/descriptorCache'
4+
5+
/**
6+
* Vite-specific HMR handling
7+
*/
8+
export async function handleHotUpdate(file: string, modules: any[]) {
9+
if (!file.endsWith('.vue')) {
10+
return
11+
}
12+
13+
const prevDescriptor = getDescriptor(file)
14+
if (!prevDescriptor) {
15+
// file hasn't been requested yet (e.g. async component)
16+
return
17+
}
18+
19+
let content = fs.readFileSync(file, 'utf-8')
20+
if (!content) {
21+
await untilModified(file)
22+
content = fs.readFileSync(file, 'utf-8')
23+
}
24+
25+
const { descriptor } = parse(content, {
26+
filename: file,
27+
sourceMap: true,
28+
sourceRoot: process.cwd(),
29+
})
30+
setDescriptor(file, descriptor)
31+
32+
let needRerender = false
33+
const filteredModules = []
34+
35+
const reload = () => {
36+
console.log(`[vue:reload] ${file}`)
37+
return modules.filter((m) => /type=script/.test(m.id))
38+
}
39+
40+
if (
41+
!isEqualBlock(descriptor.script, prevDescriptor.script) ||
42+
!isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup)
43+
) {
44+
return reload()
45+
}
46+
47+
if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
48+
needRerender = true
49+
}
50+
51+
let didUpdateStyle = false
52+
const prevStyles = prevDescriptor.styles || []
53+
const nextStyles = descriptor.styles || []
54+
55+
// css modules update causes a reload because the $style object is changed
56+
// and it may be used in JS. It also needs to trigger a vue-style-update
57+
// event so the client busts the sw cache.
58+
if (
59+
prevStyles.some((s) => s.module != null) ||
60+
nextStyles.some((s) => s.module != null)
61+
) {
62+
return reload()
63+
}
64+
65+
// force reload if CSS vars injection changed
66+
if (descriptor.cssVars) {
67+
if (prevDescriptor.cssVars.join('') !== descriptor.cssVars.join('')) {
68+
return reload()
69+
}
70+
}
71+
72+
// force reload if scoped status has changed
73+
if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
74+
return reload()
75+
}
76+
77+
// only need to update styles if not reloading, since reload forces
78+
// style updates as well.
79+
nextStyles.forEach((_, i) => {
80+
if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {
81+
didUpdateStyle = true
82+
filteredModules.push(modules.find((m) => m.id.includes(`index=${i}`)))
83+
}
84+
})
85+
86+
const prevCustoms = prevDescriptor.customBlocks || []
87+
const nextCustoms = descriptor.customBlocks || []
88+
89+
// custom blocks update causes a reload
90+
// because the custom block contents is changed and it may be used in JS.
91+
if (
92+
nextCustoms.some(
93+
(_, i) => !prevCustoms[i] || !isEqualBlock(prevCustoms[i], nextCustoms[i])
94+
)
95+
) {
96+
return reload()
97+
}
98+
99+
if (needRerender) {
100+
filteredModules.push(modules.find((m) => /type=template/.test(m.id)))
101+
}
102+
103+
let updateType = []
104+
if (needRerender) {
105+
updateType.push(`template`)
106+
}
107+
if (didUpdateStyle) {
108+
updateType.push(`style`)
109+
}
110+
if (updateType.length) {
111+
console.log(`[vue:update(${updateType.join('&')})] ${file}`)
112+
}
113+
return filteredModules
114+
}
115+
116+
// vitejs/vite#610 when hot-reloading Vue files, we read immediately on file
117+
// change event and sometimes this can be too early and get an empty buffer.
118+
// Poll until the file's modified time has changed before reading again.
119+
async function untilModified(file: string) {
120+
const mtime = fs.statSync(file).mtimeMs
121+
return new Promise((r) => {
122+
let n = 0
123+
const poll = async () => {
124+
n++
125+
const newMtime = fs.statSync(file).mtimeMs
126+
if (newMtime !== mtime || n > 10) {
127+
r(0)
128+
} else {
129+
setTimeout(poll, 10)
130+
}
131+
}
132+
setTimeout(poll, 10)
133+
})
134+
}
135+
136+
function isEqualBlock(a: SFCBlock | null, b: SFCBlock | null) {
137+
if (!a && !b) return true
138+
if (!a || !b) return false
139+
// src imports will trigger their own updates
140+
if (a.src && b.src && a.src === b.src) return true
141+
if (a.content !== b.content) return false
142+
const keysA = Object.keys(a.attrs)
143+
const keysB = Object.keys(b.attrs)
144+
if (keysA.length !== keysB.length) {
145+
return false
146+
}
147+
return keysA.every((key) => a.attrs[key] === b.attrs[key])
148+
}

src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import fs from 'fs'
1515
import createDebugger from 'debug'
1616
import { Plugin } from 'rollup'
17-
import { createFilter } from 'rollup-pluginutils'
17+
import { createFilter } from '@rollup/pluginutils'
1818
import { transformSFCEntry } from './sfc'
1919
import { transformTemplate } from './template'
2020
import { transformStyle } from './style'
@@ -23,13 +23,15 @@ import { getDescriptor, setDescriptor } from './utils/descriptorCache'
2323
import { parseVuePartRequest } from './utils/query'
2424
import { normalizeSourceMap } from './utils/sourceMap'
2525
import { getResolvedScript } from './script'
26+
import { handleHotUpdate } from './handleHotUpdate'
2627

2728
const debug = createDebugger('rollup-plugin-vue')
2829

2930
export interface Options {
3031
include: string | RegExp | (string | RegExp)[]
3132
exclude: string | RegExp | (string | RegExp)[]
3233
target: 'node' | 'browser'
34+
hmr: boolean
3335
exposeFilename: boolean
3436

3537
customBlocks?: string[]
@@ -58,6 +60,7 @@ export interface Options {
5860
const defaultOptions: Options = {
5961
include: /\.vue$/,
6062
exclude: [],
63+
hmr: false,
6164
target: 'browser',
6265
exposeFilename: false,
6366
customBlocks: [],
@@ -173,6 +176,9 @@ export default function PluginVue(userOptions: Partial<Options> = {}): Plugin {
173176
}
174177
return null
175178
},
179+
180+
// @ts-ignore
181+
handleHotUpdate,
176182
}
177183
}
178184

src/script.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function resolveScript(
4040
resolved = compileScript(descriptor, {
4141
id: scopeId,
4242
isProd,
43-
inlineTemplate: true,
43+
inlineTemplate: !options.hmr,
4444
templateOptions: getTemplateCompilerOptions(
4545
options,
4646
descriptor,

src/sfc.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ export function transformSFCEntry(
4242
// feature information
4343
const hasScoped = descriptor.styles.some((s) => s.scoped)
4444

45-
const isTemplateInlined =
46-
descriptor.scriptSetup && !(descriptor.template && descriptor.template.src)
47-
const hasTemplateImport = descriptor.template && !isTemplateInlined
45+
const useInlineTemplate =
46+
!options.hmr &&
47+
descriptor.scriptSetup &&
48+
!(descriptor.template && descriptor.template.src)
49+
const hasTemplateImport = descriptor.template && !useInlineTemplate
4850

4951
const templateImport = hasTemplateImport
5052
? genTemplateCode(descriptor, scopeId, isServer)
@@ -84,6 +86,17 @@ export function transformSFCEntry(
8486
)
8587
}
8688
output.push('export default script')
89+
90+
if (options.hmr) {
91+
output.push(`script.__hmrId = ${JSON.stringify(scopeId)}`)
92+
output.push(`__VUE_HMR_RUNTIME__.createRecord(script.__hmrId, script)`)
93+
output.push(
94+
`import.meta.hot.accept(({ default: script }) => {
95+
__VUE_HMR_RUNTIME__.reload(script.__hmrId, script)
96+
})`
97+
)
98+
}
99+
87100
return {
88101
code: output.join('\n'),
89102
map: {

src/template.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,15 @@ export function transformTemplate(
4646
)
4747
}
4848

49+
let returnCode = result.code
50+
if (options.hmr) {
51+
returnCode += `\nimport.meta.hot.accept(({ render }) => {
52+
__VUE_HMR_RUNTIME__.rerender(${JSON.stringify(query.id)}, render)
53+
})`
54+
}
55+
4956
return {
50-
code: result.code,
57+
code: returnCode,
5158
map: normalizeSourceMap(result.map!, request),
5259
}
5360
}

src/utils/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export function createRollupError(
55
id: string,
66
error: CompilerError | SyntaxError
77
): RollupError {
8+
debugger
89
if ('code' in error) {
910
return {
1011
id,

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,14 @@
523523
estree-walker "^1.0.1"
524524
picomatch "^2.2.2"
525525

526+
"@rollup/pluginutils@^4.1.0":
527+
version "4.1.0"
528+
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.0.tgz#0dcc61c780e39257554feb7f77207dceca13c838"
529+
integrity sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ==
530+
dependencies:
531+
estree-walker "^2.0.1"
532+
picomatch "^2.2.2"
533+
526534
"@samverschueren/stream-to-observable@^0.3.0":
527535
version "0.3.0"
528536
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"

0 commit comments

Comments
 (0)