wip(ssr): proper scope analysis for ssr vnode slot fallback

This commit is contained in:
Evan You 2020-02-07 13:56:18 -05:00
parent b7a74d0439
commit a51e710396
11 changed files with 246 additions and 94 deletions

View File

@ -546,6 +546,25 @@ export const locStub: SourceLocation = {
end: { line: 1, column: 1, offset: 0 } 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( export function createArrayExpression(
elements: ArrayExpression['elements'], elements: ArrayExpression['elements'],
loc: SourceLocation = locStub loc: SourceLocation = locStub

View File

@ -11,6 +11,8 @@ export { baseParse, TextModes } from './parse'
export { export {
transform, transform,
TransformContext, TransformContext,
createTransformContext,
traverseNode,
createStructuralDirectiveTransform, createStructuralDirectiveTransform,
NodeTransform, NodeTransform,
StructuralDirectiveTransform, StructuralDirectiveTransform,

View File

@ -21,7 +21,8 @@ import {
SourceLocation, SourceLocation,
TextNode, TextNode,
TemplateChildNode, TemplateChildNode,
InterpolationNode InterpolationNode,
createRoot
} from './ast' } from './ast'
import { extend } from '@vue/shared' import { extend } from '@vue/shared'
@ -72,20 +73,10 @@ export function baseParse(
): RootNode { ): RootNode {
const context = createParserContext(content, options) const context = createParserContext(content, options)
const start = getCursor(context) const start = getCursor(context)
return createRoot(
return { parseChildren(context, TextModes.DATA, []),
type: NodeTypes.ROOT, getSelection(context, start)
children: parseChildren(context, TextModes.DATA, []), )
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc: getSelection(context, start)
}
} }
function createParserContext( function createParserContext(

View File

@ -109,7 +109,7 @@ export interface TransformContext extends Required<TransformOptions> {
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
} }
function createTransformContext( export function createTransformContext(
root: RootNode, root: RootNode,
{ {
prefixIdentifiers = false, prefixIdentifiers = false,

View File

@ -40,11 +40,15 @@ export const transformExpression: NodeTransform = (node, context) => {
const dir = node.props[i] const dir = node.props[i]
// do not process for v-on & v-for since they are special handled // do not process for v-on & v-for since they are special handled
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') { if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
const exp = dir.exp as SimpleExpressionNode | undefined const exp = dir.exp
const arg = dir.arg as SimpleExpressionNode | undefined const arg = dir.arg
// do not process exp if this is v-on:arg - we need special handling // do not process exp if this is v-on:arg - we need special handling
// for wrapping inline statements. // for wrapping inline statements.
if (exp && !(dir.name === 'on' && arg)) { if (
exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg)
) {
dir.exp = processExpression( dir.exp = processExpression(
exp, exp,
context, context,
@ -52,7 +56,7 @@ export const transformExpression: NodeTransform = (node, context) => {
dir.name === 'slot' dir.name === 'slot'
) )
} }
if (arg && !arg.isStatic) { if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) {
dir.arg = processExpression(arg, context) dir.arg = processExpression(arg, context)
} }
} }

View File

@ -3,7 +3,6 @@ import {
baseParse, baseParse,
CompilerOptions, CompilerOptions,
CodegenResult, CodegenResult,
isBuiltInType,
ParserOptions, ParserOptions,
RootNode, RootNode,
noopDirectiveTransform, noopDirectiveTransform,
@ -18,21 +17,12 @@ import { transformVText } from './transforms/vText'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn' import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow' import { transformShow } from './transforms/vShow'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { warnTransitionChildren } from './transforms/warnTransitionChildren' import { warnTransitionChildren } from './transforms/warnTransitionChildren'
export const parserOptions = __BROWSER__ export const parserOptions = __BROWSER__
? parserOptionsMinimal ? parserOptionsMinimal
: parserOptionsStandard : 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( export function getDOMTransformPreset(
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
): TransformPreset { ): TransformPreset {
@ -71,8 +61,7 @@ export function compile(
directiveTransforms: { directiveTransforms: {
...directiveTransforms, ...directiveTransforms,
...(options.directiveTransforms || {}) ...(options.directiveTransforms || {})
}, }
isBuiltInComponent: isBuiltInDOMComponent
}) })
} }

View File

@ -3,9 +3,11 @@ import {
ParserOptions, ParserOptions,
ElementNode, ElementNode,
Namespaces, Namespaces,
NodeTypes NodeTypes,
isBuiltInType
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared' import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
const isRawTextContainer = /*#__PURE__*/ makeMap( const isRawTextContainer = /*#__PURE__*/ makeMap(
'style,iframe,script,noscript', 'style,iframe,script,noscript',
@ -23,6 +25,14 @@ export const parserOptionsMinimal: ParserOptions = {
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag), isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre', 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 // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces { getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
let ns = parent ? parent.ns : DOMNamespaces.HTML let ns = parent ? parent.ns : DOMNamespaces.HTML

View File

@ -49,7 +49,7 @@ describe('ssr: components', () => {
describe('slots', () => { describe('slots', () => {
test('implicit default slot', () => { test('implicit default slot', () => {
expect(compile(`<foo>hello<div/></foo>`).code).toMatchInlineSnapshot(` expect(compile(`<foo>hello<div/></foo>`).code).toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\") "const { resolveComponent, createVNode, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
@ -75,7 +75,7 @@ describe('ssr: components', () => {
test('explicit default slot', () => { test('explicit default slot', () => {
expect(compile(`<foo v-slot="{ msg }">{{ msg + outer }}</foo>`).code) expect(compile(`<foo v-slot="{ msg }">{{ msg + outer }}</foo>`).code)
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\") "const { resolveComponent, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent, _ssrInterpolate } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent, _ssrInterpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
@ -87,7 +87,7 @@ describe('ssr: components', () => {
_push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`) _push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
} else { } else {
return [ 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> <template v-slot:named>bar</template>
</foo>`).code </foo>`).code
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\") "const { resolveComponent, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
@ -141,7 +141,7 @@ describe('ssr: components', () => {
<template v-slot:named v-if="ok">foo</template> <template v-slot:named v-if="ok">foo</template>
</foo>`).code </foo>`).code
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { resolveComponent, createSlots } = require(\\"vue\\") "const { resolveComponent, createTextVNode, createSlots } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { 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> <template v-for="key in names" v-slot:[key]="{ msg }">{{ msg + key + bar }}</template>
</foo>`).code </foo>`).code
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { resolveComponent, renderList, createSlots } = require(\\"vue\\") "const { resolveComponent, createTextVNode, renderList, createSlots } = require(\\"vue\\")
const { _ssrRenderComponent, _ssrInterpolate } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent, _ssrInterpolate } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
@ -184,7 +184,13 @@ describe('ssr: components', () => {
return { return {
name: key, name: key,
fn: ({ msg }, _push, _parent, _scopeId) => { 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', () => { test('built-in fallthroughs', () => {
// no fragment // no fragment
expect(compile(`<transition><div/></transition>`).code) expect(compile(`<transition><div/></transition>`).code)

View File

@ -23,7 +23,7 @@ describe('ssr: scopeId', () => {
scopeId scopeId
}).code }).code
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\") "const { resolveComponent, createTextVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
@ -51,7 +51,7 @@ describe('ssr: scopeId', () => {
scopeId scopeId
}).code }).code
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\") "const { resolveComponent, createVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
@ -79,12 +79,12 @@ describe('ssr: scopeId', () => {
scopeId scopeId
}).code }).code
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\") "const { resolveComponent, createVNode } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\") const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
const _component_bar = resolveComponent(\\"bar\\")
const _component_foo = resolveComponent(\\"foo\\") const _component_foo = resolveComponent(\\"foo\\")
const _component_bar = resolveComponent(\\"bar\\")
_push(_ssrRenderComponent(_component_foo, null, { _push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => { default: (_, _push, _parent, _scopeId) => {

View File

@ -10,12 +10,14 @@ import {
trackSlotScopes, trackSlotScopes,
noopDirectiveTransform, noopDirectiveTransform,
transformBind, transformBind,
transformStyle, transformStyle
isBuiltInDOMComponent
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement' import { ssrTransformElement } from './transforms/ssrTransformElement'
import { ssrTransformComponent } from './transforms/ssrTransformComponent' import {
ssrTransformComponent,
rawOptionsMap
} from './transforms/ssrTransformComponent'
import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet' import { ssrTransformSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrTransformIf } from './transforms/ssrVIf' import { ssrTransformIf } from './transforms/ssrVIf'
import { ssrTransformFor } from './transforms/ssrVFor' import { ssrTransformFor } from './transforms/ssrVFor'
@ -41,6 +43,10 @@ export function compile(
const ast = baseParse(template, options) 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, { transform(ast, {
...options, ...options,
nodeTransforms: [ nodeTransforms: [
@ -66,8 +72,7 @@ export function compile(
cloak: noopDirectiveTransform, cloak: noopDirectiveTransform,
once: noopDirectiveTransform, once: noopDirectiveTransform,
...(options.directiveTransforms || {}) // user transforms ...(options.directiveTransforms || {}) // user transforms
}, }
isBuiltInComponent: isBuiltInDOMComponent
}) })
// traverse the template AST and convert into SSR codegen AST // traverse the template AST and convert into SSR codegen AST

View File

@ -17,13 +17,19 @@ import {
createIfStatement, createIfStatement,
createSimpleExpression, createSimpleExpression,
getDOMTransformPreset, getDOMTransformPreset,
transform,
createReturnStatement, createReturnStatement,
ReturnStatement, ReturnStatement,
Namespaces, Namespaces,
locStub, locStub,
RootNode, RootNode,
TransformContext TransformContext,
CompilerOptions,
TransformOptions,
createRoot,
createTransformContext,
traverseNode,
ExpressionNode,
TemplateNode
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers' import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import { import {
@ -55,12 +61,26 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
return 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() { return function ssrPostTransformComponent() {
const component = resolveComponentType(node, context, true /* ssr */) buildSlots(clonedNode, context, (props, children) => {
if (isSymbol(component)) { vnodeBranches.push(createVNodeSlotBranch(props, children, context))
componentTypeMap.set(node, component) return createFunctionExpression(undefined)
return // built-in component: fallthrough })
}
const props = const props =
node.props.length > 0 node.props.length > 0
@ -86,7 +106,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
// build the children using normal vnode-based transforms // build the children using normal vnode-based transforms
// TODO fixme: `children` here has already been mutated at this point // TODO fixme: `children` here has already been mutated at this point
// so the sub-transform runs into errors :/ // so the sub-transform runs into errors :/
vnodeBranch: createVNodeSlotBranch(clone(children), context) vnodeBranch: vnodeBranches[wipEntries.length]
}) })
return fn return fn
} }
@ -143,47 +163,79 @@ export function ssrProcessComponent(
} }
} }
function createVNodeSlotBranch( export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
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 const [vnodeNodeTransforms, vnodeDirectiveTransforms] = getDOMTransformPreset(
// back to current root 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( ;(['helpers', 'components', 'directives', 'imports'] as const).forEach(
key => { 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 { function clone(v: any): any {
@ -192,7 +244,7 @@ function clone(v: any): any {
} else if (isObject(v)) { } else if (isObject(v)) {
const res: any = {} const res: any = {}
for (const key in v) { for (const key in v) {
res[key] = v[key] res[key] = clone(v[key])
} }
return res return res
} else { } else {