/* @flow */ // The SSR codegen is essentially extending the default codegen to handle // SSR-optimizable nodes and turn them into string render fns. In cases where // a node is not optimizable it simply falls back to the default codegen. import { genIf, genFor, genData, genText, genElement, genChildren, CodegenState } from 'compiler/codegen/index' import { genAttrSegments, genDOMPropSegments, genClassSegments, genStyleSegments, applyModelTransform } from './modules' import { escape } from 'web/server/util' import { optimizability } from './optimizer' import type { CodegenResult } from 'compiler/codegen/index' export type StringSegment = { type: number; value: string; }; // segment types export const RAW = 0 export const INTERPOLATION = 1 export const EXPRESSION = 2 export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genSSRElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } } function genSSRElement (el: ASTElement, state: CodegenState): string { if (el.for && !el.forProcessed) { return genFor(el, state, genSSRElement) } else if (el.if && !el.ifProcessed) { return genIf(el, state, genSSRElement) } else if (el.tag === 'template' && !el.slotTarget) { return el.ssrOptimizability === optimizability.FULL ? genChildrenAsStringNode(el, state) : genSSRChildren(el, state) || 'void 0' } switch (el.ssrOptimizability) { case optimizability.FULL: // stringify whole tree return genStringElement(el, state) case optimizability.SELF: // stringify self and check children return genStringElementWithChildren(el, state) case optimizability.CHILDREN: // generate self as VNode and stringify children return genNormalElement(el, state, true) case optimizability.PARTIAL: // generate self as VNode and check children return genNormalElement(el, state, false) default: // bail whole tree return genElement(el, state) } } function genNormalElement (el, state, stringifyChildren) { const data = el.plain ? undefined : genData(el, state) const children = stringifyChildren ? `[${genChildrenAsStringNode(el, state)}]` : genSSRChildren(el, state, true) return `_c('${el.tag}'${ data ? `,${data}` : '' }${ children ? `,${children}` : '' })` } function genSSRChildren (el, state, checkSkip) { return genChildren(el, state, checkSkip, genSSRElement, genSSRNode) } function genSSRNode (el, state) { return el.type === 1 ? genSSRElement(el, state) : genText(el) } function genChildrenAsStringNode (el, state) { return el.children.length ? `_ssrNode(${flattenSegments(childrenToSegments(el, state))})` : '' } function genStringElement (el, state) { return `_ssrNode(${elementToString(el, state)})` } function genStringElementWithChildren (el, state) { const children = genSSRChildren(el, state, true) return `_ssrNode(${ flattenSegments(elementToOpenTagSegments(el, state)) },""${ children ? `,${children}` : '' })` } function elementToString (el, state) { return `(${flattenSegments(elementToSegments(el, state))})` } function elementToSegments (el, state): Array { // v-for / v-if if (el.for && !el.forProcessed) { el.forProcessed = true return [{ type: EXPRESSION, value: genFor(el, state, elementToString, '_ssrList') }] } else if (el.if && !el.ifProcessed) { el.ifProcessed = true return [{ type: EXPRESSION, value: genIf(el, state, elementToString, '""') }] } else if (el.tag === 'template') { return childrenToSegments(el, state) } const openSegments = elementToOpenTagSegments(el, state) const childrenSegments = childrenToSegments(el, state) const { isUnaryTag } = state.options const close = (isUnaryTag && isUnaryTag(el.tag)) ? [] : [{ type: RAW, value: `` }] return openSegments.concat(childrenSegments, close) } function elementToOpenTagSegments (el, state): Array { applyModelTransform(el, state) let binding const segments = [{ type: RAW, value: `<${el.tag}` }] // attrs if (el.attrs) { segments.push.apply(segments, genAttrSegments(el.attrs)) } // domProps if (el.props) { segments.push.apply(segments, genDOMPropSegments(el.props, el.attrs)) } // v-bind="object" if ((binding = el.attrsMap['v-bind'])) { segments.push({ type: EXPRESSION, value: `_ssrAttrs(${binding})` }) } // v-bind.prop="object" if ((binding = el.attrsMap['v-bind.prop'])) { segments.push({ type: EXPRESSION, value: `_ssrDOMProps(${binding})` }) } // class if (el.staticClass || el.classBinding) { segments.push.apply( segments, genClassSegments(el.staticClass, el.classBinding) ) } // style & v-show if (el.staticStyle || el.styleBinding || el.attrsMap['v-show']) { segments.push.apply( segments, genStyleSegments( el.attrsMap.style, el.staticStyle, el.styleBinding, el.attrsMap['v-show'] ) ) } // _scopedId if (state.options.scopeId) { segments.push({ type: RAW, value: ` ${state.options.scopeId}` }) } segments.push({ type: RAW, value: `>` }) return segments } function childrenToSegments (el, state): Array { let binding if ((binding = el.attrsMap['v-html'])) { return [{ type: EXPRESSION, value: `_s(${binding})` }] } if ((binding = el.attrsMap['v-text'])) { return [{ type: INTERPOLATION, value: `_s(${binding})` }] } if (el.tag === 'textarea' && (binding = el.attrsMap['v-model'])) { return [{ type: INTERPOLATION, value: `_s(${binding})` }] } return el.children ? nodesToSegments(el.children, state) : [] } function nodesToSegments ( children: Array, state: CodegenState ): Array { const segments = [] for (let i = 0; i < children.length; i++) { const c = children[i] if (c.type === 1) { segments.push.apply(segments, elementToSegments(c, state)) } else if (c.type === 2) { segments.push({ type: INTERPOLATION, value: c.expression }) } else if (c.type === 3) { let text = escape(c.text) if (c.isComment) { text = '' } segments.push({ type: RAW, value: text }) } } return segments } function flattenSegments (segments: Array): string { const mergedSegments = [] let textBuffer = '' const pushBuffer = () => { if (textBuffer) { mergedSegments.push(JSON.stringify(textBuffer)) textBuffer = '' } } for (let i = 0; i < segments.length; i++) { const s = segments[i] if (s.type === RAW) { textBuffer += s.value } else if (s.type === INTERPOLATION) { pushBuffer() mergedSegments.push(`_ssrEscape(${s.value})`) } else if (s.type === EXPRESSION) { pushBuffer() mergedSegments.push(`(${s.value})`) } } pushBuffer() return mergedSegments.join('+') }