]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
dx(runtime-core): check current and parent components in formatComponentName (#7220)
authorskirtle <65301168+skirtles-code@users.noreply.github.com>
Mon, 24 Nov 2025 06:50:43 +0000 (06:50 +0000)
committerGitHub <noreply@github.com>
Mon, 24 Nov 2025 06:50:43 +0000 (14:50 +0800)
packages/runtime-core/__tests__/component.spec.ts [new file with mode: 0644]
packages/runtime-core/src/component.ts

diff --git a/packages/runtime-core/__tests__/component.spec.ts b/packages/runtime-core/__tests__/component.spec.ts
new file mode 100644 (file)
index 0000000..7b29e77
--- /dev/null
@@ -0,0 +1,157 @@
+import {
+  type ComponentInternalInstance,
+  getCurrentInstance,
+  h,
+  nodeOps,
+  render,
+} from '@vue/runtime-test'
+import { formatComponentName } from '../src/component'
+
+describe('formatComponentName', () => {
+  test('default name', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    render(h(Comp), nodeOps.createElement('div'))
+
+    expect(formatComponentName(null, Comp)).toBe('Anonymous')
+    expect(formatComponentName(null, Comp, true)).toBe('App')
+    expect(formatComponentName(instance, Comp)).toBe('Anonymous')
+    expect(formatComponentName(instance, Comp, true)).toBe('App')
+  })
+
+  test('name option', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      name: 'number-input',
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    render(h(Comp), nodeOps.createElement('div'))
+
+    expect(formatComponentName(null, Comp)).toBe('NumberInput')
+    expect(formatComponentName(instance, Comp, true)).toBe('NumberInput')
+  })
+
+  test('self recursive name', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      components: {} as any,
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    Comp.components.ToggleButton = Comp
+    render(h(Comp), nodeOps.createElement('div'))
+
+    expect(formatComponentName(instance, Comp)).toBe('ToggleButton')
+  })
+
+  test('name from parent', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    const Parent = {
+      components: {
+        list_item: Comp,
+      },
+      render() {
+        return h(Comp)
+      },
+    }
+    render(h(Parent), nodeOps.createElement('div'))
+
+    expect(formatComponentName(instance, Comp)).toBe('ListItem')
+  })
+
+  test('functional components', () => {
+    const UserAvatar = () => null
+    expect(formatComponentName(null, UserAvatar)).toBe('UserAvatar')
+    UserAvatar.displayName = 'UserPicture'
+    expect(formatComponentName(null, UserAvatar)).toBe('UserPicture')
+    expect(formatComponentName(null, () => null)).toBe('Anonymous')
+  })
+
+  test('Name from file', () => {
+    const Comp = {
+      __file: './src/locale-dropdown.vue',
+    }
+
+    expect(formatComponentName(null, Comp)).toBe('LocaleDropdown')
+  })
+
+  test('inferred name', () => {
+    const Comp = {
+      __name: 'MainSidebar',
+    }
+
+    expect(formatComponentName(null, Comp)).toBe('MainSidebar')
+  })
+
+  test('global component', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Comp = {
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    render(h(Comp), nodeOps.createElement('div'))
+
+    instance!.appContext.components.FieldLabel = Comp
+
+    expect(formatComponentName(instance, Comp)).toBe('FieldLabel')
+  })
+
+  test('name precedence', () => {
+    let instance: ComponentInternalInstance | null = null
+    const Dummy = () => null
+    const Comp: Record<string, any> = {
+      components: { Dummy },
+      setup() {
+        instance = getCurrentInstance()
+        return () => null
+      },
+    }
+    const Parent = {
+      components: { Dummy } as any,
+      render() {
+        return h(Comp)
+      },
+    }
+    render(h(Parent), nodeOps.createElement('div'))
+
+    expect(formatComponentName(instance, Comp)).toBe('Anonymous')
+    expect(formatComponentName(instance, Comp, true)).toBe('App')
+
+    instance!.appContext.components.CompA = Comp
+    expect(formatComponentName(instance, Comp)).toBe('CompA')
+    expect(formatComponentName(instance, Comp, true)).toBe('CompA')
+
+    Parent.components.CompB = Comp
+    expect(formatComponentName(instance, Comp)).toBe('CompB')
+
+    Comp.components.CompC = Comp
+    expect(formatComponentName(instance, Comp)).toBe('CompC')
+
+    Comp.__file = './CompD.js'
+    expect(formatComponentName(instance, Comp)).toBe('CompD')
+
+    Comp.__name = 'CompE'
+    expect(formatComponentName(instance, Comp)).toBe('CompE')
+
+    Comp.name = 'CompF'
+    expect(formatComponentName(instance, Comp)).toBe('CompF')
+  })
+})
index baa673ff96c54d18d20986ca2d409c71bc3e999f..4e1aa5e4d38dffef4351343401b7e6be9234f2fb 100644 (file)
@@ -897,7 +897,7 @@ function setupStatefulComponent(
         // bail here and wait for re-entry.
         instance.asyncDep = setupResult
         if (__DEV__ && !instance.suspense) {
-          const name = Component.name ?? 'Anonymous'
+          const name = formatComponentName(instance, Component)
           warn(
             `Component <${name}>: setup function returned a promise, but no ` +
               `<Suspense> boundary was found in the parent component tree. ` +
@@ -1229,9 +1229,11 @@ export function formatComponentName(
     }
   }
 
-  if (!name && instance && instance.parent) {
+  if (!name && instance) {
     // try to infer the name based on reverse resolution
-    const inferFromRegistry = (registry: Record<string, any> | undefined) => {
+    const inferFromRegistry = (
+      registry: Record<string, any> | undefined | null,
+    ) => {
       for (const key in registry) {
         if (registry[key] === Component) {
           return key
@@ -1239,10 +1241,12 @@ export function formatComponentName(
       }
     }
     name =
-      inferFromRegistry(
-        instance.components ||
+      inferFromRegistry(instance.components) ||
+      (instance.parent &&
+        inferFromRegistry(
           (instance.parent.type as ComponentOptions).components,
-      ) || inferFromRegistry(instance.appContext.components)
+        )) ||
+      inferFromRegistry(instance.appContext.components)
   }
 
   return name ? classify(name) : isRoot ? `App` : `Anonymous`