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 {
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) => {

View 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 => (
alreadyExists = props.properties.some(
p =>
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === propKeyName
))
)
}
if (!alreadyExists) {
props.properties.unshift(prop)

View 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>&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,
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',

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,
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)
}
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) {