From ba3ca6a304b23a416f87f1a177892ca063d03d7f Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Tue, 6 Feb 2024 02:35:52 +0800 Subject: [PATCH] feat(compiler-vapor): props merging (#118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 三咲智子 Kevin Deng --- .../transformElement.spec.ts.snap | 29 +++ .../transforms/transformElement.spec.ts | 147 ++++++++++++- .../__tests__/transforms/vBind.spec.ts | 194 ++++++++++-------- .../__tests__/transforms/vOnce.spec.ts | 36 ++-- .../compiler-vapor/src/generators/prop.ts | 35 +++- packages/compiler-vapor/src/ir.ts | 11 +- .../src/transforms/transformElement.ts | 118 +++++++---- 7 files changed, 419 insertions(+), 151 deletions(-) create mode 100644 packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap new file mode 100644 index 000000000..683911628 --- /dev/null +++ b/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("
") + 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("
") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _renderEffect(() => { + _setStyle(n1, ["color: green", { color: 'red' }]) + }) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index 42c304f46..9b712729e 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/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( + `
`, + ) + 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( + `
`, + ) + 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( + `
`, + ) + + 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, + }, + ], + }, + }, + ], + }, + ]) + }) +}) diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index c7303d012..60b99572c 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/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: '^', }, diff --git a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts index 74838c523..636a70309 100644 --- a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts +++ b/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, + }, + ], }, }, ]) diff --git a/packages/compiler-vapor/src/generators/prop.ts b/packages/compiler-vapor/src/generators/prop.ts index 1b0d8eefb..21ad55690 100644 --- a/packages/compiler-vapor/src/generators/prop.ts +++ b/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)), + ) +} diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index bfdfd552e..9e7e8f09b 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -88,18 +88,21 @@ export interface FragmentFactoryIRNode extends BaseIRNode { type: IRNodeTypes.FRAGMENT_FACTORY } +export interface IRProp extends Omit { + 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 { diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 9c289564b..98e589e39 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/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 - if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) { - pushMergeArg() - } - // has dynamic key or v-bind="{}" - if (dynamicArgs.length) { + if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) { + // take rest of props as dynamic props + pushMergeArg() 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, - 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 = 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) +}