]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(ssr): ssr slot vnode fallback
authorEvan You <yyx990803@gmail.com>
Fri, 7 Feb 2020 06:06:51 +0000 (01:06 -0500)
committerEvan You <yyx990803@gmail.com>
Fri, 7 Feb 2020 06:06:51 +0000 (01:06 -0500)
15 files changed:
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/compile.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformText.ts
packages/compiler-dom/src/index.ts
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts
packages/template-explorer/src/index.ts

index 2123fe829fc6c297050b4a3a937aebee057d6926..8d2a8df74a6cbc3bfa948324402c781ec278c5a6 100644 (file)
@@ -51,7 +51,8 @@ export const enum NodeTypes {
   JS_BLOCK_STATEMENT,
   JS_TEMPLATE_LITERAL,
   JS_IF_STATEMENT,
-  JS_ASSIGNMENT_EXPRESSION
+  JS_ASSIGNMENT_EXPRESSION,
+  JS_RETURN_STATEMENT
 }
 
 export const enum ElementTypes {
@@ -294,7 +295,7 @@ export interface FunctionExpression extends Node {
   type: NodeTypes.JS_FUNCTION_EXPRESSION
   params: ExpressionNode | string | (ExpressionNode | string)[] | undefined
   returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
-  body?: BlockStatement
+  body?: BlockStatement | IfStatement
   newline: boolean
   // so that codegen knows it needs to generate ScopeId wrapper
   isSlot: boolean
@@ -322,7 +323,12 @@ export interface CacheExpression extends Node {
 
 // SSR-specific Node Types -----------------------------------------------------
 
-export type SSRCodegenNode = BlockStatement | TemplateLiteral | IfStatement
+export type SSRCodegenNode =
+  | BlockStatement
+  | TemplateLiteral
+  | IfStatement
+  | AssignmentExpression
+  | ReturnStatement
 
 export interface BlockStatement extends Node {
   type: NodeTypes.JS_BLOCK_STATEMENT
@@ -338,7 +344,7 @@ export interface IfStatement extends Node {
   type: NodeTypes.JS_IF_STATEMENT
   test: ExpressionNode
   consequent: BlockStatement
-  alternate: IfStatement | BlockStatement | undefined
+  alternate: IfStatement | BlockStatement | ReturnStatement | undefined
 }
 
 export interface AssignmentExpression extends Node {
@@ -347,6 +353,11 @@ export interface AssignmentExpression extends Node {
   right: JSChildNode
 }
 
+export interface ReturnStatement extends Node {
+  type: NodeTypes.JS_RETURN_STATEMENT
+  returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
+}
+
 // Codegen Node Types ----------------------------------------------------------
 
 // createVNode(...)
@@ -733,3 +744,13 @@ export function createAssignmentExpression(
     loc: locStub
   }
 }
+
+export function createReturnStatement(
+  returns: ReturnStatement['returns']
+): ReturnStatement {
+  return {
+    type: NodeTypes.JS_RETURN_STATEMENT,
+    returns,
+    loc: locStub
+  }
+}
index cc0637fc21f2b19a72a0c8403e995ca5ec43bd40..ecebf5135ed981d117e5490a54f6c97408c0b5f3 100644 (file)
@@ -22,7 +22,8 @@ import {
   SSRCodegenNode,
   TemplateLiteral,
   IfStatement,
-  AssignmentExpression
+  AssignmentExpression,
+  ReturnStatement
 } from './ast'
 import { SourceMapGenerator, RawSourceMap } from 'source-map'
 import {
@@ -44,8 +45,7 @@ import {
   CREATE_TEXT,
   PUSH_SCOPE_ID,
   POP_SCOPE_ID,
-  WITH_SCOPE_ID,
-  CREATE_BLOCK
+  WITH_SCOPE_ID
 } from './runtimeHelpers'
 import { ImportItem } from './transform'
 
@@ -334,24 +334,12 @@ function genModulePreamble(
   context: CodegenContext,
   genScopeId: boolean
 ) {
-  const { push, helper, newline, scopeId, runtimeModuleName, ssr } = context
+  const { push, helper, newline, scopeId, runtimeModuleName } = context
 
-  if (!__BROWSER__) {
-    // in ssr mode, `withId` helper is only needed if the template contains
-    // de-optimized component slots (which uses the createVNode helper)
-    if (
-      ssr &&
-      !(
-        ast.helpers.includes(CREATE_VNODE) || ast.helpers.includes(CREATE_BLOCK)
-      )
-    ) {
-      genScopeId = false
-    }
-    if (genScopeId) {
-      ast.helpers.push(WITH_SCOPE_ID)
-      if (ast.hoists.length) {
-        ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
-      }
+  if (!__BROWSER__ && genScopeId) {
+    ast.helpers.push(WITH_SCOPE_ID)
+    if (ast.hoists.length) {
+      ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
     }
   }
 
@@ -572,6 +560,9 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
     case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
       !__BROWSER__ && genAssignmentExpression(node, context)
       break
+    case NodeTypes.JS_RETURN_STATEMENT:
+      !__BROWSER__ && genReturnStatement(node, context)
+      break
 
     /* istanbul ignore next */
     default:
@@ -851,3 +842,15 @@ function genAssignmentExpression(
   context.push(` = `)
   genNode(node.right, context)
 }
+
+function genReturnStatement(
+  { returns }: ReturnStatement,
+  context: CodegenContext
+) {
+  context.push(`return `)
+  if (isArray(returns)) {
+    genNodeListAsArray(returns, context)
+  } else {
+    genNode(returns, context)
+  }
+}
index 7699dae8de5ba6d9168c0770dc8d126588099648..f7717228a45d04f70858be027ec6ab80b271b4d7 100644 (file)
@@ -1,6 +1,6 @@
 import { CompilerOptions } from './options'
 import { baseParse } from './parse'
-import { transform } from './transform'
+import { transform, NodeTransform, DirectiveTransform } from './transform'
 import { generate, CodegenResult } from './codegen'
 import { RootNode } from './ast'
 import { isString } from '@vue/shared'
@@ -17,6 +17,39 @@ import { transformOnce } from './transforms/vOnce'
 import { transformModel } from './transforms/vModel'
 import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
 
+export type TransformPreset = [
+  NodeTransform[],
+  Record<string, DirectiveTransform>
+]
+
+export function getBaseTransformPreset(
+  prefixIdentifiers?: boolean
+): TransformPreset {
+  return [
+    [
+      transformOnce,
+      transformIf,
+      transformFor,
+      ...(!__BROWSER__ && prefixIdentifiers
+        ? [
+            // order is important
+            trackVForSlotScopes,
+            transformExpression
+          ]
+        : []),
+      transformSlotOutlet,
+      transformElement,
+      trackSlotScopes,
+      transformText
+    ],
+    {
+      on: transformOn,
+      bind: transformBind,
+      model: transformModel
+    }
+  ]
+}
+
 // we name it `baseCompile` so that higher order compilers like
 // @vue/compiler-dom can export `compile` while re-exporting everything else.
 export function baseCompile(
@@ -44,30 +77,18 @@ export function baseCompile(
   }
 
   const ast = isString(template) ? baseParse(template, options) : template
+  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
+    prefixIdentifiers
+  )
   transform(ast, {
     ...options,
     prefixIdentifiers,
     nodeTransforms: [
-      transformOnce,
-      transformIf,
-      transformFor,
-      ...(prefixIdentifiers
-        ? [
-            // order is important
-            trackVForSlotScopes,
-            transformExpression
-          ]
-        : []),
-      transformSlotOutlet,
-      transformElement,
-      trackSlotScopes,
-      transformText,
+      ...nodeTransforms,
       ...(options.nodeTransforms || []) // user transforms
     ],
     directiveTransforms: {
-      on: transformOn,
-      bind: transformBind,
-      model: transformModel,
+      ...directiveTransforms,
       ...(options.directiveTransforms || {}) // user transforms
     }
   })
index 56c908fedbb20d57f4b7a300e8b16725a7ebcc5e..945d0f06e1886015e121d5bf95e5a4792931ac0b 100644 (file)
@@ -10,8 +10,8 @@ export {
 export { baseParse, TextModes } from './parse'
 export {
   transform,
-  createStructuralDirectiveTransform,
   TransformContext,
+  createStructuralDirectiveTransform,
   NodeTransform,
   StructuralDirectiveTransform,
   DirectiveTransform
@@ -23,18 +23,16 @@ export {
   CompilerError,
   createCompilerError
 } from './errors'
+
 export * from './ast'
 export * from './utils'
-export { registerRuntimeHelpers } from './runtimeHelpers'
-export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
+export * from './runtimeHelpers'
 
-// expose transforms so higher-order compilers can import and extend them
+export { getBaseTransformPreset, TransformPreset } from './compile'
 export { transformModel } from './transforms/vModel'
 export { transformOn } from './transforms/vOn'
 export { transformBind } from './transforms/vBind'
-
-// exported for compiler-ssr
-export * from './runtimeHelpers'
+export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
 export { processIf } from './transforms/vIf'
 export { processFor, createForLoopParams } from './transforms/vFor'
 export {
index 578e08d8eb3884e776e9bd78913ae4800d8c145b..1020481b6207abec03044d7b279d791b89530849 100644 (file)
@@ -85,8 +85,8 @@ export interface TransformContext extends Required<TransformOptions> {
   components: Set<string>
   directives: Set<string>
   hoists: JSChildNode[]
-  temps: number
   imports: Set<ImportItem>
+  temps: number
   cached: number
   identifiers: { [name: string]: number | undefined }
   scopes: {
@@ -141,8 +141,8 @@ function createTransformContext(
     components: new Set(),
     directives: new Set(),
     hoists: [],
-    temps: 0,
     imports: new Set(),
+    temps: 0,
     cached: 0,
     identifiers: {},
     scopes: {
index a15a2fd18071837b77c2fe2716d714e2a12e0759..c60e86dce425ba40064d1c49b17339c22e40e9aa 100644 (file)
@@ -77,7 +77,7 @@ export const transformText: NodeTransform = (node, context) => {
             callArgs.push(child)
           }
           // mark dynamic text with flag so it gets patched inside a block
-          if (child.type !== NodeTypes.TEXT) {
+          if (!context.ssr && child.type !== NodeTypes.TEXT) {
             callArgs.push(
               `${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`
             )
index 674a3ef8350c24d7c9040f63eca4d20ecfd93222..60989f14c9ff693037297f74cf8923cc12410d93 100644 (file)
@@ -6,7 +6,9 @@ import {
   isBuiltInType,
   ParserOptions,
   RootNode,
-  noopDirectiveTransform
+  noopDirectiveTransform,
+  TransformPreset,
+  getBaseTransformPreset
 } from '@vue/compiler-core'
 import { parserOptionsMinimal } from './parserOptionsMinimal'
 import { parserOptionsStandard } from './parserOptionsStandard'
@@ -31,25 +33,43 @@ export const isBuiltInDOMComponent = (tag: string): symbol | undefined => {
   }
 }
 
+export function getDOMTransformPreset(
+  prefixIdentifiers?: boolean
+): TransformPreset {
+  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
+    prefixIdentifiers
+  )
+  return [
+    [
+      ...nodeTransforms,
+      transformStyle,
+      ...(__DEV__ ? [warnTransitionChildren] : [])
+    ],
+    {
+      ...directiveTransforms,
+      cloak: noopDirectiveTransform,
+      html: transformVHtml,
+      text: transformVText,
+      model: transformModel, // override compiler-core
+      on: transformOn, // override compiler-core
+      show: transformShow
+    }
+  ]
+}
+
 export function compile(
   template: string,
   options: CompilerOptions = {}
 ): CodegenResult {
+  const [nodeTransforms, directiveTransforms] = getDOMTransformPreset(
+    options.prefixIdentifiers
+  )
   return baseCompile(template, {
     ...parserOptions,
     ...options,
-    nodeTransforms: [
-      transformStyle,
-      ...(__DEV__ ? [warnTransitionChildren] : []),
-      ...(options.nodeTransforms || [])
-    ],
+    nodeTransforms: [...nodeTransforms, ...(options.nodeTransforms || [])],
     directiveTransforms: {
-      cloak: noopDirectiveTransform,
-      html: transformVHtml,
-      text: transformVText,
-      model: transformModel, // override compiler-core
-      on: transformOn,
-      show: transformShow,
+      ...directiveTransforms,
       ...(options.directiveTransforms || {})
     },
     isBuiltInComponent: isBuiltInDOMComponent
index 8611a38f08e7906e685b15a8bc34436194a755c6..c70654bff09bc90ffea08dd074ac552eef867d62 100644 (file)
@@ -57,10 +57,13 @@ describe('ssr: components', () => {
 
           _push(_ssrRenderComponent(_component_foo, null, {
             default: (_, _push, _parent, _scopeId) => {
-              if (_scopeId) {
-                _push(\`hello<div \${_scopeId}></div>\`)
+              if (_push) {
+                _push(\`hello<div\${_scopeId}></div>\`)
               } else {
-                _push(\`hello<div></div>\`)
+                return [
+                  createTextVNode(\\"hello\\"),
+                  createVNode(\\"div\\")
+                ]
               }
             },
             _compiled: true
@@ -80,7 +83,13 @@ describe('ssr: components', () => {
 
           _push(_ssrRenderComponent(_component_foo, null, {
             default: ({ msg }, _push, _parent, _scopeId) => {
-              _push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
+              if (_push) {
+                _push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
+              } else {
+                return [
+                  createTextVNode(toDisplayString(_ctx.msg + _ctx.outer))
+                ]
+              }
             },
             _compiled: true
           }, _parent))
@@ -103,10 +112,22 @@ describe('ssr: components', () => {
 
           _push(_ssrRenderComponent(_component_foo, null, {
             default: (_, _push, _parent, _scopeId) => {
-              _push(\`foo\`)
+              if (_push) {
+                _push(\`foo\`)
+              } else {
+                return [
+                  createTextVNode(\\"foo\\")
+                ]
+              }
             },
             named: (_, _push, _parent, _scopeId) => {
-              _push(\`bar\`)
+              if (_push) {
+                _push(\`bar\`)
+              } else {
+                return [
+                  createTextVNode(\\"bar\\")
+                ]
+              }
             },
             _compiled: true
           }, _parent))
@@ -131,7 +152,13 @@ describe('ssr: components', () => {
               ? {
                   name: \\"named\\",
                   fn: (_, _push, _parent, _scopeId) => {
-                    _push(\`foo\`)
+                    if (_push) {
+                      _push(\`foo\`)
+                    } else {
+                      return [
+                        createTextVNode(\\"foo\\")
+                      ]
+                    }
                   }
                 }
               : undefined
index 31b1ef28efa45683d542149f3c2b4ac333fe0ca9..ecb492fccad20ca7082639e62bf1bca63d03b8fd 100644 (file)
@@ -31,7 +31,13 @@ describe('ssr: scopeId', () => {
 
         _push(_ssrRenderComponent(_component_foo, null, {
           default: (_, _push, _parent, _scopeId) => {
-            _push(\`foo\`)
+            if (_push) {
+              _push(\`foo\`)
+            } else {
+              return [
+                createTextVNode(\\"foo\\")
+              ]
+            }
           },
           _compiled: true
         }, _parent))
@@ -53,10 +59,12 @@ describe('ssr: scopeId', () => {
 
         _push(_ssrRenderComponent(_component_foo, null, {
           default: (_, _push, _parent, _scopeId) => {
-            if (_scopeId) {
-              _push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`)
+            if (_push) {
+              _push(\`<span data-v-xxxxxxx\${_scopeId}>hello</span>\`)
             } else {
-              _push(\`<span data-v-xxxxxxx>hello</span>\`)
+              return [
+                createVNode(\\"span\\", null, \\"hello\\")
+              ]
             }
           },
           _compiled: true
@@ -80,30 +88,30 @@ describe('ssr: scopeId', () => {
 
         _push(_ssrRenderComponent(_component_foo, null, {
           default: (_, _push, _parent, _scopeId) => {
-            if (_scopeId) {
-              _push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`)
+            if (_push) {
+              _push(\`<span data-v-xxxxxxx\${_scopeId}>hello</span>\`)
               _push(_ssrRenderComponent(_component_bar, null, {
                 default: (_, _push, _parent, _scopeId) => {
-                  if (_scopeId) {
-                    _push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`)
+                  if (_push) {
+                    _push(\`<span data-v-xxxxxxx\${_scopeId}></span>\`)
                   } else {
-                    _push(\`<span data-v-xxxxxxx></span>\`)
+                    return [
+                      createVNode(\\"span\\")
+                    ]
                   }
                 },
                 _compiled: true
               }, _parent))
             } else {
-              _push(\`<span data-v-xxxxxxx>hello</span>\`)
-              _push(_ssrRenderComponent(_component_bar, null, {
-                default: (_, _push, _parent, _scopeId) => {
-                  if (_scopeId) {
-                    _push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`)
-                  } else {
-                    _push(\`<span data-v-xxxxxxx></span>\`)
-                  }
-                },
-                _compiled: true
-              }, _parent))
+              return [
+                createVNode(\\"span\\", null, \\"hello\\"),
+                createVNode(_component_bar, null, {
+                  default: () => [
+                    createVNode(\\"span\\")
+                  ],
+                  _compiled: true
+                })
+              ]
             }
           },
           _compiled: true
index c5012e8693b4ce50dc091114a28bd6d62e19e6e2..b45f0a95fa3c47f5cc30e9ad9b0ae4d237eb500d 100644 (file)
@@ -28,7 +28,7 @@ import { ssrProcessElement } from './transforms/ssrTransformElement'
 // passing it to codegen.
 
 export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
-  const context = createSSRTransformContext(options)
+  const context = createSSRTransformContext(ast, options)
   const isFragment =
     ast.children.length > 1 && ast.children.some(c => !isText(c))
   processChildren(ast.children, context, isFragment)
@@ -46,6 +46,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
 export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
 
 function createSSRTransformContext(
+  root: RootNode,
   options: CompilerOptions,
   helpers: Set<symbol> = new Set(),
   withSlotScopeId = false
@@ -54,6 +55,7 @@ function createSSRTransformContext(
   let currentString: TemplateLiteral | null = null
 
   return {
+    root,
     options,
     body,
     helpers,
@@ -91,6 +93,7 @@ function createChildContext(
 ): SSRTransformContext {
   // ensure child inherits parent helpers
   return createSSRTransformContext(
+    parent.root,
     parent.options,
     parent.helpers,
     withSlotScopeId
index 2ee5fd33efee0e5acaaa10e67366043316582140..0d5403923483d9f92bbe9ececfc97210a77a9924 100644 (file)
@@ -8,7 +8,6 @@ import {
   ComponentNode,
   SlotFnBuilder,
   createFunctionExpression,
-  createBlockStatement,
   buildSlots,
   FunctionExpression,
   TemplateChildNode,
@@ -17,7 +16,14 @@ import {
   TRANSITION_GROUP,
   createIfStatement,
   createSimpleExpression,
-  isText
+  getDOMTransformPreset,
+  transform,
+  createReturnStatement,
+  ReturnStatement,
+  Namespaces,
+  locStub,
+  RootNode,
+  TransformContext
 } from '@vue/compiler-dom'
 import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
 import {
@@ -25,7 +31,7 @@ import {
   processChildren,
   processChildrenAsStatement
 } from '../ssrCodegenTransform'
-import { isSymbol } from '@vue/shared'
+import { isSymbol, isObject, isArray } from '@vue/shared'
 
 // We need to construct the slot functions in the 1st pass to ensure proper
 // scope tracking, but the children of each slot cannot be processed until
@@ -36,6 +42,7 @@ const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
 interface WIPSlotEntry {
   fn: FunctionExpression
   children: TemplateChildNode[]
+  vnodeBranch: ReturnStatement
 }
 
 const componentTypeMap = new WeakMap<ComponentNode, symbol>()
@@ -55,26 +62,32 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
       return // built-in component: fallthrough
     }
 
-    // note we are not passing ssr: true here because for components, v-on
-    // handlers should still be passed
     const props =
-      node.props.length > 0 ? buildProps(node, context).props || `null` : `null`
+      node.props.length > 0
+        ? // note we are not passing ssr: true here because for components, v-on
+          // handlers should still be passed
+          buildProps(node, context).props || `null`
+        : `null`
 
     const wipEntries: WIPSlotEntry[] = []
     wipMap.set(node, wipEntries)
 
     const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
-      // An SSR slot function has the signature of
-      //   (props, _push, _parent, _scopeId) => void
-      // See server-renderer/src/helpers/renderSlot.ts
       const fn = createFunctionExpression(
         [props || `_`, `_push`, `_parent`, `_scopeId`],
         undefined, // no return, assign body later
         true, // newline
-        false, // isSlot: pass false since we don't need client scopeId codegen
+        true, // isSlot
         loc
       )
-      wipEntries.push({ fn, children })
+      wipEntries.push({
+        fn,
+        children,
+        // 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)
+      })
       return fn
     }
 
@@ -82,9 +95,6 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
       ? buildSlots(node, context, buildSSRSlotFn).slots
       : `null`
 
-    // TODO option for slots bail out
-    // TODO scopeId
-
     node.ssrCodegenNode = createCallExpression(
       context.helper(SSR_RENDER_COMPONENT),
       [component, props, slots, `_parent`]
@@ -113,26 +123,79 @@ export function ssrProcessComponent(
     // finish up slot function expressions from the 1st pass.
     const wipEntries = wipMap.get(node) || []
     for (let i = 0; i < wipEntries.length; i++) {
-      const { fn, children } = wipEntries[i]
-      const hasNonTextChild = children.some(c => !isText(c))
-      if (hasNonTextChild) {
-        // SSR slots need to handled potential presence of scopeId of the child
-        // component. To avoid the cost of concatenation when it's unnecessary,
-        // we split the code into two paths, one with slot scopeId and one without.
-        fn.body = createBlockStatement([
-          createIfStatement(
-            createSimpleExpression(`_scopeId`, false),
-            // branch with scopeId concatenation
-            processChildrenAsStatement(children, context, false, true),
-            // branch without scopeId concatenation
-            processChildrenAsStatement(children, context, false, false)
-          )
-        ])
-      } else {
-        // only text, no need for scopeId branching.
-        fn.body = processChildrenAsStatement(children, context)
-      }
+      const { fn, children, vnodeBranch } = wipEntries[i]
+      // For each slot, we generate two branches: one SSR-optimized branch and
+      // one normal vnode-based branch. The branches are taken based on the
+      // presence of the 2nd `_push` argument (which is only present if the slot
+      // is called by `_ssrRenderSlot`.
+      fn.body = createIfStatement(
+        createSimpleExpression(`_push`, false),
+        processChildrenAsStatement(
+          children,
+          context,
+          false,
+          true /* withSlotScopeId */
+        ),
+        vnodeBranch
+      )
     }
     context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode]))
   }
 }
+
+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
+  })
+
+  // merge helpers/components/directives/imports from the childRoot
+  // back to current root
+  ;(['helpers', 'components', 'directives', 'imports'] as const).forEach(
+    key => {
+      root[key] = [...new Set([...root[key], ...childRoot[key]])] as any
+    }
+  )
+
+  return createReturnStatement(children)
+}
+
+function clone(v: any): any {
+  if (isArray(v)) {
+    return v.map(clone)
+  } else if (isObject(v)) {
+    const res: any = {}
+    for (const key in v) {
+      res[key] = v[key]
+    }
+    return res
+  } else {
+    return v
+  }
+}
index 000266b095e767856a8c30a9f1957bfa04b3c386..6354fd791d8550a1a4d3168dfe88c7a68e5aaa21 100644 (file)
@@ -309,7 +309,6 @@ export function ssrProcessElement(
 
   // Handle slot scopeId
   if (context.withSlotScopeId) {
-    context.pushStringPart(` `)
     context.pushStringPart(createSimpleExpression(`_scopeId`, false))
   }
 
index 3200dd3d10bcc61c46b3014b63d34c42a0f5db38..32d89268c0c50abd9f2b646d1b40c94ee8bf0f6c 100644 (file)
@@ -8,6 +8,7 @@ import {
   VNode
 } from '../vnode'
 import { PatchFlags } from '@vue/shared'
+import { warn } from '../warning'
 
 export function renderSlot(
   slots: Record<string, Slot>,
@@ -17,7 +18,17 @@ export function renderSlot(
   // the compiler and guaranteed to be an array
   fallback?: VNodeArrayChildren
 ): VNode {
-  const slot = slots[name]
+  let slot = slots[name]
+
+  if (__DEV__ && slot.length > 1) {
+    warn(
+      `SSR-optimized slot function detected in a non-SSR-optimized render ` +
+        `function. You need to mark this component with $dynamic-slots in the ` +
+        `parent template.`
+    )
+    slot = () => []
+  }
+
   return (
     openBlock(),
     createBlock(
index 0ffc8176aee0a72077da0e266bbff484233bf513..3caaf4421e543e15c5e3c4724f1dc37573e137e5 100644 (file)
@@ -25,7 +25,7 @@ export function ssrRenderSlot(
     if (slotFn.length > 1) {
       // only ssr-optimized slot fns accept more than 1 arguments
       const scopeId = parentComponent && parentComponent.type.__scopeId
-      slotFn(slotProps, push, parentComponent, scopeId ? scopeId + `-s` : null)
+      slotFn(slotProps, push, parentComponent, scopeId ? ` ${scopeId}-s` : ``)
     } else {
       // normal slot
       renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
index b774a7794cd429227378cdc78d2b272c9fb9f439..cf97e8e69f7a9cdafde105e63a4cd14a5aac0c61 100644 (file)
@@ -30,7 +30,7 @@ window.init = () => {
   ssrMode.value = persistedState.ssr
   Object.assign(compilerOptions, persistedState.options)
 
-  let lastSuccessfulCode: string = `/* See console for error */`
+  let lastSuccessfulCode: string
   let lastSuccessfulMap: SourceMapConsumer | undefined = undefined
   function compileCode(source: string): string {
     console.clear()
@@ -57,6 +57,9 @@ window.init = () => {
       lastSuccessfulMap = new window._deps['source-map'].SourceMapConsumer(map)
       lastSuccessfulMap!.computeColumnSpans()
     } catch (e) {
+      lastSuccessfulCode = `/* ERROR: ${
+        e.message
+      } (see console for more info) */`
       console.error(e)
     }
     return lastSuccessfulCode