From a51e7103967c2b3a59cf19520914556e00f9698c Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 7 Feb 2020 13:56:18 -0500 Subject: [PATCH] wip(ssr): proper scope analysis for ssr vnode slot fallback --- packages/compiler-core/src/ast.ts | 19 +++ packages/compiler-core/src/index.ts | 2 + packages/compiler-core/src/parse.ts | 21 +-- packages/compiler-core/src/transform.ts | 2 +- .../src/transforms/transformExpression.ts | 12 +- packages/compiler-dom/src/index.ts | 13 +- .../compiler-dom/src/parserOptionsMinimal.ts | 12 +- .../__tests__/ssrComponent.spec.ts | 94 ++++++++++++- .../compiler-ssr/__tests__/ssrScopeId.spec.ts | 8 +- packages/compiler-ssr/src/index.ts | 15 +- .../src/transforms/ssrTransformComponent.ts | 132 ++++++++++++------ 11 files changed, 241 insertions(+), 89 deletions(-) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 8d2a8df74a..602af41dba 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -546,6 +546,25 @@ export const locStub: SourceLocation = { 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 diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 945d0f06e1..63cc6c15fd 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -11,6 +11,8 @@ export { baseParse, TextModes } from './parse' export { transform, TransformContext, + createTransformContext, + traverseNode, createStructuralDirectiveTransform, NodeTransform, StructuralDirectiveTransform, diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 3a4b8dadcb..f856d06027 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -21,7 +21,8 @@ import { SourceLocation, TextNode, TemplateChildNode, - InterpolationNode + InterpolationNode, + createRoot } from './ast' import { extend } from '@vue/shared' @@ -72,20 +73,10 @@ export function baseParse( ): 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( diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 1020481b62..ee8f98f6ca 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -109,7 +109,7 @@ export interface TransformContext extends Required { cache(exp: T, isVNode?: boolean): CacheExpression | T } -function createTransformContext( +export function createTransformContext( root: RootNode, { prefixIdentifiers = false, diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 1dde9258fe..12d8fc0c29 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -40,11 +40,15 @@ export const transformExpression: NodeTransform = (node, context) => { 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, @@ -52,7 +56,7 @@ export const transformExpression: NodeTransform = (node, context) => { dir.name === 'slot' ) } - if (arg && !arg.isStatic) { + if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) { dir.arg = processExpression(arg, context) } } diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 60989f14c9..273d1f0839 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -3,7 +3,6 @@ import { baseParse, CompilerOptions, CodegenResult, - isBuiltInType, ParserOptions, RootNode, noopDirectiveTransform, @@ -18,21 +17,12 @@ import { transformVText } from './transforms/vText' 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 { @@ -71,8 +61,7 @@ export function compile( directiveTransforms: { ...directiveTransforms, ...(options.directiveTransforms || {}) - }, - isBuiltInComponent: isBuiltInDOMComponent + } }) } diff --git a/packages/compiler-dom/src/parserOptionsMinimal.ts b/packages/compiler-dom/src/parserOptionsMinimal.ts index 0a0f25bb7c..7573109734 100644 --- a/packages/compiler-dom/src/parserOptionsMinimal.ts +++ b/packages/compiler-dom/src/parserOptionsMinimal.ts @@ -3,9 +3,11 @@ import { 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', @@ -23,6 +25,14 @@ export const parserOptionsMinimal: ParserOptions = { 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 diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index c70654bff0..ecc2407c8a 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -49,7 +49,7 @@ describe('ssr: components', () => { describe('slots', () => { test('implicit default slot', () => { expect(compile(`hello
`).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(`{{ msg + outer }}`).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', () => { `).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', () => { `).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', () => { `).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(` + + + `).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(\`\`) + _ssrRenderList(list, (i) => { + _push(\`\`) + }) + _push(\`
\`) + } 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(\`\`) + _ssrRenderList(_ctx.list, (i) => { + _push(\`\`) + }) + _push(\`\`) + } 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(`
`).code) diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts index ecb492fcca..e9e442f79b 100644 --- a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts @@ -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) => { diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts index e12acde64a..96ad623409 100644 --- a/packages/compiler-ssr/src/index.ts +++ b/packages/compiler-ssr/src/index.ts @@ -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 diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index 0d54039234..500e4ee88b 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -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( } } +export const rawOptionsMap = new WeakMap() + +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 { @@ -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 { -- 2.47.3