JS_BLOCK_STATEMENT,
JS_TEMPLATE_LITERAL,
JS_IF_STATEMENT,
- JS_ASSIGNMENT_EXPRESSION
+ JS_ASSIGNMENT_EXPRESSION,
+ JS_RETURN_STATEMENT
}
export const enum ElementTypes {
type: NodeTypes.JS_FUNCTION_EXPRESSION
params: ExpressionNode | string | (ExpressionNode | string)[] | undefined
returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
- body?: BlockStatement
+ body?: BlockStatement | IfStatement
newline: boolean
// so that codegen knows it needs to generate ScopeId wrapper
isSlot: boolean
// SSR-specific Node Types -----------------------------------------------------
-export type SSRCodegenNode = BlockStatement | TemplateLiteral | IfStatement
+export type SSRCodegenNode =
+ | BlockStatement
+ | TemplateLiteral
+ | IfStatement
+ | AssignmentExpression
+ | ReturnStatement
export interface BlockStatement extends Node {
type: NodeTypes.JS_BLOCK_STATEMENT
type: NodeTypes.JS_IF_STATEMENT
test: ExpressionNode
consequent: BlockStatement
- alternate: IfStatement | BlockStatement | undefined
+ alternate: IfStatement | BlockStatement | ReturnStatement | undefined
}
export interface AssignmentExpression extends Node {
right: JSChildNode
}
+export interface ReturnStatement extends Node {
+ type: NodeTypes.JS_RETURN_STATEMENT
+ returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
+}
+
// Codegen Node Types ----------------------------------------------------------
// createVNode(...)
loc: locStub
}
}
+
+export function createReturnStatement(
+ returns: ReturnStatement['returns']
+): ReturnStatement {
+ return {
+ type: NodeTypes.JS_RETURN_STATEMENT,
+ returns,
+ loc: locStub
+ }
+}
SSRCodegenNode,
TemplateLiteral,
IfStatement,
- AssignmentExpression
+ AssignmentExpression,
+ ReturnStatement
} from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import {
CREATE_TEXT,
PUSH_SCOPE_ID,
POP_SCOPE_ID,
- WITH_SCOPE_ID,
- CREATE_BLOCK
+ WITH_SCOPE_ID
} from './runtimeHelpers'
import { ImportItem } from './transform'
context: CodegenContext,
genScopeId: boolean
) {
- const { push, helper, newline, scopeId, runtimeModuleName, ssr } = context
+ const { push, helper, newline, scopeId, runtimeModuleName } = context
- if (!__BROWSER__) {
- // in ssr mode, `withId` helper is only needed if the template contains
- // de-optimized component slots (which uses the createVNode helper)
- if (
- ssr &&
- !(
- ast.helpers.includes(CREATE_VNODE) || ast.helpers.includes(CREATE_BLOCK)
- )
- ) {
- genScopeId = false
- }
- if (genScopeId) {
- ast.helpers.push(WITH_SCOPE_ID)
- if (ast.hoists.length) {
- ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
- }
+ if (!__BROWSER__ && genScopeId) {
+ ast.helpers.push(WITH_SCOPE_ID)
+ if (ast.hoists.length) {
+ ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
}
case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
!__BROWSER__ && genAssignmentExpression(node, context)
break
+ case NodeTypes.JS_RETURN_STATEMENT:
+ !__BROWSER__ && genReturnStatement(node, context)
+ break
/* istanbul ignore next */
default:
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)
+ }
+}
import { CompilerOptions } from './options'
import { baseParse } from './parse'
-import { transform } from './transform'
+import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast'
import { isString } from '@vue/shared'
import { transformModel } from './transforms/vModel'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
+export type TransformPreset = [
+ NodeTransform[],
+ Record<string, DirectiveTransform>
+]
+
+export function getBaseTransformPreset(
+ prefixIdentifiers?: boolean
+): TransformPreset {
+ return [
+ [
+ transformOnce,
+ transformIf,
+ transformFor,
+ ...(!__BROWSER__ && prefixIdentifiers
+ ? [
+ // order is important
+ trackVForSlotScopes,
+ transformExpression
+ ]
+ : []),
+ transformSlotOutlet,
+ transformElement,
+ trackSlotScopes,
+ transformText
+ ],
+ {
+ on: transformOn,
+ bind: transformBind,
+ model: transformModel
+ }
+ ]
+}
+
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
}
const ast = isString(template) ? baseParse(template, options) : template
+ const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
+ prefixIdentifiers
+ )
transform(ast, {
...options,
prefixIdentifiers,
nodeTransforms: [
- transformOnce,
- transformIf,
- transformFor,
- ...(prefixIdentifiers
- ? [
- // order is important
- trackVForSlotScopes,
- transformExpression
- ]
- : []),
- transformSlotOutlet,
- transformElement,
- trackSlotScopes,
- transformText,
+ ...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
- on: transformOn,
- bind: transformBind,
- model: transformModel,
+ ...directiveTransforms,
...(options.directiveTransforms || {}) // user transforms
}
})
export { baseParse, TextModes } from './parse'
export {
transform,
- createStructuralDirectiveTransform,
TransformContext,
+ createStructuralDirectiveTransform,
NodeTransform,
StructuralDirectiveTransform,
DirectiveTransform
CompilerError,
createCompilerError
} from './errors'
+
export * from './ast'
export * from './utils'
-export { registerRuntimeHelpers } from './runtimeHelpers'
-export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
+export * from './runtimeHelpers'
-// expose transforms so higher-order compilers can import and extend them
+export { getBaseTransformPreset, TransformPreset } from './compile'
export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn'
export { transformBind } from './transforms/vBind'
-
-// exported for compiler-ssr
-export * from './runtimeHelpers'
+export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
export { processIf } from './transforms/vIf'
export { processFor, createForLoopParams } from './transforms/vFor'
export {
components: Set<string>
directives: Set<string>
hoists: JSChildNode[]
- temps: number
imports: Set<ImportItem>
+ temps: number
cached: number
identifiers: { [name: string]: number | undefined }
scopes: {
components: new Set(),
directives: new Set(),
hoists: [],
- temps: 0,
imports: new Set(),
+ temps: 0,
cached: 0,
identifiers: {},
scopes: {
callArgs.push(child)
}
// mark dynamic text with flag so it gets patched inside a block
- if (child.type !== NodeTypes.TEXT) {
+ if (!context.ssr && child.type !== NodeTypes.TEXT) {
callArgs.push(
`${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`
)
isBuiltInType,
ParserOptions,
RootNode,
- noopDirectiveTransform
+ noopDirectiveTransform,
+ TransformPreset,
+ getBaseTransformPreset
} from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard'
}
}
+export function getDOMTransformPreset(
+ prefixIdentifiers?: boolean
+): TransformPreset {
+ const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
+ prefixIdentifiers
+ )
+ return [
+ [
+ ...nodeTransforms,
+ transformStyle,
+ ...(__DEV__ ? [warnTransitionChildren] : [])
+ ],
+ {
+ ...directiveTransforms,
+ cloak: noopDirectiveTransform,
+ html: transformVHtml,
+ text: transformVText,
+ model: transformModel, // override compiler-core
+ on: transformOn, // override compiler-core
+ show: transformShow
+ }
+ ]
+}
+
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
+ const [nodeTransforms, directiveTransforms] = getDOMTransformPreset(
+ options.prefixIdentifiers
+ )
return baseCompile(template, {
...parserOptions,
...options,
- nodeTransforms: [
- transformStyle,
- ...(__DEV__ ? [warnTransitionChildren] : []),
- ...(options.nodeTransforms || [])
- ],
+ nodeTransforms: [...nodeTransforms, ...(options.nodeTransforms || [])],
directiveTransforms: {
- cloak: noopDirectiveTransform,
- html: transformVHtml,
- text: transformVText,
- model: transformModel, // override compiler-core
- on: transformOn,
- show: transformShow,
+ ...directiveTransforms,
...(options.directiveTransforms || {})
},
isBuiltInComponent: isBuiltInDOMComponent
_push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => {
- if (_scopeId) {
- _push(\`hello<div \${_scopeId}></div>\`)
+ if (_push) {
+ _push(\`hello<div\${_scopeId}></div>\`)
} else {
- _push(\`hello<div></div>\`)
+ return [
+ createTextVNode(\\"hello\\"),
+ createVNode(\\"div\\")
+ ]
}
},
_compiled: true
_push(_ssrRenderComponent(_component_foo, null, {
default: ({ msg }, _push, _parent, _scopeId) => {
- _push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
+ if (_push) {
+ _push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
+ } else {
+ return [
+ createTextVNode(toDisplayString(_ctx.msg + _ctx.outer))
+ ]
+ }
},
_compiled: true
}, _parent))
_push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => {
- _push(\`foo\`)
+ if (_push) {
+ _push(\`foo\`)
+ } else {
+ return [
+ createTextVNode(\\"foo\\")
+ ]
+ }
},
named: (_, _push, _parent, _scopeId) => {
- _push(\`bar\`)
+ if (_push) {
+ _push(\`bar\`)
+ } else {
+ return [
+ createTextVNode(\\"bar\\")
+ ]
+ }
},
_compiled: true
}, _parent))
? {
name: \\"named\\",
fn: (_, _push, _parent, _scopeId) => {
- _push(\`foo\`)
+ if (_push) {
+ _push(\`foo\`)
+ } else {
+ return [
+ createTextVNode(\\"foo\\")
+ ]
+ }
}
}
: undefined
_push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => {
- _push(\`foo\`)
+ if (_push) {
+ _push(\`foo\`)
+ } else {
+ return [
+ createTextVNode(\\"foo\\")
+ ]
+ }
},
_compiled: true
}, _parent))
_push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => {
- if (_scopeId) {
- _push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`)
+ if (_push) {
+ _push(\`<span data-v-xxxxxxx\${_scopeId}>hello</span>\`)
} else {
- _push(\`<span data-v-xxxxxxx>hello</span>\`)
+ return [
+ createVNode(\\"span\\", null, \\"hello\\")
+ ]
}
},
_compiled: true
_push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => {
- if (_scopeId) {
- _push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`)
+ if (_push) {
+ _push(\`<span data-v-xxxxxxx\${_scopeId}>hello</span>\`)
_push(_ssrRenderComponent(_component_bar, null, {
default: (_, _push, _parent, _scopeId) => {
- if (_scopeId) {
- _push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`)
+ if (_push) {
+ _push(\`<span data-v-xxxxxxx\${_scopeId}></span>\`)
} else {
- _push(\`<span data-v-xxxxxxx></span>\`)
+ return [
+ createVNode(\\"span\\")
+ ]
}
},
_compiled: true
}, _parent))
} else {
- _push(\`<span data-v-xxxxxxx>hello</span>\`)
- _push(_ssrRenderComponent(_component_bar, null, {
- default: (_, _push, _parent, _scopeId) => {
- if (_scopeId) {
- _push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`)
- } else {
- _push(\`<span data-v-xxxxxxx></span>\`)
- }
- },
- _compiled: true
- }, _parent))
+ return [
+ createVNode(\\"span\\", null, \\"hello\\"),
+ createVNode(_component_bar, null, {
+ default: () => [
+ createVNode(\\"span\\")
+ ],
+ _compiled: true
+ })
+ ]
}
},
_compiled: true
// passing it to codegen.
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
- const context = createSSRTransformContext(options)
+ const context = createSSRTransformContext(ast, options)
const isFragment =
ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment)
export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
function createSSRTransformContext(
+ root: RootNode,
options: CompilerOptions,
helpers: Set<symbol> = new Set(),
withSlotScopeId = false
let currentString: TemplateLiteral | null = null
return {
+ root,
options,
body,
helpers,
): SSRTransformContext {
// ensure child inherits parent helpers
return createSSRTransformContext(
+ parent.root,
parent.options,
parent.helpers,
withSlotScopeId
ComponentNode,
SlotFnBuilder,
createFunctionExpression,
- createBlockStatement,
buildSlots,
FunctionExpression,
TemplateChildNode,
TRANSITION_GROUP,
createIfStatement,
createSimpleExpression,
- isText
+ getDOMTransformPreset,
+ transform,
+ createReturnStatement,
+ ReturnStatement,
+ Namespaces,
+ locStub,
+ RootNode,
+ TransformContext
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import {
processChildren,
processChildrenAsStatement
} from '../ssrCodegenTransform'
-import { isSymbol } from '@vue/shared'
+import { isSymbol, isObject, isArray } from '@vue/shared'
// We need to construct the slot functions in the 1st pass to ensure proper
// scope tracking, but the children of each slot cannot be processed until
interface WIPSlotEntry {
fn: FunctionExpression
children: TemplateChildNode[]
+ vnodeBranch: ReturnStatement
}
const componentTypeMap = new WeakMap<ComponentNode, symbol>()
return // built-in component: fallthrough
}
- // note we are not passing ssr: true here because for components, v-on
- // handlers should still be passed
const props =
- node.props.length > 0 ? buildProps(node, context).props || `null` : `null`
+ node.props.length > 0
+ ? // note we are not passing ssr: true here because for components, v-on
+ // handlers should still be passed
+ buildProps(node, context).props || `null`
+ : `null`
const wipEntries: WIPSlotEntry[] = []
wipMap.set(node, wipEntries)
const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
- // An SSR slot function has the signature of
- // (props, _push, _parent, _scopeId) => void
- // See server-renderer/src/helpers/renderSlot.ts
const fn = createFunctionExpression(
[props || `_`, `_push`, `_parent`, `_scopeId`],
undefined, // no return, assign body later
true, // newline
- false, // isSlot: pass false since we don't need client scopeId codegen
+ true, // isSlot
loc
)
- wipEntries.push({ fn, children })
+ wipEntries.push({
+ fn,
+ children,
+ // build the children using normal vnode-based transforms
+ // TODO fixme: `children` here has already been mutated at this point
+ // so the sub-transform runs into errors :/
+ vnodeBranch: createVNodeSlotBranch(clone(children), context)
+ })
return fn
}
? buildSlots(node, context, buildSSRSlotFn).slots
: `null`
- // TODO option for slots bail out
- // TODO scopeId
-
node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_COMPONENT),
[component, props, slots, `_parent`]
// finish up slot function expressions from the 1st pass.
const wipEntries = wipMap.get(node) || []
for (let i = 0; i < wipEntries.length; i++) {
- const { fn, children } = wipEntries[i]
- const hasNonTextChild = children.some(c => !isText(c))
- if (hasNonTextChild) {
- // SSR slots need to handled potential presence of scopeId of the child
- // component. To avoid the cost of concatenation when it's unnecessary,
- // we split the code into two paths, one with slot scopeId and one without.
- fn.body = createBlockStatement([
- createIfStatement(
- createSimpleExpression(`_scopeId`, false),
- // branch with scopeId concatenation
- processChildrenAsStatement(children, context, false, true),
- // branch without scopeId concatenation
- processChildrenAsStatement(children, context, false, false)
- )
- ])
- } else {
- // only text, no need for scopeId branching.
- fn.body = processChildrenAsStatement(children, context)
- }
+ const { fn, children, vnodeBranch } = wipEntries[i]
+ // For each slot, we generate two branches: one SSR-optimized branch and
+ // one normal vnode-based branch. The branches are taken based on the
+ // presence of the 2nd `_push` argument (which is only present if the slot
+ // is called by `_ssrRenderSlot`.
+ fn.body = createIfStatement(
+ createSimpleExpression(`_push`, false),
+ processChildrenAsStatement(
+ children,
+ context,
+ false,
+ true /* withSlotScopeId */
+ ),
+ vnodeBranch
+ )
}
context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode]))
}
}
+
+function createVNodeSlotBranch(
+ children: TemplateChildNode[],
+ context: TransformContext
+): ReturnStatement {
+ // we need to process the slot children using client-side transforms.
+ // in order to do that we need to construct a fresh root.
+ // in addition, wrap the children with a wrapper template for proper child
+ // treatment.
+ const { root } = context
+ const childRoot: RootNode = {
+ ...root,
+ children: [
+ {
+ type: NodeTypes.ELEMENT,
+ ns: Namespaces.HTML,
+ tag: 'template',
+ tagType: ElementTypes.TEMPLATE,
+ isSelfClosing: false,
+ props: [],
+ children,
+ loc: locStub,
+ codegenNode: undefined
+ }
+ ]
+ }
+ const [nodeTransforms, directiveTransforms] = getDOMTransformPreset(true)
+ transform(childRoot, {
+ ...context, // copy transform options on context
+ nodeTransforms,
+ directiveTransforms
+ })
+
+ // merge helpers/components/directives/imports from the childRoot
+ // back to current root
+ ;(['helpers', 'components', 'directives', 'imports'] as const).forEach(
+ key => {
+ root[key] = [...new Set([...root[key], ...childRoot[key]])] as any
+ }
+ )
+
+ return createReturnStatement(children)
+}
+
+function clone(v: any): any {
+ if (isArray(v)) {
+ return v.map(clone)
+ } else if (isObject(v)) {
+ const res: any = {}
+ for (const key in v) {
+ res[key] = v[key]
+ }
+ return res
+ } else {
+ return v
+ }
+}
// Handle slot scopeId
if (context.withSlotScopeId) {
- context.pushStringPart(` `)
context.pushStringPart(createSimpleExpression(`_scopeId`, false))
}
VNode
} from '../vnode'
import { PatchFlags } from '@vue/shared'
+import { warn } from '../warning'
export function renderSlot(
slots: Record<string, Slot>,
// the compiler and guaranteed to be an array
fallback?: VNodeArrayChildren
): VNode {
- const slot = slots[name]
+ let slot = slots[name]
+
+ if (__DEV__ && slot.length > 1) {
+ warn(
+ `SSR-optimized slot function detected in a non-SSR-optimized render ` +
+ `function. You need to mark this component with $dynamic-slots in the ` +
+ `parent template.`
+ )
+ slot = () => []
+ }
+
return (
openBlock(),
createBlock(
if (slotFn.length > 1) {
// only ssr-optimized slot fns accept more than 1 arguments
const scopeId = parentComponent && parentComponent.type.__scopeId
- slotFn(slotProps, push, parentComponent, scopeId ? scopeId + `-s` : null)
+ slotFn(slotProps, push, parentComponent, scopeId ? ` ${scopeId}-s` : ``)
} else {
// normal slot
renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
ssrMode.value = persistedState.ssr
Object.assign(compilerOptions, persistedState.options)
- let lastSuccessfulCode: string = `/* See console for error */`
+ let lastSuccessfulCode: string
let lastSuccessfulMap: SourceMapConsumer | undefined = undefined
function compileCode(source: string): string {
console.clear()
lastSuccessfulMap = new window._deps['source-map'].SourceMapConsumer(map)
lastSuccessfulMap!.computeColumnSpans()
} catch (e) {
+ lastSuccessfulCode = `/* ERROR: ${
+ e.message
+ } (see console for more info) */`
console.error(e)
}
return lastSuccessfulCode