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) => {
SlotOutletCodegenNode,
ComponentCodegenNode,
ExpressionNode,
- IfBranchNode
+ IfBranchNode,
+ TextNode,
+ InterpolationNode
} from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
])
}
+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'
}
// 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)
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(`<foo>`)).toMatchInlineSnapshot(`"\`<foo>\`"`)
+ })
+
test('nested elements with static text', () => {
expect(
- getElementString(`<div><span>hello</span>><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>><span>bye</span></div>\`"`
+ `"\`<div><span>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
)
})
})
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,
// traverse the template AST and convert into SSR codegen AST
// by replacing ast.codegenNode.
- ssrCodegenTransform(ast)
+ ssrCodegenTransform(ast, options)
return generate(ast, {
mode: 'cjs',
-//
+import { registerRuntimeHelpers } from '@vue/compiler-core'
+
+export const INTERPOLATE = Symbol(`interpolate`)
+
+registerRuntimeHelpers({
+ [INTERPOLATE]: `interpolate`
+})
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
// 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(`<!---->`)
}
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) {
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) {
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) {
}
} 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) {