From baa8954884d6f61b2f4b874032ef9203718df360 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 21 Sep 2019 17:42:12 -0400 Subject: [PATCH] feat(compiler): element transform --- .../__tests__/directives/vFor.spec.ts | 48 ++--- .../__tests__/directives/vIf.spec.ts | 26 +-- .../compiler-core/__tests__/transform.spec.ts | 28 +-- packages/compiler-core/src/ast.ts | 64 +++++- packages/compiler-core/src/errors.ts | 3 +- packages/compiler-core/src/index.ts | 19 +- packages/compiler-core/src/parse.ts | 15 +- packages/compiler-core/src/transform.ts | 74 ++++--- .../compiler-core/src/transforms/element.ts | 192 ++++++++++++++---- packages/compiler-core/src/transforms/vFor.ts | 21 +- packages/compiler-core/src/transforms/vIf.ts | 6 +- packages/compiler-core/src/transforms/vPre.ts | 1 - packages/compiler-dom/src/index.ts | 12 +- 13 files changed, 349 insertions(+), 160 deletions(-) delete mode 100644 packages/compiler-core/src/transforms/vPre.ts diff --git a/packages/compiler-core/__tests__/directives/vFor.spec.ts b/packages/compiler-core/__tests__/directives/vFor.spec.ts index b23537e3..64ed06e7 100644 --- a/packages/compiler-core/__tests__/directives/vFor.spec.ts +++ b/packages/compiler-core/__tests__/directives/vFor.spec.ts @@ -1,6 +1,6 @@ import { parse } from '../../src/parse' import { transform } from '../../src/transform' -import { transformFor } from '../../src/directives/vFor' +import { transformFor } from '../../src/transforms/vFor' import { ForNode, NodeTypes } from '../../src/ast' import { ErrorCodes } from '../../src/errors' @@ -9,7 +9,7 @@ describe('v-for', () => { test('number expression', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -25,7 +25,7 @@ describe('v-for', () => { test('value', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -41,7 +41,7 @@ describe('v-for', () => { test('object de-structured value', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -57,7 +57,7 @@ describe('v-for', () => { test('array de-structured value', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -73,7 +73,7 @@ describe('v-for', () => { test('value and key', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -89,7 +89,7 @@ describe('v-for', () => { test('value, key and index', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -107,7 +107,7 @@ describe('v-for', () => { test('skipped key', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -124,7 +124,7 @@ describe('v-for', () => { test('skipped value and key', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -141,7 +141,7 @@ describe('v-for', () => { test('unbracketed value', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -157,7 +157,7 @@ describe('v-for', () => { test('unbracketed value and key', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -174,7 +174,7 @@ describe('v-for', () => { test('unbracketed value, key and index', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -192,7 +192,7 @@ describe('v-for', () => { test('unbracketed skipped key', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -209,7 +209,7 @@ describe('v-for', () => { test('unbracketed skipped value and key', () => { const node = parse('') - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -226,7 +226,7 @@ describe('v-for', () => { test('missing expression', () => { const node = parse('') const onError = jest.fn() - transform(node, { transforms: [transformFor], onError }) + transform(node, { nodeTransforms: [transformFor], onError }) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith( @@ -239,7 +239,7 @@ describe('v-for', () => { test('empty expression', () => { const node = parse('') const onError = jest.fn() - transform(node, { transforms: [transformFor], onError }) + transform(node, { nodeTransforms: [transformFor], onError }) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith( @@ -252,7 +252,7 @@ describe('v-for', () => { test('invalid expression', () => { const node = parse('') const onError = jest.fn() - transform(node, { transforms: [transformFor], onError }) + transform(node, { nodeTransforms: [transformFor], onError }) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith( @@ -265,7 +265,7 @@ describe('v-for', () => { test('missing source', () => { const node = parse('') const onError = jest.fn() - transform(node, { transforms: [transformFor], onError }) + transform(node, { nodeTransforms: [transformFor], onError }) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith( @@ -278,7 +278,7 @@ describe('v-for', () => { test('missing value', () => { const node = parse('') const onError = jest.fn() - transform(node, { transforms: [transformFor], onError }) + transform(node, { nodeTransforms: [transformFor], onError }) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith( @@ -293,7 +293,7 @@ describe('v-for', () => { const source = '' const node = parse(source) - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -328,7 +328,7 @@ describe('v-for', () => { const source = '' const node = parse(source) - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -363,7 +363,7 @@ describe('v-for', () => { const source = '' const node = parse(source) - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -398,7 +398,7 @@ describe('v-for', () => { const source = '' const node = parse(source) - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) @@ -455,7 +455,7 @@ describe('v-for', () => { const source = '' const node = parse(source) - transform(node, { transforms: [transformFor] }) + transform(node, { nodeTransforms: [transformFor] }) expect(node.children.length).toBe(1) diff --git a/packages/compiler-core/__tests__/directives/vIf.spec.ts b/packages/compiler-core/__tests__/directives/vIf.spec.ts index 50390101..86809b9b 100644 --- a/packages/compiler-core/__tests__/directives/vIf.spec.ts +++ b/packages/compiler-core/__tests__/directives/vIf.spec.ts @@ -1,6 +1,6 @@ import { parse } from '../../src/parse' import { transform } from '../../src/transform' -import { transformIf } from '../../src/directives/vIf' +import { transformIf } from '../../src/transforms/vIf' import { IfNode, NodeTypes, @@ -15,7 +15,7 @@ describe('compiler: v-if', () => { test('basic v-if', () => { const ast = parse(`
`) transform(ast, { - transforms: [transformIf] + nodeTransforms: [transformIf] }) const node = ast.children[0] as IfNode expect(node.type).toBe(NodeTypes.IF) @@ -29,7 +29,7 @@ describe('compiler: v-if', () => { test('template v-if', () => { const ast = parse(``) transform(ast, { - transforms: [transformIf] + nodeTransforms: [transformIf] }) const node = ast.children[0] as IfNode expect(node.type).toBe(NodeTypes.IF) @@ -47,7 +47,7 @@ describe('compiler: v-if', () => { test('v-if + v-else', () => { const ast = parse(`

`) transform(ast, { - transforms: [transformIf] + nodeTransforms: [transformIf] }) // should fold branches expect(ast.children.length).toBe(1) @@ -72,7 +72,7 @@ describe('compiler: v-if', () => { test('v-if + v-else-if', () => { const ast = parse(`

`) transform(ast, { - transforms: [transformIf] + nodeTransforms: [transformIf] }) // should fold branches expect(ast.children.length).toBe(1) @@ -99,7 +99,7 @@ describe('compiler: v-if', () => { `

` ) transform(ast, { - transforms: [transformIf] + nodeTransforms: [transformIf] }) // should fold branches expect(ast.children.length).toBe(1) @@ -136,7 +136,7 @@ describe('compiler: v-if', () => { `) transform(ast, { - transforms: [transformIf] + nodeTransforms: [transformIf] }) // should fold branches expect(ast.children.length).toBe(1) @@ -172,7 +172,7 @@ describe('compiler: v-if', () => { const ast = parse(`

`) const spy = jest.fn() transform(ast, { - transforms: [transformIf], + nodeTransforms: [transformIf], onError: spy }) expect(spy.mock.calls[0]).toMatchObject([ @@ -185,7 +185,7 @@ describe('compiler: v-if', () => { const ast2 = parse(`
`) const spy2 = jest.fn() transform(ast2, { - transforms: [transformIf], + nodeTransforms: [transformIf], onError: spy2 }) expect(spy2.mock.calls[0]).toMatchObject([ @@ -198,7 +198,7 @@ describe('compiler: v-if', () => { const ast3 = parse(`
foo
`) const spy3 = jest.fn() transform(ast3, { - transforms: [transformIf], + nodeTransforms: [transformIf], onError: spy3 }) expect(spy3.mock.calls[0]).toMatchObject([ @@ -213,7 +213,7 @@ describe('compiler: v-if', () => { const ast = parse(`
`) const spy = jest.fn() transform(ast, { - transforms: [transformIf], + nodeTransforms: [transformIf], onError: spy }) expect(spy.mock.calls[0]).toMatchObject([ @@ -226,7 +226,7 @@ describe('compiler: v-if', () => { const ast2 = parse(`
`) const spy2 = jest.fn() transform(ast2, { - transforms: [transformIf], + nodeTransforms: [transformIf], onError: spy2 }) expect(spy2.mock.calls[0]).toMatchObject([ @@ -239,7 +239,7 @@ describe('compiler: v-if', () => { const ast3 = parse(`
foo
`) const spy3 = jest.fn() transform(ast3, { - transforms: [transformIf], + nodeTransforms: [transformIf], onError: spy3 }) expect(spy3.mock.calls[0]).toMatchObject([ diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index e6d8ef05..c8aa3a97 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -1,5 +1,5 @@ import { parse } from '../src/parse' -import { transform, Transform } from '../src/transform' +import { transform, NodeTransform } from '../src/transform' import { ElementNode, NodeTypes } from '../src/ast' import { ErrorCodes, createCompilerError } from '../src/errors' @@ -10,12 +10,12 @@ describe('compiler: transform', () => { // manually store call arguments because context is mutable and shared // across calls const calls: any[] = [] - const plugin: Transform = (node, context) => { + const plugin: NodeTransform = (node, context) => { calls.push([node, Object.assign({}, context)]) } transform(ast, { - transforms: [plugin] + nodeTransforms: [plugin] }) const div = ast.children[0] as ElementNode @@ -48,7 +48,7 @@ describe('compiler: transform', () => { test('context.replaceNode', () => { const ast = parse(`
`) - const plugin: Transform = (node, context) => { + const plugin: NodeTransform = (node, context) => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { // change the node to

context.replaceNode( @@ -67,7 +67,7 @@ describe('compiler: transform', () => { } const spy = jest.fn(plugin) transform(ast, { - transforms: [spy] + nodeTransforms: [spy] }) expect(ast.children.length).toBe(2) @@ -85,14 +85,14 @@ describe('compiler: transform', () => { const c1 = ast.children[0] const c2 = ast.children[2] - const plugin: Transform = (node, context) => { + const plugin: NodeTransform = (node, context) => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { context.removeNode() } } const spy = jest.fn(plugin) transform(ast, { - transforms: [spy] + nodeTransforms: [spy] }) expect(ast.children.length).toBe(2) @@ -111,7 +111,7 @@ describe('compiler: transform', () => { const c1 = ast.children[0] const c2 = ast.children[2] - const plugin: Transform = (node, context) => { + const plugin: NodeTransform = (node, context) => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { context.removeNode() // remove previous sibling @@ -120,7 +120,7 @@ describe('compiler: transform', () => { } const spy = jest.fn(plugin) transform(ast, { - transforms: [spy] + nodeTransforms: [spy] }) expect(ast.children.length).toBe(1) @@ -138,7 +138,7 @@ describe('compiler: transform', () => { const c1 = ast.children[0] const d1 = ast.children[1] - const plugin: Transform = (node, context) => { + const plugin: NodeTransform = (node, context) => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { context.removeNode() // remove next sibling @@ -147,7 +147,7 @@ describe('compiler: transform', () => { } const spy = jest.fn(plugin) transform(ast, { - transforms: [spy] + nodeTransforms: [spy] }) expect(ast.children.length).toBe(1) @@ -163,14 +163,14 @@ describe('compiler: transform', () => { test('onError option', () => { const ast = parse(`

`) const loc = ast.children[0].loc.start - const plugin: Transform = (node, context) => { - context.emitError( + const plugin: NodeTransform = (node, context) => { + context.onError( createCompilerError(ErrorCodes.X_INVALID_END_TAG, node.loc.start) ) } const spy = jest.fn() transform(ast, { - transforms: [plugin], + nodeTransforms: [plugin], onError: spy }) expect(spy.mock.calls[0]).toMatchObject([ diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 7c3b7a35..d97b976a 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -73,8 +73,7 @@ export interface ElementNode extends Node { tag: string tagType: ElementTypes isSelfClosing: boolean - attrs: AttributeNode[] - directives: DirectiveNode[] + props: Array children: ChildNode[] codegenNode: CallExpression | undefined } @@ -161,3 +160,64 @@ export interface ArrayExpression extends Node { type: NodeTypes.ARRAY_EXPRESSION elements: Array } + +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 + } +} diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index 5467715f..6f6959b6 100644 --- a/packages/compiler-core/src/errors.ts +++ b/packages/compiler-core/src/errors.ts @@ -65,7 +65,8 @@ export const enum ErrorCodes { X_ELSE_IF_NO_ADJACENT_IF, X_ELSE_NO_ADJACENT_IF, X_FOR_NO_EXPRESSION, - X_FOR_MALFORMED_EXPRESSION + X_FOR_MALFORMED_EXPRESSION, + X_V_BIND_NO_EXPRESSION } export const errorMessages: { [code: number]: string } = { diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 3950c5cb..62fc7317 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -14,10 +14,14 @@ export function compile( transform(ast, { ...options, - transforms: [ + nodeTransforms: [ // 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) @@ -27,11 +31,11 @@ export function compile( export { parse, ParserOptions, TextModes } from './parse' export { transform, - createDirectiveTransform, + createStructuralDirectiveTransform, TransformOptions, TransformContext, - Transform, - DirectiveTransform + NodeTransform as Transform, + StructuralDirectiveTransform } from './transform' export { generate, @@ -41,3 +45,6 @@ export { } from './codegen' export { ErrorCodes, CompilerError, createCompilerError } from './errors' export * from './ast' + +// debug +export { prepareElementForCodegen } from './transforms/element' diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 0a9521ad..73aa22f2 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -376,8 +376,7 @@ function parseTag( const start = getCursor(context) const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)! const tag = match[1] - const attrs = [] - const directives = [] + const props = [] const ns = context.options.getNamespace(tag, parent) advanceBy(context, match[0].length) @@ -402,11 +401,7 @@ function parseTag( const attr = parseAttribute(context, attributeNames) if (type === TagType.Start) { - if (attr.type === NodeTypes.DIRECTIVE) { - directives.push(attr) - } else { - attrs.push(attr) - } + props.push(attr) } if (/^[^\t\r\n\f />]/.test(context.source)) { @@ -438,11 +433,11 @@ function parseTag( ns, tag, tagType, - attrs, - directives, + props, isSelfClosing, children: [], - loc: getSelection(context, start) + loc: getSelection(context, start), + codegenNode: undefined // to be created during transform phase } } diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index e200e127..4e3fda5e 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -4,27 +4,45 @@ import { ParentNode, ChildNode, ElementNode, - DirectiveNode + DirectiveNode, + Property } from './ast' import { isString } from '@vue/shared' 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 = ( + 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, dir: DirectiveNode, context: TransformContext -) => false | void +) => void export interface TransformOptions { - transforms?: Transform[] + nodeTransforms?: NodeTransform[] + directiveTransforms?: { [name: string]: DirectiveTransform } onError?: (error: CompilerError) => void } -export interface TransformContext { - transforms: Transform[] - emitError: (error: CompilerError) => void +export interface TransformContext extends Required { parent: ParentNode ancestors: ParentNode[] childIndex: number @@ -44,8 +62,9 @@ function createTransformContext( options: TransformOptions ): TransformContext { const context: TransformContext = { - transforms: options.transforms || [], - emitError: options.onError || defaultOnError, + nodeTransforms: options.nodeTransforms || [], + directiveTransforms: options.directiveTransforms || {}, + onError: options.onError || defaultOnError, parent: root, ancestors: [], childIndex: 0, @@ -109,9 +128,9 @@ function traverseNode( ancestors: ParentNode[] ) { // apply transform plugins - const { transforms } = context - for (let i = 0; i < transforms.length; i++) { - const plugin = transforms[i] + const { nodeTransforms } = context + for (let i = 0; i < nodeTransforms.length; i++) { + const plugin = nodeTransforms[i] plugin(node, context) if (!context.currentNode) { return @@ -135,34 +154,27 @@ function traverseNode( } } -const identity = (_: T): T => _ - -export function createDirectiveTransform( +export function createStructuralDirectiveTransform( name: string | RegExp, - fn: DirectiveTransform -): Transform { + fn: StructuralDirectiveTransform +): NodeTransform { const matches = isString(name) ? (n: string) => n === name : (n: string) => name.test(n) return (node, context) => { if (node.type === NodeTypes.ELEMENT) { - const dirs = node.directives - let didRemove = false - for (let i = 0; i < dirs.length; i++) { - if (matches(dirs[i].name)) { - const res = fn(node, dirs[i], context) - // Directives are removed after transformation by default. A transform - // returning false means the directive should not be removed. - if (res !== false) { - ;(dirs as any)[i] = undefined - didRemove = true - } + const { props } = node + for (let i = 0; i < props.length; i++) { + const prop = props[i] + if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) { + fn(node, prop, context) + // structural directives are removed after being processed + // to avoid infinite recursion + props.splice(i, 1) + i-- } } - if (didRemove) { - node.directives = dirs.filter(identity) - } } } } diff --git a/packages/compiler-core/src/transforms/element.ts b/packages/compiler-core/src/transforms/element.ts index 263af75f..780b7729 100644 --- a/packages/compiler-core/src/transforms/element.ts +++ b/packages/compiler-core/src/transforms/element.ts @@ -1,29 +1,43 @@ -import { Transform, TransformContext } from '../transform' +import { NodeTransform, TransformContext } from '../transform' import { NodeTypes, ElementTypes, CallExpression, ObjectExpression, - ElementNode + ElementNode, + DirectiveNode, + ExpressionNode, + ArrayExpression, + createCallExpression, + createArrayExpression, + createObjectProperty, + createExpression, + createObjectExpression } from '../ast' +import { isArray } from '@vue/shared' +import { createCompilerError, ErrorCodes } from '../errors' // 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.tagType === ElementTypes.ELEMENT || node.tagType === ElementTypes.COMPONENT ) { 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 + let runtimeDirectives: DirectiveNode[] | undefined const args: CallExpression['arguments'] = [ + // TODO inject resolveComponent dep to root isComponent ? node.tag : `"${node.tag}"` ] // props if (hasProps) { - args.push(buildProps(node)) + const { props, directives } = buildProps(node, context) + args.push(props) + runtimeDirectives = directives } // children if (hasChildren) { @@ -34,53 +48,155 @@ export const prepareElementForCodegen: Transform = (node, context) => { args.push(isComponent ? buildSlots(node, context) : node.children) } - node.codegenNode = { - type: NodeTypes.CALL_EXPRESSION, - loc: node.loc, - callee: `h`, - arguments: args + const { loc } = node + const vnode = createCallExpression(`h`, args, loc) + + if (runtimeDirectives) { + 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) { + // + // TODO + } else if (node.tagType === ElementTypes.TEMPLATE) { + // do nothing } } } -function buildProps({ loc, attrs }: ElementNode): ObjectExpression { - return { - type: NodeTypes.OBJECT_EXPRESSION, - loc, - // 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 buildProps( + { loc, props }: ElementNode, + context: TransformContext +): { + props: ObjectExpression | CallExpression + directives: DirectiveNode[] +} { + let properties: ObjectExpression['properties'] = [] + const mergeArgs: Array = [] + 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 { + props: ret, + directives: runtimeDirectives + } +} + +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( { loc, children }: ElementNode, context: TransformContext ): ObjectExpression { - const slots: ObjectExpression = { - type: NodeTypes.OBJECT_EXPRESSION, - loc, - properties: [] - } - + const slots = createObjectExpression([], loc) // TODO return slots diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 668fbd12..8b8e79e2 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -1,5 +1,5 @@ -import { createDirectiveTransform } from '../transform' -import { NodeTypes, ExpressionNode } from '../ast' +import { createStructuralDirectiveTransform } from '../transform' +import { NodeTypes, ExpressionNode, createExpression } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { getInnerRange } from '../utils' @@ -7,7 +7,7 @@ const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g -export const transformFor = createDirectiveTransform( +export const transformFor = createStructuralDirectiveTransform( 'for', (node, dir, context) => { if (dir.exp) { @@ -27,7 +27,7 @@ export const transformFor = createDirectiveTransform( children: [node] }) } else { - context.emitError( + context.onError( createCompilerError( ErrorCodes.X_FOR_MALFORMED_EXPRESSION, dir.loc.start @@ -35,7 +35,7 @@ export const transformFor = createDirectiveTransform( ) } } else { - context.emitError( + context.onError( createCompilerError(ErrorCodes.X_FOR_NO_EXPRESSION, dir.loc.start) ) } @@ -118,11 +118,10 @@ function maybeCreateExpression( node: ExpressionNode ): ExpressionNode | undefined { if (alias) { - return { - type: NodeTypes.EXPRESSION, - loc: getInnerRange(node.loc, alias.offset, alias.content.length), - content: alias.content, - isStatic: false - } + return createExpression( + alias.content, + false, + getInnerRange(node.loc, alias.offset, alias.content.length) + ) } } diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index cbdf8be9..494823c9 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -1,4 +1,4 @@ -import { createDirectiveTransform } from '../transform' +import { createStructuralDirectiveTransform } from '../transform' import { NodeTypes, ElementTypes, @@ -8,7 +8,7 @@ import { } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' -export const transformIf = createDirectiveTransform( +export const transformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, (node, dir, context) => { if (dir.name === 'if') { @@ -38,7 +38,7 @@ export const transformIf = createDirectiveTransform( } sibling.branches.push(branch) } else { - context.emitError( + context.onError( createCompilerError( dir.name === 'else' ? ErrorCodes.X_ELSE_NO_ADJACENT_IF diff --git a/packages/compiler-core/src/transforms/vPre.ts b/packages/compiler-core/src/transforms/vPre.ts deleted file mode 100644 index 70b786d1..00000000 --- a/packages/compiler-core/src/transforms/vPre.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 7b636355..6c09e0bc 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -6,6 +6,8 @@ import { import { parserOptionsMinimal } from './parserOptionsMinimal' import { parserOptionsStandard } from './parserOptionsStandard' +export * from '@vue/compiler-core' + export function compile( template: string, options: CompilerOptions = {} @@ -13,11 +15,9 @@ export function compile( return baseCompile(template, { ...options, ...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard), - transforms: [ - // TODO include DOM-specific transforms - ...(options.transforms || []) // extra user transforms - ] + directiveTransforms: { + // TODO include DOM-specific directiveTransforms + ...(options.directiveTransforms || {}) + } }) } - -export * from '@vue/compiler-core'