wip(compiler-ssr): text and interpolation

This commit is contained in:
Evan You 2020-02-02 22:28:54 -05:00
parent d1eed36452
commit 63e4486645
6 changed files with 87 additions and 36 deletions

View File

@ -1,22 +1,15 @@
import { NodeTransform } from '../transform' import { NodeTransform } from '../transform'
import { import {
NodeTypes, NodeTypes,
TemplateChildNode,
TextNode,
InterpolationNode,
CompoundExpressionNode, CompoundExpressionNode,
createCallExpression, createCallExpression,
CallExpression, CallExpression,
ElementTypes ElementTypes
} from '../ast' } from '../ast'
import { isText } from '../utils'
import { CREATE_TEXT } from '../runtimeHelpers' import { CREATE_TEXT } from '../runtimeHelpers'
import { PatchFlags, PatchFlagNames } from '@vue/shared' 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 // 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. // e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
export const transformText: NodeTransform = (node, context) => { export const transformText: NodeTransform = (node, context) => {

View File

@ -22,7 +22,9 @@ import {
SlotOutletCodegenNode, SlotOutletCodegenNode,
ComponentCodegenNode, ComponentCodegenNode,
ExpressionNode, ExpressionNode,
IfBranchNode IfBranchNode,
TextNode,
InterpolationNode
} from './ast' } from './ast'
import { parse } from 'acorn' import { parse } from 'acorn'
import { walk } from 'estree-walker' 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 { export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot' return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
} }
@ -257,10 +265,11 @@ export function injectProp(
// check existing key to avoid overriding user provided keys // check existing key to avoid overriding user provided keys
if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) { if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
const propKeyName = prop.key.content const propKeyName = prop.key.content
alreadyExists = props.properties.some(p => ( alreadyExists = props.properties.some(
p =>
p.key.type === NodeTypes.SIMPLE_EXPRESSION && p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === propKeyName p.key.content === propKeyName
)) )
} }
if (!alreadyExists) { if (!alreadyExists) {
props.properties.unshift(prop) props.properties.unshift(prop)

View File

@ -1,33 +1,62 @@
import { compile } from '../src' import { compile } from '../src'
function getElementString(src: string): string { function getString(src: string): string {
return compile(src).code.match(/_push\((.*)\)/)![1] return compile(src).code.match(/_push\((.*)\)/)![1]
} }
describe('ssr compile integration test', () => { describe('element', () => {
test('basic elements', () => { test('basic elements', () => {
expect(getElementString(`<div></div>`)).toMatchInlineSnapshot( expect(getString(`<div></div>`)).toMatchInlineSnapshot(`"\`<div></div>\`"`)
`"\`<div></div>\`"` expect(getString(`<div/>`)).toMatchInlineSnapshot(`"\`<div></div>\`"`)
)
}) })
test('static attrs', () => { test('static attrs', () => {
expect( expect(getString(`<div id="foo" class="bar"></div>`)).toMatchInlineSnapshot(
getElementString(`<div id="foo" class="bar"></div>`) `"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`
).toMatchInlineSnapshot(`"\`<div id=\\"foo\\" class=\\"bar\\"></div>\`"`) )
}) })
test('nested elements', () => { test('nested elements', () => {
expect( expect(
getElementString(`<div><span></span><span></span></div>`) getString(`<div><span></span><span></span></div>`)
).toMatchInlineSnapshot(`"\`<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', () => { test('nested elements with static text', () => {
expect( expect(
getElementString(`<div><span>hello</span>&gt;<span>bye</span></div>`) getString(`<div><span>hello</span><span>bye</span></div>`)
).toMatchInlineSnapshot( ).toMatchInlineSnapshot(
`"\`<div><span>hello</span>&gt;<span>bye</span></div>\`"` `"\`<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>\${interpolate(_ctx.foo)} bar</span><span>baz \${interpolate(_ctx.qux)}</span></div>\`"`
) )
}) })
}) })

View File

@ -22,10 +22,13 @@ export function compile(
template: string, template: string,
options: SSRCompilerOptions = {} options: SSRCompilerOptions = {}
): CodegenResult { ): CodegenResult {
const ast = baseParse(template, { // apply DOM-specific parsing options
options = {
...parserOptions, ...parserOptions,
...options ...options
}) }
const ast = baseParse(template, options)
transform(ast, { transform(ast, {
...options, ...options,
@ -52,7 +55,7 @@ export function compile(
// traverse the template AST and convert into SSR codegen AST // traverse the template AST and convert into SSR codegen AST
// by replacing ast.codegenNode. // by replacing ast.codegenNode.
ssrCodegenTransform(ast) ssrCodegenTransform(ast, options)
return generate(ast, { return generate(ast, {
mode: 'cjs', mode: 'cjs',

View File

@ -1 +1,7 @@
// import { registerRuntimeHelpers } from '@vue/compiler-core'
export const INTERPOLATE = Symbol(`interpolate`)
registerRuntimeHelpers({
[INTERPOLATE]: `interpolate`
})

View File

@ -8,9 +8,12 @@ import {
NodeTypes, NodeTypes,
TemplateChildNode, TemplateChildNode,
ElementTypes, ElementTypes,
createBlockStatement createBlockStatement,
CompilerOptions,
isText
} from '@vue/compiler-dom' } 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 // Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal // (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 // transform pass to convert the template AST into a fresh JS AST before
// passing it to codegen. // passing it to codegen.
export function ssrCodegenTransform(ast: RootNode) { export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const context = createSSRTransformContext() const context = createSSRTransformContext(options)
const isFragment = ast.children.length > 1 const isFragment =
ast.children.length > 1 && !ast.children.every(c => isText(c))
if (isFragment) { if (isFragment) {
context.pushStringPart(`<!---->`) context.pushStringPart(`<!---->`)
} }
@ -35,12 +39,13 @@ export function ssrCodegenTransform(ast: RootNode) {
type SSRTransformContext = ReturnType<typeof createSSRTransformContext> type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
function createSSRTransformContext() { function createSSRTransformContext(options: CompilerOptions) {
const body: BlockStatement['body'] = [] const body: BlockStatement['body'] = []
let currentCall: CallExpression | null = null let currentCall: CallExpression | null = null
let currentString: TemplateLiteral | null = null let currentString: TemplateLiteral | null = null
return { return {
options,
body, body,
pushStringPart(part: TemplateLiteral['elements'][0]) { pushStringPart(part: TemplateLiteral['elements'][0]) {
if (!currentCall) { if (!currentCall) {
@ -66,6 +71,7 @@ function processChildren(
children: TemplateChildNode[], children: TemplateChildNode[],
context: SSRTransformContext context: SSRTransformContext
) { ) {
const isVoidTag = context.options.isVoidTag || NO
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (child.type === NodeTypes.ELEMENT) { if (child.type === NodeTypes.ELEMENT) {
@ -77,8 +83,11 @@ function processChildren(
if (child.children.length) { if (child.children.length) {
processChildren(child.children, context) processChildren(child.children, context)
} }
if (!isVoidTag(child.tag)) {
// push closing tag // push closing tag
context.pushStringPart(`</${child.tag}>`) context.pushStringPart(`</${child.tag}>`)
}
} else if (child.tagType === ElementTypes.COMPONENT) { } else if (child.tagType === ElementTypes.COMPONENT) {
// TODO // TODO
} else if (child.tagType === ElementTypes.SLOT) { } else if (child.tagType === ElementTypes.SLOT) {
@ -86,6 +95,8 @@ function processChildren(
} }
} else if (child.type === NodeTypes.TEXT) { } else if (child.type === NodeTypes.TEXT) {
context.pushStringPart(escapeHtml(child.content)) context.pushStringPart(escapeHtml(child.content))
} else if (child.type === NodeTypes.INTERPOLATION) {
context.pushStringPart(createCallExpression(INTERPOLATE, [child.content]))
} else if (child.type === NodeTypes.IF) { } else if (child.type === NodeTypes.IF) {
// TODO // TODO
} else if (child.type === NodeTypes.FOR) { } else if (child.type === NodeTypes.FOR) {