Skip to content

Commit 63e4486

Browse files
committed
wip(compiler-ssr): text and interpolation
1 parent d1eed36 commit 63e4486

File tree

6 files changed

+87
-36
lines changed

6 files changed

+87
-36
lines changed

packages/compiler-core/src/transforms/transformText.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
import { NodeTransform } from '../transform'
22
import {
33
NodeTypes,
4-
TemplateChildNode,
5-
TextNode,
6-
InterpolationNode,
74
CompoundExpressionNode,
85
createCallExpression,
96
CallExpression,
107
ElementTypes
118
} from '../ast'
9+
import { isText } from '../utils'
1210
import { CREATE_TEXT } from '../runtimeHelpers'
1311
import { PatchFlags, PatchFlagNames } from '@vue/shared'
1412

15-
const isText = (
16-
node: TemplateChildNode
17-
): node is TextNode | InterpolationNode =>
18-
node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
19-
2013
// Merge adjacent text nodes and expressions into a single expression
2114
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
2215
export const transformText: NodeTransform = (node, context) => {

packages/compiler-core/src/utils.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import {
2222
SlotOutletCodegenNode,
2323
ComponentCodegenNode,
2424
ExpressionNode,
25-
IfBranchNode
25+
IfBranchNode,
26+
TextNode,
27+
InterpolationNode
2628
} from './ast'
2729
import { parse } from 'acorn'
2830
import { walk } from 'estree-walker'
@@ -213,6 +215,12 @@ export function createBlockExpression(
213215
])
214216
}
215217

