--- /dev/null
+// 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\\")
+ ]
+ }
+}"
+`;
-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 [
--- /dev/null
+// Integration tests for parser + transform + codegen
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', () => {
}
])
})
+
+ 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)
+ })
})
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)
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 {
export function createExpression(
content: string,
isStatic: boolean,
- loc: SourceLocation
+ loc: SourceLocation,
+ isInterpolation = false
): ExpressionNode {
return {
type: NodeTypes.EXPRESSION,
loc,
content,
isStatic,
- isInterpolation: false
+ isInterpolation
}
}
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
}
{
mode = 'function',
prefixIdentifiers = false,
+ sourceMap = false,
filename = `template.vue.html`
}: CodegenOptions
): CodegenContext {
const context: CodegenContext = {
mode,
prefixIdentifiers,
+ sourceMap,
filename,
source: ast.loc.source,
code: ``,
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
}
}
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
}
context: CodegenContext,
asRoot: boolean = false
) {
+ if (!children.length) {
+ return context.push(`null`)
+ }
const child = children[0]
if (
children.length === 1 &&
}
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
}
function genIfBranch(
- { condition, children }: IfBranchNode,
+ { condition, children, isRoot }: IfBranchNode,
branches: IfBranchNode[],
nextIndex: number,
context: CodegenContext
indent()
context.indentLevel++
push(`? `)
- genChildren(children, context)
+ genChildren(children, context, isRoot)
context.indentLevel--
newline()
push(`: `)
} else {
// v-else
__DEV__ && assert(nextIndex === branches.length)
- genChildren(children, context)
+ genChildren(children, context, isRoot)
}
}
// 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`
} 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:
//
}
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
}: TransformOptions
): TransformContext {
const context: TransformContext = {
+ root,
imports: new Set(),
statements: [],
identifiers: {},
directiveTransforms,
onError,
parent: root,
- ancestors: [],
childIndex: 0,
currentNode: null,
replaceNode(node) {
parent: ParentNode,
context: TransformContext
) {
- const ancestors = context.ancestors.concat(parent)
let i = 0
const nodeRemoved = () => {
i--
if (isString(child)) continue
context.currentNode = child
context.parent = parent
- context.ancestors = ancestors
context.childIndex = i
context.onNodeRemoved = nodeRemoved
traverseNode(child, context)
}
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) {
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
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]
}
}
)
-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
}
}