]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(compiler-ssr): text and interpolation
authorEvan You <yyx990803@gmail.com>
Mon, 3 Feb 2020 03:28:54 +0000 (22:28 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 3 Feb 2020 03:28:54 +0000 (22:28 -0500)
packages/compiler-core/src/transforms/transformText.ts
packages/compiler-core/src/utils.ts
packages/compiler-ssr/__tests__/ssrCompile.spec.ts
packages/compiler-ssr/src/index.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts

index 4a4d3fdc8c0414f717069f7c67cf55a4cd3f0e0c..a15a2fd18071837b77c2fe2716d714e2a12e0759 100644 (file)
@@ -1,22 +1,15 @@
 import { NodeTransform } from '../transform'
 import {
   NodeTypes,
-  TemplateChildNode,
-  TextNode,
-  InterpolationNode,
   CompoundExpressionNode,
   createCallExpression,
   CallExpression,
   ElementTypes
 } from '../ast'
+import { isText } from '../utils'
 import { CREATE_TEXT } from '../runtimeHelpers'
 import { PatchFlags, PatchFlagNames } from '@vue/shared'
 
-const isText = (
-  node: TemplateChildNode
-): node is TextNode | InterpolationNode =>
-  node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
-
 // Merge adjacent text nodes and expressions into a single expression
 // e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
 export const transformText: NodeTransform = (node, context) => {
index 280e9c5e2dc5f00056f2db999701fada49cef38b..78e952e43ede85b312cbfb223d417745e5030293 100644 (file)
@@ -22,7 +22,9 @@ import {
   SlotOutletCodegenNode,
   ComponentCodegenNode,
   ExpressionNode,
-  IfBranchNode
+  IfBranchNode,
+  TextNode,
+  InterpolationNode
 } from './ast'
 import { parse } from 'acorn'
 import { walk } from 'estree-walker'
@@ -213,6 +215,12 @@ export function createBlockExpression(
   ])
 }
 
+export function isText(
+  node: TemplateChildNode
+): node is TextNode | InterpolationNode {
+  return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
+}
+
 export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
   return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
 }
