feat(compiler): element codegen

This commit is contained in:
Evan You 2019-09-22 16:50:57 -04:00
parent 40307d9642
commit 3a177a18d2
15 changed files with 369 additions and 120 deletions

View File

@ -8,15 +8,24 @@ describe('compiler: codegen', () => {
const { code, map } = generate(ast, {
filename: `foo.vue`
})
expect(code).toBe(`["hello ", world]`)
expect(code).toBe(
`return function render() {
with (this) {
return [
"hello ",
world
]
}
}`
)
expect(map!.sources).toEqual([`foo.vue`])
expect(map!.sourcesContent).toEqual([source])
const consumer = await new SourceMapConsumer(map as RawSourceMap)
const pos = consumer.originalPositionFor({
line: 1,
column: 11
line: 5,
column: 6
})
expect(pos).toMatchObject({
line: 1,

View File

@ -20,10 +20,10 @@ export const enum NodeTypes {
IF_BRANCH,
FOR,
// codegen
CALL_EXPRESSION,
OBJECT_EXPRESSION,
PROPERTY,
ARRAY_EXPRESSION
JS_CALL_EXPRESSION,
JS_OBJECT_EXPRESSION,
JS_PROPERTY,
JS_ARRAY_EXPRESSION
}
export const enum ElementTypes {
@ -129,36 +129,35 @@ export interface ForNode extends Node {
children: ChildNode[]
}
// We also include a subset of JavaScript AST for code generation
// purposes. The AST is intentioanlly minimal just to meet the exact needs of
// We also include a number of JavaScript AST nodes for code generation.
// The AST is an intentioanlly minimal subset just to meet the exact needs of
// Vue render function generation.
type CodegenNode =
| string
export type JSChildNode =
| CallExpression
| ObjectExpression
| ArrayExpression
| ExpressionNode
export interface CallExpression extends Node {
type: NodeTypes.CALL_EXPRESSION
type: NodeTypes.JS_CALL_EXPRESSION
callee: string // can only be imported runtime helpers, so no source location
arguments: Array<CodegenNode | ChildNode[]>
arguments: Array<string | JSChildNode | ChildNode[]>
}
export interface ObjectExpression extends Node {
type: NodeTypes.OBJECT_EXPRESSION
type: NodeTypes.JS_OBJECT_EXPRESSION
properties: Array<Property>
}
export interface Property extends Node {
type: NodeTypes.PROPERTY
type: NodeTypes.JS_PROPERTY
key: ExpressionNode
value: ExpressionNode
}
export interface ArrayExpression extends Node {
type: NodeTypes.ARRAY_EXPRESSION
elements: Array<CodegenNode>
type: NodeTypes.JS_ARRAY_EXPRESSION
elements: Array<string | JSChildNode>
}
export function createArrayExpression(
@ -166,7 +165,7 @@ export function createArrayExpression(
loc: SourceLocation
): ArrayExpression {
return {
type: NodeTypes.ARRAY_EXPRESSION,
type: NodeTypes.JS_ARRAY_EXPRESSION,
loc,
elements
}
@ -177,7 +176,7 @@ export function createObjectExpression(
loc: SourceLocation
): ObjectExpression {
return {
type: NodeTypes.OBJECT_EXPRESSION,
type: NodeTypes.JS_OBJECT_EXPRESSION,
loc,
properties
}
@ -189,7 +188,7 @@ export function createObjectProperty(
loc: SourceLocation
): Property {
return {
type: NodeTypes.PROPERTY,
type: NodeTypes.JS_PROPERTY,
loc,
key,
value
@ -215,7 +214,7 @@ export function createCallExpression(
loc: SourceLocation
): CallExpression {
return {
type: NodeTypes.CALL_EXPRESSION,
type: NodeTypes.JS_CALL_EXPRESSION,
loc,
callee,
arguments: args

View File

@ -7,16 +7,26 @@ import {
TextNode,
CommentNode,
ExpressionNode,
NodeTypes
NodeTypes,
JSChildNode,
CallExpression,
ArrayExpression,
ObjectExpression,
IfBranchNode
} from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { advancePositionWithMutation } from './utils'
import { advancePositionWithMutation, assert } from './utils'
import { isString, isArray } from '@vue/shared'
import { RENDER_LIST_HELPER } from './transforms/vFor'
type CodegenNode = ChildNode | JSChildNode
export interface CodegenOptions {
// Assume ES module environment. If true, will generate import statements for
// will generate import statements for
// runtime helpers; otherwise will grab the helpers from global `Vue`.
// default: false
module?: boolean
mode?: 'module' | 'function'
useWith?: boolean
// Filename for source map generation.
filename?: string
}
@ -32,52 +42,34 @@ export interface CodegenContext extends Required<CodegenOptions> {
line: number
column: number
offset: number
indent: number
indentLevel: number
imports: Set<string>
knownIdentifiers: Set<string>
map?: SourceMapGenerator
push(generatedCode: string, astNode?: ChildNode): void
}
export function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
if (context.module) {
// TODO inject import statements on RootNode
context.push(`export function render() {\n`)
context.indent++
context.push(` return `)
}
if (ast.children.length === 1) {
genNode(ast.children[0], context)
} else {
genChildren(ast.children, context)
}
if (context.module) {
context.indent--
context.push(`\n}`)
}
return {
code: context.code,
map: context.map ? context.map.toJSON() : undefined
}
push(code: string, node?: CodegenNode): void
indent(): void
deindent(): void
newline(): void
}
function createCodegenContext(
ast: RootNode,
{ module = false, filename = `template.vue.html` }: CodegenOptions
{
mode = 'function',
useWith = true,
filename = `template.vue.html`
}: CodegenOptions
): CodegenContext {
const context: CodegenContext = {
module,
mode,
useWith,
filename,
source: ast.loc.source,
code: ``,
column: 1,
line: 1,
offset: 0,
indent: 0,
indentLevel: 0,
imports: new Set(),
knownIdentifiers: new Set(),
@ -86,9 +78,8 @@ function createCodegenContext(
? undefined
: new (require('source-map')).SourceMapGenerator(),
push(generatedCode, node) {
// TODO handle indent
context.code += generatedCode
push(code, node?: CodegenNode) {
context.code += code
if (context.map) {
if (node) {
context.map.addMapping({
@ -103,30 +94,113 @@ function createCodegenContext(
}
})
}
advancePositionWithMutation(
context,
generatedCode,
generatedCode.length
)
}
advancePositionWithMutation(context, code, code.length)
}
},
indent() {
newline(++context.indentLevel)
},
deindent() {
newline(--context.indentLevel)
},
newline() {
newline(context.indentLevel)
}
}
const newline = (n: number) => context.push('\n' + ` `.repeat(n))
if (!__BROWSER__) {
context.map!.setSourceContent(filename, context.source)
}
return context
}
function genChildren(children: ChildNode[], context: CodegenContext) {
context.push(`[`)
for (let i = 0; i < children.length; i++) {
genNode(children[i], context)
if (i < children.length - 1) context.push(', ')
export function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
// TODO handle different output for module mode and IIFE mode
const { mode, push, useWith, indent, deindent } = context
if (mode === 'function') {
// TODO generate const declarations for helpers
push(`return `)
} else {
// TODO generate import statements for helpers
push(`export default `)
}
push(`function render() {`)
if (useWith) {
indent()
push(`with (this) {`)
}
indent()
push(`return `)
genChildren(ast.children, context)
if (useWith) {
deindent()
push(`}`)
}
deindent()
push(`}`)
return {
code: context.code,
map: context.map ? context.map.toJSON() : undefined
}
}
// This will generate a single vnode call if the list has length === 1.
function genChildren(children: ChildNode[], context: CodegenContext) {
if (children.length === 1) {
genNode(children[0], context)
} else {
genNodeListAsArray(children, context)
}
}
function genNodeListAsArray(
nodes: (string | CodegenNode | ChildNode[])[],
context: CodegenContext
) {
const multilines = nodes.length > 1
context.push(`[`)
multilines && context.indent()
genNodeList(nodes, context, multilines)
multilines && context.deindent()
context.push(`]`)
}
function genNode(node: ChildNode, context: CodegenContext) {
function genNodeList(
nodes: (string | CodegenNode | ChildNode[])[],
context: CodegenContext,
multilines: boolean = false
) {
const { push, newline } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
// plain code string
// note not adding quotes here because this can be any code,
// not just plain strings.
push(node)
} else if (isArray(node)) {
// child VNodes in a h() call
// not using genChildren here because we want them to always be an array
genNodeListAsArray(node, context)
} else {
genNode(node, context)
}
if (i < nodes.length - 1) {
if (multilines) {
push(',')
newline()
} else {
push(', ')
}
}
}
}
function genNode(node: CodegenNode, context: CodegenContext) {
switch (node.type) {
case NodeTypes.ELEMENT:
genElement(node, context)
@ -146,20 +220,55 @@ function genNode(node: ChildNode, context: CodegenContext) {
case NodeTypes.FOR:
genFor(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION:
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
}
}
function genElement(node: ElementNode, context: CodegenContext) {}
function genElement(node: ElementNode, context: CodegenContext) {
__DEV__ &&
assert(
node.codegenNode != null,
`AST is not transformed for codegen. ` +
`Apply appropriate transforms first.`
)
genCallExpression(node.codegenNode!, context, false)
}
function genText(node: TextNode | ExpressionNode, context: CodegenContext) {
context.push(JSON.stringify(node.content), node)
}
function genExpression(node: ExpressionNode, context: CodegenContext) {
if (!__BROWSER__) {
// TODO parse expression content and rewrite identifiers
// if (node.codegenNode) {
// TODO handle transformed expression
// }
const text = node.isStatic ? JSON.stringify(node.content) : node.content
context.push(text, node)
}
function genExpressionAsPropertyKey(
node: ExpressionNode,
context: CodegenContext
) {
// if (node.codegenNode) {
// TODO handle transformed expression
// }
if (node.isStatic) {
// only quote keys if necessary
const text = /^\d|[^\w]/.test(node.content)
? JSON.stringify(node.content)
: node.content
context.push(text, node)
} else {
context.push(`[${node.content}]`, node)
}
context.push(node.content, node)
}
function genComment(node: CommentNode, context: CodegenContext) {
@ -167,6 +276,107 @@ function genComment(node: CommentNode, context: CodegenContext) {
}
// control flow
function genIf(node: IfNode, context: CodegenContext) {}
function genIf(node: IfNode, context: CodegenContext) {
genIfBranch(node.branches[0], node.branches, 1, context)
}
function genFor(node: ForNode, context: CodegenContext) {}
function genIfBranch(
{ condition, children }: IfBranchNode,
branches: IfBranchNode[],
nextIndex: number,
context: CodegenContext
) {
if (condition) {
// v-if or v-else-if
context.push(`(${condition.content})`, condition)
context.push(`?`)
genChildren(children, context)
context.push(`:`)
if (nextIndex < branches.length) {
genIfBranch(branches[nextIndex], branches, nextIndex + 1, context)
} else {
context.push(`null`)
}
} else {
// v-else
__DEV__ && assert(nextIndex === branches.length)
genChildren(children, context)
}
}
function genFor(node: ForNode, context: CodegenContext) {
const { push } = context
const { source, keyAlias, valueAlias, objectIndexAlias, children } = node
push(`${RENDER_LIST_HELPER}(`, node)
genExpression(source, context)
context.push(`(`)
if (valueAlias) {
// not using genExpression here because these aliases can only be code
// that is valid in the function argument position, so the parse rule can
// be off and they don't need identifier prefixing anyway.
push(valueAlias.content, valueAlias)
push(`, `)
}
if (keyAlias) {
if (!valueAlias) {
push(`_, `)
}
push(keyAlias.content, keyAlias)
push(`, `)
}
if (objectIndexAlias) {
if (!keyAlias) {
if (!valueAlias) {
push(`_, `)
}
push(`_, `)
}
push(objectIndexAlias.content, objectIndexAlias)
}
context.push(`) => `)
genChildren(children, context)
context.push(`)`)
}
// JavaScript
function genCallExpression(
node: CallExpression,
context: CodegenContext,
multilines = node.arguments.length > 1
) {
context.push(node.callee + `(`, node)
multilines && context.indent()
genNodeList(node.arguments, context, multilines)
multilines && context.deindent()
context.push(`)`)
}
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
const { push, indent, deindent, newline } = context
const { properties } = node
const multilines = properties.length > 1
push(`{`, node)
multilines && indent()
for (let i = 0; i < properties.length; i++) {
const { key, value } = properties[i]
// key
genExpressionAsPropertyKey(key, context)
push(`: `)
// value
genExpression(value, context)
if (i < properties.length - 1) {
if (multilines) {
push(`,`)
newline()
} else {
push(`, `)
}
}
}
multilines && deindent()
push(`}`)
}
function genArrayExpression(node: ArrayExpression, context: CodegenContext) {
genNodeListAsArray(node.elements, context)
}

View File

@ -3,6 +3,9 @@ import { transform, TransformOptions } from './transform'
import { generate, CodegenOptions, CodegenResult } from './codegen'
import { RootNode } from './ast'
import { isString } from '@vue/shared'
import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor'
import { prepareElementForCodegen } from './transforms/element'
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
@ -15,7 +18,9 @@ export function compile(
transform(ast, {
...options,
nodeTransforms: [
// TODO include built-in core transforms
transformIf,
transformFor,
prepareElementForCodegen,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {

View File

@ -24,11 +24,16 @@ export const prepareElementForCodegen: NodeTransform = (node, context) => {
node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT
) {
const isComponent = node.tagType === ElementTypes.ELEMENT
const isComponent = node.tagType === ElementTypes.COMPONENT
const hasProps = node.props.length > 0
const hasChildren = node.children.length > 0
let runtimeDirectives: DirectiveNode[] | undefined
if (isComponent) {
// TODO inject import for `resolveComponent`
// TODO inject statement for resolving component
}
const args: CallExpression['arguments'] = [
// TODO inject resolveComponent dep to root
isComponent ? node.tag : `"${node.tag}"`
@ -49,9 +54,11 @@ export const prepareElementForCodegen: NodeTransform = (node, context) => {
}
const { loc } = node
// TODO inject import for `h`
const vnode = createCallExpression(`h`, args, loc)
if (runtimeDirectives) {
if (runtimeDirectives && runtimeDirectives.length) {
// TODO inject import for `applyDirectives`
node.codegenNode = createCallExpression(
`applyDirectives`,
[
@ -170,7 +177,8 @@ function createDirectiveArgs(
dir: DirectiveNode,
context: TransformContext
): ArrayExpression {
// TODO inject resolveDirective dep to root
// TODO inject import for `resolveDirective`
// TODO inject statement for resolving directive
const dirArgs: ArrayExpression['elements'] = [dir.name]
const { loc } = dir
if (dir.exp) dirArgs.push(dir.exp)

View File

@ -0,0 +1,9 @@
// - Parse expressions in templates into more detailed JavaScript ASTs so that
// source-maps are more accurate
//
// - Prefix identifiers with `_ctx.` so that they are accessed from the render
// context
//
// - This transform is only applied in non-browser builds because it relies on
// an additional JavaScript parser. In the browser, there is no source-map
// support and the code is wrapped in `with (this) { ... }`.

View File

@ -7,13 +7,17 @@ const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export const RENDER_LIST_HELPER = `renderList`
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
if (dir.exp) {
// TODO inject helper import
const aliases = parseAliasExpressions(dir.exp.content)
if (aliases) {
// TODO inject identifiers to context
context.replaceNode({
type: NodeTypes.FOR,
loc: node.loc,

View File

@ -61,6 +61,6 @@ export function advancePositionWithMutation(
export function assert(condition: boolean, msg?: string) {
if (!condition) {
throw new Error(msg || `unexpected parser condition`)
throw new Error(msg || `unexpected compiler condition`)
}
}

View File

@ -6,8 +6,6 @@ import {
import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard'
export * from '@vue/compiler-core'
export function compile(
template: string,
options: CompilerOptions = {}
@ -21,3 +19,5 @@ export function compile(
}
})
}
export * from '@vue/compiler-core'

View File

@ -1,7 +1,6 @@
import {
ComponentInternalInstance,
Data,
currentInstance,
Component,
SetupContext
} from './component'
@ -11,9 +10,7 @@ import {
isString,
isObject,
isArray,
EMPTY_OBJ,
capitalize,
camelize
EMPTY_OBJ
} from '@vue/shared'
import { computed } from './apiReactivity'
import { watch, WatchOptions, CleanupRegistrator } from './apiWatch'
@ -29,12 +26,10 @@ import {
onUnmounted
} from './apiLifecycle'
import { DebuggerEvent, reactive } from '@vue/reactivity'
import { warn } from './warning'
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
import { Directive } from './directives'
import { VNodeChild } from './vnode'
import { ComponentPublicInstance } from './componentPublicInstanceProxy'
import { currentRenderingInstance } from './componentRenderUtils'
interface ComponentOptionsBase<
Props,
@ -387,32 +382,3 @@ function applyMixins(
applyOptions(instance, mixins[i], true)
}
}
export function resolveComponent(name: string): Component | undefined {
return resolveAsset('components', name) as any
}
export function resolveDirective(name: string): Directive | undefined {
return resolveAsset('directives', name) as any
}
function resolveAsset(type: 'components' | 'directives', name: string) {
const instance = currentRenderingInstance || currentInstance
if (instance) {
let camelized
const registry = instance[type]
const res =
registry[name] ||
registry[(camelized = camelize(name))] ||
registry[capitalize(camelized)]
if (__DEV__ && !res) {
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
}
return res
} else if (__DEV__) {
warn(
`resolve${capitalize(type.slice(0, -1))} ` +
`can only be used in render() or setup().`
)
}
}

View File

@ -40,6 +40,7 @@ export const PublicInstanceProxyHandlers = {
// return the value from propsProxy for ref unwrapping and readonly
return (propsProxy as any)[key]
} else {
// TODO simplify this?
switch (key) {
case '$data':
return data
@ -79,6 +80,7 @@ export const PublicInstanceProxyHandlers = {
},
has(target: ComponentInternalInstance, key: string): boolean {
const { renderContext, data, props } = target
// TODO handle $xxx properties
return (
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
hasOwn(renderContext, key) ||

View File

@ -0,0 +1,2 @@
// TODO
export function renderList() {}

View File

@ -0,0 +1,34 @@
import { currentRenderingInstance } from '../componentRenderUtils'
import { currentInstance, Component } from '../component'
import { Directive } from '../directives'
import { camelize, capitalize } from '@vue/shared'
import { warn } from '../warning'
export function resolveComponent(name: string): Component | undefined {
return resolveAsset('components', name) as any
}
export function resolveDirective(name: string): Directive | undefined {
return resolveAsset('directives', name) as any
}
function resolveAsset(type: 'components' | 'directives', name: string) {
const instance = currentRenderingInstance || currentInstance
if (instance) {
let camelized
const registry = instance[type]
const res =
registry[name] ||
registry[(camelized = camelize(name))] ||
registry[capitalize(camelized)]
if (__DEV__ && !res) {
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
}
return res
} else if (__DEV__) {
warn(
`resolve${capitalize(type.slice(0, -1))} ` +
`can only be used in render() or setup().`
)
}
}

View File

@ -37,7 +37,8 @@ export {
// Internal, for compiler generated code
export { applyDirectives } from './directives'
export { resolveComponent, resolveDirective } from './componentOptions'
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
export { renderList } from './helpers/renderList'
// Internal, for integration with runtime compiler
export { registerRuntimeCompiler } from './component'

View File

@ -8,7 +8,7 @@ function compileToFunction(
options?: CompilerOptions
): RenderFunction {
const { code } = compile(template, options)
return new Function(`with(this){return ${code}}`) as RenderFunction
return new Function(code)() as RenderFunction
}
registerRuntimeCompiler(compileToFunction)