diff --git a/packages/plugin-vue/src/handleHotUpdate.ts b/packages/plugin-vue/src/handleHotUpdate.ts index f6fc455e..2b15a4e9 100644 --- a/packages/plugin-vue/src/handleHotUpdate.ts +++ b/packages/plugin-vue/src/handleHotUpdate.ts @@ -202,14 +202,25 @@ export function isOnlyTemplateChanged( ) } -function deepEqual(obj1: any, obj2: any, excludeProps: string[] = []): boolean { +function deepEqual( + obj1: any, + obj2: any, + excludeProps: string[] = [], + deepParentsOfObj1: any[] = [], +): 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') { + // or circular reference + if ( + obj1 == null || + obj2 == null || + typeof obj1 !== 'object' || + deepParentsOfObj1.includes(obj1) + ) { return obj1 === obj2 } @@ -229,7 +240,12 @@ function deepEqual(obj1: any, obj2: any, excludeProps: string[] = []): boolean { continue } - if (!deepEqual(obj1[key], obj2[key], excludeProps)) { + if ( + !deepEqual(obj1[key], obj2[key], excludeProps, [ + ...deepParentsOfObj1, + obj1, + ]) + ) { return false } } diff --git a/playground/vue/HmrCircularReference.vue b/playground/vue/HmrCircularReference.vue new file mode 100644 index 00000000..98dbd3f7 --- /dev/null +++ b/playground/vue/HmrCircularReference.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/vue/HmrCircularReferenceFile.d.ts b/playground/vue/HmrCircularReferenceFile.d.ts new file mode 100644 index 00000000..ef34a5c1 --- /dev/null +++ b/playground/vue/HmrCircularReferenceFile.d.ts @@ -0,0 +1,8 @@ +// this file will become a TypeScope through option `script.globalTypeFiles` +// which has a circular reference on `TypeScope._ownerScope` (issue 325) + +declare namespace App { + interface User { + name: string + } +} diff --git a/playground/vue/Main.vue b/playground/vue/Main.vue index b34155e4..b29ae881 100644 --- a/playground/vue/Main.vue +++ b/playground/vue/Main.vue @@ -8,6 +8,7 @@
+ @@ -60,6 +61,7 @@ import PreCompiled from './pre-compiled/foo.vue' import PreCompiledExternalScoped from './pre-compiled/external-scoped.vue' import PreCompiledExternalCssModules from './pre-compiled/external-cssmodules.vue' import ParserOptions from './ParserOptions.vue' +import HmrCircularReference from './HmrCircularReference.vue' const TsGeneric = defineAsyncComponent(() => import('./TsGeneric.vue')) diff --git a/playground/vue/__tests__/vue.spec.ts b/playground/vue/__tests__/vue.spec.ts index e5ae6e53..aa88ec19 100644 --- a/playground/vue/__tests__/vue.spec.ts +++ b/playground/vue/__tests__/vue.spec.ts @@ -225,6 +225,16 @@ describe('hmr', () => { 'updatedCount is 0', ) }) + + test('should handle circular reference (issue 325)', async () => { + editFile('HmrCircularReference.vue', (code) => + code.replace('let foo: number = 0', 'let foo: number = 100'), + ) + await untilUpdated( + () => page.textContent('.hmr-circular-reference-inc'), + 'count is 100', + ) + }) }) describe('src imports', () => { diff --git a/playground/vue/vite.config.ts b/playground/vue/vite.config.ts index 5a719cb5..7b971526 100644 --- a/playground/vue/vite.config.ts +++ b/playground/vue/vite.config.ts @@ -1,3 +1,4 @@ +import { resolve } from 'node:path' import { defineConfig, splitVendorChunkPlugin } from 'vite' import vuePlugin from '@vitejs/plugin-vue' import { vueI18nPlugin } from './CustomBlockPlugin' @@ -11,6 +12,9 @@ export default defineConfig({ }, plugins: [ vuePlugin({ + script: { + globalTypeFiles: [resolve(__dirname, 'HmrCircularReferenceFile.d.ts')], + }, template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('my-'),