Skip to content

Commit dbadc58

Browse files
committed
feat: skip hmr when script is merely formatted
ref: vitejs/vite-plugin-vue@8383f49
1 parent 2c2326c commit dbadc58

File tree

6 files changed

+118
-13
lines changed

6 files changed

+118
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"vite": "^5.0.11"
9090
},
9191
"devDependencies": {
92+
"@babel/types": "^7.23.6",
9293
"@jridgewell/gen-mapping": "^0.3.3",
9394
"@jridgewell/trace-mapping": "^0.3.20",
9495
"@sxzz/eslint-config": "^3.7.6",

pnpm-lock.yaml

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/handleHotUpdate.ts

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ import {
99
import {
1010
getResolvedScript,
1111
invalidateScript,
12+
resolveScript,
1213
setResolvedScript,
1314
} from './script'
1415
import type { SFCBlock, SFCDescriptor } from 'vue/compiler-sfc'
1516
import type { ResolvedOptions } from '.'
1617

18+
import type * as t from '@babel/types'
19+
1720
const debug = _debug('vite:hmr')
1821

19-
// eslint-disable-next-line unicorn/better-regex
20-
const directRequestRE = /(?:\?|&)direct\b/
22+
const directRequestRE = /[&?]direct\b/
2123

2224
/**
2325
* Vite-specific HMR handling
@@ -40,6 +42,8 @@ export async function handleHotUpdate(
4042
const mainModule = getMainModule(modules)
4143
const templateModule = modules.find((m) => /type=template/.test(m.url))
4244

45+
// trigger resolveScript for descriptor so that we'll have the AST ready
46+
resolveScript('vite', descriptor, options, false)
4347
const scriptChanged = hasScriptChanged(prevDescriptor, descriptor)
4448
if (scriptChanged) {
4549
affectedModules.add(getScriptModule(modules) || mainModule)
@@ -192,11 +196,88 @@ export function isOnlyTemplateChanged(
192196
)
193197
}
194198

199+
function deepEqual(obj1: any, obj2: any, excludeProps: string[] = []): boolean {
200+
// Check if both objects are of the same type
201+
if (typeof obj1 !== typeof obj2) {
202+
return false
203+
}
204+
205+
// Check if both objects are primitive types or null
206+
if (obj1 == null || obj2 == null || typeof obj1 !== 'object') {
207+
return obj1 === obj2
208+
}
209+
210+
// Get the keys of the objects
211+
const keys1 = Object.keys(obj1)
212+
const keys2 = Object.keys(obj2)
213+
214+
// Check if the number of keys is the same
215+
if (keys1.length !== keys2.length) {
216+
return false
217+
}
218+
219+
// Iterate through the keys and recursively compare the values
220+
for (const key of keys1) {
221+
// Check if the current key should be excluded
222+
if (excludeProps.includes(key)) {
223+
continue
224+
}
225+
226+
if (!deepEqual(obj1[key], obj2[key], excludeProps)) {
227+
return false
228+
}
229+
}
230+
231+
// If all comparisons passed, the objects are deep equal
232+
return true
233+
}
234+
235+
function isEqualAst(prev?: t.Statement[], next?: t.Statement[]): boolean {
236+
if (typeof prev === 'undefined' || typeof next === 'undefined') {
237+
return prev === next
238+
}
239+
240+
// deep equal, but ignore start/end/loc/range/leadingComments/trailingComments/innerComments
241+
if (prev.length !== next.length) {
242+
return false
243+
}
244+
245+
for (const [i, prevNode] of prev.entries()) {
246+
const nextNode = next[i]
247+
if (
248+
!deepEqual(prevNode, nextNode, [
249+
'start',
250+
'end',
251+
'loc',
252+
'range',
253+
'leadingComments',
254+
'trailingComments',
255+
'innerComments',
256+
])
257+
) {
258+
return false
259+
}
260+
}
261+
262+
return true
263+
}
264+
195265
function hasScriptChanged(prev: SFCDescriptor, next: SFCDescriptor): boolean {
196-
if (!isEqualBlock(prev.script, next.script)) {
266+
// check for scriptAst/scriptSetupAst changes
267+
// note that the next ast is not available yet, so we need to trigger parsing
268+
const prevScript = getResolvedScript(prev, false)
269+
const nextScript = getResolvedScript(next, false)
270+
271+
if (
272+
!isEqualBlock(prev.script, next.script) &&
273+
!isEqualAst(prevScript?.scriptAst, nextScript?.scriptAst)
274+
) {
197275
return true
198276
}
199-
if (!isEqualBlock(prev.scriptSetup, next.scriptSetup)) {
277+
if (
278+
!isEqualBlock(prev.scriptSetup, next.scriptSetup) &&
279+
!isEqualAst(prevScript?.scriptSetupAst, nextScript?.scriptSetupAst)
280+
) {
200281
return true
201282
}
202283

src/core/main.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,16 @@ async function genScriptCode(
333333
let scriptCode = `const ${scriptIdentifier} = {}`
334334
let map: RawSourceMap | undefined
335335

336-
const script = resolveScript(pluginContext, descriptor, options, ssr)
336+
const script = resolveScript(
337+
pluginContext.framework,
338+
descriptor,
339+
options,
340+
ssr,
341+
)
337342
if (script) {
338343
// If the script is js/ts and has no external src, it can be directly placed
339344
// in the main module.
340-
if (canInlineMain(pluginContext, descriptor, options)) {
345+
if (canInlineMain(pluginContext.framework, descriptor, options)) {
341346
if (!options.compiler.version) {
342347
// if compiler-sfc exposes no version, it's < 3.3 and doesn't support
343348
// genDefaultAs option.

src/core/script.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function isUseInlineTemplate(
5151
export const scriptIdentifier = `_sfc_main`
5252

5353
export function resolveScript(
54-
pluginContext: UnpluginContextMeta,
54+
framework: UnpluginContextMeta['framework'],
5555
descriptor: SFCDescriptor,
5656
options: ResolvedOptions,
5757
ssr: boolean,
@@ -74,7 +74,7 @@ export function resolveScript(
7474
inlineTemplate: isUseInlineTemplate(options, descriptor),
7575
templateOptions: resolveTemplateCompilerOptions(descriptor, options, ssr),
7676
sourceMap: options.sourceMap,
77-
genDefaultAs: canInlineMain(pluginContext, descriptor, options)
77+
genDefaultAs: canInlineMain(framework, descriptor, options)
7878
? scriptIdentifier
7979
: undefined,
8080
})
@@ -103,7 +103,7 @@ export function resolveScript(
103103
// If the script is js/ts and has no external src, it can be directly placed
104104
// in the main module. Skip for build
105105
export function canInlineMain(
106-
pluginContext: UnpluginContextMeta,
106+
framework: UnpluginContextMeta['framework'],
107107
descriptor: SFCDescriptor,
108108
options: ResolvedOptions,
109109
): boolean {
@@ -116,8 +116,7 @@ export function canInlineMain(
116116
}
117117
if (
118118
lang === 'ts' &&
119-
(options.devServer ||
120-
['esbuild', 'rspack'].includes(pluginContext.framework))
119+
(options.devServer || ['esbuild', 'rspack'].includes(framework))
121120
) {
122121
return true
123122
}

src/core/template.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function compile(
6969
ssr: boolean,
7070
) {
7171
const filename = descriptor.filename
72-
resolveScript(pluginContext, descriptor, options, ssr)
72+
resolveScript(pluginContext.framework, descriptor, options, ssr)
7373
const result = options.compiler.compileTemplate({
7474
...resolveTemplateCompilerOptions(descriptor, options, ssr)!,
7575
source: code,

0 commit comments

Comments
 (0)