import { CodegenOptions } from './options' import { RootNode, TemplateChildNode, TextNode, CommentNode, ExpressionNode, NodeTypes, JSChildNode, CallExpression, ArrayExpression, ObjectExpression, Position, InterpolationNode, CompoundExpressionNode, SimpleExpressionNode, FunctionExpression, ConditionalExpression, CacheExpression, locStub, SSRCodegenNode, TemplateLiteral, IfStatement, AssignmentExpression, ReturnStatement, VNodeCall } from './ast' import { SourceMapGenerator, RawSourceMap } from 'source-map' import { advancePositionWithMutation, assert, isSimpleIdentifier, loadDep, toValidAssetId } from './utils' import { isString, isArray, isSymbol } from '@vue/shared' import { helperNameMap, TO_DISPLAY_STRING, CREATE_VNODE, RESOLVE_COMPONENT, RESOLVE_DIRECTIVE, SET_BLOCK_TRACKING, CREATE_COMMENT, CREATE_TEXT, PUSH_SCOPE_ID, POP_SCOPE_ID, WITH_SCOPE_ID, WITH_DIRECTIVES, CREATE_BLOCK, OPEN_BLOCK, CREATE_STATIC } from './runtimeHelpers' import { ImportItem } from './transform' type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode export interface CodegenResult { code: string ast: RootNode map?: RawSourceMap } export interface CodegenContext extends Required { source: string code: string line: number column: number offset: number indentLevel: number map?: SourceMapGenerator helper(key: symbol): string push(code: string, node?: CodegenNode): void indent(): void deindent(withoutNewLine?: boolean): void newline(): void } function createCodegenContext( ast: RootNode, { mode = 'function', prefixIdentifiers = mode === 'module', sourceMap = false, filename = `template.vue.html`, scopeId = null, optimizeBindings = false, runtimeGlobalName = `Vue`, runtimeModuleName = `vue`, ssr = false }: CodegenOptions ): CodegenContext { const context: CodegenContext = { mode, prefixIdentifiers, sourceMap, filename, scopeId, optimizeBindings, runtimeGlobalName, runtimeModuleName, ssr, source: ast.loc.source, code: ``, column: 1, line: 1, offset: 0, indentLevel: 0, map: undefined, helper(key) { return `_${helperNameMap[key]}` }, push(code, node) { context.code += code if (!__BROWSER__ && context.map) { if (node) { let name if (node.type === NodeTypes.SIMPLE_EXPRESSION && !node.isStatic) { const content = node.content.replace(/^_ctx\./, '') if (content !== node.content && isSimpleIdentifier(content)) { name = content } } addMapping(node.loc.start, name) } advancePositionWithMutation(context, code) if (node && node.loc !== locStub) { addMapping(node.loc.end) } } }, indent() { newline(++context.indentLevel) }, deindent(withoutNewLine = false) { if (withoutNewLine) { --context.indentLevel } else { newline(--context.indentLevel) } }, newline() { newline(context.indentLevel) } } function newline(n: number) { context.push('\n' + ` `.repeat(n)) } function addMapping(loc: Position, name?: string) { context.map!.addMapping({ name, source: context.filename, original: { line: loc.line, column: loc.column - 1 // source-map column is 0 based }, generated: { line: context.line, column: context.column - 1 } }) } if (!__BROWSER__ && sourceMap) { // lazy require source-map implementation, only in non-browser builds context.map = new (loadDep('source-map')).SourceMapGenerator() context.map!.setSourceContent(filename, context.source) } return context } export function generate( ast: RootNode, options: CodegenOptions = {} ): CodegenResult { const context = createCodegenContext(ast, options) const { mode, push, prefixIdentifiers, indent, deindent, newline, scopeId, ssr } = context const hasHelpers = ast.helpers.length > 0 const useWithBlock = !prefixIdentifiers && mode !== 'module' const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' // preambles if (!__BROWSER__ && mode === 'module') { genModulePreamble(ast, context, genScopeId) } else { genFunctionPreamble(ast, context) } // enter render function if (genScopeId && !ssr) { push(`const render = _withId(`) } if (!ssr) { push(`function render(_ctx, _cache) {`) } else { push(`function ssrRender(_ctx, _push, _parent) {`) } indent() if (useWithBlock) { push(`with (this) {`) indent() // function mode const declarations should be inside with block // also they should be renamed to avoid collision with user properties if (hasHelpers) { push( `const { ${ast.helpers .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`) .join(', ')} } = _Vue` ) push(`\n`) newline() } } // generate asset resolution statements if (ast.components.length) { genAssets(ast.components, 'component', context) if (ast.directives.length || ast.temps > 0) { newline() } } if (ast.directives.length) { genAssets(ast.directives, 'directive', context) if (ast.temps > 0) { newline() } } if (ast.temps > 0) { push(`let `) for (let i = 0; i < ast.temps; i++) { push(`${i > 0 ? `, ` : ``}_temp${i}`) } } if (ast.components.length || ast.directives.length || ast.temps) { push(`\n`) newline() } // generate the VNode tree expression if (!ssr) { push(`return `) } if (ast.codegenNode) { genNode(ast.codegenNode, context) } else { push(`null`) } if (useWithBlock) { deindent() push(`}`) } deindent() push(`}`) if (genScopeId && !ssr) { push(`)`) } return { ast, code: context.code, // SourceMapGenerator does have toJSON() method but it's not in the types map: context.map ? (context.map as any).toJSON() : undefined } } function genFunctionPreamble(ast: RootNode, context: CodegenContext) { const { ssr, prefixIdentifiers, push, newline, runtimeModuleName, runtimeGlobalName } = context const VueBinding = !__BROWSER__ && ssr ? `require(${JSON.stringify(runtimeModuleName)})` : runtimeGlobalName const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}` // Generate const declaration for helpers // In prefix mode, we place the const declaration at top so it's done // only once; But if we not prefixing, we place the declaration inside the // with block so it doesn't incur the `in` check cost for every helper access. if (ast.helpers.length > 0) { if (!__BROWSER__ && prefixIdentifiers) { push( `const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n` ) } else { // "with" mode. // save Vue in a separate variable to avoid collision push(`const _Vue = ${VueBinding}\n`) // in "with" mode, helpers are declared inside the with block to avoid // has check cost, but hoists are lifted out of the function - we need // to provide the helper here. if (ast.hoists.length) { const staticHelpers = [ CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT, CREATE_STATIC ] .filter(helper => ast.helpers.includes(helper)) .map(aliasHelper) .join(', ') push(`const { ${staticHelpers} } = _Vue\n`) } } } // generate variables for ssr helpers if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) { // ssr guaruntees prefixIdentifier: true push( `const { ${ast.ssrHelpers .map(aliasHelper) .join(', ')} } = require("@vue/server-renderer")\n` ) } genHoists(ast.hoists, context) newline() push(`return `) } function genModulePreamble( ast: RootNode, context: CodegenContext, genScopeId: boolean ) { const { push, helper, newline, scopeId, optimizeBindings, runtimeModuleName } = context if (genScopeId) { ast.helpers.push(WITH_SCOPE_ID) if (ast.hoists.length) { ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID) } } // generate import statements for helpers if (ast.helpers.length) { if (optimizeBindings) { // when bundled with webpack with code-split, calling an import binding // as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`, // incurring both payload size increase and potential perf overhead. // therefore we assign the imports to vairables (which is a constant ~50b // cost per-component instead of scaling with template size) push( `import { ${ast.helpers .map(s => helperNameMap[s]) .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n` ) push( `\n// Binding optimization for webpack code-split\nconst ${ast.helpers .map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`) .join(', ')}\n` ) } else { push( `import { ${ast.helpers .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`) .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n` ) } } if (ast.ssrHelpers && ast.ssrHelpers.length) { push( `import { ${ast.ssrHelpers .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`) .join(', ')} } from "@vue/server-renderer"\n` ) } if (ast.imports.length) { genImports(ast.imports, context) newline() } if (genScopeId) { push(`const _withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`) newline() } genHoists(ast.hoists, context) newline() push(`export `) } function genAssets( assets: string[], type: 'component' | 'directive', { helper, push, newline }: CodegenContext ) { const resolver = helper( type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE ) for (let i = 0; i < assets.length; i++) { const id = assets[i] push( `const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})` ) if (i < assets.length - 1) { newline() } } } function genHoists(hoists: JSChildNode[], context: CodegenContext) { if (!hoists.length) { return } const { push, newline, helper, scopeId, mode } = context const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function' newline() // push scope Id before initilaizing hoisted vnodes so that these vnodes // get the proper scopeId as well. if (genScopeId) { push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`) newline() } hoists.forEach((exp, i) => { push(`const _hoisted_${i + 1} = `) genNode(exp, context) newline() }) if (genScopeId) { push(`${helper(POP_SCOPE_ID)}()`) newline() } } function genImports(importsOptions: ImportItem[], context: CodegenContext) { if (!importsOptions.length) { return } importsOptions.forEach(imports => { context.push(`import `) genNode(imports.exp, context) context.push(` from '${imports.path}'`) context.newline() }) } function isText(n: string | CodegenNode) { return ( isString(n) || n.type === NodeTypes.SIMPLE_EXPRESSION || n.type === NodeTypes.TEXT || n.type === NodeTypes.INTERPOLATION || n.type === NodeTypes.COMPOUND_EXPRESSION ) } function genNodeListAsArray( nodes: (string | CodegenNode | TemplateChildNode[])[], context: CodegenContext ) { const multilines = nodes.length > 3 || ((!__BROWSER__ || __DEV__) && nodes.some(n => isArray(n) || !isText(n))) context.push(`[`) multilines && context.indent() genNodeList(nodes, context, multilines) multilines && context.deindent() context.push(`]`) } function genNodeList( nodes: (string | symbol | CodegenNode | TemplateChildNode[])[], context: CodegenContext, multilines: boolean = false, comma: boolean = true ) { const { push, newline } = context for (let i = 0; i < nodes.length; i++) { const node = nodes[i] if (isString(node)) { push(node) } else if (isArray(node)) { genNodeListAsArray(node, context) } else { genNode(node, context) } if (i < nodes.length - 1) { if (multilines) { comma && push(',') newline() } else { comma && push(', ') } } } } function genNode(node: CodegenNode | symbol | string, context: CodegenContext) { if (isString(node)) { context.push(node) return } if (isSymbol(node)) { context.push(context.helper(node)) return } switch (node.type) { case NodeTypes.ELEMENT: case NodeTypes.IF: case NodeTypes.FOR: __DEV__ && assert( node.codegenNode != null, `Codegen node is missing for element/if/for node. ` + `Apply appropriate transforms first.` ) genNode(node.codegenNode!, context) break case NodeTypes.TEXT: genText(node, context) break case NodeTypes.SIMPLE_EXPRESSION: genExpression(node, context) break case NodeTypes.INTERPOLATION: genInterpolation(node, context) break case NodeTypes.TEXT_CALL: genNode(node.codegenNode, context) break case NodeTypes.COMPOUND_EXPRESSION: genCompoundExpression(node, context) break case NodeTypes.COMMENT: genComment(node, context) break case NodeTypes.VNODE_CALL: genVNodeCall(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) break case NodeTypes.JS_FUNCTION_EXPRESSION: genFunctionExpression(node, context) break case NodeTypes.JS_CONDITIONAL_EXPRESSION: genConditionalExpression(node, context) break case NodeTypes.JS_CACHE_EXPRESSION: genCacheExpression(node, context) break // SSR only types case NodeTypes.JS_BLOCK_STATEMENT: !__BROWSER__ && genNodeList(node.body, context, true, false) break case NodeTypes.JS_TEMPLATE_LITERAL: !__BROWSER__ && genTemplateLiteral(node, context) break case NodeTypes.JS_IF_STATEMENT: !__BROWSER__ && genIfStatement(node, context) break case NodeTypes.JS_ASSIGNMENT_EXPRESSION: !__BROWSER__ && genAssignmentExpression(node, context) break case NodeTypes.JS_RETURN_STATEMENT: !__BROWSER__ && genReturnStatement(node, context) break /* istanbul ignore next */ default: if (__DEV__) { assert(false, `unhandled codegen node type: ${(node as any).type}`) // make sure we exhaust all possible types const exhaustiveCheck: never = node return exhaustiveCheck } } } function genText( node: TextNode | SimpleExpressionNode, context: CodegenContext ) { context.push(JSON.stringify(node.content), node) } function genExpression(node: SimpleExpressionNode, context: CodegenContext) { const { content, isStatic } = node context.push(isStatic ? JSON.stringify(content) : content, node) } function genInterpolation(node: InterpolationNode, context: CodegenContext) { const { push, helper } = context push(`${helper(TO_DISPLAY_STRING)}(`) genNode(node.content, context) push(`)`) } function genCompoundExpression( node: CompoundExpressionNode, context: CodegenContext ) { for (let i = 0; i < node.children!.length; i++) { const child = node.children![i] if (isString(child)) { context.push(child) } else { genNode(child, context) } } } function genExpressionAsPropertyKey( node: ExpressionNode, context: CodegenContext ) { const { push } = context if (node.type === NodeTypes.COMPOUND_EXPRESSION) { push(`[`) genCompoundExpression(node, context) push(`]`) } else if (node.isStatic) { // only quote keys if necessary const text = isSimpleIdentifier(node.content) ? node.content : JSON.stringify(node.content) push(text, node) } else { push(`[${node.content}]`, node) } } function genComment(node: CommentNode, context: CodegenContext) { if (__DEV__) { const { push, helper } = context push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node) } } function genVNodeCall(node: VNodeCall, context: CodegenContext) { const { push, helper } = context const { tag, props, children, patchFlag, dynamicProps, directives, isBlock, isForBlock } = node if (directives) { push(helper(WITH_DIRECTIVES) + `(`) } if (isBlock) { push(`(${helper(OPEN_BLOCK)}(${isForBlock ? `true` : ``}), `) } push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node) genNodeList( genNullableArgs([tag, props, children, patchFlag, dynamicProps]), context ) push(`)`) if (isBlock) { push(`)`) } if (directives) { push(`, `) genNode(directives, context) push(`)`) } } function genNullableArgs(args: any[]): CallExpression['arguments'] { let i = args.length while (i--) { if (args[i] != null) break } return args.slice(0, i + 1).map(arg => arg || `null`) } // JavaScript function genCallExpression(node: CallExpression, context: CodegenContext) { const callee = isString(node.callee) ? node.callee : context.helper(node.callee) context.push(callee + `(`, node) genNodeList(node.arguments, context) context.push(`)`) } function genObjectExpression(node: ObjectExpression, context: CodegenContext) { const { push, indent, deindent, newline } = context const { properties } = node if (!properties.length) { push(`{}`, node) return } const multilines = properties.length > 1 || ((!__BROWSER__ || __DEV__) && properties.some(p => p.value.type !== NodeTypes.SIMPLE_EXPRESSION)) push(multilines ? `{` : `{ `) multilines && indent() for (let i = 0; i < properties.length; i++) { const { key, value } = properties[i] // key genExpressionAsPropertyKey(key, context) push(`: `) // value genNode(value, context) if (i < properties.length - 1) { // will only reach this if it's multilines push(`,`) newline() } } multilines && deindent() push(multilines ? `}` : ` }`) } function genArrayExpression(node: ArrayExpression, context: CodegenContext) { genNodeListAsArray(node.elements, context) } function genFunctionExpression( node: FunctionExpression, context: CodegenContext ) { const { push, indent, deindent, scopeId, mode } = context const { params, returns, body, newline, isSlot } = node // slot functions also need to push scopeId before rendering its content const genScopeId = !__BROWSER__ && isSlot && scopeId != null && mode !== 'function' if (genScopeId) { push(`_withId(`) } push(`(`, node) if (isArray(params)) { genNodeList(params, context) } else if (params) { genNode(params, context) } push(`) => `) if (newline || body) { push(`{`) indent() } if (returns) { if (newline) { push(`return `) } if (isArray(returns)) { genNodeListAsArray(returns, context) } else { genNode(returns, context) } } else if (body) { genNode(body, context) } if (newline || body) { deindent() push(`}`) } if (genScopeId) { push(`)`) } } function genConditionalExpression( node: ConditionalExpression, context: CodegenContext ) { const { test, consequent, alternate, newline: needNewline } = node const { push, indent, deindent, newline } = context if (test.type === NodeTypes.SIMPLE_EXPRESSION) { const needsParens = !isSimpleIdentifier(test.content) needsParens && push(`(`) genExpression(test, context) needsParens && push(`)`) } else { push(`(`) genNode(test, context) push(`)`) } needNewline && indent() context.indentLevel++ needNewline || push(` `) push(`? `) genNode(consequent, context) context.indentLevel-- needNewline && newline() needNewline || push(` `) push(`: `) const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION if (!isNested) { context.indentLevel++ } genNode(alternate, context) if (!isNested) { context.indentLevel-- } needNewline && deindent(true /* without newline */) } function genCacheExpression(node: CacheExpression, context: CodegenContext) { const { push, helper, indent, deindent, newline } = context push(`_cache[${node.index}] || (`) if (node.isVNode) { indent() push(`${helper(SET_BLOCK_TRACKING)}(-1),`) newline() } push(`_cache[${node.index}] = `) genNode(node.value, context) if (node.isVNode) { push(`,`) newline() push(`${helper(SET_BLOCK_TRACKING)}(1),`) newline() push(`_cache[${node.index}]`) deindent() } push(`)`) } function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) { const { push, indent, deindent } = context push('`') const l = node.elements.length const multilines = l > 3 for (let i = 0; i < l; i++) { const e = node.elements[i] if (isString(e)) { push(e.replace(/`/g, '\\`')) } else { push('${') if (multilines) indent() genNode(e, context) if (multilines) deindent() push('}') } } push('`') } function genIfStatement(node: IfStatement, context: CodegenContext) { const { push, indent, deindent } = context const { test, consequent, alternate } = node push(`if (`) genNode(test, context) push(`) {`) indent() genNode(consequent, context) deindent() push(`}`) if (alternate) { push(` else `) if (alternate.type === NodeTypes.JS_IF_STATEMENT) { genIfStatement(alternate, context) } else { push(`{`) indent() genNode(alternate, context) deindent() push(`}`) } } } function genAssignmentExpression( node: AssignmentExpression, context: CodegenContext ) { genNode(node.left, context) context.push(` = `) genNode(node.right, context) } function genReturnStatement( { returns }: ReturnStatement, context: CodegenContext ) { context.push(`return `) if (isArray(returns)) { genNodeListAsArray(returns, context) } else { genNode(returns, context) } }