test: tests for codegen

This commit is contained in:
Evan You 2019-09-24 15:49:02 -04:00
parent 76a1196935
commit 7a46e51815
12 changed files with 486 additions and 26 deletions

View File

@ -0,0 +1,109 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: codegen callExpression + objectExpression + arrayExpression 1`] = `
"return function render() {
with (this) {
return createVNode(\\"div\\", {
id: \\"foo\\",
[prop]: bar
}, [
foo,
createVNode(\\"p\\")
])
}
}"
`;
exports[`compiler: codegen comment 1`] = `
"return function render() {
with (this) {
return createVNode(Comment, 0, \\"foo\\")
}
}"
`;
exports[`compiler: codegen forNode 1`] = `
"return function render() {
with (this) {
return renderList(list, (v, k, i) => toString(v))
}
}"
`;
exports[`compiler: codegen function mode preamble 1`] = `
"const { helperOne, helperTwo } = Vue
return function render() {
with (this) {
return null
}
}"
`;
exports[`compiler: codegen ifNode 1`] = `
"return function render() {
with (this) {
return (foo)
? \\"foo\\"
: (bar)
? toString(bye)
: createVNode(Comment, 0, \\"foo\\")
}
}"
`;
exports[`compiler: codegen interpolation 1`] = `
"return function render() {
with (this) {
return toString(hello)
}
}"
`;
exports[`compiler: codegen module mode preamble 1`] = `
"import { helperOne, helperTwo } from 'vue'
export default function render() {
with (this) {
return null
}
}"
`;
exports[`compiler: codegen prefixIdentifiers: true should inject _ctx statement 1`] = `
"return function render() {
const _ctx = this
return null
}"
`;
exports[`compiler: codegen statement preambles 1`] = `
"return function render() {
const a = 1
const b = 2
with (this) {
return null
}
}"
`;
exports[`compiler: codegen static text 1`] = `
"return function render() {
with (this) {
return \\"hello\\"
}
}"
`;
exports[`compiler: codegen text + comment + interpolation 1`] = `
"return function render() {
with (this) {
return [
\\"foo\\",
toString(hello),
createVNode(Comment, 0, \\"foo\\")
]
}
}"
`;

View File

@ -1,14 +1,298 @@
import { parse, generate } from '../src'
import {
parse,
generate,
NodeTypes,
RootNode,
SourceLocation,
createExpression,
Namespaces,
ElementTypes,
createObjectExpression,
createObjectProperty,
createArrayExpression
} from '../src'
import { SourceMapConsumer, RawSourceMap } from 'source-map'
import { CREATE_VNODE, COMMENT, TO_STRING } from '../src/runtimeConstants'
const mockLoc: SourceLocation = {
source: ``,
start: {
offset: 0,
line: 1,
column: 1
},
end: {
offset: 3,
line: 1,
column: 4
}
}
function createRoot(options: Partial<RootNode> = {}): RootNode {
return {
type: NodeTypes.ROOT,
children: [],
imports: [],
statements: [],
loc: mockLoc,
...options
}
}
describe('compiler: codegen', () => {
test('module mode preamble', () => {
const root = createRoot({
imports: [`helperOne`, `helperTwo`]
})
const { code } = generate(root, { mode: 'module' })
expect(code).toMatch(`import { helperOne, helperTwo } from 'vue'`)
expect(code).toMatchSnapshot()
})
test('function mode preamble', () => {
const root = createRoot({
imports: [`helperOne`, `helperTwo`]
})
const { code } = generate(root, { mode: 'function' })
expect(code).toMatch(`const { helperOne, helperTwo } = Vue`)
expect(code).toMatchSnapshot()
})
test('statement preambles', () => {
const root = createRoot({
statements: [`const a = 1`, `const b = 2`]
})
const { code } = generate(root, { mode: 'function' })
expect(code).toMatch(`const a = 1\n`)
expect(code).toMatch(`const b = 2\n`)
expect(code).toMatchSnapshot()
})
test('prefixIdentifiers: true should inject _ctx statement', () => {
const { code } = generate(createRoot(), { prefixIdentifiers: true })
expect(code).toMatch(`const _ctx = this\n`)
expect(code).toMatchSnapshot()
})
test('static text', () => {
const { code } = generate(
createRoot({
children: [
{
type: NodeTypes.TEXT,
content: 'hello',
isEmpty: false,
loc: mockLoc
}
]
})
)
expect(code).toMatch(`return "hello"`)
expect(code).toMatchSnapshot()
})
test('interpolation', () => {
const { code } = generate(
createRoot({
children: [createExpression(`hello`, false, mockLoc, true)]
})
)
expect(code).toMatch(`return toString(hello)`)
expect(code).toMatchSnapshot()
})
test('comment', () => {
const { code } = generate(
createRoot({
children: [
{
type: NodeTypes.COMMENT,
content: 'foo',
loc: mockLoc
}
]
})
)
expect(code).toMatch(`return ${CREATE_VNODE}(${COMMENT}, 0, "foo")`)
expect(code).toMatchSnapshot()
})
test('text + comment + interpolation', () => {
const { code } = generate(
createRoot({
children: [
{
type: NodeTypes.TEXT,
content: 'foo',
isEmpty: false,
loc: mockLoc
},
createExpression(`hello`, false, mockLoc, true),
{
type: NodeTypes.COMMENT,
content: 'foo',
loc: mockLoc
}
]
})
)
expect(code).toMatch(`
return [
"foo",
toString(hello),
${CREATE_VNODE}(${COMMENT}, 0, "foo")
]`)
expect(code).toMatchSnapshot()
})
test('ifNode', () => {
const { code } = generate(
createRoot({
children: [
{
type: NodeTypes.IF,
loc: mockLoc,
isRoot: true,
branches: [
{
type: NodeTypes.IF_BRANCH,
condition: createExpression('foo', false, mockLoc),
loc: mockLoc,
isRoot: true,
children: [
{
type: NodeTypes.TEXT,
content: 'foo',
isEmpty: false,
loc: mockLoc
}
]
},
{
type: NodeTypes.IF_BRANCH,
condition: createExpression('bar', false, mockLoc),
loc: mockLoc,
isRoot: true,
children: [createExpression(`bye`, false, mockLoc, true)]
},
{
type: NodeTypes.IF_BRANCH,
condition: undefined,
loc: mockLoc,
isRoot: true,
children: [
{
type: NodeTypes.COMMENT,
content: 'foo',
loc: mockLoc
}
]
}
]
}
]
})
)
expect(code).toMatch(`
return (foo)
? "foo"
: (bar)
? ${TO_STRING}(bye)
: ${CREATE_VNODE}(${COMMENT}, 0, "foo")`)
expect(code).toMatchSnapshot()
})
test('forNode', () => {
const { code } = generate(
createRoot({
children: [
{
type: NodeTypes.FOR,
loc: mockLoc,
source: createExpression(`list`, false, mockLoc),
valueAlias: createExpression(`v`, false, mockLoc),
keyAlias: createExpression(`k`, false, mockLoc),
objectIndexAlias: createExpression(`i`, false, mockLoc),
children: [createExpression(`v`, false, mockLoc, true)]
}
]
})
)
expect(code).toMatch(`renderList(list, (v, k, i) => toString(v))`)
expect(code).toMatchSnapshot()
})
test('callExpression + objectExpression + arrayExpression', () => {
const { code } = generate(
createRoot({
children: [
{
type: NodeTypes.ELEMENT,
loc: mockLoc,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
isSelfClosing: false,
props: [],
children: [],
codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION,
loc: mockLoc,
callee: CREATE_VNODE,
arguments: [
`"div"`,
createObjectExpression(
[
createObjectProperty(
createExpression(`id`, true, mockLoc),
createExpression(`foo`, true, mockLoc),
mockLoc
),
createObjectProperty(
createExpression(`prop`, false, mockLoc),
createExpression(`bar`, false, mockLoc),
mockLoc
)
],
mockLoc
),
createArrayExpression(
[
'foo',
{
type: NodeTypes.JS_CALL_EXPRESSION,
loc: mockLoc,
callee: CREATE_VNODE,
arguments: [`"p"`]
}
],
mockLoc
)
]
}
}
]
})
)
expect(code).toMatch(`
return ${CREATE_VNODE}("div", {
id: "foo",
[prop]: bar
}, [
foo,
${CREATE_VNODE}("p")
])`)
expect(code).toMatchSnapshot()
})
test('basic source map support', async () => {
const source = `hello {{ world }}`
const ast = parse(source)
const { code, map } = generate(ast, {
sourceMap: true,
filename: `foo.vue`
})
expect(code).toBe(
expect(code).toMatch(
`return function render() {
with (this) {
return [

View File

@ -0,0 +1 @@
// Integration tests for parser + transform + codegen

View File

@ -2,6 +2,7 @@ import { parse } from '../src/parse'
import { transform, NodeTransform } from '../src/transform'
import { ElementNode, NodeTypes } from '../src/ast'
import { ErrorCodes, createCompilerError } from '../src/errors'
import { TO_STRING, CREATE_VNODE, COMMENT } from '../src/runtimeConstants'
describe('compiler: transform', () => {
test('context state', () => {
@ -180,4 +181,17 @@ describe('compiler: transform', () => {
}
])
})
test('should inject toString helper for interpolations', () => {
const ast = parse(`{{ foo }}`)
transform(ast, {})
expect(ast.imports).toContain(TO_STRING)
})
test('should inject createVNode and Comment for comments', () => {
const ast = parse(`<!--foo-->`)
transform(ast, {})
expect(ast.imports).toContain(CREATE_VNODE)
expect(ast.imports).toContain(COMMENT)
})
})

View File

@ -0,0 +1 @@
// TODO

View File

@ -29,7 +29,9 @@ describe('compiler: transform v-if', () => {
test('basic v-if', () => {
const node = parseWithIfTransform(`<div v-if="ok"/>`)
expect(node.type).toBe(NodeTypes.IF)
expect(node.isRoot).toBe(true)
expect(node.branches.length).toBe(1)
expect(node.branches[0].isRoot).toBe(true)
expect(node.branches[0].condition!.content).toBe(`ok`)
expect(node.branches[0].children.length).toBe(1)
expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)

View File

@ -0,0 +1 @@
// TODO

View File

@ -117,12 +117,14 @@ export interface ExpressionNode extends Node {
export interface IfNode extends Node {
type: NodeTypes.IF
branches: IfBranchNode[]
isRoot: boolean
}
export interface IfBranchNode extends Node {
type: NodeTypes.IF_BRANCH
condition: ExpressionNode | undefined // else
children: ChildNode[]
isRoot: boolean
}
export interface ForNode extends Node {
@ -203,14 +205,15 @@ export function createObjectProperty(
export function createExpression(
content: string,
isStatic: boolean,
loc: SourceLocation
loc: SourceLocation,
isInterpolation = false
): ExpressionNode {
return {
type: NodeTypes.EXPRESSION,
loc,
content,
isStatic,
isInterpolation: false
isInterpolation
}
}

View File

@ -17,17 +17,33 @@ import {
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { advancePositionWithMutation, assert } from './utils'
import { isString, isArray } from '@vue/shared'
import { RENDER_LIST, TO_STRING } from './runtimeConstants'
import {
RENDER_LIST,
TO_STRING,
CREATE_VNODE,
COMMENT
} from './runtimeConstants'
type CodegenNode = ChildNode | JSChildNode
export interface CodegenOptions {
// will generate import statements for
// runtime helpers; otherwise will grab the helpers from global `Vue`.
// default: false
// - Module mode will generate ES module import statements for helpers
// and export the render function as the default export.
// - Function mode will generate a single `const { helpers... } = Vue`
// statement and return the render function. It is meant to be used with
// `new Function(code)()` to generate a render function at runtime.
// Default: 'function'
mode?: 'module' | 'function'
// Prefix suitable identifiers with _ctx.
// If this option is false, the generated code will be wrapped in a
// `with (this) { ... }` block.
// Default: false
prefixIdentifiers?: boolean
// Generate source map?
// Default: false
sourceMap?: boolean
// Filename for source map generation.
// Default: `template.vue.html`
filename?: string
}
@ -55,12 +71,14 @@ function createCodegenContext(
{
mode = 'function',
prefixIdentifiers = false,
sourceMap = false,
filename = `template.vue.html`
}: CodegenOptions
): CodegenContext {
const context: CodegenContext = {
mode,
prefixIdentifiers,
sourceMap,
filename,
source: ast.loc.source,
code: ``,
@ -70,9 +88,10 @@ function createCodegenContext(
indentLevel: 0,
// lazy require source-map implementation, only in non-browser builds!
map: __BROWSER__
? undefined
: new (require('source-map')).SourceMapGenerator(),
map:
__BROWSER__ || !sourceMap
? undefined
: new (require('source-map')).SourceMapGenerator(),
push(code, node?: CodegenNode) {
context.code += code
@ -108,8 +127,8 @@ function createCodegenContext(
}
}
const newline = (n: number) => context.push('\n' + ` `.repeat(n))
if (!__BROWSER__) {
context.map!.setSourceContent(filename, context.source)
if (!__BROWSER__ && context.map) {
context.map.setSourceContent(filename, context.source)
}
return context
}
@ -174,6 +193,9 @@ function genChildren(
context: CodegenContext,
asRoot: boolean = false
) {
if (!children.length) {
return context.push(`null`)
}
const child = children[0]
if (
children.length === 1 &&
@ -321,7 +343,12 @@ function genCompoundExpression(node: ExpressionNode, context: CodegenContext) {
}
function genComment(node: CommentNode, context: CodegenContext) {
context.push(`<!--${node.content}-->`, node)
if (__DEV__) {
context.push(
`${CREATE_VNODE}(${COMMENT}, 0, ${JSON.stringify(node.content)})`,
node
)
}
}
// control flow
@ -330,7 +357,7 @@ function genIf(node: IfNode, context: CodegenContext) {
}
function genIfBranch(
{ condition, children }: IfBranchNode,
{ condition, children, isRoot }: IfBranchNode,
branches: IfBranchNode[],
nextIndex: number,
context: CodegenContext
@ -344,7 +371,7 @@ function genIfBranch(
indent()
context.indentLevel++
push(`? `)
genChildren(children, context)
genChildren(children, context, isRoot)
context.indentLevel--
newline()
push(`: `)
@ -357,7 +384,7 @@ function genIfBranch(
} else {
// v-else
__DEV__ && assert(nextIndex === branches.length)
genChildren(children, context)
genChildren(children, context, isRoot)
}
}

View File

@ -1,5 +1,10 @@
// Name mapping constants for runtime helpers that need to be imported in
// generated code. Make sure these are correctly exported in the runtime!
export const FRAGMENT = `Fragment`
export const PORTAL = `Portal`
export const COMMENT = `Comment`
export const TEXT = `Text`
export const SUSPENSE = `Suspense`
export const CREATE_VNODE = `createVNode`
export const RESOLVE_COMPONENT = `resolveComponent`
export const RESOLVE_DIRECTIVE = `resolveDirective`

View File

@ -10,7 +10,7 @@ import {
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
import { TO_STRING } from './runtimeConstants'
import { TO_STRING, COMMENT, CREATE_VNODE } from './runtimeConstants'
// There are two types of transforms:
//
@ -49,11 +49,11 @@ export interface TransformOptions {
}
export interface TransformContext extends Required<TransformOptions> {
root: RootNode
imports: Set<string>
statements: string[]
identifiers: { [name: string]: number | undefined }
parent: ParentNode
ancestors: ParentNode[]
childIndex: number
currentNode: ChildNode | null
replaceNode(node: ChildNode): void
@ -73,6 +73,7 @@ function createTransformContext(
}: TransformOptions
): TransformContext {
const context: TransformContext = {
root,
imports: new Set(),
statements: [],
identifiers: {},
@ -81,7 +82,6 @@ function createTransformContext(
directiveTransforms,
onError,
parent: root,
ancestors: [],
childIndex: 0,
currentNode: null,
replaceNode(node) {
@ -139,7 +139,6 @@ export function traverseChildren(
parent: ParentNode,
context: TransformContext
) {
const ancestors = context.ancestors.concat(parent)
let i = 0
const nodeRemoved = () => {
i--
@ -149,7 +148,6 @@ export function traverseChildren(
if (isString(child)) continue
context.currentNode = child
context.parent = parent
context.ancestors = ancestors
context.childIndex = i
context.onNodeRemoved = nodeRemoved
traverseNode(child, context)
@ -180,6 +178,12 @@ export function traverseNode(node: ChildNode, context: TransformContext) {
}
switch (node.type) {
case NodeTypes.COMMENT:
context.imports.add(CREATE_VNODE)
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.imports.add(COMMENT)
break
case NodeTypes.EXPRESSION:
// no need to traverse, but we need to inject toString helper
if (node.isInterpolation) {

View File

@ -19,10 +19,14 @@ export const transformIf = createStructuralDirectiveTransform(
processExpression(dir.exp, context)
}
if (dir.name === 'if') {
// check if this v-if is root - so that in codegen we can avoid generating
// arrays for each branch
const isRoot = context.parent === context.root
context.replaceNode({
type: NodeTypes.IF,
loc: node.loc,
branches: [createIfBranch(node, dir)]
branches: [createIfBranch(node, dir, isRoot)],
isRoot
})
} else {
// locate the adjacent v-if
@ -39,7 +43,7 @@ export const transformIf = createStructuralDirectiveTransform(
if (sibling && sibling.type === NodeTypes.IF) {
// move the node to the if node's branches
context.removeNode()
const branch = createIfBranch(node, dir)
const branch = createIfBranch(node, dir, sibling.isRoot)
if (__DEV__ && comments.length) {
branch.children = [...comments, ...branch.children]
}
@ -63,11 +67,16 @@ export const transformIf = createStructuralDirectiveTransform(
}
)
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
function createIfBranch(
node: ElementNode,
dir: DirectiveNode,
isRoot: boolean
): IfBranchNode {
return {
type: NodeTypes.IF_BRANCH,
loc: node.loc,
condition: dir.name === 'else' ? undefined : dir.exp,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
isRoot
}
}