test(compiler): transformIf

This commit is contained in:
Evan You 2019-09-19 15:41:17 -04:00
parent 81fd694dd7
commit 2b4f06b24c
5 changed files with 371 additions and 26 deletions

View File

@ -0,0 +1,257 @@
import { parse } from '../../src/parse'
import { transform } from '../../src/transform'
import { transformIf } from '../../src/directives/vIf'
import {
IfNode,
NodeTypes,
ElementNode,
TextNode,
CommentNode
} from '../../src/ast'
import { ErrorCodes } from '../../src/errors'
describe('compiler: v-if', () => {
describe('transform', () => {
test('basic v-if', () => {
const ast = parse(`<div v-if="ok"/>`)
transform(ast, {
transforms: [transformIf]
})
const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(1)
expect(node.branches[0].condition!.content).toBe(`ok`)
expect(node.branches[0].children.length).toBe(1)
expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)
expect((node.branches[0].children[0] as ElementNode).tag).toBe(`div`)
})
test('template v-if', () => {
const ast = parse(`<template v-if="ok"><div/>hello<p/></template>`)
transform(ast, {
transforms: [transformIf]
})
const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(1)
expect(node.branches[0].condition!.content).toBe(`ok`)
expect(node.branches[0].children.length).toBe(3)
expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)
expect((node.branches[0].children[0] as ElementNode).tag).toBe(`div`)
expect(node.branches[0].children[1].type).toBe(NodeTypes.TEXT)
expect((node.branches[0].children[1] as TextNode).content).toBe(`hello`)
expect(node.branches[0].children[2].type).toBe(NodeTypes.ELEMENT)
expect((node.branches[0].children[2] as ElementNode).tag).toBe(`p`)
})
test('v-if + v-else', () => {
const ast = parse(`<div v-if="ok"/><p v-else/>`)
transform(ast, {
transforms: [transformIf]
})
// should fold branches
expect(ast.children.length).toBe(1)
const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(2)
const b1 = node.branches[0]
expect(b1.condition!.content).toBe(`ok`)
expect(b1.children.length).toBe(1)
expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
expect((b1.children[0] as ElementNode).tag).toBe(`div`)
const b2 = node.branches[1]
expect(b2.condition).toBeUndefined()
expect(b2.children.length).toBe(1)
expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
expect((b2.children[0] as ElementNode).tag).toBe(`p`)
})
test('v-if + v-else-if', () => {
const ast = parse(`<div v-if="ok"/><p v-else-if="orNot"/>`)
transform(ast, {
transforms: [transformIf]
})
// should fold branches
expect(ast.children.length).toBe(1)
const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(2)
const b1 = node.branches[0]
expect(b1.condition!.content).toBe(`ok`)
expect(b1.children.length).toBe(1)
expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
expect((b1.children[0] as ElementNode).tag).toBe(`div`)
const b2 = node.branches[1]
expect(b2.condition!.content).toBe(`orNot`)
expect(b2.children.length).toBe(1)
expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
expect((b2.children[0] as ElementNode).tag).toBe(`p`)
})
test('v-if + v-else-if + v-else', () => {
const ast = parse(
`<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`
)
transform(ast, {
transforms: [transformIf]
})
// should fold branches
expect(ast.children.length).toBe(1)
const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(3)
const b1 = node.branches[0]
expect(b1.condition!.content).toBe(`ok`)
expect(b1.children.length).toBe(1)
expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
expect((b1.children[0] as ElementNode).tag).toBe(`div`)
const b2 = node.branches[1]
expect(b2.condition!.content).toBe(`orNot`)
expect(b2.children.length).toBe(1)
expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
expect((b2.children[0] as ElementNode).tag).toBe(`p`)
const b3 = node.branches[2]
expect(b3.condition).toBeUndefined()
expect(b3.children.length).toBe(1)
expect(b3.children[0].type).toBe(NodeTypes.TEXT)
expect((b3.children[0] as TextNode).content).toBe(`fine`)
})
test('comment between branches', () => {
const ast = parse(`
<div v-if="ok"/>
<!--foo-->
<p v-else-if="orNot"/>
<!--bar-->
<template v-else>fine</template>
`)
transform(ast, {
transforms: [transformIf]
})
// should fold branches
expect(ast.children.length).toBe(1)
const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF)
expect(node.branches.length).toBe(3)
const b1 = node.branches[0]
expect(b1.condition!.content).toBe(`ok`)
expect(b1.children.length).toBe(1)
expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
expect((b1.children[0] as ElementNode).tag).toBe(`div`)
const b2 = node.branches[1]
expect(b2.condition!.content).toBe(`orNot`)
expect(b2.children.length).toBe(2)
expect(b2.children[0].type).toBe(NodeTypes.COMMENT)
expect((b2.children[0] as CommentNode).content).toBe(`foo`)
expect(b2.children[1].type).toBe(NodeTypes.ELEMENT)
expect((b2.children[1] as ElementNode).tag).toBe(`p`)
const b3 = node.branches[2]
expect(b3.condition).toBeUndefined()
expect(b3.children.length).toBe(2)
expect(b3.children[0].type).toBe(NodeTypes.COMMENT)
expect((b3.children[0] as CommentNode).content).toBe(`bar`)
expect(b3.children[1].type).toBe(NodeTypes.TEXT)
expect((b3.children[1] as TextNode).content).toBe(`fine`)
})
test('error on v-else missing adjacent v-if', () => {
const ast = parse(`<div v-else/>`)
const spy = jest.fn()
transform(ast, {
transforms: [transformIf],
onError: spy
})
expect(spy.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
loc: ast.children[0].loc.start
}
])
const ast2 = parse(`<div/><div v-else/>`)
const spy2 = jest.fn()
transform(ast2, {
transforms: [transformIf],
onError: spy2
})
expect(spy2.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
loc: ast2.children[1].loc.start
}
])
const ast3 = parse(`<div/>foo<div v-else/>`)
const spy3 = jest.fn()
transform(ast3, {
transforms: [transformIf],
onError: spy3
})
expect(spy3.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
loc: ast3.children[2].loc.start
}
])
})
test('error on v-else-if missing adjacent v-if', () => {
const ast = parse(`<div v-else-if="foo"/>`)
const spy = jest.fn()
transform(ast, {
transforms: [transformIf],
onError: spy
})
expect(spy.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
loc: ast.children[0].loc.start
}
])
const ast2 = parse(`<div/><div v-else-if="foo"/>`)
const spy2 = jest.fn()
transform(ast2, {
transforms: [transformIf],
onError: spy2
})
expect(spy2.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
loc: ast2.children[1].loc.start
}
])
const ast3 = parse(`<div/>foo<div v-else-if="foo"/>`)
const spy3 = jest.fn()
transform(ast3, {
transforms: [transformIf],
onError: spy3
})
expect(spy3.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
loc: ast3.children[2].loc.start
}
])
})
})
describe('codegen', () => {
// TODO
})
})

