end: { line: 1, column: 1, offset: 0 }
}
+export function createRoot(
+ children: TemplateChildNode[],
+ loc = locStub
+): RootNode {
+ return {
+ type: NodeTypes.ROOT,
+ children,
+ helpers: [],
+ components: [],
+ directives: [],
+ hoists: [],
+ imports: [],
+ cached: 0,
+ temps: 0,
+ codegenNode: undefined,
+ loc
+ }
+}
+
export function createArrayExpression(
elements: ArrayExpression['elements'],
loc: SourceLocation = locStub
export {
transform,
TransformContext,
+ createTransformContext,
+ traverseNode,
createStructuralDirectiveTransform,
NodeTransform,
StructuralDirectiveTransform,
SourceLocation,
TextNode,
TemplateChildNode,
- InterpolationNode
+ InterpolationNode,
+ createRoot
} from './ast'
import { extend } from '@vue/shared'
): RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
-
- return {
- type: NodeTypes.ROOT,
- children: parseChildren(context, TextModes.DATA, []),
- helpers: [],
- components: [],
- directives: [],
- hoists: [],
- imports: [],
- cached: 0,
- temps: 0,
- codegenNode: undefined,
- loc: getSelection(context, start)
- }
+ return createRoot(
+ parseChildren(context, TextModes.DATA, []),
+ getSelection(context, start)
+ )
}
function createParserContext(
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
}
-function createTransformContext(
+export function createTransformContext(
root: RootNode,
{
prefixIdentifiers = false,
const dir = node.props[i]
// do not process for v-on & v-for since they are special handled
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
- const exp = dir.exp as SimpleExpressionNode | undefined
- const arg = dir.arg as SimpleExpressionNode | undefined
+ const exp = dir.exp
+ const arg = dir.arg
// do not process exp if this is v-on:arg - we need special handling
// for wrapping inline statements.
- if (exp && !(dir.name === 'on' && arg)) {
+ if (
+ exp &&
+ exp.type === NodeTypes.SIMPLE_EXPRESSION &&
+ !(dir.name === 'on' && arg)
+ ) {
dir.exp = processExpression(
exp,
context,
dir.name === 'slot'
)
}
- if (arg && !arg.isStatic) {
+ if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) {
dir.arg = processExpression(arg, context)
}
}
baseParse,
CompilerOptions,
CodegenResult,
- isBuiltInType,
ParserOptions,
RootNode,
noopDirectiveTransform,
import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow'
-import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
export const parserOptions = __BROWSER__
? parserOptionsMinimal
: parserOptionsStandard
-export const isBuiltInDOMComponent = (tag: string): symbol | undefined => {
- if (isBuiltInType(tag, `Transition`)) {
- return TRANSITION
- } else if (isBuiltInType(tag, `TransitionGroup`)) {
- return TRANSITION_GROUP
- }
-}
-
export function getDOMTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
directiveTransforms: {
...directiveTransforms,
...(options.directiveTransforms || {})
- },
- isBuiltInComponent: isBuiltInDOMComponent
+ }
})
}
ParserOptions,
ElementNode,
Namespaces,
- NodeTypes
+ NodeTypes,
+ isBuiltInType
} from '@vue/compiler-core'
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
+import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
const isRawTextContainer = /*#__PURE__*/ makeMap(
'style,iframe,script,noscript',
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre',
+ isBuiltInComponent: (tag: string): symbol | undefined => {
+ if (isBuiltInType(tag, `Transition`)) {
+ return TRANSITION
+ } else if (isBuiltInType(tag, `TransitionGroup`)) {
+ return TRANSITION_GROUP
+ }
+ },
+
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
let ns = parent ? parent.ns : DOMNamespaces.HTML
describe('slots', () => {
test('implicit default slot', () => {
expect(compile(`<foo>hello<div/></foo>`).code).toMatchInlineSnapshot(`
- "const { resolveComponent } = require(\\"vue\\")
+ "const { resolveComponent, createVNode, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
test('explicit default slot', () => {
expect(compile(`<foo v-slot="{ msg }">{{ msg + outer }}</foo>`).code)
.toMatchInlineSnapshot(`
- "const { resolveComponent } = require(\\"vue\\")
+ "const { resolveComponent, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent, _ssrInterpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
} else {
return [
- createTextVNode(toDisplayString(_ctx.msg + _ctx.outer))
+ createTextVNode(toDisplayString(msg + _ctx.outer))
]
}
},
<template v-slot:named>bar</template>
</foo>`).code
).toMatchInlineSnapshot(`
- "const { resolveComponent } = require(\\"vue\\")
+ "const { resolveComponent, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
<template v-slot:named v-if="ok">foo</template>
</foo>`).code
).toMatchInlineSnapshot(`
- "const { resolveComponent, createSlots } = require(\\"vue\\")
+ "const { resolveComponent, createTextVNode, createSlots } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
<template v-for="key in names" v-slot:[key]="{ msg }">{{ msg + key + bar }}</template>
</foo>`).code
).toMatchInlineSnapshot(`
- "const { resolveComponent, renderList, createSlots } = require(\\"vue\\")
+ "const { resolveComponent, createTextVNode, renderList, createSlots } = require(\\"vue\\")
const { _ssrRenderComponent, _ssrInterpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
return {
name: key,
fn: ({ msg }, _push, _parent, _scopeId) => {
- _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
+ if (_push) {
+ _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
+ } else {
+ return [
+ createTextVNode(toDisplayString(msg + _ctx.key + _ctx.bar))
+ ]
+ }
}
}
})
`)
})
+ test('nested transform scoping in vnode branch', () => {
+ expect(
+ compile(`<foo>
+ <template v-slot:foo="{ list }">
+ <div v-if="ok">
+ <span v-for="i in list"></span>
+ </div>
+ </template>
+ <template v-slot:bar="{ ok }">
+ <div v-if="ok">
+ <span v-for="i in list"></span>
+ </div>
+ </template>
+ </foo>`).code
+ ).toMatchInlineSnapshot(`
+ "const { resolveComponent, renderList, openBlock, createBlock, Fragment, createVNode, createCommentVNode } = require(\\"vue\\")
+ const { _ssrRenderComponent, _ssrRenderList } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ const _component_foo = resolveComponent(\\"foo\\")
+
+ _push(_ssrRenderComponent(_component_foo, null, {
+ foo: ({ list }, _push, _parent, _scopeId) => {
+ if (_push) {
+ if (_ctx.ok) {
+ _push(\`<div\${_scopeId}><!---->\`)
+ _ssrRenderList(list, (i) => {
+ _push(\`<span\${_scopeId}></span>\`)
+ })
+ _push(\`<!----></div>\`)
+ } else {
+ _push(\`<!---->\`)
+ }
+ } else {
+ return [
+ (openBlock(), (_ctx.ok)
+ ? createBlock(\\"div\\", { key: 0 }, [
+ (openBlock(false), createBlock(Fragment, null, renderList(list, (i) => {
+ return (openBlock(), createBlock(\\"span\\"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ ])
+ : createCommentVNode(\\"v-if\\", true))
+ ]
+ }
+ },
+ bar: ({ ok }, _push, _parent, _scopeId) => {
+ if (_push) {
+ if (ok) {
+ _push(\`<div\${_scopeId}><!---->\`)
+ _ssrRenderList(_ctx.list, (i) => {
+ _push(\`<span\${_scopeId}></span>\`)
+ })
+ _push(\`<!----></div>\`)
+ } else {
+ _push(\`<!---->\`)
+ }
+ } else {
+ return [
+ (openBlock(), ok
+ ? createBlock(\\"div\\", { key: 0 }, [
+ (openBlock(false), createBlock(Fragment, null, renderList(_ctx.list, (i) => {
+ return (openBlock(), createBlock(\\"span\\"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ ])
+ : createCommentVNode(\\"v-if\\", true))
+ ]
+ }
+ },
+ _compiled: true
+ }, _parent))
+ }"
+ `)
+ })
+
test('built-in fallthroughs', () => {
// no fragment
expect(compile(`<transition><div/></transition>`).code)
scopeId
}).code
).toMatchInlineSnapshot(`
- "const { resolveComponent } = require(\\"vue\\")
+ "const { resolveComponent, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
scopeId
}).code
).toMatchInlineSnapshot(`
- "const { resolveComponent } = require(\\"vue\\")
+ "const { resolveComponent, createVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
scopeId
}).code
).toMatchInlineSnapshot(`
- "const { resolveComponent } = require(\\"vue\\")
+ "const { resolveComponent, createVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
- const _component_bar = resolveComponent(\\"bar\\")
const _component_foo = resolveComponent(\\"foo\\")
+ const _component_bar = resolveComponent(\\"bar\\")
_push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => {
trackSlotScopes,
noopDirectiveTransform,
transformBind,
- transformStyle,
- isBuiltInDOMComponent
+ transformStyle
} from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement'
-import { ssrTransformComponent } from './transforms/ssrTransformComponent'
+import {
+ ssrTransformComponent,
+ rawOptionsMap
+} from './transforms/ssrTransformComponent'
import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrTransformIf } from './transforms/ssrVIf'
import { ssrTransformFor } from './transforms/ssrVFor'
const ast = baseParse(template, options)
+ // Save raw options for AST. This is needed when performing sub-transforms
+ // on slot vnode branches.
+ rawOptionsMap.set(ast, options)
+
transform(ast, {
...options,
nodeTransforms: [
cloak: noopDirectiveTransform,
once: noopDirectiveTransform,
...(options.directiveTransforms || {}) // user transforms
- },
- isBuiltInComponent: isBuiltInDOMComponent
+ }
})
// traverse the template AST and convert into SSR codegen AST
createIfStatement,
createSimpleExpression,
getDOMTransformPreset,
- transform,
createReturnStatement,
ReturnStatement,
Namespaces,
locStub,
RootNode,
- TransformContext
+ TransformContext,
+ CompilerOptions,
+ TransformOptions,
+ createRoot,
+ createTransformContext,
+ traverseNode,
+ ExpressionNode,
+ TemplateNode
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import {
return
}
+ const component = resolveComponentType(node, context, true /* ssr */)
+ if (isSymbol(component)) {
+ componentTypeMap.set(node, component)
+ return // built-in component: fallthrough
+ }
+
+ // Build the fallback vnode-based branch for the component's slots.
+ // We need to clone the node into a fresh copy and use the buildSlots' logic
+ // to get access to the children of each slot. We then compile them with
+ // a child transform pipeline using vnode-based transforms (instead of ssr-
+ // based ones), and save the result branch (a ReturnStatement) in an array.
+ // The branch is retrieved when processing slots again in ssr mode.
+ const vnodeBranches: ReturnStatement[] = []
+ const clonedNode = clone(node)
+
return function ssrPostTransformComponent() {
- const component = resolveComponentType(node, context, true /* ssr */)
- if (isSymbol(component)) {
- componentTypeMap.set(node, component)
- return // built-in component: fallthrough
- }
+ buildSlots(clonedNode, context, (props, children) => {
+ vnodeBranches.push(createVNodeSlotBranch(props, children, context))
+ return createFunctionExpression(undefined)
+ })
const props =
node.props.length > 0
// 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)
+ vnodeBranch: vnodeBranches[wipEntries.length]
})
return fn
}
}
}
+export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
+
+const [vnodeNodeTransforms, vnodeDirectiveTransforms] = getDOMTransformPreset(
+ true
+)
+
function createVNodeSlotBranch(
+ props: ExpressionNode | undefined,
children: TemplateChildNode[],
- context: TransformContext
+ parentContext: 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: [
+ // apply a sub-transform using vnode-based transforms.
+ const rawOptions = rawOptionsMap.get(parentContext.root)!
+ const subOptions = {
+ ...rawOptions,
+ // overwrite with vnode-based transforms
+ nodeTransforms: [
+ ...vnodeNodeTransforms,
+ ...(rawOptions.nodeTransforms || [])
+ ],
+ directiveTransforms: {
+ ...vnodeDirectiveTransforms,
+ ...(rawOptions.directiveTransforms || {})
+ }
+ }
+
+ // wrap the children with a wrapper template for proper children treatment.
+ const wrapperNode: TemplateNode = {
+ type: NodeTypes.ELEMENT,
+ ns: Namespaces.HTML,
+ tag: 'template',
+ tagType: ElementTypes.TEMPLATE,
+ isSelfClosing: false,
+ // important: provide v-slot="props" on the wrapper for proper
+ // scope analysis
+ props: [
{
- type: NodeTypes.ELEMENT,
- ns: Namespaces.HTML,
- tag: 'template',
- tagType: ElementTypes.TEMPLATE,
- isSelfClosing: false,
- props: [],
- children,
- loc: locStub,
- codegenNode: undefined
+ type: NodeTypes.DIRECTIVE,
+ name: 'slot',
+ exp: props,
+ arg: undefined,
+ modifiers: [],
+ loc: locStub
}
- ]
+ ],
+ 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
+ subTransform(wrapperNode, subOptions, parentContext)
+ return createReturnStatement(children)
+}
+
+function subTransform(
+ node: TemplateChildNode,
+ options: TransformOptions,
+ parentContext: TransformContext
+) {
+ const childRoot = createRoot([node])
+ const childContext = createTransformContext(childRoot, options)
+ // inherit parent scope analysis state
+ childContext.scopes = { ...parentContext.scopes }
+ childContext.identifiers = { ...parentContext.identifiers }
+ // traverse
+ traverseNode(childRoot, childContext)
+ // merge helpers/components/directives/imports into parent context
;(['helpers', 'components', 'directives', 'imports'] as const).forEach(
key => {
- root[key] = [...new Set([...root[key], ...childRoot[key]])] as any
+ childContext[key].forEach((value: any) => {
+ ;(parentContext[key] as any).add(value)
+ })
}
)
-
- return createReturnStatement(children)
}
function clone(v: any): any {
} else if (isObject(v)) {
const res: any = {}
for (const key in v) {
- res[key] = v[key]
+ res[key] = clone(v[key])
}
return res
} else {