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

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Rizumu Ayaka 2024-02-06 02:35:52 +08:00 committed by GitHub
parent 75b0937d31
commit ba3ca6a304
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 419 additions and 151 deletions

View File

@ -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
}"
`;

View File

@ -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,
},
],
},
},
],
},
])
})
})

View File

@ -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: '^',
},

View File

@ -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,
},
],
},
},
])

View File

@ -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)),
)
}

View File

@ -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 {

View File

@ -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<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)
}