]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
dx(runtime-core): warn when innerHTML/textContent overrides children (#15194)
authorPENCIL_キムテソン <131854855+KimMaru10@users.noreply.github.com>
Tue, 4 Aug 2026 07:53:42 +0000 (16:53 +0900)
committerGitHub <noreply@github.com>
Tue, 4 Aug 2026 07:53:42 +0000 (15:53 +0800)
close #5081

packages/runtime-core/__tests__/vnode.spec.ts
packages/runtime-core/src/vnode.ts
packages/server-renderer/__tests__/render.spec.ts

index 3a88f302bc835ead5d95e6febc76715a9658f8ef..000ded43844839abeb025aa79979ea27a6463963 100644 (file)
@@ -92,6 +92,50 @@ describe('vnode', () => {
     expect(`VNode created with invalid key (NaN)`).toHaveBeenWarned()
   })
 
+  // #5081
+  describe('children overridden by innerHTML / textContent', () => {
+    test('warn on text children', () => {
+      createVNode('div', { innerHTML: '<span/>' }, 'hello')
+      expect(
+        `The \`innerHTML\` prop on <div> will override its children`,
+      ).toHaveBeenWarned()
+    })
+
+    test('warn on array children', () => {
+      createVNode('div', { innerHTML: '<span/>' }, [createVNode('span')])
+      expect(
+        `The \`innerHTML\` prop on <div> will override its children`,
+      ).toHaveBeenWarned()
+    })
+
+    test('warn on textContent', () => {
+      createVNode('div', { textContent: 'text' }, 'hello')
+      expect(
+        `The \`textContent\` prop on <div> will override its children`,
+      ).toHaveBeenWarned()
+    })
+
+    test('no warning when the prop is nullish', () => {
+      createVNode('div', { innerHTML: undefined }, 'hello')
+      createVNode('div', { innerHTML: null }, 'hello')
+      createVNode('div', { textContent: undefined }, 'hello')
+      expect(`will override its children`).not.toHaveBeenWarned()
+    })
+
+    test('no warning without renderable children', () => {
+      createVNode('div', { innerHTML: '<span/>' })
+      createVNode('div', { innerHTML: '<span/>' }, '')
+      createVNode('div', { innerHTML: '<span/>' }, [])
+      expect(`will override its children`).not.toHaveBeenWarned()
+    })
+
+    test('no warning for component vnodes', () => {
+      const Comp = { props: ['innerHTML'], render: () => null }
+      createVNode(Comp, { innerHTML: '<span/>' }, { default: () => 'slot' })
+      expect(`will override its children`).not.toHaveBeenWarned()
+    })
+  })
+
   test('create with class component', () => {
     class Component {
       $props: any
index e9745c50321f37c753ec9c658fd3d71765cedeaf..30d7fdfda1755d2a163616a4bc6505384cdfb28e 100644 (file)
@@ -499,6 +499,25 @@ function createBaseVNode(
     warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
   }
 
+  // #5081 validate children that will be silently discarded by innerHTML /
+  // textContent. The template compiler already errors on `v-html` / `v-text`
+  // used with children, but render functions have no such check.
+  if (__DEV__ && props && vnode.shapeFlag & ShapeFlags.ELEMENT) {
+    const overwritingProp =
+      props.innerHTML != null
+        ? 'innerHTML'
+        : props.textContent != null
+          ? 'textContent'
+          : null
+    if (overwritingProp && hasContentChildren(vnode.children)) {
+      warn(
+        `The \`${overwritingProp}\` prop on <${vnode.type as string}> will ` +
+          `override its children. Remove either the \`${overwritingProp}\` ` +
+          `prop or the children.`,
+      )
+    }
+  }
+
   // track vnode for block tree
   if (
     isBlockTreeEnabled > 0 &&
@@ -528,6 +547,18 @@ function createBaseVNode(
 
 export { createBaseVNode as createElementVNode }
 
+/**
+ * dev only
+ * Whether children would actually render something. Empty text and empty
+ * arrays are ignored, mirroring the compiler's `node.children.length` check
+ * for `v-html` / `v-text`.
+ */
+function hasContentChildren(children: VNode['children']): boolean {
+  if (isString(children)) return children !== ''
+  if (isArray(children)) return children.length > 0
+  return false
+}
+
 export const createVNode = (
   __DEV__ ? createVNodeWithArgsTransform : _createVNode
 ) as typeof _createVNode
index f537a6219f2c822a9fe8175a8dd88a21569b84d1..37de08bd4c7410581e6b6457fc7ae1cf95512f1d 100644 (file)
@@ -664,6 +664,9 @@ function testRender(type: string, render: typeof renderToString) {
             ),
           ),
         ).toBe(`<div><span>hello</span></div>`)
+        expect(
+          `The \`innerHTML\` prop on <div> will override its children`,
+        ).toHaveBeenWarned()
       })
 
       test('textContent', async () => {
@@ -678,6 +681,9 @@ function testRender(type: string, render: typeof renderToString) {
             ),
           ),
         ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
+        expect(
+          `The \`textContent\` prop on <div> will override its children`,
+        ).toHaveBeenWarned()
       })
 
       test('textarea value', async () => {