]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-dom): restrict createStaticVNode usage with option elements (#10846)
authorskirtle <65301168+skirtles-code@users.noreply.github.com>
Wed, 1 May 2024 16:03:17 +0000 (17:03 +0100)
committerGitHub <noreply@github.com>
Wed, 1 May 2024 16:03:17 +0000 (00:03 +0800)
close #6568
close #7434

packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
packages/compiler-dom/src/transforms/stringifyStatic.ts

index 33fb37a58f97e96e07e2d887240ee1567de33583..57d880a03f6e13ebe1e4949fd5186c5aa3667297 100644 (file)
@@ -1,5 +1,24 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`stringify static html > should bail for <option> elements with number values 1`] = `
+"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
+
+const _hoisted_1 = /*#__PURE__*/_createElementVNode("select", null, [
+  /*#__PURE__*/_createElementVNode("option", { value: 1 }),
+  /*#__PURE__*/_createElementVNode("option", { value: 1 }),
+  /*#__PURE__*/_createElementVNode("option", { value: 1 }),
+  /*#__PURE__*/_createElementVNode("option", { value: 1 }),
+  /*#__PURE__*/_createElementVNode("option", { value: 1 })
+], -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
+
+return function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
+}"
+`;
+
 exports[`stringify static html > should bail on bindings that are hoisted but not stringifiable 1`] = `
 "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
 
@@ -20,6 +39,19 @@ return function render(_ctx, _cache) {
 }"
 `;
 
+exports[`stringify static html > should work for <option> elements with string values 1`] = `
+"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
+
+const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
+const _hoisted_2 = [
+  _hoisted_1
+]
+
+return function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
+}"
+`;
+
 exports[`stringify static html > should work with bindings that are non-static but stringifiable 1`] = `
 "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
 
index b0eb515c2540c0a387522d4391032c28465b1033..67267c13deea0ed4c1eb77fa703a5acfde1d0ab3 100644 (file)
@@ -485,4 +485,51 @@ describe('stringify static html', () => {
     expect(code).toMatch(`<code>text1</code>`)
     expect(code).toMatchSnapshot()
   })
+
+  test('should work for <option> elements with string values', () => {
+    const { ast, code } = compileWithStringify(
+      `<div><select>${repeat(
+        `<option value="1" />`,
+        StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
+      )}</select></div>`,
+    )
+    // should be optimized now
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_STATIC,
+        arguments: [
+          JSON.stringify(
+            `<select>${repeat(
+              `<option value="1"></option>`,
+              StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
+            )}</select>`,
+          ),
+          '1',
+        ],
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION,
+      },
+    ])
+    expect(code).toMatchSnapshot()
+  })
+
+  test('should bail for <option> elements with number values', () => {
+    const { ast, code } = compileWithStringify(
+      `<div><select>${repeat(
+        `<option :value="1" />`,
+        StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
+      )}</select></div>`,
+    )
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.VNODE_CALL,
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION,
+      },
+    ])
+    expect(code).toMatchSnapshot()
+  })
 })
index a5ff770f4e82c2d9c335296a2f4426c3a6241640..7d740798ebbef7f4f3b6dc7b8ac5c471de7635e4 100644 (file)
@@ -17,6 +17,7 @@ import {
   type TextCallNode,
   type TransformContext,
   createCallExpression,
+  isStaticArgOf,
 } from '@vue/compiler-core'
 import {
   escapeHtml,
@@ -200,6 +201,7 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
   // probably only need to check for most common case
   // i.e. non-phrasing-content tags inside `<p>`
   function walk(node: ElementNode): boolean {
+    const isOptionTag = node.tag === 'option' && node.ns === Namespaces.HTML
     for (let i = 0; i < node.props.length; i++) {
       const p = node.props[i]
       // bail on non-attr bindings
@@ -225,6 +227,16 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
         ) {
           return bail()
         }
+        // <option :value="1"> cannot be safely stringified
+        if (
+          isOptionTag &&
+          isStaticArgOf(p.arg, 'value') &&
+          p.exp &&
+          p.exp.ast &&
+          p.exp.ast.type !== 'StringLiteral'
+        ) {
+          return bail()
+        }
       }
     }
     for (let i = 0; i < node.children.length; i++) {