diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap index 527f8b109..e38886872 100644 --- a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap @@ -44,7 +44,7 @@ exports[`compiler: codegen compound expression 1`] = ` " return function render() { with (this) { - return _toString(_ctx.foo) + return _ctx.foo + _toString(bar) } }" `; diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index 19a1f0b18..93de38fbc 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -226,17 +226,23 @@ describe('compiler: codegen', () => { const { code } = generate( createRoot({ children: [ - createInterpolation( - createCompoundExpression( - [`_ctx.`, createSimpleExpression(`foo`, false, mockLoc)], - mockLoc - ), + createCompoundExpression( + [ + `_ctx.`, + createSimpleExpression(`foo`, false, mockLoc), + ` + `, + { + type: NodeTypes.INTERPOLATION, + loc: mockLoc, + content: createSimpleExpression(`bar`, false, mockLoc) + } + ], mockLoc ) ] }) ) - expect(code).toMatch(`return _${TO_STRING}(_ctx.foo)`) + expect(code).toMatch(`return _ctx.foo + _${TO_STRING}(bar)`) expect(code).toMatchSnapshot() }) diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index 8ebf221f9..b23340344 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -25,22 +25,29 @@ describe('compiler: transform', () => { }) const div = ast.children[0] as ElementNode - expect(calls.length).toBe(3) + expect(calls.length).toBe(4) expect(calls[0]).toMatchObject([ + ast, + { + parent: null, + currentNode: ast + } + ]) + expect(calls[1]).toMatchObject([ div, { parent: ast, currentNode: div } ]) - expect(calls[1]).toMatchObject([ + expect(calls[2]).toMatchObject([ div.children[0], { parent: div, currentNode: div.children[0] } ]) - expect(calls[2]).toMatchObject([ + expect(calls[3]).toMatchObject([ div.children[1], { parent: div, @@ -76,11 +83,11 @@ describe('compiler: transform', () => { expect(ast.children.length).toBe(2) const newElement = ast.children[0] as ElementNode expect(newElement.tag).toBe('p') - expect(spy).toHaveBeenCalledTimes(3) + expect(spy).toHaveBeenCalledTimes(4) // should traverse the children of replaced node - expect(spy.mock.calls[1][0]).toBe(newElement.children[0]) + expect(spy.mock.calls[2][0]).toBe(newElement.children[0]) // should traverse the node after the replaced node - expect(spy.mock.calls[2][0]).toBe(ast.children[1]) + expect(spy.mock.calls[3][0]).toBe(ast.children[1]) }) test('context.removeNode', () => { @@ -103,10 +110,10 @@ describe('compiler: transform', () => { expect(ast.children[1]).toBe(c2) // should not traverse children of remove node - expect(spy).toHaveBeenCalledTimes(3) + expect(spy).toHaveBeenCalledTimes(4) // should traverse nodes around removed - expect(spy.mock.calls[0][0]).toBe(c1) - expect(spy.mock.calls[2][0]).toBe(c2) + expect(spy.mock.calls[1][0]).toBe(c1) + expect(spy.mock.calls[3][0]).toBe(c2) }) test('context.removeNode (prev sibling)', () => { @@ -118,7 +125,7 @@ describe('compiler: transform', () => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { context.removeNode() // remove previous sibling - context.removeNode(context.parent.children[0]) + context.removeNode(context.parent!.children[0]) } } const spy = jest.fn(plugin) @@ -129,11 +136,11 @@ describe('compiler: transform', () => { expect(ast.children.length).toBe(1) expect(ast.children[0]).toBe(c2) - expect(spy).toHaveBeenCalledTimes(3) + expect(spy).toHaveBeenCalledTimes(4) // should still traverse first span before removal - expect(spy.mock.calls[0][0]).toBe(c1) + expect(spy.mock.calls[1][0]).toBe(c1) // should still traverse last span - expect(spy.mock.calls[2][0]).toBe(c2) + expect(spy.mock.calls[3][0]).toBe(c2) }) test('context.removeNode (next sibling)', () => { @@ -145,7 +152,7 @@ describe('compiler: transform', () => { if (node.type === NodeTypes.ELEMENT && node.tag === 'div') { context.removeNode() // remove next sibling - context.removeNode(context.parent.children[1]) + context.removeNode(context.parent!.children[1]) } } const spy = jest.fn(plugin) @@ -156,20 +163,22 @@ describe('compiler: transform', () => { expect(ast.children.length).toBe(1) expect(ast.children[0]).toBe(c1) - expect(spy).toHaveBeenCalledTimes(2) + expect(spy).toHaveBeenCalledTimes(3) // should still traverse first span before removal - expect(spy.mock.calls[0][0]).toBe(c1) + expect(spy.mock.calls[1][0]).toBe(c1) // should not traverse last span - expect(spy.mock.calls[1][0]).toBe(d1) + expect(spy.mock.calls[2][0]).toBe(d1) }) test('context.hoist', () => { const ast = parse(`
`) const hoisted: ExpressionNode[] = [] const mock: NodeTransform = (node, context) => { - const dir = (node as ElementNode).props[0] as DirectiveNode - hoisted.push(dir.exp!) - dir.exp = context.hoist(dir.exp!) + if (node.type === NodeTypes.ELEMENT) { + const dir = node.props[0] as DirectiveNode + hoisted.push(dir.exp!) + dir.exp = context.hoist(dir.exp!) + } } transform(ast, { nodeTransforms: [mock] diff --git a/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts b/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts new file mode 100644 index 000000000..64ed2da0d --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/optimizeText.spec.ts @@ -0,0 +1,112 @@ +import { CompilerOptions, parse, transform, NodeTypes } from '../../src' +import { optimizeText } from '../../src/transforms/optimizeText' +import { transformExpression } from '../../src/transforms/transformExpression' + +function transformWithTextOpt(template: string, options: CompilerOptions = {}) { + const ast = parse(template) + transform(ast, { + nodeTransforms: [ + ...(options.prefixIdentifiers ? [transformExpression] : []), + optimizeText + ], + ...options + }) + return ast +} + +describe('compiler: optimize interpolation', () => { + test('no consecutive text', () => { + const root = transformWithTextOpt(`{{ foo }}`) + expect(root.children[0]).toMatchObject({ + type: NodeTypes.INTERPOLATION, + content: { + content: `foo` + } + }) + }) + + test('consecutive text', () => { + const root = transformWithTextOpt(`{{ foo }} bar {{ baz }}`) + expect(root.children.length).toBe(1) + expect(root.children[0]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + }) + + test('consecutive text between elements', () => { + const root = transformWithTextOpt(`{{ foo }} bar {{ baz }}`) + expect(root.children.length).toBe(3) + expect(root.children[0].type).toBe(NodeTypes.ELEMENT) + expect(root.children[1]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + expect(root.children[2].type).toBe(NodeTypes.ELEMENT) + }) + + test('consecutive text mixed with elements', () => { + const root = transformWithTextOpt( + `{{ foo }} bar {{ baz }}{{ foo }} bar {{ baz }}` + ) + expect(root.children.length).toBe(5) + expect(root.children[0].type).toBe(NodeTypes.ELEMENT) + expect(root.children[1]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + expect(root.children[2].type).toBe(NodeTypes.ELEMENT) + expect(root.children[3]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { type: NodeTypes.INTERPOLATION, content: { content: `baz` } } + ] + }) + expect(root.children[4].type).toBe(NodeTypes.ELEMENT) + }) + + test('with prefixIdentifiers: true', () => { + const root = transformWithTextOpt(`{{ foo }} bar {{ baz + qux }}`, { + prefixIdentifiers: true + }) + expect(root.children.length).toBe(1) + expect(root.children[0]).toMatchObject({ + type: NodeTypes.COMPOUND_EXPRESSION, + children: [ + { type: NodeTypes.INTERPOLATION, content: { content: `_ctx.foo` } }, + ` + `, + { type: NodeTypes.TEXT, content: ` bar ` }, + ` + `, + { + type: NodeTypes.INTERPOLATION, + content: { + type: NodeTypes.COMPOUND_EXPRESSION, + children: [{ content: `_ctx.baz` }, ` + `, { content: `_ctx.qux` }] + } + } + ] + }) + }) +}) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 97a92f5b0..30a3d6c07 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -64,6 +64,7 @@ export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode export type ChildNode = | ElementNode | InterpolationNode + | CompoundExpressionNode | TextNode | CommentNode | IfNode @@ -130,7 +131,7 @@ export interface InterpolationNode extends Node { // always dynamic export interface CompoundExpressionNode extends Node { type: NodeTypes.COMPOUND_EXPRESSION - children: (SimpleExpressionNode | string)[] + children: (SimpleExpressionNode | InterpolationNode | TextNode | string)[] // an expression parsed as the params of a function will track // the identifiers declared inside the function body. identifiers?: string[] diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 0fb5a5954..ee0d5d2b6 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -283,6 +283,7 @@ function genChildren( (allowSingle || type === NodeTypes.TEXT || type === NodeTypes.INTERPOLATION || + type === NodeTypes.COMPOUND_EXPRESSION || (type === NodeTypes.ELEMENT && (child as ElementNode).tagType === ElementTypes.SLOT)) ) { @@ -423,7 +424,7 @@ function genCompoundExpression( if (isString(child)) { context.push(child) } else { - genExpression(child, context) + genNode(child, context) } } } diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 111f97e1d..e6f51c6ca 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -13,6 +13,7 @@ import { transformOn } from './transforms/vOn' import { transformBind } from './transforms/vBind' import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { trackSlotScopes } from './transforms/vSlot' +import { optimizeText } from './transforms/optimizeText' export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions @@ -45,6 +46,7 @@ export function baseCompile( transformIf, transformFor, ...(prefixIdentifiers ? [transformExpression, trackSlotScopes] : []), + optimizeText, transformStyle, transformSlotOutlet, transformElement, diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 6a4f893a0..263c77dd7 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -20,7 +20,7 @@ import { TO_STRING, COMMENT, CREATE_VNODE } from './runtimeConstants' // Transforms that operate directly on a ChildNode. NodeTransforms may mutate, // replace or remove the node being processed. export type NodeTransform = ( - node: ChildNode, + node: RootNode | ChildNode, context: TransformContext ) => void | (() => void) | (() => void)[] @@ -56,9 +56,9 @@ export interface TransformContext extends Required