From: Evan You Date: Tue, 24 Sep 2019 19:49:02 +0000 (-0400) Subject: test: tests for codegen X-Git-Tag: v3.0.0-alpha.0~708 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7a46e51815c8b2f5e0235e815bbbcd311468d891;p=thirdparty%2Fvuejs%2Fcore.git test: tests for codegen --- diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap new file mode 100644 index 0000000000..1bafff968c --- /dev/null +++ b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap @@ -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\\") + ] + } +}" +`; diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index 16c846fc01..daf84568ad 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -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 { + 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 [ diff --git a/packages/compiler-core/__tests__/compile.spec.ts b/packages/compiler-core/__tests__/compile.spec.ts new file mode 100644 index 0000000000..6f3f60bb52 --- /dev/null +++ b/packages/compiler-core/__tests__/compile.spec.ts @@ -0,0 +1 @@ +// Integration tests for parser + transform + codegen diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index 3d262f2e93..e752698b4f 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -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(``) + transform(ast, {}) + expect(ast.imports).toContain(CREATE_VNODE) + expect(ast.imports).toContain(COMMENT) + }) }) diff --git a/packages/compiler-core/__tests__/transforms/vBind.spec.ts b/packages/compiler-core/__tests__/transforms/vBind.spec.ts new file mode 100644 index 0000000000..70b786d12e --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/vBind.spec.ts @@ -0,0 +1 @@ +// TODO diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index a9da468896..2c6ddc4632 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -29,7 +29,9 @@ describe('compiler: transform v-if', () => { test('basic v-if', () => { const node = parseWithIfTransform(`
`) 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) diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts new file mode 100644 index 0000000000..70b786d12e --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts @@ -0,0 +1 @@ +// TODO diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 68d2a288d6..24b8401100 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -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 } } diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index e07a20f773..0e0c085e2f 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -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) + 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) } } diff --git a/packages/compiler-core/src/runtimeConstants.ts b/packages/compiler-core/src/runtimeConstants.ts index bc58599568..74178b9568 100644 --- a/packages/compiler-core/src/runtimeConstants.ts +++ b/packages/compiler-core/src/runtimeConstants.ts @@ -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` diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index d5840f9126..b0a1278c67 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -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 { + root: RootNode imports: Set 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) { diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 1d8fe08aaf..70120d5f1d 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -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 } }