wip(ssr): proper scope analysis for ssr vnode slot fallback
This commit is contained in:
@@ -49,7 +49,7 @@ describe('ssr: components', () => {
|
||||
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) {
|
||||
@@ -75,7 +75,7 @@ describe('ssr: components', () => {
|
||||
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) {
|
||||
@@ -87,7 +87,7 @@ describe('ssr: components', () => {
|
||||
_push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
|
||||
} else {
|
||||
return [
|
||||
createTextVNode(toDisplayString(_ctx.msg + _ctx.outer))
|
||||
createTextVNode(toDisplayString(msg + _ctx.outer))
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -104,7 +104,7 @@ describe('ssr: components', () => {
|
||||
<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) {
|
||||
@@ -141,7 +141,7 @@ describe('ssr: components', () => {
|
||||
<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) {
|
||||
@@ -173,7 +173,7 @@ describe('ssr: components', () => {
|
||||
<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) {
|
||||
@@ -184,7 +184,13 @@ describe('ssr: components', () => {
|
||||
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))
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -193,6 +199,80 @@ describe('ssr: components', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('ssr: scopeId', () => {
|
||||
scopeId
|
||||
}).code
|
||||
).toMatchInlineSnapshot(`
|
||||
"const { resolveComponent } = require(\\"vue\\")
|
||||
"const { resolveComponent, createTextVNode } = require(\\"vue\\")
|
||||
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent) {
|
||||
@@ -51,7 +51,7 @@ describe('ssr: scopeId', () => {
|
||||
scopeId
|
||||
}).code
|
||||
).toMatchInlineSnapshot(`
|
||||
"const { resolveComponent } = require(\\"vue\\")
|
||||
"const { resolveComponent, createVNode } = require(\\"vue\\")
|
||||
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
|
||||
|
||||
return function ssrRender(_ctx, _push, _parent) {
|
||||
@@ -79,12 +79,12 @@ describe('ssr: scopeId', () => {
|
||||
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) => {
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
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'
|
||||
@@ -41,6 +43,10 @@ export function compile(
|
||||
|
||||
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: [
|
||||
@@ -66,8 +72,7 @@ export function compile(
|
||||
cloak: noopDirectiveTransform,
|
||||
once: noopDirectiveTransform,
|
||||
...(options.directiveTransforms || {}) // user transforms
|
||||
},
|
||||
isBuiltInComponent: isBuiltInDOMComponent
|
||||
}
|
||||
})
|
||||
|
||||
// traverse the template AST and convert into SSR codegen AST
|
||||
|
||||
@@ -17,13 +17,19 @@ import {
|
||||
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 {
|
||||
@@ -55,12 +61,26 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||
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
|
||||
@@ -86,7 +106,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
||||
// 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
|
||||
}
|
||||
@@ -143,47 +163,79 @@ export function ssrProcessComponent(
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
|
||||
|
||||
// merge helpers/components/directives/imports from the childRoot
|
||||
// back to current root
|
||||
const [vnodeNodeTransforms, vnodeDirectiveTransforms] = getDOMTransformPreset(
|
||||
true
|
||||
)
|
||||
|
||||
function createVNodeSlotBranch(
|
||||
props: ExpressionNode | undefined,
|
||||
children: TemplateChildNode[],
|
||||
parentContext: TransformContext
|
||||
): ReturnStatement {
|
||||
// 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.DIRECTIVE,
|
||||
name: 'slot',
|
||||
exp: props,
|
||||
arg: undefined,
|
||||
modifiers: [],
|
||||
loc: locStub
|
||||
}
|
||||
],
|
||||
children,
|
||||
loc: locStub,
|
||||
codegenNode: undefined
|
||||
}
|
||||
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 {
|
||||
@@ -192,7 +244,7 @@ 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 {
|
||||
|
||||
Reference in New Issue
Block a user