diff --git a/packages/compiler-core/src/transforms/transformText.ts b/packages/compiler-core/src/transforms/transformText.ts
index 4a4d3fdc8..a15a2fd18 100644
--- a/packages/compiler-core/src/transforms/transformText.ts
+++ b/packages/compiler-core/src/transforms/transformText.ts
@@ -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.
abc {{ d }} {{ e }}
should have a single expression node as child.
export const transformText: NodeTransform = (node, context) => {
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index 280e9c5e2..78e952e43 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -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)
diff --git a/packages/compiler-ssr/__tests__/ssrCompile.spec.ts b/packages/compiler-ssr/__tests__/ssrCompile.spec.ts
index 14dd767ec..ab9013a09 100644
--- a/packages/compiler-ssr/__tests__/ssrCompile.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrCompile.spec.ts
@@ -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(``)).toMatchInlineSnapshot(
- `"\`\`"`
- )
+ expect(getString(``)).toMatchInlineSnapshot(`"\`\`"`)
+ expect(getString(``)).toMatchInlineSnapshot(`"\`\`"`)
})
test('static attrs', () => {
- expect(
- getElementString(``)
- ).toMatchInlineSnapshot(`"\`\`"`)
+ expect(getString(``)).toMatchInlineSnapshot(
+ `"\`\`"`
+ )
})
test('nested elements', () => {
expect(
- getElementString(`
`)
+ getString(`
`)
).toMatchInlineSnapshot(`"\`
\`"`)
})
+ test('void element', () => {
+ expect(getString(``)).toMatchInlineSnapshot(`"\`\`"`)
+ })
+})
+
+describe('text', () => {
+ test('static text', () => {
+ expect(getString(`foo`)).toMatchInlineSnapshot(`"\`foo\`"`)
+ })
+
+ test('static text escape', () => {
+ expect(getString(`<foo>`)).toMatchInlineSnapshot(`"\`<foo>\`"`)
+ })
+
test('nested elements with static text', () => {
expect(
- getElementString(`hello>bye
`)
+ getString(`hellobye
`)
).toMatchInlineSnapshot(
- `"\`hello>bye
\`"`
+ `"\`hellobye
\`"`
+ )
+ })
+
+ test('interpolation', () => {
+ expect(getString(`foo {{ bar }} baz`)).toMatchInlineSnapshot(
+ `"\`foo \${interpolate(_ctx.bar)} baz\`"`
+ )
+ })
+
+ test('nested elements with interpolation', () => {
+ expect(
+ getString(
+ `{{ foo }} barbaz {{ qux }}
`
+ )
+ ).toMatchInlineSnapshot(
+ `"\`\${interpolate(_ctx.foo)} barbaz \${interpolate(_ctx.qux)}
\`"`
)
})
})
diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts
index 32307c089..cbc954ac6 100644
--- a/packages/compiler-ssr/src/index.ts
+++ b/packages/compiler-ssr/src/index.ts
@@ -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',
diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts
index 8337712ea..3af2ec552 100644
--- a/packages/compiler-ssr/src/runtimeHelpers.ts
+++ b/packages/compiler-ssr/src/runtimeHelpers.ts
@@ -1 +1,7 @@
-//
+import { registerRuntimeHelpers } from '@vue/compiler-core'
+
+export const INTERPOLATE = Symbol(`interpolate`)
+
+registerRuntimeHelpers({
+ [INTERPOLATE]: `interpolate`
+})
diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts
index 30cf872f5..9eec2c6ae 100644
--- a/packages/compiler-ssr/src/ssrCodegenTransform.ts
+++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts
@@ -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
-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) {