@@ -257,10 +265,11 @@ export function injectProp(
     // check existing key to avoid overriding user provided keys
     if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
       const propKeyName = prop.key.content
-      alreadyExists = props.properties.some(p => (
-        p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
-        p.key.content === propKeyName
-      ))
+      alreadyExists = props.properties.some(
+        p =>
+          p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
+          p.key.content === propKeyName
+      )
     }
     if (!alreadyExists) {
       props.properties.unshift(prop)
index 14dd767ec31781696ac982b44fb2d620abad560b..ab9013a0914ae2f7625b81c53e74d58e82645fb4 100644 (file)
@@ -1,33 +1,62 @@
 import { compile } from '../src'
 
-function getElementString(src: string): string {
+function getString(src: string): string {
   return compile(src).code.match(/_push\((.*)\)/)![1]
 }
 
-describe('ssr compile integration test', () => {
+describe('element', () => {
   test('basic elements', () => {
-    expect(getElementString(`<div></div>`)).toMatchInlineSnapshot(
-      `"\`<div></div>\`"`
-    )
+    expect(getString(`<div></div>`)).toMatchInlineSnapshot(`"\`<div></div>\`"`)
+    expect(getString(`<div/>`)).toMatchInlineSnapshot(`"\`<div></div>\`"`)
   })
 
   test('static attrs', () => {
-    expect(
-      getElementString(`<div id="foo" class="bar"></div>`)
-    ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`)
+    expect(getString(`<div id="foo" class="bar"></div>`)).toMatchInlineSnapshot(
+      `"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`
+    )
   })
 
   test('nested elements', () => {
     expect(
-      getElementString(`<div><span></span><span></span></div>`)
+      getString(`<div><span></span><span></span></div>`)
     ).toMatchInlineSnapshot(`"\`<div><span></span><span></span></div>\`"`)
   })
 
+  test('void element', () => {
+    expect(getString(`<input>`)).toMatchInlineSnapshot(`"\`<input>\`"`)
+  })
+})
+
+describe('text', () => {
+  test('static text', () => {
+    expect(getString(`foo`)).toMatchInlineSnapshot(`"\`foo\`"`)
+  })
+
+  test('static text escape', () => {
+    expect(getString(`&lt;foo&gt;`)).toMatchInlineSnapshot(`"\`&lt;foo&gt;\`"`)
+  })
+
   test('nested elements with static text', () => {
     expect(
-      getElementString(`<div><span>hello</span>&gt;<span>bye</span></div>`)
+      getString(`<div><span>hello</span><span>bye</span></div>`)
+    ).toMatchInlineSnapshot(
+      `"\`<div><span>hello</span><span>bye</span></div>\`"`
+    )
+  })
+
+  test('interpolation', () => {
+    expect(getString(`foo {{ bar }} baz`)).toMatchInlineSnapshot(
+      `"\`foo \${interpolate(_ctx.bar)} baz\`"`
+    )
+  })
+
+  test('nested elements with interpolation', () => {
+    expect(
+      getString(
+        `<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`
+      )
     ).toMatchInlineSnapshot(
-      `"\`<div><span>hello</span>&gt;<span>bye</span></div>\`"`
+      `"\`<div><span>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
     )
   })
 })
index 32307c0896b88c9dbbb95ac8299efb9fe638a405..cbc954ac65d7aafd3452386bfee3f692202e6425 100644 (file)
@@ -22,10 +22,13 @@ export function compile(
   template: string,
   options: SSRCompilerOptions = {}
 ): CodegenResult {
-  const ast = baseParse(template, {
+  // apply DOM-specific parsing options
+  options = {
     ...parserOptions,
     ...options
-  })
+  }
+
+  const ast = baseParse(template, options)
 
   transform(ast, {
     ...options,
@@ -52,7 +55,7 @@ export function compile(
 
   // traverse the template AST and convert into SSR codegen AST
   // by replacing ast.codegenNode.
-  ssrCodegenTransform(ast)
+  ssrCodegenTransform(ast, options)
 
   return generate(ast, {
     mode: 'cjs',
index 8337712ea57f00733b5709b27efb3de797e95575..3af2ec552cb8382d88d1f7f3639c7042c19a6781 100644 (file)
@@ -1 +1,7 @@
-//
+import { registerRuntimeHelpers } from '@vue/compiler-core'
+
+export const INTERPOLATE = Symbol(`interpolate`)
+
+registerRuntimeHelpers({
+  [INTERPOLATE]: `interpolate`
+})
index 30cf872f52ba537420b21c487a51042bcd9b837b..9eec2c6aebd5870c9648418304bcce376fb5ece3 100644 (file)
@@ -8,9 +8,12 @@ import {
   NodeTypes,
   TemplateChildNode,
   ElementTypes,
-  createBlockStatement
+  createBlockStatement,
+  CompilerOptions,
+  isText
 } from '@vue/compiler-dom'
-import { isString, escapeHtml } from '@vue/shared'
+import { isString, escapeHtml, NO } from '@vue/shared'
+import { INTERPOLATE } from './runtimeHelpers'
 
 // Because SSR codegen output is completely different from client-side output
 // (e.g. multiple elements can be concatenated into a single template literal
@@ -18,10 +21,11 @@ import { isString, escapeHtml } from '@vue/shared'
 // transform pass to convert the template AST into a fresh JS AST before
 // passing it to codegen.
 
-export function ssrCodegenTransform(ast: RootNode) {
-  const context = createSSRTransformContext()
+export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
+  const context = createSSRTransformContext(options)
 
-  const isFragment = ast.children.length > 1
+  const isFragment =
+    ast.children.length > 1 && !ast.children.every(c => isText(c))
   if (isFragment) {
     context.pushStringPart(`<!---->`)
   }
@@ -35,12 +39,13 @@ export function ssrCodegenTransform(ast: RootNode) {
 
 type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
 
-function createSSRTransformContext() {
+function createSSRTransformContext(options: CompilerOptions) {
   const body: BlockStatement['body'] = []
   let currentCall: CallExpression | null = null
   let currentString: TemplateLiteral | null = null
 
   return {
+    options,
     body,
     pushStringPart(part: TemplateLiteral['elements'][0]) {
       if (!currentCall) {
@@ -66,6 +71,7 @@ function processChildren(
   children: TemplateChildNode[],
   context: SSRTransformContext
 ) {
+  const isVoidTag = context.options.isVoidTag || NO
   for (let i = 0; i < children.length; i++) {
     const child = children[i]
     if (child.type === NodeTypes.ELEMENT) {
@@ -77,8 +83,11 @@ function processChildren(
         if (child.children.length) {
           processChildren(child.children, context)
         }
-        // push closing tag
-        context.pushStringPart(`</${child.tag}>`)
+
+        if (!isVoidTag(child.tag)) {
+          // push closing tag
+          context.pushStringPart(`</${child.tag}>`)
+        }
       } else if (child.tagType === ElementTypes.COMPONENT) {
         // TODO
       } else if (child.tagType === ElementTypes.SLOT) {
@@ -86,6 +95,8 @@ 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]))
     } else if (child.type === NodeTypes.IF) {
       // TODO
     } else if (child.type === NodeTypes.FOR) {