JS_FUNCTION_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
JS_CONDITIONAL_EXPRESSION,
- JS_CACHE_EXPRESSION
+ JS_CACHE_EXPRESSION,
+
+ // ssr codegen
+ JS_BLOCK_STATEMENT,
+ JS_TEMPLATE_LITERAL,
+ JS_IF_STATEMENT
}
export const enum ElementTypes {
hoists: JSChildNode[]
imports: ImportItem[]
cached: number
- codegenNode: TemplateChildNode | JSChildNode | undefined
+ codegenNode: TemplateChildNode | JSChildNode | BlockStatement | undefined
}
export type ElementNode =
| CacheExpression // when cached by v-once
| SequenceExpression // when turned into a block
| undefined
+ ssrCodegenNode?: TemplateLiteral
}
export interface ComponentNode extends BaseElementNode {
export interface TemplateNode extends BaseElementNode {
tagType: ElementTypes.TEMPLATE
- codegenNode: ElementCodegenNode | undefined | CacheExpression
+ // TemplateNode is a container type that always gets compiled away
}
export interface TextNode extends Node {
codegenNode: CallExpression
}
+// JS Node Types ---------------------------------------------------------------
+
// We also include a number of JavaScript AST nodes for code generation.
// The AST is an intentionally minimal subset just to meet the exact needs of
// Vue render function generation.
+
export type JSChildNode =
| CallExpression
| ObjectExpression
| string
| symbol
| JSChildNode
+ | SSRCodegenNode
| TemplateChildNode
| TemplateChildNode[])[]
}
export interface FunctionExpression extends Node {
type: NodeTypes.JS_FUNCTION_EXPRESSION
params: ExpressionNode | ExpressionNode[] | undefined
- returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
+ returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
+ body?: BlockStatement
newline: boolean
+ // so that codegen knows it needs to generate ScopeId wrapper
isSlot: boolean
}
isVNode: boolean
}
+// SSR-specific Node Types -----------------------------------------------------
+
+export type SSRCodegenNode = BlockStatement | TemplateLiteral | IfStatement
+
+export interface BlockStatement extends Node {
+ type: NodeTypes.JS_BLOCK_STATEMENT
+ body: (JSChildNode | IfStatement)[]
+}
+
+export interface TemplateLiteral extends Node {
+ type: NodeTypes.JS_TEMPLATE_LITERAL
+ elements: (string | JSChildNode)[]
+}
+
+export interface IfStatement extends Node {
+ type: NodeTypes.JS_IF_STATEMENT
+ test: ExpressionNode
+ consequent: BlockStatement
+ alternate: IfStatement | BlockStatement
+}
+
// Codegen Node Types ----------------------------------------------------------
// createVNode(...)
loc: locStub
}
}
+
+export function createTemplateLiteral(
+ elements: TemplateLiteral['elements']
+): TemplateLiteral {
+ return {
+ type: NodeTypes.JS_TEMPLATE_LITERAL,
+ elements,
+ loc: locStub
+ }
+}
SequenceExpression,
ConditionalExpression,
CacheExpression,
- locStub
+ locStub,
+ SSRCodegenNode,
+ TemplateLiteral
} from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import {
} from './runtimeHelpers'
import { ImportItem } from './transform'
-type CodegenNode = TemplateChildNode | JSChildNode
+type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
export interface CodegenResult {
code: string
prefixIdentifiers = mode === 'module' || mode === 'cjs',
sourceMap = false,
filename = `template.vue.html`,
- scopeId = null
+ scopeId = null,
+ ssr = false
}: CodegenOptions
): CodegenContext {
const context: CodegenContext = {
sourceMap,
filename,
scopeId,
+ ssr,
source: ast.loc.source,
code: ``,
column: 1,
indent,
deindent,
newline,
- scopeId
+ scopeId,
+ ssr
} = context
const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module'
}
// enter render function
- if (genScopeId) {
+ if (genScopeId && !ssr) {
push(`const render = withId(`)
}
- push(`function render() {`)
+ if (!ssr) {
+ push(`function render() {`)
+ } else {
+ push(`function ssrRender(_ctx, _push, _parent) {`)
+ }
indent()
if (useWithBlock) {
}
newline()
}
- } else {
+ } else if (!ssr) {
push(`const _ctx = this`)
if (ast.cached > 0) {
newline()
}
// generate the VNode tree expression
- push(`return `)
+ if (!ssr) {
+ push(`return `)
+ }
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
deindent()
push(`}`)
- if (genScopeId) {
+ if (genScopeId && !ssr) {
push(`)`)
}
return
}
const { push, newline, helper, scopeId, mode } = context
- const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
+ const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
newline()
// push scope Id before initilaizing hoisted vnodes so that these vnodes
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
+
+ // SSR only types
+ case NodeTypes.JS_BLOCK_STATEMENT:
+ !__BROWSER__ && genNodeList(node.body, context, true)
+ break
+ case NodeTypes.JS_TEMPLATE_LITERAL:
+ !__BROWSER__ && genTemplateLiteral(node, context)
+ break
+ case NodeTypes.JS_IF_STATEMENT:
+ // TODO
+ break
+
/* istanbul ignore next */
default:
if (__DEV__) {
context: CodegenContext
) {
const { push, indent, deindent, scopeId, mode } = context
- const { params, returns, newline, isSlot } = node
+ 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 === 'module'
+ !__BROWSER__ && isSlot && scopeId != null && mode !== 'function'
if (genScopeId) {
push(`withId(`)
genNode(params, context)
}
push(`) => `)
- if (newline) {
+ if (newline || body) {
push(`{`)
indent()
- push(`return `)
}
- if (isArray(returns)) {
- genNodeListAsArray(returns, context)
- } else {
- genNode(returns, context)
+ if (returns) {
+ if (newline) {
+ push(`return `)
+ }
+ if (isArray(returns)) {
+ genNodeListAsArray(returns, context)
+ } else {
+ genNode(returns, context)
+ }
+ } else if (body) {
+ genNode(body, context)
}
- if (newline) {
+ if (newline || body) {
deindent()
push(`}`)
}
}
push(`)`)
}
+
+function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
+ const { push } = context
+ push('`')
+ for (let i = 0; i < node.elements.length; i++) {
+ const e = node.elements[i]
+ if (isString(e)) {
+ push(e.replace(/`/g, '\\`'))
+ } else {
+ push('${')
+ genNode(e, context)
+ push('}')
+ }
+ }
+ push('`')
+}
export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn'
+// exported for compiler-ssr
+export { transformExpression } from './transforms/transformExpression'
+export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot'
+export { buildProps } from './transforms/transformElement'
+
// utility, but need to rewrite typing to avoid dts relying on @vue/shared
import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
const generateCodeFrame = _genCodeFrame as (
export interface TransformOptions {
nodeTransforms?: NodeTransform[]
- directiveTransforms?: { [name: string]: DirectiveTransform }
+ directiveTransforms?: { [name: string]: DirectiveTransform | undefined }
isBuiltInComponent?: (tag: string) => symbol | void
// Transform expressions like {{ foo }} to `_ctx.foo`.
// - This is force-enabled in module mode, since modules are by default strict
filename?: string
// SFC scoped styles ID
scopeId?: string | null
+ // generate SSR specific code?
+ ssr?: boolean
}
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
import { transformShow } from './transforms/vShow'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
-const parserOptions = __BROWSER__ ? parserOptionsMinimal : parserOptionsStandard
+export const parserOptions = __BROWSER__
+ ? parserOptionsMinimal
+ : parserOptionsStandard
export function compile(
template: string,
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-ssr#readme",
"dependencies": {
- "@vue/compiler-core": "3.0.0-alpha.4"
+ "@vue/compiler-dom": "3.0.0-alpha.4"
}
}
--- /dev/null
+import {
+ CodegenResult,
+ RootNode,
+ generate as baseGenerate,
+ CodegenOptions,
+ NodeTypes,
+ locStub,
+ BlockStatement,
+ ElementTypes,
+ createCallExpression,
+ TemplateLiteral,
+ createTemplateLiteral,
+ CallExpression,
+ TemplateChildNode
+} from '@vue/compiler-dom'
+import { isString } from '@vue/shared'
+
+export function generate(
+ ast: RootNode,
+ options: CodegenOptions
+): CodegenResult {
+ // construct a SSR-specific codegen tree to pass to core codegen
+ const body: BlockStatement['body'] = []
+ let currentCall: CallExpression | null = null
+ let currentString: TemplateLiteral | null = null
+
+ function ensureCurrentString() {
+ if (!currentCall) {
+ currentCall = createCallExpression(`_push`)
+ body.push(currentCall)
+ }
+ if (!currentString) {
+ currentString = createTemplateLiteral([])
+ currentCall.arguments.push(currentString)
+ }
+ return currentString.elements
+ }
+
+ function pushStringPart(part: TemplateLiteral['elements'][0]) {
+ const bufferedElements = ensureCurrentString()
+ const lastItem = bufferedElements[bufferedElements.length - 1]
+ if (isString(part) && isString(lastItem)) {
+ bufferedElements[bufferedElements.length - 1] += part
+ } else {
+ bufferedElements.push(part)
+ }
+ }
+
+ function processChildren(children: TemplateChildNode[]) {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (child.type === NodeTypes.ELEMENT) {
+ if (child.tagType === ElementTypes.ELEMENT) {
+ const elementsToAdd = child.ssrCodegenNode!.elements
+ for (let j = 0; j < elementsToAdd.length; j++) {
+ pushStringPart(elementsToAdd[j])
+ }
+ if (child.children.length) {
+ processChildren(child.children)
+ }
+ // push closing tag
+ pushStringPart(`</${child.tag}>`)
+ } else if (child.tagType === ElementTypes.COMPONENT) {
+ // TODO
+ } else if (child.tagType === ElementTypes.SLOT) {
+ // TODO
+ }
+ } else if (child.type === NodeTypes.TEXT) {
+ // TODO
+ } else if (child.type === NodeTypes.IF) {
+ // TODO
+ } else if (child.type === NodeTypes.FOR) {
+ // TODO
+ }
+ }
+ }
+
+ const isFragment = ast.children.length > 1
+ if (isFragment) {
+ pushStringPart(`<!---->`)
+ }
+ processChildren(ast.children)
+ if (isFragment) {
+ pushStringPart(`<!---->`)
+ }
+
+ ast.codegenNode = {
+ type: NodeTypes.JS_BLOCK_STATEMENT,
+ loc: locStub,
+ body
+ }
+
+ return baseGenerate(ast, options)
+}
-export function hello(): string {
- return 'TODO'
+import {
+ CodegenResult,
+ baseParse,
+ parserOptions,
+ transform,
+ generate,
+ CompilerOptions,
+ transformExpression,
+ trackVForSlotScopes,
+ trackSlotScopes
+} from '@vue/compiler-dom'
+import { ssrCodegenTransform } from './ssrCodegenTransform'
+import { ssrTransformIf } from './transforms/ssrVIf'
+import { ssrTransformFor } from './transforms/ssrVFor'
+import { ssrTransformElement } from './transforms/ssrTransformElement'
+import { ssrTransformComponent } from './transforms/ssrTransformComponent'
+import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet'
+
+export interface SSRCompilerOptions extends CompilerOptions {}
+
+export function compile(
+ template: string,
+ options: SSRCompilerOptions = {}
+): CodegenResult {
+ const ast = baseParse(template, {
+ ...parserOptions,
+ ...options
+ })
+
+ transform(ast, {
+ ...options,
+ prefixIdentifiers: true,
+ // disalbe optimizations that are unnecessary for ssr
+ cacheHandlers: false,
+ hoistStatic: false,
+ nodeTransforms: [
+ ssrTransformIf,
+ ssrTransformFor,
+ trackVForSlotScopes,
+ transformExpression,
+ ssrTransformSlotOutlet,
+ ssrTransformElement,
+ ssrTransformComponent,
+ trackSlotScopes,
+ ...(options.nodeTransforms || []) // user transforms
+ ],
+ directiveTransforms: {
+ // TODO server-side directive transforms
+ ...(options.directiveTransforms || {}) // user transforms
+ }
+ })
+
+ // traverse the template AST and convert into SSR codegen AST
+ // by replacing ast.codegenNode.
+ ssrCodegenTransform(ast)
+
+ return generate(ast, {
+ mode: 'cjs',
+ ...options,
+ ssr: true,
+ prefixIdentifiers: true
+ })
}
--- /dev/null
+import {
+ RootNode,
+ BlockStatement,
+ CallExpression,
+ TemplateLiteral,
+ createCallExpression,
+ createTemplateLiteral,
+ locStub,
+ NodeTypes,
+ TemplateChildNode,
+ ElementTypes
+} from '@vue/compiler-dom'
+import { isString } from '@vue/shared'
+
+// Because SSR codegen output is completely different from client-side output
+// (e.g. multiple elements can be concatenated into a single template literal
+// instead of each getting a corresponding call), we need to apply an extra
+// transform pass to convert the template AST into a fresh JS AST before
+// passing it to codegen.
+
+export function ssrCodegenTransform(ast: RootNode) {
+ const context = createSSRTransformContext()
+
+ const isFragment = ast.children.length > 1
+ if (isFragment) {
+ context.pushStringPart(`<!---->`)
+ }
+ processChildren(ast.children, context)
+ if (isFragment) {
+ context.pushStringPart(`<!---->`)
+ }
+
+ ast.codegenNode = {
+ type: NodeTypes.JS_BLOCK_STATEMENT,
+ loc: locStub,
+ body: context.body
+ }
+}
+
+type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
+
+function createSSRTransformContext() {
+ const body: BlockStatement['body'] = []
+ let currentCall: CallExpression | null = null
+ let currentString: TemplateLiteral | null = null
+
+ return {
+ body,
+ pushStringPart(part: TemplateLiteral['elements'][0]) {
+ if (!currentCall) {
+ currentCall = createCallExpression(`_push`)
+ body.push(currentCall)
+ }
+ if (!currentString) {
+ currentString = createTemplateLiteral([])
+ currentCall.arguments.push(currentString)
+ }
+ const bufferedElements = currentString.elements
+ const lastItem = bufferedElements[bufferedElements.length - 1]
+ if (isString(part) && isString(lastItem)) {
+ bufferedElements[bufferedElements.length - 1] += part
+ } else {
+ bufferedElements.push(part)
+ }
+ }
+ }
+}
+
+function processChildren(
+ children: TemplateChildNode[],
+ context: SSRTransformContext
+) {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (child.type === NodeTypes.ELEMENT) {
+ if (child.tagType === ElementTypes.ELEMENT) {
+ const elementsToAdd = child.ssrCodegenNode!.elements
+ for (let j = 0; j < elementsToAdd.length; j++) {
+ context.pushStringPart(elementsToAdd[j])
+ }
+ if (child.children.length) {
+ processChildren(child.children, context)
+ }
+ // push closing tag
+ context.pushStringPart(`</${child.tag}>`)
+ } else if (child.tagType === ElementTypes.COMPONENT) {
+ // TODO
+ } else if (child.tagType === ElementTypes.SLOT) {
+ // TODO
+ }
+ } else if (child.type === NodeTypes.TEXT) {
+ // TODO
+ } else if (child.type === NodeTypes.IF) {
+ // TODO
+ } else if (child.type === NodeTypes.FOR) {
+ // TODO
+ }
+ }
+}
--- /dev/null
+import { NodeTransform, NodeTypes, ElementTypes } from '@vue/compiler-dom'
+
+export const ssrTransformComponent: NodeTransform = (node, context) => {
+ if (
+ node.type === NodeTypes.ELEMENT &&
+ node.tagType === ElementTypes.COMPONENT
+ ) {
+ return function ssrPostTransformComponent() {
+ // generate a _push(_renderComponent) call
+ // dynamic component as well
+ // !check if we need to bail out for slots
+ // TODO also handle scopeID here
+ }
+ }
+}
--- /dev/null
+import {
+ NodeTransform,
+ NodeTypes,
+ ElementTypes,
+ TemplateLiteral,
+ createTemplateLiteral
+} from '@vue/compiler-dom'
+import { escapeHtml } from '@vue/server-renderer/src'
+
+/*
+## Simple Element
+
+``` html
+<div></div>
+```
+``` js
+return function render(_ctx, _push, _parent) {
+ _push(`<div></div>`)
+}
+```
+
+## Consecutive Elements
+
+``` html
+<div>
+ <span></span>
+</div>
+<div></div>
+```
+``` js
+return function render(_ctx, _push, _parent) {
+ _push(`<div><span></span></div><div></div>`)
+}
+```
+*/
+
+export const ssrTransformElement: NodeTransform = (node, context) => {
+ if (
+ node.type === NodeTypes.ELEMENT &&
+ node.tagType === ElementTypes.ELEMENT
+ ) {
+ return function ssrPostTransformElement() {
+ // element
+ // generate the template literal representing the open tag.
+ const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
+
+ for (let i = 0; i < node.props.length; i++) {
+ const prop = node.props[i]
+ if (prop.type === NodeTypes.DIRECTIVE) {
+ const directiveTransform = context.directiveTransforms[prop.name]
+ if (directiveTransform) {
+ // TODO directive transforms
+ } else {
+ // no corresponding ssr directive transform found.
+ // TODO emit error
+ }
+ } else {
+ // static prop
+ openTag.push(
+ ` ${prop.name}` +
+ (prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
+ )
+ }
+ }
+
+ openTag.push(`>`)
+ node.ssrCodegenNode = createTemplateLiteral(openTag)
+ }
+ }
+}
--- /dev/null
+import { NodeTransform } from '@vue/compiler-dom'
+
+export const ssrTransformSlotOutlet: NodeTransform = () => {}
--- /dev/null
+import { NodeTransform } from '@vue/compiler-dom'
+
+export const ssrTransformFor: NodeTransform = () => {}
--- /dev/null
+import { NodeTransform } from '@vue/compiler-dom'
+
+export const ssrTransformIf: NodeTransform = () => {}
{ msg: 'hello' },
{
// optimized slot using string push
- default: ({ msg }: any, push: any) => {
+ default: ({ msg }: any, push: any, p: any) => {
push(`<span>${msg}</span>`)
},
- _compiled: true // important to avoid slots being normalized
+ // important to avoid slots being normalized
+ _compiled: true as any
},
parent
)
-export { renderToString, renderComponent, renderSlot } from './renderToString'
+// public
+export { renderToString } from './renderToString'
+
+// internal
+export { renderComponent, renderSlot } from './renderToString'
export { renderClass, renderStyle, renderProps } from './renderProps'
export { escapeHtml, interpolate } from './ssrUtils'
ComponentInternalInstance,
VNode,
VNodeArrayChildren,
- VNodeNormalizedChildren,
createVNode,
Text,
Comment,
Portal,
ShapeFlags,
ssrUtils,
- Slot
+ Slot,
+ Slots
} from 'vue'
import {
isString,
export function renderComponent(
comp: Component,
props: Props | null = null,
- children: VNodeNormalizedChildren | null = null,
+ children: Slots | SSRSlots | null = null,
parentComponent: ComponentInternalInstance | null = null
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
return renderComponentVNode(
}
}
-type OptimizedSlotFn = (
+export type SSRSlots = Record<string, SSRSlot>
+
+export type SSRSlot = (
props: Props,
push: PushFn,
parentComponent: ComponentInternalInstance | null
) => void
export function renderSlot(
- slotFn: Slot | OptimizedSlotFn,
+ slotFn: Slot | SSRSlot,
slotProps: Props,
push: PushFn,
parentComponent: ComponentInternalInstance | null = null