From 69d2aaf6d16b0a05efd570958526b3d4b366df94 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 20 Sep 2023 00:57:06 +0800 Subject: [PATCH 1/4] feat: skip hmr when script is merely formatted --- packages/plugin-vue/src/handleHotUpdate.ts | 91 +++++++++++++++++++++- packages/plugin-vue/src/template.ts | 3 +- playground/vue/__tests__/vue.spec.ts | 16 ++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/packages/plugin-vue/src/handleHotUpdate.ts b/packages/plugin-vue/src/handleHotUpdate.ts index f12ef54b..846e1339 100644 --- a/packages/plugin-vue/src/handleHotUpdate.ts +++ b/packages/plugin-vue/src/handleHotUpdate.ts @@ -3,6 +3,9 @@ import type { SFCBlock, SFCDescriptor } from 'vue/compiler-sfc' import type { HmrContext, ModuleNode } from 'vite' import { isCSSRequest } from 'vite' +// eslint-disable-next-line node/no-extraneous-import +import type * as _babel_types from '@babel/types' + import { createDescriptor, getDescriptor, @@ -11,6 +14,7 @@ import { import { getResolvedScript, invalidateScript, + resolveScript, setResolvedScript, } from './script' import type { ResolvedOptions } from '.' @@ -40,6 +44,8 @@ export async function handleHotUpdate( const mainModule = getMainModule(modules) const templateModule = modules.find((m) => /type=template/.test(m.url)) + // trigger resolveScript for descriptor so that we'll have the AST ready + resolveScript(descriptor, options, false) const scriptChanged = hasScriptChanged(prevDescriptor, descriptor) if (scriptChanged) { affectedModules.add(getScriptModule(modules) || mainModule) @@ -183,11 +189,92 @@ export function isOnlyTemplateChanged( ) } +function deepEqual(obj1: any, obj2: any, excludeProps: string[] = []): boolean { + // Check if both objects are of the same type + if (typeof obj1 !== typeof obj2) { + return false + } + + // Check if both objects are primitive types or null + if (obj1 == null || obj2 == null || typeof obj1 !== 'object') { + return obj1 === obj2 + } + + // Get the keys of the objects + const keys1 = Object.keys(obj1) + const keys2 = Object.keys(obj2) + + // Check if the number of keys is the same + if (keys1.length !== keys2.length) { + return false + } + + // Iterate through the keys and recursively compare the values + for (const key of keys1) { + // Check if the current key should be excluded + if (excludeProps.includes(key)) { + continue + } + + if (!deepEqual(obj1[key], obj2[key], excludeProps)) { + return false + } + } + + // If all comparisons passed, the objects are deep equal + return true +} + +function isEqualAst( + prev?: _babel_types.Statement[], + next?: _babel_types.Statement[], +): boolean { + if (typeof prev === 'undefined' || typeof next === 'undefined') { + return prev === next + } + + // deep equal, but ignore start/end/loc/range/leadingComments/trailingComments/innerComments + if (prev.length !== next.length) { + return false + } + + for (let i = 0; i < prev.length; i++) { + const prevNode = prev[i] + const nextNode = next[i] + if ( + !deepEqual(prevNode, nextNode, [ + 'start', + 'end', + 'loc', + 'range', + 'leadingComments', + 'trailingComments', + 'innerComments', + ]) + ) { + return false + } + } + + return true +} + function hasScriptChanged(prev: SFCDescriptor, next: SFCDescriptor): boolean { - if (!isEqualBlock(prev.script, next.script)) { + // check for scriptAst/scriptSetupAst changes + // note that the next ast is not available yet, so we need to trigger parsing + const prevScript = getResolvedScript(prev, false) + const nextScript = getResolvedScript(next, false) + + if ( + !isEqualBlock(prev.script, next.script) && + !isEqualAst(prevScript?.scriptAst, nextScript?.scriptAst) + ) { return true } - if (!isEqualBlock(prev.scriptSetup, next.scriptSetup)) { + if ( + !isEqualBlock(prev.scriptSetup, next.scriptSetup) && + !isEqualAst(prevScript?.scriptSetupAst, nextScript?.scriptSetupAst) + ) { return true } diff --git a/packages/plugin-vue/src/template.ts b/packages/plugin-vue/src/template.ts index d3a6350d..133962e4 100644 --- a/packages/plugin-vue/src/template.ts +++ b/packages/plugin-vue/src/template.ts @@ -61,8 +61,7 @@ export function transformTemplateInMain( } } -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function compile( +function compile( code: string, descriptor: SFCDescriptor, options: ResolvedOptions, diff --git a/playground/vue/__tests__/vue.spec.ts b/playground/vue/__tests__/vue.spec.ts index 850e7cfe..433f1fda 100644 --- a/playground/vue/__tests__/vue.spec.ts +++ b/playground/vue/__tests__/vue.spec.ts @@ -184,6 +184,22 @@ describe('hmr', () => { expect(await page.textContent('.hmr-inc')).toMatch('count is 1') }) + test('should preserve state when script is merely formatted', async () => { + // these are the states from the previous test + expect(await getColor('.hmr-inc')).toBe('blue') + expect(await page.textContent('.hmr-inc')).toMatch('count is 1') + + editFile('Hmr.vue', (code) => + code + .replace('let foo: number = 0', ' let foo: number = 0') + // also edit the style so that we can have something to wait for + .replace('color: blue;', 'color: black;'), + ) + await untilUpdated(() => getColor('.hmr-inc'), 'black') + // should preserve state + expect(await page.textContent('.hmr-inc')).toMatch('count is 1') + }) + test('should reload and reset state when script is edited', async () => { editFile('Hmr.vue', (code) => code.replace('let foo: number = 0', 'let foo: number = 100'), From afae0982aedf18d1b0d960058d1c5753a264ed73 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 20 Sep 2023 01:13:25 +0800 Subject: [PATCH 2/4] test: don't check for the color state from the previous test Somehow it's not preserved during `test-build`. But it's not a big deal for our use case so let's just ignore it. --- playground/vue/__tests__/vue.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/playground/vue/__tests__/vue.spec.ts b/playground/vue/__tests__/vue.spec.ts index 433f1fda..317caec3 100644 --- a/playground/vue/__tests__/vue.spec.ts +++ b/playground/vue/__tests__/vue.spec.ts @@ -185,13 +185,12 @@ describe('hmr', () => { }) test('should preserve state when script is merely formatted', async () => { - // these are the states from the previous test - expect(await getColor('.hmr-inc')).toBe('blue') + // this is the state from the previous test expect(await page.textContent('.hmr-inc')).toMatch('count is 1') editFile('Hmr.vue', (code) => code - .replace('let foo: number = 0', ' let foo: number = 0') + .replace('let foo: number = 0', ' let foo: number = 0\n\n') // also edit the style so that we can have something to wait for .replace('color: blue;', 'color: black;'), ) From 6c00e924f563996ea3e3e3ec5c821a436941c9ee Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 20 Sep 2023 01:16:23 +0800 Subject: [PATCH 3/4] chore: restore the template.ts file This refactor is not related to this PR, could be in another commit. --- packages/plugin-vue/src/template.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/plugin-vue/src/template.ts b/packages/plugin-vue/src/template.ts index 133962e4..d3a6350d 100644 --- a/packages/plugin-vue/src/template.ts +++ b/packages/plugin-vue/src/template.ts @@ -61,7 +61,8 @@ export function transformTemplateInMain( } } -function compile( +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function compile( code: string, descriptor: SFCDescriptor, options: ResolvedOptions, From c46b14aaeaaf2cc3f31e855dd7ae5cdbefbe7c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 19 Nov 2023 17:41:49 +0800 Subject: [PATCH 4/4] refactor: rename types --- packages/plugin-vue/src/handleHotUpdate.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/plugin-vue/src/handleHotUpdate.ts b/packages/plugin-vue/src/handleHotUpdate.ts index 846e1339..7c1dc742 100644 --- a/packages/plugin-vue/src/handleHotUpdate.ts +++ b/packages/plugin-vue/src/handleHotUpdate.ts @@ -4,7 +4,7 @@ import type { HmrContext, ModuleNode } from 'vite' import { isCSSRequest } from 'vite' // eslint-disable-next-line node/no-extraneous-import -import type * as _babel_types from '@babel/types' +import type * as t from '@babel/types' import { createDescriptor, @@ -225,10 +225,7 @@ function deepEqual(obj1: any, obj2: any, excludeProps: string[] = []): boolean { return true } -function isEqualAst( - prev?: _babel_types.Statement[], - next?: _babel_types.Statement[], -): boolean { +function isEqualAst(prev?: t.Statement[], next?: t.Statement[]): boolean { if (typeof prev === 'undefined' || typeof next === 'undefined') { return prev === next }