Procházet zdrojové kódy

feat(compiler-vapor): props merging (#118)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
Rizumu Ayaka před 2 roky
rodič
revize
ba3ca6a304

+ 29 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap

@@ -0,0 +1,29 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: element transform > props merging: class 1`] = `
+"import { template as _template, children as _children, renderEffect as _renderEffect, setClass as _setClass } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
+  _renderEffect(() => {
+    _setClass(n1, ["foo", { bar: _ctx.isBar }])
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > props merging: style 1`] = `
+"import { template as _template, children as _children, renderEffect as _renderEffect, setStyle as _setStyle } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>")
+  const n0 = t0()
+  const { 0: [n1],} = _children(n0)
+  _renderEffect(() => {
+    _setStyle(n1, ["color: green", { color: 'red' }])
+  })
+  return n0
+}"
+`;

+ 145 - 2
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts

@@ -1,2 +1,145 @@
-// TODO: add tests for this transform
-test('baisc', () => {})
+import { makeCompile } from './_utils'
+import {
+  IRNodeTypes,
+  transformElement,
+  transformVBind,
+  transformVOn,
+} from '../../src'
+import { NodeTypes } from '@vue/compiler-core'
+
+const compileWithElementTransform = makeCompile({
+  nodeTransforms: [transformElement],
+  directiveTransforms: {
+    bind: transformVBind,
+    on: transformVOn,
+  },
+})
+
+describe('compiler: element transform', () => {
+  test.todo('baisc')
+
+  test.todo('props merging: event handlers', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div @click.foo="a" @click.bar="b" />`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(ir.operation).toMatchObject([
+      {
+        type: IRNodeTypes.SET_EVENT,
+        element: 1,
+        key: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'click',
+          isStatic: true,
+        },
+        events: [
+          {
+            // IREvent: value, modifiers, keyOverride...
+            value: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: `a`,
+              isStatic: false,
+            },
+          },
+          {
+            value: {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: `b`,
+              isStatic: false,
+            },
+          },
+        ],
+      },
+    ])
+  })
+
+  test('props merging: style', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div style="color: green" :style="{ color: 'red' }" />`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(ir.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: `{ color: 'red' }`,
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_PROP,
+            element: 1,
+            prop: {
+              key: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'style',
+                isStatic: true,
+              },
+              values: [
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: 'color: green',
+                  isStatic: true,
+                },
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: `{ color: 'red' }`,
+                  isStatic: false,
+                },
+              ],
+            },
+          },
+        ],
+      },
+    ])
+  })
+
+  test('props merging: class', () => {
+    const { code, ir } = compileWithElementTransform(
+      `<div class="foo" :class="{ bar: isBar }" />`,
+    )
+
+    expect(code).toMatchSnapshot()
+
+    expect(ir.effect).toMatchObject([
+      {
+        expressions: [
+          {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: `{ bar: isBar }`,
+            isStatic: false,
+          },
+        ],
+        operations: [
+          {
+            type: IRNodeTypes.SET_PROP,
+            element: 1,
+            prop: {
+              key: {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'class',
+                isStatic: true,
+              },
+              values: [
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: `foo`,
+                  isStatic: true,
+                },
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: `{ bar: isBar }`,
+                  isStatic: false,
+                },
+              ],
+            },
+          },
+        ],
+      },
+    ])
+  })
+})

+ 114 - 80
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts

@@ -52,16 +52,18 @@ describe('compiler v-bind', () => {
                 source: 'id',
               },
             },
-            value: {
-              type: NodeTypes.SIMPLE_EXPRESSION,
-              content: 'id',
-              isStatic: false,
-              loc: {
-                source: 'id',
-                start: { line: 1, column: 17, offset: 16 },
-                end: { line: 1, column: 19, offset: 18 },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'id',
+                isStatic: false,
+                loc: {
+                  source: 'id',
+                  start: { line: 1, column: 17, offset: 16 },
+                  end: { line: 1, column: 19, offset: 18 },
+                },
               },
-            },
+            ],
             loc: {
               start: { column: 6, line: 1, offset: 5 },
               end: { column: 20, line: 1, offset: 19 },
@@ -92,14 +94,16 @@ describe('compiler v-bind', () => {
             end: { line: 1, column: 15, offset: 14 },
           },
         },
-        value: {
-          content: `id`,
-          isStatic: false,
-          loc: {
-            start: { line: 1, column: 13, offset: 12 },
-            end: { line: 1, column: 15, offset: 14 },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+            loc: {
+              start: { line: 1, column: 13, offset: 12 },
+              end: { line: 1, column: 15, offset: 14 },
+            },
           },
-        },
+        ],
       },
     })
     expect(code).contains('_setDynamicProp(n1, "id", _ctx.id)')
@@ -116,10 +120,12 @@ describe('compiler v-bind', () => {
           content: `camel-case`,
           isStatic: true,
         },
-        value: {
-          content: `camelCase`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `camelCase`,
+            isStatic: false,
+          },
+        ],
       },
     })
     expect(code).contains('_setDynamicProp(n1, "camel-case", _ctx.camelCase)')
@@ -141,11 +147,13 @@ describe('compiler v-bind', () => {
               content: 'id',
               isStatic: false,
             },
-            value: {
-              type: NodeTypes.SIMPLE_EXPRESSION,
-              content: 'id',
-              isStatic: false,
-            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'id',
+                isStatic: false,
+              },
+            ],
           },
           {
             key: {
@@ -153,11 +161,13 @@ describe('compiler v-bind', () => {
               content: 'title',
               isStatic: false,
             },
-            value: {
-              type: NodeTypes.SIMPLE_EXPRESSION,
-              content: 'title',
-              isStatic: false,
-            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'title',
+                isStatic: false,
+              },
+            ],
           },
         ],
       ],
@@ -183,11 +193,13 @@ describe('compiler v-bind', () => {
               content: 'id',
               isStatic: false,
             },
-            value: {
-              type: NodeTypes.SIMPLE_EXPRESSION,
-              content: 'id',
-              isStatic: false,
-            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'id',
+                isStatic: false,
+              },
+            ],
           },
           {
             key: {
@@ -195,11 +207,13 @@ describe('compiler v-bind', () => {
               content: 'foo',
               isStatic: true,
             },
-            value: {
-              type: NodeTypes.SIMPLE_EXPRESSION,
-              content: 'bar',
-              isStatic: true,
-            },
+            values: [
+              {
+                type: NodeTypes.SIMPLE_EXPRESSION,
+                content: 'bar',
+                isStatic: true,
+              },
+            ],
           },
           {
             key: {
@@ -247,10 +261,12 @@ describe('compiler v-bind', () => {
           content: `fooBar`,
           isStatic: true,
         },
-        value: {
-          content: `id`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: undefined,
       },
@@ -270,10 +286,12 @@ describe('compiler v-bind', () => {
           content: `fooBar`,
           isStatic: true,
         },
-        value: {
-          content: `fooBar`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: undefined,
       },
@@ -294,10 +312,12 @@ describe('compiler v-bind', () => {
               content: `foo`,
               isStatic: false,
             },
-            value: {
-              content: `id`,
-              isStatic: false,
-            },
+            values: [
+              {
+                content: `id`,
+                isStatic: false,
+              },
+            ],
             runtimeCamelize: true,
             modifier: undefined,
           },
@@ -324,10 +344,12 @@ describe('compiler v-bind', () => {
           content: `fooBar`,
           isStatic: true,
         },
-        value: {
-          content: `id`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: '.',
       },
@@ -346,10 +368,12 @@ describe('compiler v-bind', () => {
           content: `fooBar`,
           isStatic: true,
         },
-        value: {
-          content: `fooBar`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: '.',
       },
@@ -371,10 +395,12 @@ describe('compiler v-bind', () => {
               content: `fooBar`,
               isStatic: false,
             },
-            value: {
-              content: `id`,
-              isStatic: false,
-            },
+            values: [
+              {
+                content: `id`,
+                isStatic: false,
+              },
+            ],
             runtimeCamelize: false,
             modifier: '.',
           },
@@ -399,10 +425,12 @@ describe('compiler v-bind', () => {
           content: `fooBar`,
           isStatic: true,
         },
-        value: {
-          content: `id`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: '.',
       },
@@ -421,10 +449,12 @@ describe('compiler v-bind', () => {
           content: `fooBar`,
           isStatic: true,
         },
-        value: {
-          content: `fooBar`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: '.',
       },
@@ -443,10 +473,12 @@ describe('compiler v-bind', () => {
           content: `foo-bar`,
           isStatic: true,
         },
-        value: {
-          content: `id`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `id`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: '^',
       },
@@ -465,10 +497,12 @@ describe('compiler v-bind', () => {
           content: `foo-bar`,
           isStatic: true,
         },
-        value: {
-          content: `fooBar`,
-          isStatic: false,
-        },
+        values: [
+          {
+            content: `fooBar`,
+            isStatic: false,
+          },
+        ],
         runtimeCamelize: false,
         modifier: '^',
       },

+ 21 - 15
packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts

@@ -50,11 +50,13 @@ describe('compiler: v-once', () => {
             content: 'class',
             isStatic: true,
           },
-          value: {
-            type: NodeTypes.SIMPLE_EXPRESSION,
-            content: 'clz',
-            isStatic: false,
-          },
+          values: [
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'clz',
+              isStatic: false,
+            },
+          ],
         },
       },
       {
@@ -81,11 +83,13 @@ describe('compiler: v-once', () => {
             content: 'id',
             isStatic: true,
           },
-          value: {
-            type: NodeTypes.SIMPLE_EXPRESSION,
-            content: 'foo',
-            isStatic: false,
-          },
+          values: [
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'foo',
+              isStatic: false,
+            },
+          ],
         },
       },
     ])
@@ -111,11 +115,13 @@ describe('compiler: v-once', () => {
             content: 'id',
             isStatic: true,
           },
-          value: {
-            type: NodeTypes.SIMPLE_EXPRESSION,
-            content: 'foo',
-            isStatic: false,
-          },
+          values: [
+            {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: 'foo',
+              isStatic: false,
+            },
+          ],
         },
       },
     ])

+ 27 - 8
packages/compiler-vapor/src/generators/prop.ts

@@ -1,8 +1,16 @@
+import {
+  NewlineType,
+  type SimpleExpressionNode,
+  isSimpleIdentifier,
+} from '@vue/compiler-core'
 import { type CodeFragment, type CodegenContext, NEWLINE } from '../generate'
-import type { SetDynamicPropsIRNode, SetPropIRNode, VaporHelper } from '../ir'
+import type {
+  IRProp,
+  SetDynamicPropsIRNode,
+  SetPropIRNode,
+  VaporHelper,
+} from '../ir'
 import { genExpression } from './expression'
-import type { DirectiveTransformResult } from '../transform'
-import { NewlineType, isSimpleIdentifier } from '@vue/compiler-core'
 
 // only the static key prop will reach here
 export function genSetProp(
@@ -11,7 +19,7 @@ export function genSetProp(
 ): CodeFragment[] {
   const { call, vaporHelper } = context
   const {
-    prop: { key, value, modifier },
+    prop: { key, values, modifier },
   } = oper
 
   const keyName = key.content
@@ -36,7 +44,7 @@ export function genSetProp(
       vaporHelper(helperName),
       `n${oper.element}`,
       omitKey ? false : genExpression(key, context),
-      genExpression(value, context),
+      genPropValue(values, context),
     ),
   ]
 }
@@ -63,7 +71,7 @@ export function genDynamicProps(
 }
 
 function genLiteralObjectProps(
-  props: DirectiveTransformResult[],
+  props: IRProp[],
   context: CodegenContext,
 ): CodeFragment[] {
   const { multi } = context
@@ -72,13 +80,13 @@ function genLiteralObjectProps(
     ...props.map(prop => [
       ...genPropertyKey(prop, context),
       `: `,
-      ...genExpression(prop.value, context),
+      ...genPropValue(prop.values, context),
     ]),
   )
 }
 
 function genPropertyKey(
-  { key: node, runtimeCamelize, modifier }: DirectiveTransformResult,
+  { key: node, runtimeCamelize, modifier }: IRProp,
   context: CodegenContext,
 ): CodeFragment[] {
   const { call, helper } = context
@@ -111,3 +119,14 @@ function genPropertyKey(
 
   return [`[`, ...key, `]`]
 }
+
+function genPropValue(values: SimpleExpressionNode[], context: CodegenContext) {
+  if (values.length === 1) {
+    return genExpression(values[0], context)
+  }
+  const { multi } = context
+  return multi(
+    ['[', ']', ', '],
+    ...values.map(expr => genExpression(expr, context)),
+  )
+}

+ 7 - 4
packages/compiler-vapor/src/ir.ts

@@ -88,18 +88,21 @@ export interface FragmentFactoryIRNode extends BaseIRNode {
   type: IRNodeTypes.FRAGMENT_FACTORY
 }
 
+export interface IRProp extends Omit<DirectiveTransformResult, 'value'> {
+  values: SimpleExpressionNode[]
+}
+export type IRProps = IRProp[] | SimpleExpressionNode
+
 export interface SetPropIRNode extends BaseIRNode {
   type: IRNodeTypes.SET_PROP
   element: number
-  prop: DirectiveTransformResult
+  prop: IRProp
 }
 
-export type PropsExpression = DirectiveTransformResult[] | SimpleExpressionNode
-
 export interface SetDynamicPropsIRNode extends BaseIRNode {
   type: IRNodeTypes.SET_DYNAMIC_PROPS
   element: number
-  props: PropsExpression[]
+  props: IRProps[]
 }
 
 export interface SetTextIRNode extends BaseIRNode {

+ 75 - 41
packages/compiler-vapor/src/transforms/transformElement.ts

@@ -8,7 +8,12 @@ import {
   createCompilerError,
   createSimpleExpression,
 } from '@vue/compiler-dom'
-import { isBuiltInDirective, isReservedProp, isVoidTag } from '@vue/shared'
+import {
+  extend,
+  isBuiltInDirective,
+  isReservedProp,
+  isVoidTag,
+} from '@vue/shared'
 import type {
   DirectiveTransformResult,
   NodeTransform,
@@ -16,7 +21,8 @@ import type {
 } from '../transform'
 import {
   IRNodeTypes,
-  type PropsExpression,
+  type IRProp,
+  type IRProps,
   type VaporDirectiveNode,
 } from '../ir'
 
@@ -61,7 +67,7 @@ function buildProps(
   props: (VaporDirectiveNode | AttributeNode)[] = node.props as any,
   isComponent: boolean,
 ) {
-  const dynamicArgs: PropsExpression[] = []
+  const dynamicArgs: IRProps[] = []
   const dynamicExpr: SimpleExpressionNode[] = []
   let results: DirectiveTransformResult[] = []
 
@@ -75,19 +81,11 @@ function buildProps(
 
   function pushMergeArg() {
     if (results.length) {
-      dynamicArgs.push(results)
+      dynamicArgs.push(dedupeProperties(results))
       results = []
     }
   }
 
-  // treat all props as dynamic key
-  const asDynamic = props.some(
-    prop =>
-      prop.type === NodeTypes.DIRECTIVE &&
-      prop.name === 'bind' &&
-      (!prop.arg || !prop.arg.isStatic),
-  )
-
   for (const prop of props) {
     if (
       prop.type === NodeTypes.DIRECTIVE &&
@@ -106,20 +104,17 @@ function buildProps(
       continue
     }
 
-    const result = transformProp(prop, node, context, asDynamic)
+    const result = transformProp(prop, node, context)
     if (result) {
       results.push(result)
-      asDynamic && pushDynamicExpressions(result.key, result.value)
+      pushDynamicExpressions(result.key, result.value)
     }
   }
 
-  // take rest of props as dynamic props
+  // has dynamic key or v-bind="{}"
   if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) {
+    // take rest of props as dynamic props
     pushMergeArg()
-  }
-
-  // has dynamic key or v-bind="{}"
-  if (dynamicArgs.length) {
     context.registerEffect(dynamicExpr, [
       {
         type: IRNodeTypes.SET_DYNAMIC_PROPS,
@@ -128,17 +123,22 @@ function buildProps(
       },
     ])
   } else {
-    for (const result of results) {
-      context.registerEffect(
-        [result.value],
-        [
+    const irProps = dedupeProperties(results)
+    for (const prop of irProps) {
+      const { key, values } = prop
+      if (key.isStatic && values.length === 1 && values[0].isStatic) {
+        context.template += ` ${key.content}`
+        if (values[0].content) context.template += `="${values[0].content}"`
+      } else {
+        const expressions = values.filter(v => !v.isStatic)
+        context.registerEffect(expressions, [
           {
             type: IRNodeTypes.SET_PROP,
             element: context.reference(),
-            prop: result,
+            prop: prop,
           },
-        ],
-      )
+        ])
+      }
     }
   }
 }
@@ -147,32 +147,27 @@ function transformProp(
   prop: VaporDirectiveNode | AttributeNode,
   node: ElementNode,
   context: TransformContext<ElementNode>,
-  asDynamic: boolean,
 ): DirectiveTransformResult | void {
   const { name } = prop
   if (isReservedProp(name)) return
 
   if (prop.type === NodeTypes.ATTRIBUTE) {
-    if (asDynamic) {
-      return {
-        key: createSimpleExpression(prop.name, true, prop.nameLoc),
-        value: createSimpleExpression(
-          prop.value ? prop.value.content : '',
-          true,
-          prop.value && prop.value.loc,
-        ),
-      }
-    } else {
-      context.template += ` ${name}`
-      if (prop.value) context.template += `="${prop.value.content}"`
-      return
+    return {
+      key: createSimpleExpression(prop.name, true, prop.nameLoc),
+      value: createSimpleExpression(
+        prop.value ? prop.value.content : '',
+        true,
+        prop.value && prop.value.loc,
+      ),
     }
   }
 
   const directiveTransform = context.options.directiveTransforms[name]
   if (directiveTransform) {
     return directiveTransform(prop, node, context)
-  } else if (!isBuiltInDirective(name)) {
+  }
+
+  if (!isBuiltInDirective(name)) {
     context.registerOperation({
       type: IRNodeTypes.WITH_DIRECTIVE,
       element: context.reference(),
@@ -180,3 +175,42 @@ function transformProp(
     })
   }
 }
+
+// Dedupe props in an object literal.
+// Literal duplicated attributes would have been warned during the parse phase,
+// however, it's possible to encounter duplicated `onXXX` handlers with different
+// modifiers. We also need to merge static and dynamic class / style attributes.
+function dedupeProperties(results: DirectiveTransformResult[]): IRProp[] {
+  const knownProps: Map<string, IRProp> = new Map()
+  const deduped: IRProp[] = []
+
+  for (const result of results) {
+    const prop = normalizeIRProp(result)
+    // dynamic keys are always allowed
+    if (!prop.key.isStatic) {
+      deduped.push(prop)
+      continue
+    }
+    const name = prop.key.content
+    const existing = knownProps.get(name)
+    if (existing) {
+      if (name === 'style' || name === 'class') {
+        mergeAsArray(existing, prop)
+      }
+      // unexpected duplicate, should have emitted error during parse
+    } else {
+      knownProps.set(name, prop)
+      deduped.push(prop)
+    }
+  }
+  return deduped
+}
+
+function normalizeIRProp(prop: DirectiveTransformResult): IRProp {
+  return extend({}, prop, { value: undefined, values: [prop.value] })
+}
+
+function mergeAsArray(existing: IRProp, incoming: IRProp) {
+  const newValues = incoming.values
+  existing.values.push(...newValues)
+}