From 2b4f06b24cedc7762e94e8529ef4d49b10dad3cf Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 19 Sep 2019 15:41:17 -0400 Subject: [PATCH] test(compiler): transformIf --- .../__tests__/directives/vIf.spec.ts | 257 ++++++++++++++++++ .../compiler-core/__tests__/transform.spec.ts | 63 ++++- packages/compiler-core/src/directives/vIf.ts | 17 +- packages/compiler-core/src/parse.ts | 4 + packages/compiler-core/src/transform.ts | 56 ++-- 5 files changed, 371 insertions(+), 26 deletions(-) create mode 100644 packages/compiler-core/__tests__/directives/vIf.spec.ts diff --git a/packages/compiler-core/__tests__/directives/vIf.spec.ts b/packages/compiler-core/__tests__/directives/vIf.spec.ts new file mode 100644 index 00000000..50390101 --- /dev/null +++ b/packages/compiler-core/__tests__/directives/vIf.spec.ts @@ -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(`
`) + 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(``) + 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(`

`) + 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(`

`) + 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( + `

` + ) + 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(` +

+ +

+ + + `) + 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(`

`) + 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(`
`) + 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(`
foo
`) + 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(`
`) + 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(`
`) + 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(`
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 + }) +}) diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index 2875e095..87cb9326 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -25,7 +25,7 @@ describe('compiler: transform', () => { { parent: ast, ancestors: [ast], - childIndex: 0 + currentNode: div } ]) expect(calls[1]).toMatchObject([ @@ -33,7 +33,7 @@ describe('compiler: transform', () => { { parent: div, ancestors: [ast, div], - childIndex: 0 + currentNode: div.children[0] } ]) expect(calls[2]).toMatchObject([ @@ -41,7 +41,7 @@ describe('compiler: transform', () => { { parent: div, ancestors: [ast, div], - childIndex: 1 + currentNode: div.children[1] } ]) }) @@ -81,7 +81,7 @@ describe('compiler: transform', () => { }) test('context.removeNode', () => { - const ast = parse(`
`) + const ast = parse(`
hello
`) const c1 = ast.children[0] const c2 = ast.children[2] @@ -99,12 +99,67 @@ describe('compiler: transform', () => { expect(ast.children[0]).toBe(c1) expect(ast.children[1]).toBe(c2) + // should not traverse children of remove node expect(spy).toHaveBeenCalledTimes(3) // should traverse nodes around removed expect(spy.mock.calls[0][0]).toBe(c1) expect(spy.mock.calls[2][0]).toBe(c2) }) + test('context.removeNode (prev sibling)', () => { + const ast = parse(`
`) + 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(`
`) + 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', () => { const ast = parse(`
`) const loc = ast.children[0].loc.start diff --git a/packages/compiler-core/src/directives/vIf.ts b/packages/compiler-core/src/directives/vIf.ts index 61fae264..09898997 100644 --- a/packages/compiler-core/src/directives/vIf.ts +++ b/packages/compiler-core/src/directives/vIf.ts @@ -20,16 +20,23 @@ export const transformIf = createDirectiveTransform( } else { // locate the adjacent v-if const siblings = context.parent.children - let i = context.childIndex - while (i--) { + const comments = [] + let i = siblings.indexOf(node) + while (i-- >= -1) { const sibling = siblings[i] - if (sibling.type === NodeTypes.COMMENT) { + if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) { + context.removeNode(sibling) + comments.unshift(sibling) continue } - if (sibling.type === NodeTypes.IF) { + if (sibling && sibling.type === NodeTypes.IF) { // move the node to the if node's branches 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 { context.onError( createCompilerError( diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 2fd5f2a0..54959c40 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -186,6 +186,10 @@ function pushNode( nodes: ChildNode[], node: ChildNode ): void { + // ignore comments in production + if (!__DEV__ && node.type === NodeTypes.COMMENT) { + return + } if (context.ignoreSpaces && node.type === NodeTypes.TEXT && node.isEmpty) { return } diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index c7615e39..7db56d3b 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -26,9 +26,10 @@ export interface TransformContext extends Required { parent: ParentNode ancestors: ParentNode[] childIndex: number + currentNode: ChildNode | null replaceNode(node: ChildNode): void - removeNode(): void - nodeRemoved: boolean + removeNode(node?: ChildNode): void + onNodeRemoved: () => void } export function transform(root: RootNode, options: TransformOptions) { @@ -48,17 +49,37 @@ function createTransformContext( parent: root, ancestors: [], childIndex: 0, + currentNode: null, replaceNode(node) { - if (__DEV__ && context.nodeRemoved) { - throw new Error(`node being replaced is already removed`) + if (__DEV__ && !context.currentNode) { + throw new Error(`node being replaced is already removed.`) } - context.parent.children[context.childIndex] = node + context.parent.children[context.childIndex] = context.currentNode = node }, - removeNode() { - context.parent.children.splice(context.childIndex, 1) - context.nodeRemoved = true + removeNode(node) { + const list = context.parent.children + 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 } @@ -69,14 +90,16 @@ function traverseChildren( ancestors: ParentNode[] ) { 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.ancestors = ancestors context.childIndex = i - traverseNode(parent.children[i], context, ancestors) - if (context.nodeRemoved) { - i-- - } + context.onNodeRemoved = nodeRemoved + traverseNode((context.currentNode = parent.children[i]), context, ancestors) } } @@ -89,13 +112,12 @@ function traverseNode( const transforms = context.transforms for (let i = 0; i < transforms.length; i++) { const plugin = transforms[i] - context.nodeRemoved = false plugin(node, context) - if (context.nodeRemoved) { + if (!context.currentNode) { return } else { // node may have been replaced - node = context.parent.children[context.childIndex] + node = context.currentNode } }