218+
export function isText(
219+
node: TemplateChildNode
220+
): node is TextNode | InterpolationNode {
221+
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
222+
}
223+
216224
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
217225
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
218226
}
@@ -257,10 +265,11 @@ export function injectProp(
257265
// check existing key to avoid overriding user provided keys
258266
if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
259267
const propKeyName = prop.key.content
260-
alreadyExists = props.properties.some(p => (
261-
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
262-
p.key.content === propKeyName
263-
))
268+
alreadyExists = props.properties.some(
269+
p =>
270+
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
271+
p.key.content === propKeyName
272+
)
264273
}
265274
if (!alreadyExists) {
266275
props.properties.unshift(prop)
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,62 @@
11
import { compile } from '../src'
22

3-
function getElementString(src: string): string {
3+
function getString(src: string): string {
44
return compile(src).code.match(/_push\((.*)\)/)![1]
55
}
66

7-
describe('ssr compile integration test', () => {
7+
describe('element', () => {
88
test('basic elements', () => {
9-
expect(getElementString(`<div></div>`)).toMatchInlineSnapshot(
10-
`"\`<div></div>\`"`
11-
)
9+
expect(getString(`<div></div>`)).toMatchInlineSnapshot(`"\`<div></div>\`"`)
10+
expect(getString(`<div/>`)).toMatchInlineSnapshot(`"\`<div></div>\`"`)
1211
})
1312

1413
test('static attrs', () => {
15-
expect(
16-
getElementString(`<div id="foo" class="bar"></div>`)
17-
).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`)
14+
expect(getString(`<div id="foo" class="bar"></div>`)).toMatchInlineSnapshot(
15+
`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`
16+
)
1817
})
1918

2019
test('nested elements', () => {
2120
expect(
22-
getElementString(`<div><span></span><span></span></div>`)
21+
getString(`<div><span></span><span></span></div>`)
2322
).toMatchInlineSnapshot(`"\`<div><span></span><span></span></div>\`"`)
2423
})
2524

25+
test('void element', () => {
26+
expect(getString(`<input>`)).toMatchInlineSnapshot(`"\`<input>\`"`)
27+
})
28+
})
29+
30+
describe('text', () => {
31+
test('static text', () => {
32+
expect(getString(`foo`)).toMatchInlineSnapshot(`"\`foo\`"`)
33+
})
34+
35+
test('static text escape', () => {
36+
expect(getString(`&lt;foo&gt;`)).toMatchInlineSnapshot(`"\`&lt;foo&gt;\`"`)
37+
})
38+
2639
test('nested elements with static text', () => {
2740
expect(
28-
getElementString(`<div><span>hello</span>&gt;<span>bye</span></div>`)
41+
getString(`<div><span>hello</span><span>bye</span></div>`)
42+
).toMatchInlineSnapshot(
43+
`"\`<div><span>hello</span><span>bye</span></div>\`"`
44+
)
45+
})
46+
47+
test('interpolation', () => {
48+
expect(getString(`foo {{ bar }} baz`)).toMatchInlineSnapshot(
49+
`"\`foo \${interpolate(_ctx.bar)} baz\`"`
50+
)
51+
})
52+
53+
test('nested elements with interpolation', () => {
54+
expect(
55+
getString(
56+
`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`
57+
)
2958
).toMatchInlineSnapshot(
30-
`"\`<div><span>hello</span>&gt;<span>bye</span></div>\`"`
59+
`"\`<div><span>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
3160
)
3261
})
3362
})

packages/compiler-ssr/src/index.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ export function compile(
2222
template: string,
2323
options: SSRCompilerOptions = {}
2424
): CodegenResult {
25-
const ast = baseParse(template, {
25+
// apply DOM-specific parsing options
26+
options = {
2627
...parserOptions,
2728
...options
28-
})
29+
}
30+
31+
const ast = baseParse(template, options)
2932

3033
transform(ast, {
3134
...options,
@@ -52,7 +55,7 @@ export function compile(
5255

5356
// traverse the template AST and convert into SSR codegen AST
5457
// by replacing ast.codegenNode.
55-
ssrCodegenTransform(ast)
58+
ssrCodegenTransform(ast, options)
5659

5760
return generate(ast, {
5861
mode: 'cjs',
+7-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
//
1+
import { registerRuntimeHelpers } from '@vue/compiler-core'
2+
3+
export const INTERPOLATE = Symbol(`interpolate`)
4+
5+
registerRuntimeHelpers({
6+
[INTERPOLATE]: `interpolate`
7+
})

packages/compiler-ssr/src/ssrCodegenTransform.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@ import {
88
NodeTypes,
99
TemplateChildNode,
1010
ElementTypes,
11-
createBlockStatement
11+
createBlockStatement,
12+
CompilerOptions,
13+
isText
1214
} from '@vue/compiler-dom'
13-
import { isString, escapeHtml } from '@vue/shared'
15+
import { isString, escapeHtml, NO } from '@vue/shared'
16+
import { INTERPOLATE } from './runtimeHelpers'
1417

1518
// Because SSR codegen output is completely different from client-side output
1619
// (e.g. multiple elements can be concatenated into a single template literal
1720
// instead of each getting a corresponding call), we need to apply an extra
1821
// transform pass to convert the template AST into a fresh JS AST before
1922
// passing it to codegen.
2023

21-
export function ssrCodegenTransform(ast: RootNode) {
22-
const context = createSSRTransformContext()
24+
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
25+
const context = createSSRTransformContext(options)
2326

24-
const isFragment = ast.children.length > 1
27+
const isFragment =
28+
ast.children.length > 1 && !ast.children.every(c => isText(c))
2529
if (isFragment) {
2630
context.pushStringPart(`<!---->`)
2731
}
@@ -35,12 +39,13 @@ export function ssrCodegenTransform(ast: RootNode) {
3539

3640
type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
3741

38-
function createSSRTransformContext() {
42+
function createSSRTransformContext(options: CompilerOptions) {
3943
const body: BlockStatement['body'] = []
4044
let currentCall: CallExpression | null = null
4145
let currentString: TemplateLiteral | null = null
4246

4347
return {
48+
options,
4449
body,
4550
pushStringPart(part: TemplateLiteral['elements'][0]) {
4651
if (!currentCall) {
@@ -66,6 +71,7 @@ function processChildren(
6671
children: TemplateChildNode[],
6772
context: SSRTransformContext
6873
) {
74+
const isVoidTag = context.options.isVoidTag || NO
6975
for (let i = 0; i < children.length; i++) {
7076
const child = children[i]
7177
if (child.type === NodeTypes.ELEMENT) {
@@ -77,15 +83,20 @@ function processChildren(
7783
if (child.children.length) {
7884
processChildren(child.children, context)
7985
}
80-
// push closing tag
81-
context.pushStringPart(`</${child.tag}>`)
86+
87+
if (!isVoidTag(child.tag)) {
88+
// push closing tag
89+
context.pushStringPart(`</${child.tag}>`)
90+
}
8291
} else if (child.tagType === ElementTypes.COMPONENT) {
8392
// TODO
8493
} else if (child.tagType === ElementTypes.SLOT) {
8594
// TODO
8695
}
8796
} else if (child.type === NodeTypes.TEXT) {
8897
context.pushStringPart(escapeHtml(child.content))
98+
} else if (child.type === NodeTypes.INTERPOLATION) {
99+
context.pushStringPart(createCallExpression(INTERPOLATE, [child.content]))
89100
} else if (child.type === NodeTypes.IF) {
90101
// TODO
91102
} else if (child.type === NodeTypes.FOR) {

0 commit comments

Comments
 (0)