View File

@ -25,7 +25,7 @@ describe('compiler: transform', () => {
{ {
parent: ast, parent: ast,
ancestors: [ast], ancestors: [ast],
childIndex: 0 currentNode: div
} }
]) ])
expect(calls[1]).toMatchObject([ expect(calls[1]).toMatchObject([
@ -33,7 +33,7 @@ describe('compiler: transform', () => {
{ {
parent: div, parent: div,
ancestors: [ast, div], ancestors: [ast, div],
childIndex: 0 currentNode: div.children[0]
} }
]) ])
expect(calls[2]).toMatchObject([ expect(calls[2]).toMatchObject([
@ -41,7 +41,7 @@ describe('compiler: transform', () => {
{ {
parent: div, parent: div,
ancestors: [ast, div], ancestors: [ast, div],
childIndex: 1 currentNode: div.children[1]
} }
]) ])
}) })
@ -81,7 +81,7 @@ describe('compiler: transform', () => {
}) })
test('context.removeNode', () => { test('context.removeNode', () => {
const ast = parse(`<span/><div/><span/>`) const ast = parse(`<span/><div>hello</div><span/>`)
const c1 = ast.children[0] const c1 = ast.children[0]
const c2 = ast.children[2] const c2 = ast.children[2]
@ -99,12 +99,67 @@ describe('compiler: transform', () => {
expect(ast.children[0]).toBe(c1) expect(ast.children[0]).toBe(c1)
expect(ast.children[1]).toBe(c2) expect(ast.children[1]).toBe(c2)
// should not traverse children of remove node
expect(spy).toHaveBeenCalledTimes(3) expect(spy).toHaveBeenCalledTimes(3)
// should traverse nodes around removed // should traverse nodes around removed
expect(spy.mock.calls[0][0]).toBe(c1) expect(spy.mock.calls[0][0]).toBe(c1)
expect(spy.mock.calls[2][0]).toBe(c2) expect(spy.mock.calls[2][0]).toBe(c2)
}) })
test('context.removeNode (prev sibling)', () => {
const ast = parse(`<span/><div/><span/>`)
const c1 = ast.children[0]
const c2 = ast.children[2]
const plugin: Transform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
context.removeNode()
// remove previous sibling
context.removeNode(context.parent.children[0])
}
}
const spy = jest.fn(plugin)
transform(ast, {
transforms: [spy]
})
expect(ast.children.length).toBe(1)
expect(ast.children[0]).toBe(c2)
expect(spy).toHaveBeenCalledTimes(3)
// should still traverse first span before removal
expect(spy.mock.calls[0][0]).toBe(c1)
// should still traverse last span
expect(spy.mock.calls[2][0]).toBe(c2)
})
test('context.removeNode (next sibling)', () => {
const ast = parse(`<span/><div/><span/>`)
const c1 = ast.children[0]
const d1 = ast.children[1]
const plugin: Transform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
context.removeNode()
// remove next sibling
context.removeNode(context.parent.children[1])
}
}
const spy = jest.fn(plugin)
transform(ast, {
transforms: [spy]
})
expect(ast.children.length).toBe(1)
expect(ast.children[0]).toBe(c1)
expect(spy).toHaveBeenCalledTimes(2)
// should still traverse first span before removal
expect(spy.mock.calls[0][0]).toBe(c1)
// should not traverse last span
expect(spy.mock.calls[1][0]).toBe(d1)
})
test('onError option', () => { test('onError option', () => {
const ast = parse(`<div/>`) const ast = parse(`<div/>`)
const loc = ast.children[0].loc.start const loc = ast.children[0].loc.start

View File

@ -20,16 +20,23 @@ export const transformIf = createDirectiveTransform(
} else { } else {
// locate the adjacent v-if // locate the adjacent v-if
const siblings = context.parent.children const siblings = context.parent.children
let i = context.childIndex const comments = []
while (i--) { let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i] const sibling = siblings[i]
if (sibling.type === NodeTypes.COMMENT) { if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
context.removeNode(sibling)
comments.unshift(sibling)
continue continue
} }
if (sibling.type === NodeTypes.IF) { if (sibling && sibling.type === NodeTypes.IF) {
// move the node to the if node's branches // move the node to the if node's branches
context.removeNode() context.removeNode()
sibling.branches.push(createIfBranch(node, dir)) const branch = createIfBranch(node, dir)
if (__DEV__ && comments.length) {
branch.children = [...comments, ...branch.children]
}
sibling.branches.push(branch)
} else { } else {
context.onError( context.onError(
createCompilerError( createCompilerError(

View File

@ -186,6 +186,10 @@ function pushNode(
nodes: ChildNode[], nodes: ChildNode[],
node: ChildNode node: ChildNode
): void { ): void {
// ignore comments in production
if (!__DEV__ && node.type === NodeTypes.COMMENT) {
return
}
if (context.ignoreSpaces && node.type === NodeTypes.TEXT && node.isEmpty) { if (context.ignoreSpaces && node.type === NodeTypes.TEXT && node.isEmpty) {
return return
} }

View File

@ -26,9 +26,10 @@ export interface TransformContext extends Required<TransformOptions> {
parent: ParentNode parent: ParentNode
ancestors: ParentNode[] ancestors: ParentNode[]
childIndex: number childIndex: number
currentNode: ChildNode | null
replaceNode(node: ChildNode): void replaceNode(node: ChildNode): void
removeNode(): void removeNode(node?: ChildNode): void
nodeRemoved: boolean onNodeRemoved: () => void
} }
export function transform(root: RootNode, options: TransformOptions) { export function transform(root: RootNode, options: TransformOptions) {
@ -48,17 +49,37 @@ function createTransformContext(
parent: root, parent: root,
ancestors: [], ancestors: [],
childIndex: 0, childIndex: 0,
currentNode: null,
replaceNode(node) { replaceNode(node) {
if (__DEV__ && context.nodeRemoved) { if (__DEV__ && !context.currentNode) {
throw new Error(`node being replaced is already removed`) throw new Error(`node being replaced is already removed.`)
} }
context.parent.children[context.childIndex] = node context.parent.children[context.childIndex] = context.currentNode = node
}, },
removeNode() { removeNode(node) {
context.parent.children.splice(context.childIndex, 1) const list = context.parent.children
context.nodeRemoved = true const removalIndex = node
? list.indexOf(node)
: context.currentNode
? context.childIndex
: -1
if (__DEV__ && removalIndex < 0) {
throw new Error(`node being removed is not a child of current parent`)
}
if (!node || node === context.currentNode) {
// current node removed
context.currentNode = null
context.onNodeRemoved()
} else {
// sibling node removed
if (context.childIndex > removalIndex) {
context.childIndex--
context.onNodeRemoved()
}
}
context.parent.children.splice(removalIndex, 1)
}, },
nodeRemoved: false onNodeRemoved: () => {}
} }
return context return context
} }
@ -69,14 +90,16 @@ function traverseChildren(
ancestors: ParentNode[] ancestors: ParentNode[]
) { ) {
ancestors = ancestors.concat(parent) ancestors = ancestors.concat(parent)
for (let i = 0; i < parent.children.length; i++) { let i = 0
const nodeRemoved = () => {
i--
}
for (; i < parent.children.length; i++) {
context.parent = parent context.parent = parent
context.ancestors = ancestors context.ancestors = ancestors
context.childIndex = i context.childIndex = i
traverseNode(parent.children[i], context, ancestors) context.onNodeRemoved = nodeRemoved
if (context.nodeRemoved) { traverseNode((context.currentNode = parent.children[i]), context, ancestors)
i--
}
} }
} }
@ -89,13 +112,12 @@ function traverseNode(
const transforms = context.transforms const transforms = context.transforms
for (let i = 0; i < transforms.length; i++) { for (let i = 0; i < transforms.length; i++) {
const plugin = transforms[i] const plugin = transforms[i]
context.nodeRemoved = false
plugin(node, context) plugin(node, context)
if (context.nodeRemoved) { if (!context.currentNode) {
return return
} else { } else {
// node may have been replaced // node may have been replaced
node = context.parent.children[context.childIndex] node = context.currentNode
} }
} }