]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(ssr): ssr helper codegen
authorEvan You <yyx990803@gmail.com>
Mon, 3 Feb 2020 22:47:06 +0000 (17:47 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 3 Feb 2020 23:31:10 +0000 (18:31 -0500)
18 files changed:
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-ssr/__tests__/ssrElement.spec.ts
packages/compiler-ssr/__tests__/ssrText.spec.ts
packages/compiler-ssr/src/index.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrVFor.ts
packages/compiler-ssr/src/transforms/ssrVIf.ts
packages/server-renderer/__tests__/interpolate.spec.ts
packages/server-renderer/__tests__/renderProps.spec.ts
packages/server-renderer/__tests__/renderToString.spec.ts
packages/server-renderer/src/index.ts

index d686c1aabf64659b5a46363c4eb0754f0776d1d9..ac8f89c4324058350487ee2198a4bd1db45ce014 100644 (file)
@@ -103,6 +103,7 @@ export interface RootNode extends Node {
   imports: ImportItem[]
   cached: number
   codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
+  ssrHelpers?: symbol[]
 }
 
 export type ElementNode =
index 245e33b0c7e28305070902c9f31964331794a994..d558ec6510977eaf42b4aa728aeac11968a27b8d 100644 (file)
@@ -74,7 +74,7 @@ function createCodegenContext(
   ast: RootNode,
   {
     mode = 'function',
-    prefixIdentifiers = mode === 'module' || mode === 'cjs',
+    prefixIdentifiers = mode === 'module',
     sourceMap = false,
     filename = `template.vue.html`,
     scopeId = null,
@@ -169,7 +169,6 @@ export function generate(
   const {
     mode,
     push,
-    helper,
     prefixIdentifiers,
     indent,
     deindent,
@@ -182,58 +181,10 @@ export function generate(
   const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
 
   // preambles
-  if (mode === 'function' || mode === 'cjs') {
-    const VueBinding = mode === 'function' ? `Vue` : `require("vue")`
-    // Generate const declaration for helpers
-    // In prefix mode, we place the const declaration at top so it's done
-    // only once; But if we not prefixing, we place the declaration inside the
-    // with block so it doesn't incur the `in` check cost for every helper access.
-    if (hasHelpers) {
-      if (prefixIdentifiers) {
-        push(
-          `const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`
-        )
-      } else {
-        // "with" mode.
-        // save Vue in a separate variable to avoid collision
-        push(`const _Vue = ${VueBinding}\n`)
-        // in "with" mode, helpers are declared inside the with block to avoid
-        // has check cost, but hoists are lifted out of the function - we need
-        // to provide the helper here.
-        if (ast.hoists.length) {
-          const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
-            .filter(helper => ast.helpers.includes(helper))
-            .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
-            .join(', ')
-          push(`const { ${staticHelpers} } = _Vue\n`)
-        }
-      }
-    }
-    genHoists(ast.hoists, context)
-    newline()
-    push(`return `)
+  if (mode === 'module') {
+    genModulePreamble(ast, context, genScopeId)
   } else {
-    // generate import statements for helpers
-    if (genScopeId) {
-      ast.helpers.push(WITH_SCOPE_ID)
-      if (ast.hoists.length) {
-        ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
-      }
-    }
-    if (hasHelpers) {
-      push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
-    }
-    if (ast.imports.length) {
-      genImports(ast.imports, context)
-      newline()
-    }
-    if (genScopeId) {
-      push(`const withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
-      newline()
-    }
-    genHoists(ast.hoists, context)
-    newline()
-    push(`export `)
+    genFunctionPreamble(ast, context)
   }
 
   // enter render function
@@ -315,6 +266,82 @@ export function generate(
   }
 }
 
+function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
+  const { mode, helper, prefixIdentifiers, push, newline } = context
+  const VueBinding = mode === 'function' ? `Vue` : `require("vue")`
+  // Generate const declaration for helpers
+  // In prefix mode, we place the const declaration at top so it's done
+  // only once; But if we not prefixing, we place the declaration inside the
+  // with block so it doesn't incur the `in` check cost for every helper access.
+  if (ast.helpers.length > 0) {
+    if (prefixIdentifiers) {
+      push(`const { ${ast.helpers.map(helper).join(', ')} } = ${VueBinding}\n`)
+    } else {
+      // "with" mode.
+      // save Vue in a separate variable to avoid collision
+      push(`const _Vue = ${VueBinding}\n`)
+      // in "with" mode, helpers are declared inside the with block to avoid
+      // has check cost, but hoists are lifted out of the function - we need
+      // to provide the helper here.
+      if (ast.hoists.length) {
+        const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
+          .filter(helper => ast.helpers.includes(helper))
+          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
+          .join(', ')
+        push(`const { ${staticHelpers} } = _Vue\n`)
+      }
+    }
+  }
+  // generate variables for ssr helpers
+  if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
+    // ssr guaruntees prefixIdentifier: true
+    push(
+      `const { ${ast.ssrHelpers
+        .map(helper)
+        .join(', ')} } = require("@vue/server-renderer")\n`
+    )
+  }
+  genHoists(ast.hoists, context)
+  newline()
+  push(`return `)
+}
+
+function genModulePreamble(
+  ast: RootNode,
+  context: CodegenContext,
+  genScopeId: boolean
+) {
+  const { push, helper, newline, scopeId } = context
+  // generate import statements for helpers
+  if (genScopeId) {
+    ast.helpers.push(WITH_SCOPE_ID)
+    if (ast.hoists.length) {
+      ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
+    }
+  }
+  if (ast.helpers.length) {
+    push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
+  }
+  if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
+    push(
+      `import { ${ast.ssrHelpers
+        .map(helper)
+        .join(', ')} } from "@vue/server-renderer"\n`
+    )
+  }
+  if (ast.imports.length) {
+    genImports(ast.imports, context)
+    newline()
+  }
+  if (genScopeId) {
+    push(`const withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
+    newline()
+  }
+  genHoists(ast.hoists, context)
+  newline()
+  push(`export `)
+}
+
 function genAssets(
   assets: string[],
   type: 'component' | 'directive',
index b87cfe421f44cc04d93c183a7b0b90c250711bb3..6b63067d13ab8f262e400ad510d8472629a0476e 100644 (file)
@@ -33,6 +33,7 @@ export { transformOn } from './transforms/vOn'
 
 // exported for compiler-ssr
 export { processIfBranches } from './transforms/vIf'
+export { processForNode } from './transforms/vFor'
 export {
   transformExpression,
   processExpression
index 7a0c866c0b50e7be9500292a11144bcd5b4ae7e8..f6dde0fa64dd2a66b03f921f02e29334f4ae9f14 100644 (file)
@@ -23,7 +23,6 @@ export interface ParserOptions {
   // this number is based on the map above, but it should be pre-computed
   // to avoid the cost on every parse() call.
   maxCRNameLength?: number
-
   onError?: (error: CompilerError) => void
 }
 
@@ -32,6 +31,8 @@ export interface TransformOptions {
   directiveTransforms?: { [name: string]: DirectiveTransform | undefined }
   isBuiltInComponent?: (tag: string) => symbol | void
   // Transform expressions like {{ foo }} to `_ctx.foo`.
+  // If this option is false, the generated code will be wrapped in a
+  // `with (this) { ... }` block.
   // - This is force-enabled in module mode, since modules are by default strict
   //   and cannot use `with`
   // - Default: mode === 'module'
@@ -48,6 +49,7 @@ export interface TransformOptions {
   //   analysis to determine if a handler is safe to cache.
   // - Default: false
   cacheHandlers?: boolean
+  ssr?: boolean
   onError?: (error: CompilerError) => void
 }
 
@@ -61,13 +63,6 @@ export interface CodegenOptions {
   //   `require('vue')`.
   // - Default: 'function'
   mode?: 'module' | 'function' | 'cjs'
-  // Prefix suitable identifiers with _ctx.
-  // If this option is false, the generated code will be wrapped in a
-  // `with (this) { ... }` block.
-  // - This is force-enabled in module mode, since modules are by default strict
-  //   and cannot use `with`
-  // - Default: mode === 'module'
-  prefixIdentifiers?: boolean
   // Generate source map?
   // - Default: false
   sourceMap?: boolean
@@ -76,7 +71,9 @@ export interface CodegenOptions {
   filename?: string
   // SFC scoped styles ID
   scopeId?: string | null
-  // generate SSR specific code?
+  // we need to know about this to generate proper preambles
+  prefixIdentifiers?: boolean
+  // generate ssr-specific code?
   ssr?: boolean
 }
 
index 7029c0851a133a2e2b68d59ff15e53a8d9884b4b..35c81e8be6f10588e39e6d9a01475148d7ae6412 100644 (file)
@@ -115,6 +115,7 @@ function createTransformContext(
     nodeTransforms = [],
     directiveTransforms = {},
     isBuiltInComponent = NOOP,
+    ssr = false,
     onError = defaultOnError
   }: TransformOptions
 ): TransformContext {
@@ -126,6 +127,7 @@ function createTransformContext(
     nodeTransforms,
     directiveTransforms,
     isBuiltInComponent,
+    ssr,
     onError,
 
     // state
@@ -256,10 +258,19 @@ export function transform(root: RootNode, options: TransformOptions) {
   if (options.hoistStatic) {
     hoistStatic(root, context)
   }
-  finalizeRoot(root, context)
+  if (!options.ssr) {
+    createRootCodegen(root, context)
+  }
+  // finalize meta information
+  root.helpers = [...context.helpers]
+  root.components = [...context.components]
+  root.directives = [...context.directives]
+  root.imports = [...context.imports]
+  root.hoists = context.hoists
+  root.cached = context.cached
 }
 
-function finalizeRoot(root: RootNode, context: TransformContext) {
+function createRootCodegen(root: RootNode, context: TransformContext) {
   const { helper } = context
   const { children } = root
   const child = children[0]
@@ -304,13 +315,6 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
   } else {
     // no children = noop. codegen will return null.
   }
-  // finalize meta information
-  root.helpers = [...context.helpers]
-  root.components = [...context.components]
-  root.directives = [...context.directives]
-  root.imports = [...context.imports]
-  root.hoists = context.hoists
-  root.cached = context.cached
 }
 
 export function traverseChildren(
@@ -359,13 +363,17 @@ export function traverseNode(
 
   switch (node.type) {
     case NodeTypes.COMMENT:
-      // inject import for the Comment symbol, which is needed for creating
-      // comment nodes with `createVNode`
-      context.helper(CREATE_COMMENT)
+      if (!context.ssr) {
+        // inject import for the Comment symbol, which is needed for creating
+        // comment nodes with `createVNode`
+        context.helper(CREATE_COMMENT)
+      }
       break
     case NodeTypes.INTERPOLATION:
       // no need to traverse, but we need to inject toString helper
-      context.helper(TO_DISPLAY_STRING)
+      if (!context.ssr) {
+        context.helper(TO_DISPLAY_STRING)
+      }
       break
 
     // for container types, further traverse downwards
index a31a47e8edf26600e10f6ea51d12b4f0569dba70..077101ab5ed7e0f14c7c28f1c69633e4c8ca2643 100644 (file)
@@ -17,7 +17,10 @@ import {
   ForCodegenNode,
   ElementCodegenNode,
   SlotOutletCodegenNode,
-  SlotOutletNode
+  SlotOutletNode,
+  ElementNode,
+  DirectiveNode,
+  ForNode
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import {
@@ -41,140 +44,162 @@ import { PatchFlags, PatchFlagNames } from '@vue/shared'
 export const transformFor = createStructuralDirectiveTransform(
   'for',
   (node, dir, context) => {
-    if (!dir.exp) {
-      context.onError(
-        createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
-      )
-      return
-    }
+    const { helper } = context
+    return processForNode(node, dir, context, (forNode, parseResult) => {
+      // create the loop render function expression now, and add the
+      // iterator on exit after all children have been traversed
+      const renderExp = createCallExpression(helper(RENDER_LIST), [
+        forNode.source
+      ])
+      const keyProp = findProp(node, `key`)
+      const fragmentFlag = keyProp
+        ? PatchFlags.KEYED_FRAGMENT
+        : PatchFlags.UNKEYED_FRAGMENT
+      forNode.codegenNode = createSequenceExpression([
+        // fragment blocks disable tracking since they always diff their children
+        createCallExpression(helper(OPEN_BLOCK), [`false`]),
+        createCallExpression(helper(CREATE_BLOCK), [
+          helper(FRAGMENT),
+          `null`,
+          renderExp,
+          `${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
+        ])
+      ]) as ForCodegenNode
 
-    const parseResult = parseForExpression(
-      // can only be simple expression because vFor transform is applied
-      // before expression transform.
-      dir.exp as SimpleExpressionNode,
-      context
+      return () => {
+        // finish the codegen now that all children have been traversed
+        let childBlock
+        const isTemplate = isTemplateNode(node)
+        const slotOutlet = isSlotOutlet(node)
+          ? node
+          : isTemplate &&
+            node.children.length === 1 &&
+            isSlotOutlet(node.children[0])
+            ? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
+            : null
+        const keyProperty = keyProp
+          ? createObjectProperty(
+              `key`,
+              keyProp.type === NodeTypes.ATTRIBUTE
+                ? createSimpleExpression(keyProp.value!.content, true)
+                : keyProp.exp!
+            )
+          : null
+        if (slotOutlet) {
+          // <slot v-for="..."> or <template v-for="..."><slot/></template>
+          childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
+          if (isTemplate && keyProperty) {
+            // <template v-for="..." :key="..."><slot/></template>
+            // we need to inject the key to the renderSlot() call.
+            // the props for renderSlot is passed as the 3rd argument.
+            injectProp(childBlock, keyProperty, context)
+          }
+        } else if (isTemplate) {
+          // <template v-for="...">
+          // should generate a fragment block for each loop
+          childBlock = createBlockExpression(
+            createCallExpression(helper(CREATE_BLOCK), [
+              helper(FRAGMENT),
+              keyProperty ? createObjectExpression([keyProperty]) : `null`,
+              node.children,
+              `${PatchFlags.STABLE_FRAGMENT} /* ${
+                PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
+              } */`
+            ]),
+            context
+          )
+        } else {
+          // Normal element v-for. Directly use the child's codegenNode
+          // arguments, but replace createVNode() with createBlock()
+          let codegenNode = node.codegenNode as ElementCodegenNode
+          if (codegenNode.callee === WITH_DIRECTIVES) {
+            codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
+          } else {
+            codegenNode.callee = helper(CREATE_BLOCK)
+          }
+          childBlock = createBlockExpression(codegenNode, context)
+        }
+
+        renderExp.arguments.push(
+          createFunctionExpression(
+            createForLoopParams(parseResult),
+            childBlock,
+            true /* force newline */
+          )
+        )
+      }
+    })
+  }
+)
+
+// target-agnostic transform used for both Client and SSR
+export function processForNode(
+  node: ElementNode,
+  dir: DirectiveNode,
+  context: TransformContext,
+  processCodegen?: (
+    forNode: ForNode,
+    parseResult: ForParseResult
+  ) => (() => void) | undefined
+) {
+  if (!dir.exp) {
+    context.onError(
+      createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
     )
+    return
+  }
 
-    if (!parseResult) {
-      context.onError(
-        createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
-      )
-      return
-    }
+  const parseResult = parseForExpression(
+    // can only be simple expression because vFor transform is applied
+    // before expression transform.
+    dir.exp as SimpleExpressionNode,
+    context
+  )
 
-    const { helper, addIdentifiers, removeIdentifiers, scopes } = context
-    const { source, value, key, index } = parseResult
+  if (!parseResult) {
+    context.onError(
+      createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
+    )
+    return
+  }
 
-    // create the loop render function expression now, and add the
-    // iterator on exit after all children have been traversed
-    const renderExp = createCallExpression(helper(RENDER_LIST), [source])
-    const keyProp = findProp(node, `key`)
-    const fragmentFlag = keyProp
-      ? PatchFlags.KEYED_FRAGMENT
-      : PatchFlags.UNKEYED_FRAGMENT
-    const codegenNode = createSequenceExpression([
-      // fragment blocks disable tracking since they always diff their children
-      createCallExpression(helper(OPEN_BLOCK), [`false`]),
-      createCallExpression(helper(CREATE_BLOCK), [
-        helper(FRAGMENT),
-        `null`,
-        renderExp,
-        `${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
-      ])
-    ]) as ForCodegenNode
+  const { addIdentifiers, removeIdentifiers, scopes } = context
+  const { source, value, key, index } = parseResult
 
-    context.replaceNode({
-      type: NodeTypes.FOR,
-      loc: dir.loc,
-      source,
-      valueAlias: value,
-      keyAlias: key,
-      objectIndexAlias: index,
-      children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
-      codegenNode
-    })
+  const forNode: ForNode = {
+    type: NodeTypes.FOR,
+    loc: dir.loc,
+    source,
+    valueAlias: value,
+    keyAlias: key,
+    objectIndexAlias: index,
+    children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
+  }
 
-    // bookkeeping
-    scopes.vFor++
-    if (!__BROWSER__ && context.prefixIdentifiers) {
-      // scope management
-      // inject identifiers to context
-      value && addIdentifiers(value)
-      key && addIdentifiers(key)
-      index && addIdentifiers(index)
-    }
+  context.replaceNode(forNode)
 
-    return () => {
-      scopes.vFor--
-      if (!__BROWSER__ && context.prefixIdentifiers) {
-        value && removeIdentifiers(value)
-        key && removeIdentifiers(key)
-        index && removeIdentifiers(index)
-      }
+  // bookkeeping
+  scopes.vFor++
+  if (!__BROWSER__ && context.prefixIdentifiers) {
+    // scope management
+    // inject identifiers to context
+    value && addIdentifiers(value)
+    key && addIdentifiers(key)
+    index && addIdentifiers(index)
+  }
 
-      // finish the codegen now that all children have been traversed
-      let childBlock
-      const isTemplate = isTemplateNode(node)
-      const slotOutlet = isSlotOutlet(node)
-        ? node
-        : isTemplate &&
-          node.children.length === 1 &&
-          isSlotOutlet(node.children[0])
-          ? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
-          : null
-      const keyProperty = keyProp
-        ? createObjectProperty(
-            `key`,
-            keyProp.type === NodeTypes.ATTRIBUTE
-              ? createSimpleExpression(keyProp.value!.content, true)
-              : keyProp.exp!
-          )
-        : null
-      if (slotOutlet) {
-        // <slot v-for="..."> or <template v-for="..."><slot/></template>
-        childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
-        if (isTemplate && keyProperty) {
-          // <template v-for="..." :key="..."><slot/></template>
-          // we need to inject the key to the renderSlot() call.
-          // the props for renderSlot is passed as the 3rd argument.
-          injectProp(childBlock, keyProperty, context)
-        }
-      } else if (isTemplate) {
-        // <template v-for="...">
-        // should generate a fragment block for each loop
-        childBlock = createBlockExpression(
-          createCallExpression(helper(CREATE_BLOCK), [
-            helper(FRAGMENT),
-            keyProperty ? createObjectExpression([keyProperty]) : `null`,
-            node.children,
-            `${PatchFlags.STABLE_FRAGMENT} /* ${
-              PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
-            } */`
-          ]),
-          context
-        )
-      } else {
-        // Normal element v-for. Directly use the child's codegenNode
-        // arguments, but replace createVNode() with createBlock()
-        let codegenNode = node.codegenNode as ElementCodegenNode
-        if (codegenNode.callee === WITH_DIRECTIVES) {
-          codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
-        } else {
-          codegenNode.callee = helper(CREATE_BLOCK)
-        }
-        childBlock = createBlockExpression(codegenNode, context)
-      }
+  const onExit = processCodegen && processCodegen(forNode, parseResult)
 
-      renderExp.arguments.push(
-        createFunctionExpression(
-          createForLoopParams(parseResult),
-          childBlock,
-          true /* force newline */
-        )
-      )
+  return () => {
+    scopes.vFor--
+    if (!__BROWSER__ && context.prefixIdentifiers) {
+      value && removeIdentifiers(value)
+      key && removeIdentifiers(key)
+      index && removeIdentifiers(index)
     }
+    if (onExit) onExit()
   }
-)
+}
 
 const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
 // This regex doesn't cover the case if key or index aliases have destructuring,
index 53e197eb04621431d008a5c1dce95a5af0b370b9..69cfb3f1ba86a8d154029b44419f35d42df8ac0f 100644 (file)
@@ -71,7 +71,8 @@ export const transformIf = createStructuralDirectiveTransform(
   }
 )
 
-export const processIfBranches = (
+// target-agnostic transform used for both Client and SSR
+export function processIfBranches(
   node: ElementNode,
   dir: DirectiveNode,
   context: TransformContext,
@@ -79,8 +80,8 @@ export const processIfBranches = (
     node: IfNode,
     branch: IfBranchNode,
     isRoot: boolean
-  ) => (() => void) | void
-) => {
+  ) => (() => void) | undefined
+) {
   if (
     dir.name !== 'else' &&
     (!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
index cf9ca3138d82fc4b6562ba0d3c70aafd64ccd3fa..521f1531f67fdfa9be54d7c868bb069646c518aa 100644 (file)
@@ -34,13 +34,13 @@ describe('ssr: element', () => {
 
   test('v-text', () => {
     expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(
-      `"\`<div>\${interpolate(_ctx.foo)}</div>\`"`
+      `"\`<div>\${_interpolate(_ctx.foo)}</div>\`"`
     )
   })
 
   test('<textarea> with dynamic value', () => {
     expect(getCompiledString(`<textarea :value="foo"/>`)).toMatchInlineSnapshot(
-      `"\`<textarea>\${interpolate(_ctx.foo)}</textarea>\`"`
+      `"\`<textarea>\${_interpolate(_ctx.foo)}</textarea>\`"`
     )
   })
 
index 35ed493405fd4ccad8607893657d89e95c5808e2..7ca7cded9c6f5d7b43949d04649a4ede9f15b87a 100644 (file)
@@ -1,3 +1,4 @@
+import { compile } from '../src'
 import { getCompiledString } from './utils'
 
 describe('ssr: text', () => {
@@ -20,18 +21,25 @@ describe('ssr: text', () => {
   })
 
   test('interpolation', () => {
-    expect(getCompiledString(`foo {{ bar }} baz`)).toMatchInlineSnapshot(
-      `"\`foo \${interpolate(_ctx.bar)} baz\`"`
-    )
+    expect(compile(`foo {{ bar }} baz`).code).toMatchInlineSnapshot(`
+      "const { _interpolate } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        _push(\`foo \${_interpolate(_ctx.bar)} baz\`)
+      }"
+    `)
   })
 
   test('nested elements with interpolation', () => {
     expect(
-      getCompiledString(
-        `<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`
-      )
-    ).toMatchInlineSnapshot(
-      `"\`<div><span>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
-    )
+      compile(`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`)
+        .code
+    ).toMatchInlineSnapshot(`
+      "const { _interpolate } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent) {
+        _push(\`<div><span>\${_interpolate(_ctx.foo)} bar</span><span>baz \${_interpolate(_ctx.qux)}</span></div>\`)
+      }"
+    `)
   })
 })
index cbc954ac65d7aafd3452386bfee3f692202e6425..3d4f5d95fb8498927a6b5be502dadafb775619af 100644 (file)
@@ -22,20 +22,23 @@ export function compile(
   template: string,
   options: SSRCompilerOptions = {}
 ): CodegenResult {
-  // apply DOM-specific parsing options
   options = {
+    mode: 'cjs',
+    ...options,
+    // apply DOM-specific parsing options
     ...parserOptions,
-    ...options
+    ssr: true,
+    // always prefix since compiler-ssr doesn't have size concern
+    prefixIdentifiers: true,
+    // disalbe optimizations that are unnecessary for ssr
+    cacheHandlers: false,
+    hoistStatic: false
   }
 
   const ast = baseParse(template, options)
 
   transform(ast, {
     ...options,
-    prefixIdentifiers: true,
-    // disalbe optimizations that are unnecessary for ssr
-    cacheHandlers: false,
-    hoistStatic: false,
     nodeTransforms: [
       ssrTransformIf,
       ssrTransformFor,
@@ -57,10 +60,5 @@ export function compile(
   // by replacing ast.codegenNode.
   ssrCodegenTransform(ast, options)
 
-  return generate(ast, {
-    mode: 'cjs',
-    ...options,
-    ssr: true,
-    prefixIdentifiers: true
-  })
+  return generate(ast, options)
 }
index 5a327dd176c683a66e601df5ebc620d01a475e70..1d5609974a7b104efab70c5ae941f44f72e615fb 100644 (file)
@@ -1,7 +1,21 @@
 import { registerRuntimeHelpers } from '@vue/compiler-dom'
 
-export const INTERPOLATE = Symbol(`interpolate`)
+export const SSR_INTERPOLATE = Symbol(`interpolate`)
+export const SSR_RENDER_COMPONENT = Symbol(`renderComponent`)
+export const SSR_RENDER_SLOT = Symbol(`renderSlot`)
+export const SSR_RENDER_CLASS = Symbol(`renderClass`)
+export const SSR_RENDER_STYLE = Symbol(`renderStyle`)
+export const SSR_RENDER_PROPS = Symbol(`renderProps`)
+export const SSR_RENDER_LIST = Symbol(`renderList`)
 
+// Note: these are helpers imported from @vue/server-renderer
+// make sure the names match!
 registerRuntimeHelpers({
-  [INTERPOLATE]: `interpolate`
+  [SSR_INTERPOLATE]: `_interpolate`,
+  [SSR_RENDER_COMPONENT]: `_renderComponent`,
+  [SSR_RENDER_SLOT]: `_renderSlot`,
+  [SSR_RENDER_CLASS]: `_renderClass`,
+  [SSR_RENDER_STYLE]: `_renderStyle`,
+  [SSR_RENDER_PROPS]: `_renderProps`,
+  [SSR_RENDER_LIST]: `_renderList`
 })
index 32366a543f1ab4078ee2599664610ef9c1437116..ade82b0c78896bcd4a55600c7b082289206d1a95 100644 (file)
@@ -14,8 +14,9 @@ import {
   CallExpression
 } from '@vue/compiler-dom'
 import { isString, escapeHtml, NO } from '@vue/shared'
-import { INTERPOLATE } from './runtimeHelpers'
+import { SSR_INTERPOLATE } from './runtimeHelpers'
 import { processIf } from './transforms/ssrVIf'
+import { processFor } from './transforms/ssrVFor'
 
 // Because SSR codegen output is completely different from client-side output
 // (e.g. multiple elements can be concatenated into a single template literal
@@ -37,17 +38,26 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
   }
 
   ast.codegenNode = createBlockStatement(context.body)
+  ast.ssrHelpers = [...context.helpers]
 }
 
 export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
 
-export function createSSRTransformContext(options: CompilerOptions) {
+function createSSRTransformContext(
+  options: CompilerOptions,
+  helpers: Set<symbol> = new Set()
+) {
   const body: BlockStatement['body'] = []
   let currentString: TemplateLiteral | null = null
 
   return {
     options,
     body,
+    helpers,
+    helper<T extends symbol>(name: T): T {
+      helpers.add(name)
+      return name
+    },
     pushStringPart(part: TemplateLiteral['elements'][0]) {
       if (!currentString) {
         const currentCall = createCallExpression(`_push`)
@@ -71,6 +81,13 @@ export function createSSRTransformContext(options: CompilerOptions) {
   }
 }
 
+export function createChildContext(
+  parent: SSRTransformContext
+): SSRTransformContext {
+  // ensure child inherits parent helpers
+  return createSSRTransformContext(parent.options, parent.helpers)
+}
+
 export function processChildren(
   children: TemplateChildNode[],
   context: SSRTransformContext
@@ -100,11 +117,13 @@ export function processChildren(
     } else if (child.type === NodeTypes.TEXT) {
       context.pushStringPart(escapeHtml(child.content))
     } else if (child.type === NodeTypes.INTERPOLATION) {
-      context.pushStringPart(createCallExpression(INTERPOLATE, [child.content]))
+      context.pushStringPart(
+        createCallExpression(context.helper(SSR_INTERPOLATE), [child.content])
+      )
     } else if (child.type === NodeTypes.IF) {
       processIf(child, context)
     } else if (child.type === NodeTypes.FOR) {
-      // TODO
+      processFor(child, context)
     }
   }
 }
index cd8fc937d1aa27925fe82cb9abc9050101b45895..49f8e998daa5853bd1b1404df5b015a116729073 100644 (file)
@@ -1,3 +1,16 @@
-import { NodeTransform } from '@vue/compiler-dom'
+import {
+  createStructuralDirectiveTransform,
+  ForNode,
+  processForNode
+} from '@vue/compiler-dom'
+import { SSRTransformContext } from '../ssrCodegenTransform'
 
-export const ssrTransformFor: NodeTransform = () => {}
+// Plugin for the first transform pass, which simply constructs the AST node
+export const ssrTransformFor = createStructuralDirectiveTransform(
+  'for',
+  processForNode
+)
+
+// This is called during the 2nd transform pass to construct the SSR-sepcific
+// codegen nodes.
+export function processFor(node: ForNode, context: SSRTransformContext) {}
index 565321e50f8a88f4a9878564feacf62c03fd2ad7..ea7d985cadb94ba8cc173931d358c87d74299659 100644 (file)
@@ -11,12 +11,11 @@ import {
 } from '@vue/compiler-dom'
 import {
   SSRTransformContext,
-  createSSRTransformContext,
+  createChildContext,
   processChildren
 } from '../ssrCodegenTransform'
 
-// This is the plugin for the first transform pass, which simply constructs the
-// if node and its branches.
+// Plugin for the first transform pass, which simply constructs the AST node
 export const ssrTransformIf = createStructuralDirectiveTransform(
   /^(if|else|else-if)$/,
   processIfBranches
@@ -64,7 +63,7 @@ function processIfBranch(
   // TODO optimize away nested fragments when the only child is a ForNode
   const needFragmentWrapper =
     children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
-  const childContext = createSSRTransformContext(context.options)
+  const childContext = createChildContext(context)
   if (needFragmentWrapper) {
     childContext.pushStringPart(`<!---->`)
   }
index 9de4bb4a0e54134175889a3757198bf4387371f1..a9c8fb34457a81cef26ae5ac70d71a5fb2e2d4f7 100644 (file)
@@ -1,15 +1,16 @@
-import { escapeHtml, interpolate } from '../src'
+import { _interpolate } from '../src'
+import { escapeHtml } from '@vue/shared'
 
 test('ssr: interpolate', () => {
-  expect(interpolate(0)).toBe(`0`)
-  expect(interpolate(`foo`)).toBe(`foo`)
-  expect(interpolate(`<div>`)).toBe(`&lt;div&gt;`)
+  expect(_interpolate(0)).toBe(`0`)
+  expect(_interpolate(`foo`)).toBe(`foo`)
+  expect(_interpolate(`<div>`)).toBe(`&lt;div&gt;`)
   // should escape interpolated values
-  expect(interpolate([1, 2, 3])).toBe(
+  expect(_interpolate([1, 2, 3])).toBe(
     escapeHtml(JSON.stringify([1, 2, 3], null, 2))
   )
   expect(
-    interpolate({
+    _interpolate({
       foo: 1,
       bar: `<div>`
     })
index 25efd80f7594e4efe987226af7419a3b937b07fd..6df970c6174ccc6dd687cf4f5610be25a3d46254 100644 (file)
@@ -1,4 +1,4 @@
-import { renderProps, renderClass, renderStyle } from '../src'
+import { renderProps, renderClass, renderStyle } from '../src/renderProps'
 
 describe('ssr: renderProps', () => {
   test('ignore reserved props', () => {
index 65515d9bb8b4712443ec3c26550b9040ea75e2c9..36a7f2736479e066c7e15add160969b547f71056 100644 (file)
@@ -6,7 +6,12 @@ import {
   resolveComponent,
   ComponentOptions
 } from 'vue'
-import { renderToString, renderComponent, renderSlot, escapeHtml } from '../src'
+import { escapeHtml } from '@vue/shared'
+import {
+  renderToString,
+  renderComponent,
+  renderSlot
+} from '../src/renderToString'
 
 describe('ssr: renderToString', () => {
   test('should apply app context', async () => {
index 0b4be1ce520efbaed1a3399f51a9e00eb74ef3d2..defb3af77fe0141ab53ca477914b1d5b3d2379ca 100644 (file)
@@ -2,15 +2,19 @@
 export { renderToString } from './renderToString'
 
 // internal
-export { renderComponent, renderSlot } from './renderToString'
-export { renderClass, renderStyle, renderProps } from './renderProps'
+export {
+  renderComponent as _renderComponent,
+  renderSlot as _renderSlot
+} from './renderToString'
+export {
+  renderClass as _renderClass,
+  renderStyle as _renderStyle,
+  renderProps as _renderProps
+} from './renderProps'
 
 // utils
-import { escapeHtml as _escapeHtml, toDisplayString } from '@vue/shared'
+import { escapeHtml, toDisplayString } from '@vue/shared'
 
-// cast type to avoid dts dependency on @vue/shared (which is inlined)
-export const escapeHtml = _escapeHtml as (raw: string) => string
-
-export function interpolate(value: unknown): string {
+export function _interpolate(value: unknown): string {
   return escapeHtml(toDisplayString(value))
 }