]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(ssr): proper scope analysis for ssr vnode slot fallback
authorEvan You <yyx990803@gmail.com>
Fri, 7 Feb 2020 18:56:18 +0000 (13:56 -0500)
committerEvan You <yyx990803@gmail.com>
Fri, 7 Feb 2020 18:56:18 +0000 (13:56 -0500)
packages/compiler-core/src/ast.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/parse.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-dom/src/index.ts
packages/compiler-dom/src/parserOptionsMinimal.ts
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
packages/compiler-ssr/src/index.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

index 8d2a8df74a6cbc3bfa948324402c781ec278c5a6..602af41dba255f9100ad27512644d5524e61747e 100644 (file)
@@ -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
index 945d0f06e1886015e121d5bf95e5a4792931ac0b..63cc6c15fdcee7af3b64ce5eb52dd17cd67b4e7a 100644 (file)
@@ -11,6 +11,8 @@ export { baseParse, TextModes } from './parse'
 export {
   transform,
   TransformContext,
+  createTransformContext,
+  traverseNode,
   createStructuralDirectiveTransform,
   NodeTransform,
   StructuralDirectiveTransform,
index 3a4b8dadcb5898605d065fe3d952b131082c6781..f856d06027d05e23414b5395344374ed30e2585e 100644 (file)
@@ -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(
index 1020481b6207abec03044d7b279d791b89530849..ee8f98f6ca3682bd35e290e9936b4701f30b7954 100644 (file)
@@ -109,7 +109,7 @@ export interface TransformContext extends Required<TransformOptions> {
   cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
 }
 
-function createTransformContext(
+export function createTransformContext(
   root: RootNode,
   {
     prefixIdentifiers = false,
index 1dde9258fe0f28497ac7dafa7d4a848804ba5d98..12d8fc0c29c05841cc3b5469c6c6b71a83926b73 100644 (file)
@@ -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)
         }
       }
index 60989f14c9ff693037297f74cf8923cc12410d93..273d1f0839d18ef54d3fc6f0fecbcd1540167d6c 100644 (file)
@@ -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
+    }
   })
 }
 
index 0a0f25bb7c2f926b6dc5cec920554f6d9f7687ee..75731097342ed8374af921379bd40c31e30cacba 100644 (file)
@@ -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
index c70654bff09bc90ffea08dd074ac552eef867d62..ecc2407c8a0a5ae126fa39c2378a9d4963d89a1d 100644 (file)
@@ -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)
index ecb492fccad20ca7082639e62bf1bca63d03b8fd..e9e442f79b0673650328e23fde6c99f8f9ec9ef1 100644 (file)
@@ -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) => {
index e12acde64ae21fe60723033abfd453b8fa4355e4..96ad62340961cc3445317a719ba18e3641c3e627 100644 (file)
@@ -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
index 0d5403923483d9f92bbe9ececfc97210a77a9924..500e4ee88bca3d67e6c6f1b7547fbb2a7a3b139d 100644 (file)
@@ -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<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 {
@@ -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 {