From 28eb839622c86adeda00eb73eb3356e359c3a7b3 Mon Sep 17 00:00:00 2001 From: lazerg Date: Thu, 2 Jul 2026 23:49:20 +0500 Subject: [PATCH 1/3] fix(devtools-kit): only treat own render property as component definition --- .../__tests__/component/replacer.test.ts | 38 +++++++++++++++++++ .../src/core/component/state/replacer.ts | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 packages/devtools-kit/__tests__/component/replacer.test.ts diff --git a/packages/devtools-kit/__tests__/component/replacer.test.ts b/packages/devtools-kit/__tests__/component/replacer.test.ts new file mode 100644 index 000000000..a3e62ab61 --- /dev/null +++ b/packages/devtools-kit/__tests__/component/replacer.test.ts @@ -0,0 +1,38 @@ +import { stringifyReplacer } from '../../src/core/component/state/replacer' + +// `stringifyReplacer` reads the value from `this[key]`, mirroring how it is +// called during `JSON.stringify`. +function replace(value: unknown) { + return stringifyReplacer.call({ value }, 'value') as { _custom?: { type?: string, displayText?: string } } +} + +describe('stringifyReplacer: component definition detection', () => { + // #1100: Pinia store state can hold plain class instances that happen to + // expose a `render` method. Those must not be reported as Vue components. + it('does not treat a plain class instance with a render method as a component', () => { + class Chart { + data = [1, 2, 3] + render() { + return 'draw the chart' + } + } + + const result = replace(new Chart()) + + expect(result?._custom?.type).not.toBe('component-definition') + }) + + it('still detects a real Vue component definition', () => { + const Button = defineComponent({ + name: 'MyButton', + render() { + return h('button', 'Click me') + }, + }) + + const result = replace(Button) + + expect(result?._custom?.type).toBe('component-definition') + expect(result?._custom?.displayText).toBe('MyButton') + }) +}) diff --git a/packages/devtools-kit/src/core/component/state/replacer.ts b/packages/devtools-kit/src/core/component/state/replacer.ts index f711abf9d..a165d54db 100644 --- a/packages/devtools-kit/src/core/component/state/replacer.ts +++ b/packages/devtools-kit/src/core/component/state/replacer.ts @@ -82,7 +82,7 @@ export function stringifyReplacer(key: string | number, _value: any, depth?: num seenInstance?.set(val, depth!) return componentVal } - else if (ensurePropertyExists(val, 'render', true) && typeof val.render === 'function') { + else if (Object.prototype.hasOwnProperty.call(val, 'render') && typeof (val as Record).render === 'function') { return getComponentDefinitionDetails(val) } else if (val.constructor && val.constructor.name === 'VNode') { From 81d40ef65f6b7dfa429e4ad3e1bfe6b2a642328a Mon Sep 17 00:00:00 2001 From: lazerg Date: Sun, 19 Jul 2026 06:03:54 +0500 Subject: [PATCH 2/3] fix(devtools-kit): don't treat reactive objects as component definitions --- .../__tests__/component/replacer.test.ts | 15 +++++++++++++++ .../src/core/component/state/replacer.ts | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/devtools-kit/__tests__/component/replacer.test.ts b/packages/devtools-kit/__tests__/component/replacer.test.ts index a3e62ab61..65dc07bcb 100644 --- a/packages/devtools-kit/__tests__/component/replacer.test.ts +++ b/packages/devtools-kit/__tests__/component/replacer.test.ts @@ -35,4 +35,19 @@ describe('stringifyReplacer: component definition detection', () => { expect(result?._custom?.type).toBe('component-definition') expect(result?._custom?.displayText).toBe('MyButton') }) + + // #1100: Pinia store state is always wrapped in `reactive()`. A reactive + // object with an own `render` method must not be reported as a component. + it('does not treat a reactive object with a render method as a component', () => { + const store = reactive({ + data: [1, 2, 3], + render() { + return 'draw the chart' + }, + }) + + const result = replace(store) + + expect(result?._custom?.type).not.toBe('component-definition') + }) }) diff --git a/packages/devtools-kit/src/core/component/state/replacer.ts b/packages/devtools-kit/src/core/component/state/replacer.ts index a165d54db..cc4a261b4 100644 --- a/packages/devtools-kit/src/core/component/state/replacer.ts +++ b/packages/devtools-kit/src/core/component/state/replacer.ts @@ -1,7 +1,7 @@ import { ensurePropertyExists } from '../utils' import { INFINITY, MAX_ARRAY_SIZE, MAX_STRING_SIZE, NAN, NEGATIVE_INFINITY, UNDEFINED } from './constants' import { getBigIntDetails, getComponentDefinitionDetails, getDateDetails, getFunctionDetails, getHTMLElementDetails, getInstanceDetails, getMapDetails, getObjectDetails, getSetDetails, getStoreDetails } from './custom' -import { isVueInstance } from './is' +import { isReactive, isVueInstance } from './is' import { sanitize } from './util' export type Replacer = (this: any, key: string | number, value: any, depth?: number, seenInstance?: Map) => any @@ -82,7 +82,7 @@ export function stringifyReplacer(key: string | number, _value: any, depth?: num seenInstance?.set(val, depth!) return componentVal } - else if (Object.prototype.hasOwnProperty.call(val, 'render') && typeof (val as Record).render === 'function') { + else if (Object.prototype.hasOwnProperty.call(val, 'render') && typeof (val as Record).render === 'function' && !isReactive(val)) { return getComponentDefinitionDetails(val) } else if (val.constructor && val.constructor.name === 'VNode') { From 90b52fa34177d3578644ce9d5a2c3705d1b2d5cd Mon Sep 17 00:00:00 2001 From: lazerg Date: Sun, 19 Jul 2026 06:54:58 +0500 Subject: [PATCH 3/3] test(devtools-kit): drop redundant comments in replacer tests --- packages/devtools-kit/__tests__/component/replacer.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/devtools-kit/__tests__/component/replacer.test.ts b/packages/devtools-kit/__tests__/component/replacer.test.ts index 65dc07bcb..69782d358 100644 --- a/packages/devtools-kit/__tests__/component/replacer.test.ts +++ b/packages/devtools-kit/__tests__/component/replacer.test.ts @@ -7,8 +7,6 @@ function replace(value: unknown) { } describe('stringifyReplacer: component definition detection', () => { - // #1100: Pinia store state can hold plain class instances that happen to - // expose a `render` method. Those must not be reported as Vue components. it('does not treat a plain class instance with a render method as a component', () => { class Chart { data = [1, 2, 3] @@ -36,8 +34,6 @@ describe('stringifyReplacer: component definition detection', () => { expect(result?._custom?.displayText).toBe('MyButton') }) - // #1100: Pinia store state is always wrapped in `reactive()`. A reactive - // object with an own `render` method must not be reported as a component. it('does not treat a reactive object with a render method as a component', () => { const store = reactive({ data: [1, 2, 3],