feat(compiler): element transform

This commit is contained in:
Evan You 2019-09-21 17:42:12 -04:00
parent 93440bba97
commit baa8954884
13 changed files with 349 additions and 160 deletions

View File

@ -1,6 +1,6 @@
import { parse } from '../../src/parse' import { parse } from '../../src/parse'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformFor } from '../../src/directives/vFor' import { transformFor } from '../../src/transforms/vFor'
import { ForNode, NodeTypes } from '../../src/ast' import { ForNode, NodeTypes } from '../../src/ast'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
@ -9,7 +9,7 @@ describe('v-for', () => {
test('number expression', () => { test('number expression', () => {
const node = parse('<span v-for="index in 5" />') const node = parse('<span v-for="index in 5" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -25,7 +25,7 @@ describe('v-for', () => {
test('value', () => { test('value', () => {
const node = parse('<span v-for="(item) in items" />') const node = parse('<span v-for="(item) in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -41,7 +41,7 @@ describe('v-for', () => {
test('object de-structured value', () => { test('object de-structured value', () => {
const node = parse('<span v-for="({ id, value }) in items" />') const node = parse('<span v-for="({ id, value }) in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -57,7 +57,7 @@ describe('v-for', () => {
test('array de-structured value', () => { test('array de-structured value', () => {
const node = parse('<span v-for="([ id, value ]) in items" />') const node = parse('<span v-for="([ id, value ]) in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -73,7 +73,7 @@ describe('v-for', () => {
test('value and key', () => { test('value and key', () => {
const node = parse('<span v-for="(item, key) in items" />') const node = parse('<span v-for="(item, key) in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -89,7 +89,7 @@ describe('v-for', () => {
test('value, key and index', () => { test('value, key and index', () => {
const node = parse('<span v-for="(value, key, index) in items" />') const node = parse('<span v-for="(value, key, index) in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -107,7 +107,7 @@ describe('v-for', () => {
test('skipped key', () => { test('skipped key', () => {
const node = parse('<span v-for="(value,,index) in items" />') const node = parse('<span v-for="(value,,index) in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -124,7 +124,7 @@ describe('v-for', () => {
test('skipped value and key', () => { test('skipped value and key', () => {
const node = parse('<span v-for="(,,index) in items" />') const node = parse('<span v-for="(,,index) in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -141,7 +141,7 @@ describe('v-for', () => {
test('unbracketed value', () => { test('unbracketed value', () => {
const node = parse('<span v-for="item in items" />') const node = parse('<span v-for="item in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -157,7 +157,7 @@ describe('v-for', () => {
test('unbracketed value and key', () => { test('unbracketed value and key', () => {
const node = parse('<span v-for="item, key in items" />') const node = parse('<span v-for="item, key in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -174,7 +174,7 @@ describe('v-for', () => {
test('unbracketed value, key and index', () => { test('unbracketed value, key and index', () => {
const node = parse('<span v-for="value, key, index in items" />') const node = parse('<span v-for="value, key, index in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -192,7 +192,7 @@ describe('v-for', () => {
test('unbracketed skipped key', () => { test('unbracketed skipped key', () => {
const node = parse('<span v-for="value, , index in items" />') const node = parse('<span v-for="value, , index in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -209,7 +209,7 @@ describe('v-for', () => {
test('unbracketed skipped value and key', () => { test('unbracketed skipped value and key', () => {
const node = parse('<span v-for=", , index in items" />') const node = parse('<span v-for=", , index in items" />')
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -226,7 +226,7 @@ describe('v-for', () => {
test('missing expression', () => { test('missing expression', () => {
const node = parse('<span v-for />') const node = parse('<span v-for />')
const onError = jest.fn() const onError = jest.fn()
transform(node, { transforms: [transformFor], onError }) transform(node, { nodeTransforms: [transformFor], onError })
expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith( expect(onError).toHaveBeenCalledWith(
@ -239,7 +239,7 @@ describe('v-for', () => {
test('empty expression', () => { test('empty expression', () => {
const node = parse('<span v-for="" />') const node = parse('<span v-for="" />')
const onError = jest.fn() const onError = jest.fn()
transform(node, { transforms: [transformFor], onError }) transform(node, { nodeTransforms: [transformFor], onError })
expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith( expect(onError).toHaveBeenCalledWith(
@ -252,7 +252,7 @@ describe('v-for', () => {
test('invalid expression', () => { test('invalid expression', () => {
const node = parse('<span v-for="items" />') const node = parse('<span v-for="items" />')
const onError = jest.fn() const onError = jest.fn()
transform(node, { transforms: [transformFor], onError }) transform(node, { nodeTransforms: [transformFor], onError })
expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith( expect(onError).toHaveBeenCalledWith(
@ -265,7 +265,7 @@ describe('v-for', () => {
test('missing source', () => { test('missing source', () => {
const node = parse('<span v-for="item in" />') const node = parse('<span v-for="item in" />')
const onError = jest.fn() const onError = jest.fn()
transform(node, { transforms: [transformFor], onError }) transform(node, { nodeTransforms: [transformFor], onError })
expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith( expect(onError).toHaveBeenCalledWith(
@ -278,7 +278,7 @@ describe('v-for', () => {
test('missing value', () => { test('missing value', () => {
const node = parse('<span v-for="in items" />') const node = parse('<span v-for="in items" />')
const onError = jest.fn() const onError = jest.fn()
transform(node, { transforms: [transformFor], onError }) transform(node, { nodeTransforms: [transformFor], onError })
expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith( expect(onError).toHaveBeenCalledWith(
@ -293,7 +293,7 @@ describe('v-for', () => {
const source = '<span v-for="item in items" />' const source = '<span v-for="item in items" />'
const node = parse(source) const node = parse(source)
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -328,7 +328,7 @@ describe('v-for', () => {
const source = '<span v-for="( item ) in items" />' const source = '<span v-for="( item ) in items" />'
const node = parse(source) const node = parse(source)
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -363,7 +363,7 @@ describe('v-for', () => {
const source = '<span v-for="( { id, key })in items" />' const source = '<span v-for="( { id, key })in items" />'
const node = parse(source) const node = parse(source)
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -398,7 +398,7 @@ describe('v-for', () => {
const source = '<span v-for="( item, key, index ) in items" />' const source = '<span v-for="( item, key, index ) in items" />'
const node = parse(source) const node = parse(source)
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)
@ -455,7 +455,7 @@ describe('v-for', () => {
const source = '<span v-for="( item,, index ) in items" />' const source = '<span v-for="( item,, index ) in items" />'
const node = parse(source) const node = parse(source)
transform(node, { transforms: [transformFor] }) transform(node, { nodeTransforms: [transformFor] })
expect(node.children.length).toBe(1) expect(node.children.length).toBe(1)

View File

@ -1,6 +1,6 @@
import { parse } from '../../src/parse' import { parse } from '../../src/parse'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformIf } from '../../src/directives/vIf' import { transformIf } from '../../src/transforms/vIf'
import { import {
IfNode, IfNode,
NodeTypes, NodeTypes,
@ -15,7 +15,7 @@ describe('compiler: v-if', () => {
test('basic v-if', () => { test('basic v-if', () => {
const ast = parse(`<div v-if="ok"/>`) const ast = parse(`<div v-if="ok"/>`)
transform(ast, { transform(ast, {
transforms: [transformIf] nodeTransforms: [transformIf]
}) })
const node = ast.children[0] as IfNode const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF) expect(node.type).toBe(NodeTypes.IF)
@ -29,7 +29,7 @@ describe('compiler: v-if', () => {
test('template v-if', () => { test('template v-if', () => {
const ast = parse(`<template v-if="ok"><div/>hello<p/></template>`) const ast = parse(`<template v-if="ok"><div/>hello<p/></template>`)
transform(ast, { transform(ast, {
transforms: [transformIf] nodeTransforms: [transformIf]
}) })
const node = ast.children[0] as IfNode const node = ast.children[0] as IfNode
expect(node.type).toBe(NodeTypes.IF) expect(node.type).toBe(NodeTypes.IF)
@ -47,7 +47,7 @@ describe('compiler: v-if', () => {
test('v-if + v-else', () => { test('v-if + v-else', () => {
const ast = parse(`<div v-if="ok"/><p v-else/>`) const ast = parse(`<div v-if="ok"/><p v-else/>`)
transform(ast, { transform(ast, {
transforms: [transformIf] nodeTransforms: [transformIf]
}) })
// should fold branches // should fold branches
expect(ast.children.length).toBe(1) expect(ast.children.length).toBe(1)
@ -72,7 +72,7 @@ describe('compiler: v-if', () => {
test('v-if + v-else-if', () => { test('v-if + v-else-if', () => {
const ast = parse(`<div v-if="ok"/><p v-else-if="orNot"/>`) const ast = parse(`<div v-if="ok"/><p v-else-if="orNot"/>`)
transform(ast, { transform(ast, {
transforms: [transformIf] nodeTransforms: [transformIf]
}) })
// should fold branches // should fold branches
expect(ast.children.length).toBe(1) expect(ast.children.length).toBe(1)
@ -99,7 +99,7 @@ describe('compiler: v-if', () => {
`<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>` `<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`
) )
transform(ast, { transform(ast, {
transforms: [transformIf] nodeTransforms: [transformIf]
}) })
// should fold branches // should fold branches
expect(ast.children.length).toBe(1) expect(ast.children.length).toBe(1)
@ -136,7 +136,7 @@ describe('compiler: v-if', () => {
<template v-else>fine</template> <template v-else>fine</template>
`) `)
transform(ast, { transform(ast, {
transforms: [transformIf] nodeTransforms: [transformIf]
}) })
// should fold branches // should fold branches
expect(ast.children.length).toBe(1) expect(ast.children.length).toBe(1)
@ -172,7 +172,7 @@ describe('compiler: v-if', () => {
const ast = parse(`<div v-else/>`) const ast = parse(`<div v-else/>`)
const spy = jest.fn() const spy = jest.fn()
transform(ast, { transform(ast, {
transforms: [transformIf], nodeTransforms: [transformIf],
onError: spy onError: spy
}) })
expect(spy.mock.calls[0]).toMatchObject([ expect(spy.mock.calls[0]).toMatchObject([
@ -185,7 +185,7 @@ describe('compiler: v-if', () => {
const ast2 = parse(`<div/><div v-else/>`) const ast2 = parse(`<div/><div v-else/>`)
const spy2 = jest.fn() const spy2 = jest.fn()
transform(ast2, { transform(ast2, {
transforms: [transformIf], nodeTransforms: [transformIf],
onError: spy2 onError: spy2
}) })
expect(spy2.mock.calls[0]).toMatchObject([ expect(spy2.mock.calls[0]).toMatchObject([
@ -198,7 +198,7 @@ describe('compiler: v-if', () => {
const ast3 = parse(`<div/>foo<div v-else/>`) const ast3 = parse(`<div/>foo<div v-else/>`)
const spy3 = jest.fn() const spy3 = jest.fn()
transform(ast3, { transform(ast3, {
transforms: [transformIf], nodeTransforms: [transformIf],
onError: spy3 onError: spy3
}) })
expect(spy3.mock.calls[0]).toMatchObject([ expect(spy3.mock.calls[0]).toMatchObject([
@ -213,7 +213,7 @@ describe('compiler: v-if', () => {
const ast = parse(`<div v-else-if="foo"/>`) const ast = parse(`<div v-else-if="foo"/>`)
const spy = jest.fn() const spy = jest.fn()
transform(ast, { transform(ast, {
transforms: [transformIf], nodeTransforms: [transformIf],
onError: spy onError: spy
}) })
expect(spy.mock.calls[0]).toMatchObject([ expect(spy.mock.calls[0]).toMatchObject([
@ -226,7 +226,7 @@ describe('compiler: v-if', () => {
const ast2 = parse(`<div/><div v-else-if="foo"/>`) const ast2 = parse(`<div/><div v-else-if="foo"/>`)
const spy2 = jest.fn() const spy2 = jest.fn()
transform(ast2, { transform(ast2, {
transforms: [transformIf], nodeTransforms: [transformIf],
onError: spy2 onError: spy2
}) })
expect(spy2.mock.calls[0]).toMatchObject([ expect(spy2.mock.calls[0]).toMatchObject([
@ -239,7 +239,7 @@ describe('compiler: v-if', () => {
const ast3 = parse(`<div/>foo<div v-else-if="foo"/>`) const ast3 = parse(`<div/>foo<div v-else-if="foo"/>`)
const spy3 = jest.fn() const spy3 = jest.fn()
transform(ast3, { transform(ast3, {
transforms: [transformIf], nodeTransforms: [transformIf],
onError: spy3 onError: spy3
}) })
expect(spy3.mock.calls[0]).toMatchObject([ expect(spy3.mock.calls[0]).toMatchObject([

View File

@ -1,5 +1,5 @@
import { parse } from '../src/parse' import { parse } from '../src/parse'
import { transform, Transform } from '../src/transform' import { transform, NodeTransform } from '../src/transform'
import { ElementNode, NodeTypes } from '../src/ast' import { ElementNode, NodeTypes } from '../src/ast'
import { ErrorCodes, createCompilerError } from '../src/errors' import { ErrorCodes, createCompilerError } from '../src/errors'
@ -10,12 +10,12 @@ describe('compiler: transform', () => {
// manually store call arguments because context is mutable and shared // manually store call arguments because context is mutable and shared
// across calls // across calls
const calls: any[] = [] const calls: any[] = []
const plugin: Transform = (node, context) => { const plugin: NodeTransform = (node, context) => {
calls.push([node, Object.assign({}, context)]) calls.push([node, Object.assign({}, context)])
} }
transform(ast, { transform(ast, {
transforms: [plugin] nodeTransforms: [plugin]
}) })
const div = ast.children[0] as ElementNode const div = ast.children[0] as ElementNode
@ -48,7 +48,7 @@ describe('compiler: transform', () => {
test('context.replaceNode', () => { test('context.replaceNode', () => {
const ast = parse(`<div/><span/>`) const ast = parse(`<div/><span/>`)
const plugin: Transform = (node, context) => { const plugin: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
// change the node to <p> // change the node to <p>
context.replaceNode( context.replaceNode(
@ -67,7 +67,7 @@ describe('compiler: transform', () => {
} }
const spy = jest.fn(plugin) const spy = jest.fn(plugin)
transform(ast, { transform(ast, {
transforms: [spy] nodeTransforms: [spy]
}) })
expect(ast.children.length).toBe(2) expect(ast.children.length).toBe(2)
@ -85,14 +85,14 @@ describe('compiler: transform', () => {
const c1 = ast.children[0] const c1 = ast.children[0]
const c2 = ast.children[2] const c2 = ast.children[2]
const plugin: Transform = (node, context) => { const plugin: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
context.removeNode() context.removeNode()
} }
} }
const spy = jest.fn(plugin) const spy = jest.fn(plugin)
transform(ast, { transform(ast, {
transforms: [spy] nodeTransforms: [spy]
}) })
expect(ast.children.length).toBe(2) expect(ast.children.length).toBe(2)
@ -111,7 +111,7 @@ describe('compiler: transform', () => {
const c1 = ast.children[0] const c1 = ast.children[0]
const c2 = ast.children[2] const c2 = ast.children[2]
const plugin: Transform = (node, context) => { const plugin: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
context.removeNode() context.removeNode()
// remove previous sibling // remove previous sibling
@ -120,7 +120,7 @@ describe('compiler: transform', () => {
} }
const spy = jest.fn(plugin) const spy = jest.fn(plugin)
transform(ast, { transform(ast, {
transforms: [spy] nodeTransforms: [spy]
}) })
expect(ast.children.length).toBe(1) expect(ast.children.length).toBe(1)
@ -138,7 +138,7 @@ describe('compiler: transform', () => {
const c1 = ast.children[0] const c1 = ast.children[0]
const d1 = ast.children[1] const d1 = ast.children[1]
const plugin: Transform = (node, context) => { const plugin: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
context.removeNode() context.removeNode()
// remove next sibling // remove next sibling
@ -147,7 +147,7 @@ describe('compiler: transform', () => {
} }
const spy = jest.fn(plugin) const spy = jest.fn(plugin)
transform(ast, { transform(ast, {
transforms: [spy] nodeTransforms: [spy]
}) })
expect(ast.children.length).toBe(1) expect(ast.children.length).toBe(1)
@ -163,14 +163,14 @@ describe('compiler: transform', () => {
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
const plugin: Transform = (node, context) => { const plugin: NodeTransform = (node, context) => {
context.emitError( context.onError(
createCompilerError(ErrorCodes.X_INVALID_END_TAG, node.loc.start) createCompilerError(ErrorCodes.X_INVALID_END_TAG, node.loc.start)
) )
} }
const spy = jest.fn() const spy = jest.fn()
transform(ast, { transform(ast, {
transforms: [plugin], nodeTransforms: [plugin],
onError: spy onError: spy
}) })
expect(spy.mock.calls[0]).toMatchObject([ expect(spy.mock.calls[0]).toMatchObject([

View File

@ -73,8 +73,7 @@ export interface ElementNode extends Node {
tag: string tag: string
tagType: ElementTypes tagType: ElementTypes
isSelfClosing: boolean isSelfClosing: boolean
attrs: AttributeNode[] props: Array<AttributeNode | DirectiveNode>
directives: DirectiveNode[]
children: ChildNode[] children: ChildNode[]
codegenNode: CallExpression | undefined codegenNode: CallExpression | undefined
} }
@ -161,3 +160,64 @@ export interface ArrayExpression extends Node {
type: NodeTypes.ARRAY_EXPRESSION type: NodeTypes.ARRAY_EXPRESSION
elements: Array<CodegenNode> elements: Array<CodegenNode>
} }
export function createArrayExpression(
elements: ArrayExpression['elements'],
loc: SourceLocation
): ArrayExpression {
return {
type: NodeTypes.ARRAY_EXPRESSION,
loc,
elements
}
}
export function createObjectExpression(
properties: Property[],
loc: SourceLocation
): ObjectExpression {
return {
type: NodeTypes.OBJECT_EXPRESSION,
loc,
properties
}
}
export function createObjectProperty(
key: ExpressionNode,
value: ExpressionNode,
loc: SourceLocation
): Property {
return {
type: NodeTypes.PROPERTY,
loc,
key,
value
}
}
export function createExpression(
content: string,
isStatic: boolean,
loc: SourceLocation
): ExpressionNode {
return {
type: NodeTypes.EXPRESSION,
loc,
content,
isStatic
}
}
export function createCallExpression(
callee: string,
args: CallExpression['arguments'],
loc: SourceLocation
): CallExpression {
return {
type: NodeTypes.CALL_EXPRESSION,
loc,
callee,
arguments: args
}
}

View File

@ -65,7 +65,8 @@ export const enum ErrorCodes {
X_ELSE_IF_NO_ADJACENT_IF, X_ELSE_IF_NO_ADJACENT_IF,
X_ELSE_NO_ADJACENT_IF, X_ELSE_NO_ADJACENT_IF,
X_FOR_NO_EXPRESSION, X_FOR_NO_EXPRESSION,
X_FOR_MALFORMED_EXPRESSION X_FOR_MALFORMED_EXPRESSION,
X_V_BIND_NO_EXPRESSION
} }
export const errorMessages: { [code: number]: string } = { export const errorMessages: { [code: number]: string } = {

View File

@ -14,10 +14,14 @@ export function compile(
transform(ast, { transform(ast, {
...options, ...options,
transforms: [ nodeTransforms: [
// TODO include built-in core transforms // TODO include built-in core transforms
...(options.transforms || []) // user transforms ...(options.nodeTransforms || []) // user transforms
] ],
directiveTransforms: {
// TODO include built-in directive transforms
...(options.directiveTransforms || {}) // user transforms
}
}) })
return generate(ast, options) return generate(ast, options)
@ -27,11 +31,11 @@ export function compile(
export { parse, ParserOptions, TextModes } from './parse' export { parse, ParserOptions, TextModes } from './parse'
export { export {
transform, transform,
createDirectiveTransform, createStructuralDirectiveTransform,
TransformOptions, TransformOptions,
TransformContext, TransformContext,
Transform, NodeTransform as Transform,
DirectiveTransform StructuralDirectiveTransform
} from './transform' } from './transform'
export { export {
generate, generate,
@ -41,3 +45,6 @@ export {
} from './codegen' } from './codegen'
export { ErrorCodes, CompilerError, createCompilerError } from './errors' export { ErrorCodes, CompilerError, createCompilerError } from './errors'
export * from './ast' export * from './ast'
// debug
export { prepareElementForCodegen } from './transforms/element'

View File

@ -376,8 +376,7 @@ function parseTag(
const start = getCursor(context) const start = getCursor(context)
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)! const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1] const tag = match[1]
const attrs = [] const props = []
const directives = []
const ns = context.options.getNamespace(tag, parent) const ns = context.options.getNamespace(tag, parent)
advanceBy(context, match[0].length) advanceBy(context, match[0].length)
@ -402,11 +401,7 @@ function parseTag(
const attr = parseAttribute(context, attributeNames) const attr = parseAttribute(context, attributeNames)
if (type === TagType.Start) { if (type === TagType.Start) {
if (attr.type === NodeTypes.DIRECTIVE) { props.push(attr)
directives.push(attr)
} else {
attrs.push(attr)
}
} }
if (/^[^\t\r\n\f />]/.test(context.source)) { if (/^[^\t\r\n\f />]/.test(context.source)) {
@ -438,11 +433,11 @@ function parseTag(
ns, ns,
tag, tag,
tagType, tagType,
attrs, props,
directives,
isSelfClosing, isSelfClosing,
children: [], children: [],
loc: getSelection(context, start) loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
} }
} }

View File

@ -4,27 +4,45 @@ import {
ParentNode, ParentNode,
ChildNode, ChildNode,
ElementNode, ElementNode,
DirectiveNode DirectiveNode,
Property
} from './ast' } from './ast'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors' import { CompilerError, defaultOnError } from './errors'
export type Transform = (node: ChildNode, context: TransformContext) => void // There are two types of transforms:
//
// - NodeTransform:
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
// replace or remove the node being processed.
export type NodeTransform = (node: ChildNode, context: TransformContext) => void
// - DirectiveTransform:
// Transforms that handles a single directive attribute on an element.
// It translates the raw directive into actual props for the VNode.
export type DirectiveTransform = ( export type DirectiveTransform = (
dir: DirectiveNode,
context: TransformContext
) => {
props: Property | Property[]
needRuntime: boolean
}
// A structural directive transform is a techically a NodeTransform;
// Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = (
node: ElementNode, node: ElementNode,
dir: DirectiveNode, dir: DirectiveNode,
context: TransformContext context: TransformContext
) => false | void ) => void
export interface TransformOptions { export interface TransformOptions {
transforms?: Transform[] nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform }
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
export interface TransformContext { export interface TransformContext extends Required<TransformOptions> {
transforms: Transform[]
emitError: (error: CompilerError) => void
parent: ParentNode parent: ParentNode
ancestors: ParentNode[] ancestors: ParentNode[]
childIndex: number childIndex: number
@ -44,8 +62,9 @@ function createTransformContext(
options: TransformOptions options: TransformOptions
): TransformContext { ): TransformContext {
const context: TransformContext = { const context: TransformContext = {
transforms: options.transforms || [], nodeTransforms: options.nodeTransforms || [],
emitError: options.onError || defaultOnError, directiveTransforms: options.directiveTransforms || {},
onError: options.onError || defaultOnError,
parent: root, parent: root,
ancestors: [], ancestors: [],
childIndex: 0, childIndex: 0,
@ -109,9 +128,9 @@ function traverseNode(
ancestors: ParentNode[] ancestors: ParentNode[]
) { ) {
// apply transform plugins // apply transform plugins
const { transforms } = context const { nodeTransforms } = context
for (let i = 0; i < transforms.length; i++) { for (let i = 0; i < nodeTransforms.length; i++) {
const plugin = transforms[i] const plugin = nodeTransforms[i]
plugin(node, context) plugin(node, context)
if (!context.currentNode) { if (!context.currentNode) {
return return
@ -135,33 +154,26 @@ function traverseNode(
} }
} }
const identity = <T>(_: T): T => _ export function createStructuralDirectiveTransform(
export function createDirectiveTransform(
name: string | RegExp, name: string | RegExp,
fn: DirectiveTransform fn: StructuralDirectiveTransform
): Transform { ): NodeTransform {
const matches = isString(name) const matches = isString(name)
? (n: string) => n === name ? (n: string) => n === name
: (n: string) => name.test(n) : (n: string) => name.test(n)
return (node, context) => { return (node, context) => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {
const dirs = node.directives const { props } = node
let didRemove = false for (let i = 0; i < props.length; i++) {
for (let i = 0; i < dirs.length; i++) { const prop = props[i]
if (matches(dirs[i].name)) { if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
const res = fn(node, dirs[i], context) fn(node, prop, context)
// Directives are removed after transformation by default. A transform // structural directives are removed after being processed
// returning false means the directive should not be removed. // to avoid infinite recursion
if (res !== false) { props.splice(i, 1)
;(dirs as any)[i] = undefined i--
didRemove = true }
}
}
}
if (didRemove) {
node.directives = dirs.filter(identity)
} }
} }
} }

View File

@ -1,29 +1,43 @@
import { Transform, TransformContext } from '../transform' import { NodeTransform, TransformContext } from '../transform'
import { import {
NodeTypes, NodeTypes,
ElementTypes, ElementTypes,
CallExpression, CallExpression,
ObjectExpression, ObjectExpression,
ElementNode ElementNode,
DirectiveNode,
ExpressionNode,
ArrayExpression,
createCallExpression,
createArrayExpression,
createObjectProperty,
createExpression,
createObjectExpression
} from '../ast' } from '../ast'
import { isArray } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
// generate a JavaScript AST for this element's codegen // generate a JavaScript AST for this element's codegen
export const prepareElementForCodegen: Transform = (node, context) => { export const prepareElementForCodegen: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {
if ( if (
node.tagType === ElementTypes.ELEMENT || node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT node.tagType === ElementTypes.COMPONENT
) { ) {
const isComponent = node.tagType === ElementTypes.ELEMENT const isComponent = node.tagType === ElementTypes.ELEMENT
const hasProps = node.attrs.length > 0 || node.directives.length > 0 const hasProps = node.props.length > 0
const hasChildren = node.children.length > 0 const hasChildren = node.children.length > 0
let runtimeDirectives: DirectiveNode[] | undefined
const args: CallExpression['arguments'] = [ const args: CallExpression['arguments'] = [
// TODO inject resolveComponent dep to root
isComponent ? node.tag : `"${node.tag}"` isComponent ? node.tag : `"${node.tag}"`
] ]
// props // props
if (hasProps) { if (hasProps) {
args.push(buildProps(node)) const { props, directives } = buildProps(node, context)
args.push(props)
runtimeDirectives = directives
} }
// children // children
if (hasChildren) { if (hasChildren) {
@ -34,53 +48,155 @@ export const prepareElementForCodegen: Transform = (node, context) => {
args.push(isComponent ? buildSlots(node, context) : node.children) args.push(isComponent ? buildSlots(node, context) : node.children)
} }
node.codegenNode = { const { loc } = node
type: NodeTypes.CALL_EXPRESSION, const vnode = createCallExpression(`h`, args, loc)
loc: node.loc,
callee: `h`, if (runtimeDirectives) {
arguments: args node.codegenNode = createCallExpression(
`applyDirectives`,
[
vnode,
createArrayExpression(
runtimeDirectives.map(dir => {
return createDirectiveArgs(dir, context)
}),
loc
)
],
loc
)
} else {
node.codegenNode = vnode
} }
} else if (node.tagType === ElementTypes.SLOT) {
// <slot [name="xxx"]/>
// TODO
} else if (node.tagType === ElementTypes.TEMPLATE) {
// do nothing
} }
} }
} }
function buildProps({ loc, attrs }: ElementNode): ObjectExpression { function buildProps(
{ loc, props }: ElementNode,
context: TransformContext
): {
props: ObjectExpression | CallExpression
directives: DirectiveNode[]
} {
let properties: ObjectExpression['properties'] = []
const mergeArgs: Array<ObjectExpression | ExpressionNode> = []
const runtimeDirectives: DirectiveNode[] = []
for (let i = 0; i < props.length; i++) {
// static attribute
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
const { loc, name, value } = prop
properties.push(
createObjectProperty(
createExpression(name, true, loc),
createExpression(
value ? value.content : '',
true,
value ? value.loc : loc
),
loc
)
)
} else {
// directives
// special case for v-bind with no argument
if (prop.name === 'bind' && !prop.arg) {
if (prop.exp) {
if (properties.length) {
mergeArgs.push(createObjectExpression(properties, loc))
properties = []
}
mergeArgs.push(prop.exp)
} else {
context.onError(
createCompilerError(
ErrorCodes.X_V_BIND_NO_EXPRESSION,
prop.loc.start
)
)
}
continue
}
const directiveTransform = context.directiveTransforms[prop.name]
if (directiveTransform) {
const { props, needRuntime } = directiveTransform(prop, context)
if (isArray(props)) {
properties.push(...props)
} else {
properties.push(props)
}
if (needRuntime) {
runtimeDirectives.push(prop)
}
} else {
// no built-in transform, this is a user custom directive.
runtimeDirectives.push(prop)
}
}
}
let ret: ObjectExpression | CallExpression
// has v-bind="object", wrap with mergeProps
if (mergeArgs.length) {
if (properties.length) {
mergeArgs.push(createObjectExpression(properties, loc))
}
if (mergeArgs.length > 1) {
ret = createCallExpression(`mergeProps`, mergeArgs, loc)
} else {
// single v-bind with nothing else - no need for a mergeProps call
ret = createObjectExpression(properties, loc)
}
} else {
ret = createObjectExpression(properties, loc)
}
return { return {
type: NodeTypes.OBJECT_EXPRESSION, props: ret,
loc, directives: runtimeDirectives
// At this stage we will only process static attrs. Directive bindings will
// be handled by their respective transforms which adds/modifies the props.
properties: attrs.map(({ name, value, loc }) => {
return {
type: NodeTypes.PROPERTY,
loc,
key: {
type: NodeTypes.EXPRESSION,
loc,
content: name,
isStatic: true
},
value: {
type: NodeTypes.EXPRESSION,
loc: value ? value.loc : loc,
content: value ? value.content : '',
isStatic: true
} }
} }
})
function createDirectiveArgs(
dir: DirectiveNode,
context: TransformContext
): ArrayExpression {
// TODO inject resolveDirective dep to root
const dirArgs: ArrayExpression['elements'] = [dir.name]
const { loc } = dir
if (dir.exp) dirArgs.push(dir.exp)
if (dir.arg) dirArgs.push(dir.arg)
if (Object.keys(dir.modifiers).length) {
dirArgs.push(
createObjectExpression(
dir.modifiers.map(modifier =>
createObjectProperty(
createExpression(modifier, true, loc),
createExpression(`true`, false, loc),
loc
)
),
loc
)
)
} }
return createArrayExpression(dirArgs, dir.loc)
} }
function buildSlots( function buildSlots(
{ loc, children }: ElementNode, { loc, children }: ElementNode,
context: TransformContext context: TransformContext
): ObjectExpression { ): ObjectExpression {
const slots: ObjectExpression = { const slots = createObjectExpression([], loc)
type: NodeTypes.OBJECT_EXPRESSION,
loc,
properties: []
}
// TODO // TODO
return slots return slots

View File

@ -1,5 +1,5 @@
import { createDirectiveTransform } from '../transform' import { createStructuralDirectiveTransform } from '../transform'
import { NodeTypes, ExpressionNode } from '../ast' import { NodeTypes, ExpressionNode, createExpression } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { getInnerRange } from '../utils' import { getInnerRange } from '../utils'
@ -7,7 +7,7 @@ const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g const stripParensRE = /^\(|\)$/g
export const transformFor = createDirectiveTransform( export const transformFor = createStructuralDirectiveTransform(
'for', 'for',
(node, dir, context) => { (node, dir, context) => {
if (dir.exp) { if (dir.exp) {
@ -27,7 +27,7 @@ export const transformFor = createDirectiveTransform(
children: [node] children: [node]
}) })
} else { } else {
context.emitError( context.onError(
createCompilerError( createCompilerError(
ErrorCodes.X_FOR_MALFORMED_EXPRESSION, ErrorCodes.X_FOR_MALFORMED_EXPRESSION,
dir.loc.start dir.loc.start
@ -35,7 +35,7 @@ export const transformFor = createDirectiveTransform(
) )
} }
} else { } else {
context.emitError( context.onError(
createCompilerError(ErrorCodes.X_FOR_NO_EXPRESSION, dir.loc.start) createCompilerError(ErrorCodes.X_FOR_NO_EXPRESSION, dir.loc.start)
) )
} }
@ -118,11 +118,10 @@ function maybeCreateExpression(
node: ExpressionNode node: ExpressionNode
): ExpressionNode | undefined { ): ExpressionNode | undefined {
if (alias) { if (alias) {
return { return createExpression(
type: NodeTypes.EXPRESSION, alias.content,
loc: getInnerRange(node.loc, alias.offset, alias.content.length), false,
content: alias.content, getInnerRange(node.loc, alias.offset, alias.content.length)
isStatic: false )
}
} }
} }

View File

@ -1,4 +1,4 @@
import { createDirectiveTransform } from '../transform' import { createStructuralDirectiveTransform } from '../transform'
import { import {
NodeTypes, NodeTypes,
ElementTypes, ElementTypes,
@ -8,7 +8,7 @@ import {
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
export const transformIf = createDirectiveTransform( export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(if|else|else-if)$/,
(node, dir, context) => { (node, dir, context) => {
if (dir.name === 'if') { if (dir.name === 'if') {
@ -38,7 +38,7 @@ export const transformIf = createDirectiveTransform(
} }
sibling.branches.push(branch) sibling.branches.push(branch)
} else { } else {
context.emitError( context.onError(
createCompilerError( createCompilerError(
dir.name === 'else' dir.name === 'else'
? ErrorCodes.X_ELSE_NO_ADJACENT_IF ? ErrorCodes.X_ELSE_NO_ADJACENT_IF

View File

@ -1 +0,0 @@
// TODO

View File

@ -6,6 +6,8 @@ import {
import { parserOptionsMinimal } from './parserOptionsMinimal' import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard' import { parserOptionsStandard } from './parserOptionsStandard'
export * from '@vue/compiler-core'
export function compile( export function compile(
template: string, template: string,
options: CompilerOptions = {} options: CompilerOptions = {}
@ -13,11 +15,9 @@ export function compile(
return baseCompile(template, { return baseCompile(template, {
...options, ...options,
...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard), ...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard),
transforms: [ directiveTransforms: {
// TODO include DOM-specific transforms // TODO include DOM-specific directiveTransforms
...(options.transforms || []) // extra user transforms ...(options.directiveTransforms || {})
] }
}) })
} }
export * from '@vue/compiler-core'