feat(compiler): handle runtime helper injection

This commit is contained in:
Evan You 2019-09-22 23:07:36 -04:00
parent 914087edea
commit 8076ce1f28
10 changed files with 272 additions and 30 deletions

View File

@ -65,6 +65,8 @@ export type ChildNode =
export interface RootNode extends Node { export interface RootNode extends Node {
type: NodeTypes.ROOT type: NodeTypes.ROOT
children: ChildNode[] children: ChildNode[]
imports: string[]
statements: string[]
} }
export interface ElementNode extends Node { export interface ElementNode extends Node {

View File

@ -17,7 +17,7 @@ import {
import { SourceMapGenerator, RawSourceMap } from 'source-map' import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { advancePositionWithMutation, assert } from './utils' import { advancePositionWithMutation, assert } from './utils'
import { isString, isArray } from '@vue/shared' import { isString, isArray } from '@vue/shared'
import { RENDER_LIST_HELPER } from './transforms/vFor' import { RENDER_LIST } from './runtimeConstants'
type CodegenNode = ChildNode | JSChildNode type CodegenNode = ChildNode | JSChildNode
@ -43,8 +43,6 @@ export interface CodegenContext extends Required<CodegenOptions> {
column: number column: number
offset: number offset: number
indentLevel: number indentLevel: number
imports: Set<string>
knownIdentifiers: Set<string>
map?: SourceMapGenerator map?: SourceMapGenerator
push(code: string, node?: CodegenNode): void push(code: string, node?: CodegenNode): void
indent(): void indent(): void
@ -70,8 +68,6 @@ function createCodegenContext(
line: 1, line: 1,
offset: 0, offset: 0,
indentLevel: 0, indentLevel: 0,
imports: new Set(),
knownIdentifiers: new Set(),
// lazy require source-map implementation, only in non-browser builds! // lazy require source-map implementation, only in non-browser builds!
map: __BROWSER__ map: __BROWSER__
@ -123,16 +119,24 @@ export function generate(
options: CodegenOptions = {} options: CodegenOptions = {}
): CodegenResult { ): CodegenResult {
const context = createCodegenContext(ast, options) const context = createCodegenContext(ast, options)
// TODO handle different output for module mode and IIFE mode
const { mode, push, useWith, indent, deindent } = context const { mode, push, useWith, indent, deindent } = context
const imports = ast.imports.join(', ')
if (mode === 'function') { if (mode === 'function') {
// TODO generate const declarations for helpers // generate const declarations for helpers
if (imports) {
push(`const { ${imports} } = Vue\n\n`)
}
push(`return `) push(`return `)
} else { } else {
// TODO generate import statements for helpers // generate import statements for helpers
if (imports) {
push(`import { ${imports} } from 'vue'\n\n`)
}
push(`export default `) push(`export default `)
} }
push(`function render() {`) push(`function render() {`)
// generate asset resolution statements
ast.statements.forEach(s => push(s + `\n`))
if (useWith) { if (useWith) {
indent() indent()
push(`with (this) {`) push(`with (this) {`)
@ -317,7 +321,7 @@ function genIfBranch(
function genFor(node: ForNode, context: CodegenContext) { function genFor(node: ForNode, context: CodegenContext) {
const { push } = context const { push } = context
const { source, keyAlias, valueAlias, objectIndexAlias, children } = node const { source, keyAlias, valueAlias, objectIndexAlias, children } = node
push(`${RENDER_LIST_HELPER}(`, node) push(`${RENDER_LIST}(`, node)
genExpression(source, context) genExpression(source, context)
push(`, (`) push(`, (`)
if (valueAlias) { if (valueAlias) {

View File

@ -82,6 +82,8 @@ export function parse(content: string, options: ParserOptions = {}): RootNode {
return { return {
type: NodeTypes.ROOT, type: NodeTypes.ROOT,
children: parseChildren(context, TextModes.DATA, []), children: parseChildren(context, TextModes.DATA, []),
imports: [],
statements: [],
loc: getSelection(context, start) loc: getSelection(context, start)
} }
} }

View File

@ -0,0 +1,8 @@
// Name mapping constants for runtime helpers that need to be imported in
// generated code. Make sure these are correctly exported in the runtime!
export const CREATE_ELEMENT = `h`
export const RESOLVE_COMPONENT = `resolveComponent`
export const RESOLVE_DIRECTIVE = `resolveDirective`
export const APPLY_DIRECTIVES = `applyDirectives`
export const RENDER_LIST = `renderList`
export const CAPITALIZE = `capitalize`

View File

@ -43,6 +43,9 @@ export interface TransformOptions {
} }
export interface TransformContext extends Required<TransformOptions> { export interface TransformContext extends Required<TransformOptions> {
imports: Set<string>
statements: string[]
identifiers: { [name: string]: true }
parent: ParentNode parent: ParentNode
ancestors: ParentNode[] ancestors: ParentNode[]
childIndex: number childIndex: number
@ -52,16 +55,14 @@ export interface TransformContext extends Required<TransformOptions> {
onNodeRemoved: () => void onNodeRemoved: () => void
} }
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseChildren(root, context)
}
function createTransformContext( function createTransformContext(
root: RootNode, root: RootNode,
options: TransformOptions options: TransformOptions
): TransformContext { ): TransformContext {
const context: TransformContext = { const context: TransformContext = {
imports: new Set(),
statements: [],
identifiers: {},
nodeTransforms: options.nodeTransforms || [], nodeTransforms: options.nodeTransforms || [],
directiveTransforms: options.directiveTransforms || {}, directiveTransforms: options.directiveTransforms || {},
onError: options.onError || defaultOnError, onError: options.onError || defaultOnError,
@ -103,11 +104,21 @@ function createTransformContext(
return context return context
} }
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseChildren(root, context)
root.imports = [...context.imports]
root.statements = context.statements
}
export function traverseChildren( export function traverseChildren(
parent: ParentNode, parent: ParentNode,
context: TransformContext context: TransformContext
) { ) {
// ancestors and identifiers need to be cached here since they may get
// replaced during a child's traversal
const ancestors = context.ancestors.concat(parent) const ancestors = context.ancestors.concat(parent)
const identifiers = context.identifiers
let i = 0 let i = 0
const nodeRemoved = () => { const nodeRemoved = () => {
i-- i--
@ -117,6 +128,7 @@ export function traverseChildren(
context.ancestors = ancestors context.ancestors = ancestors
context.childIndex = i context.childIndex = i
context.onNodeRemoved = nodeRemoved context.onNodeRemoved = nodeRemoved
context.identifiers = identifiers
traverseNode((context.currentNode = parent.children[i]), context) traverseNode((context.currentNode = parent.children[i]), context)
} }
} }

View File

@ -16,6 +16,14 @@ import {
} from '../ast' } from '../ast'
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import {
CREATE_ELEMENT,
APPLY_DIRECTIVES,
RESOLVE_DIRECTIVE,
RESOLVE_COMPONENT
} from '../runtimeConstants'
const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
// generate a JavaScript AST for this element's codegen // generate a JavaScript AST for this element's codegen
export const prepareElementForCodegen: NodeTransform = (node, context) => { export const prepareElementForCodegen: NodeTransform = (node, context) => {
@ -28,15 +36,20 @@ export const prepareElementForCodegen: NodeTransform = (node, context) => {
const hasProps = node.props.length > 0 const hasProps = node.props.length > 0
const hasChildren = node.children.length > 0 const hasChildren = node.children.length > 0
let runtimeDirectives: DirectiveNode[] | undefined let runtimeDirectives: DirectiveNode[] | undefined
let componentIdentifier: string | undefined
if (isComponent) { if (isComponent) {
// TODO inject import for `resolveComponent` context.imports.add(RESOLVE_COMPONENT)
// TODO inject statement for resolving component componentIdentifier = `_component_${toValidId(node.tag)}`
context.statements.push(
`const ${componentIdentifier} = ${RESOLVE_COMPONENT}(${JSON.stringify(
node.tag
)})`
)
} }
const args: CallExpression['arguments'] = [ const args: CallExpression['arguments'] = [
// TODO inject resolveComponent dep to root isComponent ? componentIdentifier! : `"${node.tag}"`
isComponent ? node.tag : `"${node.tag}"`
] ]
// props // props
if (hasProps) { if (hasProps) {
@ -54,13 +67,13 @@ export const prepareElementForCodegen: NodeTransform = (node, context) => {
} }
const { loc } = node const { loc } = node
// TODO inject import for `h` context.imports.add(CREATE_ELEMENT)
const vnode = createCallExpression(`h`, args, loc) const vnode = createCallExpression(CREATE_ELEMENT, args, loc)
if (runtimeDirectives && runtimeDirectives.length) { if (runtimeDirectives && runtimeDirectives.length) {
// TODO inject import for `applyDirectives` context.imports.add(APPLY_DIRECTIVES)
node.codegenNode = createCallExpression( node.codegenNode = createCallExpression(
`applyDirectives`, APPLY_DIRECTIVES,
[ [
vnode, vnode,
createArrayExpression( createArrayExpression(
@ -174,9 +187,16 @@ function createDirectiveArgs(
dir: DirectiveNode, dir: DirectiveNode,
context: TransformContext context: TransformContext
): ArrayExpression { ): ArrayExpression {
// TODO inject import for `resolveDirective` // inject import for `resolveDirective`
// TODO inject statement for resolving directive context.imports.add(RESOLVE_DIRECTIVE)
const dirArgs: ArrayExpression['elements'] = [dir.name] // inject statement for resolving directive
const dirIdentifier = `_directive_${toValidId(dir.name)}`
context.statements.push(
`const ${dirIdentifier} = _${RESOLVE_DIRECTIVE}(${JSON.stringify(
dir.name
)})`
)
const dirArgs: ArrayExpression['elements'] = [dirIdentifier]
const { loc } = dir const { loc } = dir
if (dir.exp) dirArgs.push(dir.exp) if (dir.exp) dirArgs.push(dir.exp)
if (dir.arg) dirArgs.push(dir.arg) if (dir.arg) dirArgs.push(dir.arg)

View File

@ -2,18 +2,17 @@ import { createStructuralDirectiveTransform } from '../transform'
import { NodeTypes, ExpressionNode, createExpression } 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'
import { RENDER_LIST } from '../runtimeConstants'
const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/ const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g const stripParensRE = /^\(|\)$/g
export const RENDER_LIST_HELPER = `renderList`
export const transformFor = createStructuralDirectiveTransform( export const transformFor = createStructuralDirectiveTransform(
'for', 'for',
(node, dir, context) => { (node, dir, context) => {
if (dir.exp) { if (dir.exp) {
// TODO inject helper import context.imports.add(RENDER_LIST)
const aliases = parseAliasExpressions(dir.exp.content) const aliases = parseAliasExpressions(dir.exp.content)
if (aliases) { if (aliases) {

View File

@ -1,6 +1,7 @@
import { DirectiveTransform } from '../transform' import { DirectiveTransform } from '../transform'
import { createObjectProperty, createExpression } from '../ast' import { createObjectProperty, createExpression } from '../ast'
import { capitalize } from '@vue/shared' import { capitalize } from '@vue/shared'
import { CAPITALIZE } from '../runtimeConstants'
// v-on without arg is handled directly in ./element.ts due to it affecting // v-on without arg is handled directly in ./element.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-on // codegen for the entire props object. This transform here is only for v-on
@ -9,8 +10,7 @@ export const transformOn: DirectiveTransform = (dir, context) => {
const arg = dir.arg! const arg = dir.arg!
const eventName = arg.isStatic const eventName = arg.isStatic
? createExpression(`on${capitalize(arg.content)}`, true, arg.loc) ? createExpression(`on${capitalize(arg.content)}`, true, arg.loc)
: // TODO inject capitalize helper : createExpression(`'on' + ${CAPITALIZE}(${arg.content})`, false, arg.loc)
createExpression(`'on' + capitalize(${arg.content})`, false, arg.loc)
// TODO .once modifier handling since it is platform agnostic // TODO .once modifier handling since it is platform agnostic
// other modifiers are handled in compiler-dom // other modifiers are handled in compiler-dom
return { return {

View File

@ -39,6 +39,7 @@ export {
export { applyDirectives } from './directives' export { applyDirectives } from './directives'
export { resolveComponent, resolveDirective } from './helpers/resolveAssets' export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
export { renderList } from './helpers/renderList' export { renderList } from './helpers/renderList'
export { capitalize } from '@vue/shared'
// Internal, for integration with runtime compiler // Internal, for integration with runtime compiler
export { registerRuntimeCompiler } from './component' export { registerRuntimeCompiler } from './component'