import { RootNode, CodegenContext, CodegenResult, helperNameMap, CompoundExpressionNode, SimpleExpressionNode, InterpolationNode, TextNode, NodeTypes, TO_DISPLAY_STRING, } from '@vue/compiler-core' import { isString, isSymbol } from '@vue/shared' import { CodegenOptions, CodegenScope, CodegenVForScope } from './options' interface MPCodegenContext extends Omit< CodegenContext, 'sourceMap' | 'optimizeImports' | 'ssrRuntimeModuleName' | 'ssr' | 'inSSR' > {} export function generate( ast: RootNode, options: CodegenOptions & { onContextCreated?: (context: MPCodegenContext) => void } = {} ): CodegenResult { const context = createCodegenContext(ast, options) const { push, indent, deindent, mode } = context const isSetupInlined = !!options.inline // preambles // in setup() inline mode, the preamble is generated in a sub context // and returned separately. const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context if (mode === 'module') { genModulePreamble(ast, preambleContext, isSetupInlined) } else { genFunctionPreamble(ast, preambleContext) } // enter render function const functionName = `render` const args = ['_ctx', '_cache'] if (options.bindingMetadata && !options.inline) { // binding optimization args args.push('$props', '$setup', '$data', '$options') } const signature = options.isTS ? args.map((arg) => `${arg}: any`).join(',') : args.join(', ') if (isSetupInlined) { push(`(${signature}) => {`) } else { push(`function ${functionName}(${signature}) {`) } indent() deindent() push(`}`) return { code: context.code, preamble: '', ast, } } function createCodegenContext( ast: RootNode, { mode = 'function', prefixIdentifiers = mode === 'module', filename = `template.vue.html`, scopeId = null, runtimeGlobalName = `Vue`, runtimeModuleName = `vue`, isTS = false, }: CodegenOptions ) { const context: MPCodegenContext = { mode, prefixIdentifiers, filename, scopeId, runtimeGlobalName, runtimeModuleName, isTS, source: ast.loc.source, code: ``, column: 1, line: 1, offset: 0, indentLevel: 0, pure: false, map: undefined, helper(key) { return `_${helperNameMap[key]}` }, push(code, node) { context.code += code }, 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)) } return context } function genModulePreamble( ast: RootNode, context: MPCodegenContext, inline?: boolean ) { const { push, newline, runtimeModuleName } = context // generate import statements for helpers if (ast.helpers.length) { push( `import { ${ast.helpers .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`) .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n` ) } newline() if (!inline) { push(`export `) } } function genFunctionPreamble(ast: RootNode, context: MPCodegenContext) { const { prefixIdentifiers, push, newline, runtimeGlobalName } = context const VueBinding = 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 (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. } } newline() push(`return `) } type CodegenNode = | SimpleExpressionNode | CompoundExpressionNode | InterpolationNode | TextNode | string | symbol interface GenNodeContext { code: string helper(key: symbol): string push(code: string, node?: CodegenNode): void } function createGenNodeContext() { const context: GenNodeContext = { code: '', helper(key) { return `_${helperNameMap[key]}` }, push(code) { context.code += code }, } return context } export function genNode( node: CodegenNode | symbol | string, context?: GenNodeContext ) { if (!context) { context = createGenNodeContext() } if (isString(node)) { context.push(node) return context } if (isSymbol(node)) { context.push(context.helper(node)) return context } switch (node.type) { case NodeTypes.TEXT: genText(node, context) break case NodeTypes.SIMPLE_EXPRESSION: genExpression(node, context) break case NodeTypes.INTERPOLATION: genInterpolation(node, context) break case NodeTypes.COMPOUND_EXPRESSION: genCompoundExpression(node, context) break } return context } function genText( node: TextNode | SimpleExpressionNode, context: GenNodeContext ) { context.push(JSON.stringify(node.content), node) } function genExpression(node: SimpleExpressionNode, context: GenNodeContext) { const { content, isStatic } = node context.push(isStatic ? JSON.stringify(content) : content, node) } function genInterpolation(node: InterpolationNode, context: GenNodeContext) { const { push, helper } = context push(`${helper(TO_DISPLAY_STRING)}(`) genNode(node.content, context) push(`)`) } function genCompoundExpression( node: CompoundExpressionNode, context: GenNodeContext ) { for (let i = 0; i < node.children!.length; i++) { const child = node.children![i] if (isString(child)) { context.push(child) } else { genNode(child, context) } } }