fix(compiler-dom): properly stringify class/style bindings when hoisting static strings
This commit is contained in:
		
							parent
							
								
									189a0a3b19
								
							
						
					
					
						commit
						1b9b235663
					
				@ -623,7 +623,7 @@ describe('compiler: element transform', () => {
 | 
			
		||||
 | 
			
		||||
  test(`props merging: style`, () => {
 | 
			
		||||
    const { node } = parseWithElementTransform(
 | 
			
		||||
      `<div style="color: red" :style="{ color: 'red' }" />`,
 | 
			
		||||
      `<div style="color: green" :style="{ color: 'red' }" />`,
 | 
			
		||||
      {
 | 
			
		||||
        nodeTransforms: [transformStyle, transformElement],
 | 
			
		||||
        directiveTransforms: {
 | 
			
		||||
@ -646,7 +646,7 @@ describe('compiler: element transform', () => {
 | 
			
		||||
            elements: [
 | 
			
		||||
              {
 | 
			
		||||
                type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
                content: `_hoisted_1`,
 | 
			
		||||
                content: `{"color":"green"}`,
 | 
			
		||||
                isStatic: false
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,6 @@
 | 
			
		||||
 | 
			
		||||
exports[`compile should contain standard transforms 1`] = `
 | 
			
		||||
"const _Vue = Vue
 | 
			
		||||
const { createVNode: _createVNode } = _Vue
 | 
			
		||||
 | 
			
		||||
const _hoisted_1 = {}
 | 
			
		||||
 | 
			
		||||
return function render(_ctx, _cache) {
 | 
			
		||||
  with (_ctx) {
 | 
			
		||||
@ -14,7 +11,7 @@ return function render(_ctx, _cache) {
 | 
			
		||||
      _createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]),
 | 
			
		||||
      _createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]),
 | 
			
		||||
      _createVNode(\\"div\\", null, \\"test\\"),
 | 
			
		||||
      _createVNode(\\"div\\", { style: _hoisted_1 }, \\"red\\"),
 | 
			
		||||
      _createVNode(\\"div\\", { style: {\\"color\\":\\"red\\"} }, \\"red\\"),
 | 
			
		||||
      _createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */)
 | 
			
		||||
    ], 64 /* STABLE_FRAGMENT */))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ describe('compile', () => {
 | 
			
		||||
    const { code } = compile(`<div v-text="text"></div>
 | 
			
		||||
        <div v-html="html"></div>
 | 
			
		||||
        <div v-cloak>test</div>
 | 
			
		||||
        <div style="color=red">red</div>
 | 
			
		||||
        <div style="color:red">red</div>
 | 
			
		||||
        <div :style="{color: 'green'}"></div>`)
 | 
			
		||||
 | 
			
		||||
    expect(code).toMatchSnapshot()
 | 
			
		||||
 | 
			
		||||
@ -77,8 +77,8 @@ describe('stringify static html', () => {
 | 
			
		||||
 | 
			
		||||
  test('serliazing constant bindings', () => {
 | 
			
		||||
    const { ast } = compileWithStringify(
 | 
			
		||||
      `<div><div>${repeat(
 | 
			
		||||
        `<span :class="'foo' + 'bar'">{{ 1 }} + {{ false }}</span>`,
 | 
			
		||||
      `<div><div :style="{ color: 'red' }">${repeat(
 | 
			
		||||
        `<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
 | 
			
		||||
        StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
 | 
			
		||||
      )}</div></div>`
 | 
			
		||||
    )
 | 
			
		||||
@ -89,8 +89,8 @@ describe('stringify static html', () => {
 | 
			
		||||
      callee: CREATE_STATIC,
 | 
			
		||||
      arguments: [
 | 
			
		||||
        JSON.stringify(
 | 
			
		||||
          `<div>${repeat(
 | 
			
		||||
            `<span class="foobar">1 + false</span>`,
 | 
			
		||||
          `<div style="color:red;">${repeat(
 | 
			
		||||
            `<span class="foo bar">1 + false</span>`,
 | 
			
		||||
            StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
 | 
			
		||||
          )}</div>`
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -26,17 +26,8 @@ function transformWithStyleTransform(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('compiler: style transform', () => {
 | 
			
		||||
  test('should transform into directive node and hoist value', () => {
 | 
			
		||||
    const { root, node } = transformWithStyleTransform(
 | 
			
		||||
      `<div style="color: red"/>`
 | 
			
		||||
    )
 | 
			
		||||
    expect(root.hoists).toMatchObject([
 | 
			
		||||
      {
 | 
			
		||||
        type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
        content: `{"color":"red"}`,
 | 
			
		||||
        isStatic: false
 | 
			
		||||
      }
 | 
			
		||||
    ])
 | 
			
		||||
  test('should transform into directive node', () => {
 | 
			
		||||
    const { node } = transformWithStyleTransform(`<div style="color: red"/>`)
 | 
			
		||||
    expect(node.props[0]).toMatchObject({
 | 
			
		||||
      type: NodeTypes.DIRECTIVE,
 | 
			
		||||
      name: `bind`,
 | 
			
		||||
@ -47,7 +38,7 @@ describe('compiler: style transform', () => {
 | 
			
		||||
      },
 | 
			
		||||
      exp: {
 | 
			
		||||
        type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
        content: `_hoisted_1`,
 | 
			
		||||
        content: `{"color":"red"}`,
 | 
			
		||||
        isStatic: false
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
@ -71,7 +62,7 @@ describe('compiler: style transform', () => {
 | 
			
		||||
          },
 | 
			
		||||
          value: {
 | 
			
		||||
            type: NodeTypes.SIMPLE_EXPRESSION,
 | 
			
		||||
            content: `_hoisted_1`,
 | 
			
		||||
            content: `{"color":"red"}`,
 | 
			
		||||
            isStatic: false
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,10 @@ import {
 | 
			
		||||
  isString,
 | 
			
		||||
  isSymbol,
 | 
			
		||||
  escapeHtml,
 | 
			
		||||
  toDisplayString
 | 
			
		||||
  toDisplayString,
 | 
			
		||||
  normalizeClass,
 | 
			
		||||
  normalizeStyle,
 | 
			
		||||
  stringifyStyle
 | 
			
		||||
} from '@vue/shared'
 | 
			
		||||
 | 
			
		||||
// Turn eligible hoisted static trees into stringied static nodes, e.g.
 | 
			
		||||
@ -84,8 +87,15 @@ function stringifyElement(
 | 
			
		||||
      }
 | 
			
		||||
    } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
 | 
			
		||||
      // constant v-bind, e.g. :foo="1"
 | 
			
		||||
      let evaluated = evaluateConstant(p.exp as SimpleExpressionNode)
 | 
			
		||||
      const arg = p.arg && (p.arg as SimpleExpressionNode).content
 | 
			
		||||
      if (arg === 'class') {
 | 
			
		||||
        evaluated = normalizeClass(evaluated)
 | 
			
		||||
      } else if (arg === 'style') {
 | 
			
		||||
        evaluated = stringifyStyle(normalizeStyle(evaluated))
 | 
			
		||||
      }
 | 
			
		||||
      res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
 | 
			
		||||
        evaluateConstant(p.exp as ExpressionNode)
 | 
			
		||||
        evaluated
 | 
			
		||||
      )}"`
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -151,7 +161,7 @@ function evaluateConstant(exp: ExpressionNode): string {
 | 
			
		||||
      if (c.type === NodeTypes.TEXT) {
 | 
			
		||||
        res += c.content
 | 
			
		||||
      } else if (c.type === NodeTypes.INTERPOLATION) {
 | 
			
		||||
        res += evaluateConstant(c.content)
 | 
			
		||||
        res += toDisplayString(evaluateConstant(c.content))
 | 
			
		||||
      } else {
 | 
			
		||||
        res += evaluateConstant(c)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -17,12 +17,11 @@ export const transformStyle: NodeTransform = (node, context) => {
 | 
			
		||||
    node.props.forEach((p, i) => {
 | 
			
		||||
      if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
 | 
			
		||||
        // replace p with an expression node
 | 
			
		||||
        const exp = context.hoist(parseInlineCSS(p.value.content, p.loc))
 | 
			
		||||
        node.props[i] = {
 | 
			
		||||
          type: NodeTypes.DIRECTIVE,
 | 
			
		||||
          name: `bind`,
 | 
			
		||||
          arg: createSimpleExpression(`style`, true, p.loc),
 | 
			
		||||
          exp,
 | 
			
		||||
          exp: parseInlineCSS(p.value.content, p.loc),
 | 
			
		||||
          modifiers: [],
 | 
			
		||||
          loc: p.loc
 | 
			
		||||
        }
 | 
			
		||||
@ -45,5 +44,5 @@ function parseInlineCSS(
 | 
			
		||||
      tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  return createSimpleExpression(JSON.stringify(res), false, loc)
 | 
			
		||||
  return createSimpleExpression(JSON.stringify(res), false, loc, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -101,7 +101,7 @@ describe('ssr: element', () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        getCompiledString(`<div style="color:red;" :style="bar"></div>`)
 | 
			
		||||
      ).toMatchInlineSnapshot(
 | 
			
		||||
        `"\`<div style=\\"\${_ssrRenderStyle([_hoisted_1, _ctx.bar])}\\"></div>\`"`
 | 
			
		||||
        `"\`<div style=\\"\${_ssrRenderStyle([{\\"color\\":\\"red\\"}, _ctx.bar])}\\"></div>\`"`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -184,7 +184,7 @@ describe('ssr: element', () => {
 | 
			
		||||
        )
 | 
			
		||||
      ).toMatchInlineSnapshot(`
 | 
			
		||||
        "\`<div\${_ssrRenderAttrs(_mergeProps({
 | 
			
		||||
            style: [_hoisted_1, _ctx.b]
 | 
			
		||||
            style: [{\\"color\\":\\"red\\"}, _ctx.b]
 | 
			
		||||
          }, _ctx.obj))}></div>\`"
 | 
			
		||||
      `)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -16,11 +16,9 @@ describe('ssr: v-show', () => {
 | 
			
		||||
      .toMatchInlineSnapshot(`
 | 
			
		||||
      "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\")
 | 
			
		||||
 | 
			
		||||
      const _hoisted_1 = {\\"color\\":\\"red\\"}
 | 
			
		||||
 | 
			
		||||
      return function ssrRender(_ctx, _push, _parent) {
 | 
			
		||||
        _push(\`<div style=\\"\${_ssrRenderStyle([
 | 
			
		||||
          _hoisted_1,
 | 
			
		||||
          {\\"color\\":\\"red\\"},
 | 
			
		||||
          (_ctx.foo) ? null : { display: \\"none\\" }
 | 
			
		||||
        ])}\\"></div>\`)
 | 
			
		||||
      }"
 | 
			
		||||
@ -48,11 +46,9 @@ describe('ssr: v-show', () => {
 | 
			
		||||
    ).toMatchInlineSnapshot(`
 | 
			
		||||
      "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\")
 | 
			
		||||
 | 
			
		||||
      const _hoisted_1 = {\\"color\\":\\"red\\"}
 | 
			
		||||
 | 
			
		||||
      return function ssrRender(_ctx, _push, _parent) {
 | 
			
		||||
        _push(\`<div style=\\"\${_ssrRenderStyle([
 | 
			
		||||
          _hoisted_1,
 | 
			
		||||
          {\\"color\\":\\"red\\"},
 | 
			
		||||
          { fontSize: 14 },
 | 
			
		||||
          (_ctx.foo) ? null : { display: \\"none\\" }
 | 
			
		||||
        ])}\\"></div>\`)
 | 
			
		||||
@ -69,12 +65,10 @@ describe('ssr: v-show', () => {
 | 
			
		||||
      "const { mergeProps: _mergeProps } = require(\\"vue\\")
 | 
			
		||||
      const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
 | 
			
		||||
 | 
			
		||||
      const _hoisted_1 = {\\"color\\":\\"red\\"}
 | 
			
		||||
 | 
			
		||||
      return function ssrRender(_ctx, _push, _parent) {
 | 
			
		||||
        _push(\`<div\${_ssrRenderAttrs(_mergeProps(_ctx.baz, {
 | 
			
		||||
          style: [
 | 
			
		||||
            _hoisted_1,
 | 
			
		||||
            {\\"color\\":\\"red\\"},
 | 
			
		||||
            { fontSize: 14 },
 | 
			
		||||
            (_ctx.foo) ? null : { display: \\"none\\" }
 | 
			
		||||
          ]
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,9 @@
 | 
			
		||||
import { escapeHtml } from '@vue/shared'
 | 
			
		||||
import { escapeHtml, stringifyStyle } from '@vue/shared'
 | 
			
		||||
import {
 | 
			
		||||
  normalizeClass,
 | 
			
		||||
  normalizeStyle,
 | 
			
		||||
  propsToAttrMap,
 | 
			
		||||
  hyphenate,
 | 
			
		||||
  isString,
 | 
			
		||||
  isNoUnitNumericStyleProp,
 | 
			
		||||
  isOn,
 | 
			
		||||
  isSSRSafeAttrName,
 | 
			
		||||
  isBooleanAttr,
 | 
			
		||||
@ -93,17 +91,5 @@ export function ssrRenderStyle(raw: unknown): string {
 | 
			
		||||
    return escapeHtml(raw)
 | 
			
		||||
  }
 | 
			
		||||
  const styles = normalizeStyle(raw)
 | 
			
		||||
  let ret = ''
 | 
			
		||||
  for (const key in styles) {
 | 
			
		||||
    const value = styles[key]
 | 
			
		||||
    const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key)
 | 
			
		||||
    if (
 | 
			
		||||
      isString(value) ||
 | 
			
		||||
      (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))
 | 
			
		||||
    ) {
 | 
			
		||||
      // only render valid values
 | 
			
		||||
      ret += `${normalizedKey}:${value};`
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return escapeHtml(ret)
 | 
			
		||||
  return escapeHtml(stringifyStyle(styles))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { isArray, isString, isObject } from './'
 | 
			
		||||
import { isArray, isString, isObject, hyphenate } from './'
 | 
			
		||||
import { isNoUnitNumericStyleProp } from './domAttrConfig'
 | 
			
		||||
 | 
			
		||||
export function normalizeStyle(
 | 
			
		||||
  value: unknown
 | 
			
		||||
@ -19,6 +20,27 @@ export function normalizeStyle(
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stringifyStyle(
 | 
			
		||||
  styles: Record<string, string | number> | undefined
 | 
			
		||||
): string {
 | 
			
		||||
  let ret = ''
 | 
			
		||||
  if (!styles) {
 | 
			
		||||
    return ret
 | 
			
		||||
  }
 | 
			
		||||
  for (const key in styles) {
 | 
			
		||||
    const value = styles[key]
 | 
			
		||||
    const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key)
 | 
			
		||||
    if (
 | 
			
		||||
      isString(value) ||
 | 
			
		||||
      (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))
 | 
			
		||||
    ) {
 | 
			
		||||
      // only render valid values
 | 
			
		||||
      ret += `${normalizedKey}:${value};`
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function normalizeClass(value: unknown): string {
 | 
			
		||||
  let res = ''
 | 
			
		||||
  if (isString(value)) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user