From: Rizumu Ayaka Date: Sun, 28 Jan 2024 19:42:56 +0000 (+0800) Subject: feat(compiler-vapor): `v-else` / `v-else-if` (#98) X-Git-Tag: v3.6.0-alpha.1~16^2~642 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=63a127b612a74286197ac7745e3daa053327eb3c;p=thirdparty%2Fvuejs%2Fcore.git feat(compiler-vapor): `v-else` / `v-else-if` (#98) Co-authored-by: 三咲智子 Kevin Deng --- diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap index 441f734706..a251ec008d 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap @@ -20,6 +20,30 @@ export function render(_ctx) { }" `; +exports[`compiler: v-if > comment between branches 1`] = ` +"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const t1 = _template("

") + const t2 = _template("fine") + const t3 = _fragment() + const n0 = t3() + const n1 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.orNot), () => { + const n3 = t1() + return n3 + }, () => { + const n4 = t2() + return n4 + })) + _prepend(n0, n1) + return n0 +}" +`; + exports[`compiler: v-if > dedupe same template 1`] = ` "import { template as _template, fragment as _fragment, createIf as _createIf, append as _append } from 'vue/vapor'; @@ -59,3 +83,67 @@ export function render(_ctx) { return n0 }" `; + +exports[`compiler: v-if > v-if + v-else 1`] = ` +"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const t1 = _template("

") + const t2 = _fragment() + const n0 = t2() + const n1 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => { + const n3 = t1() + return n3 + }) + _prepend(n0, n1) + return n0 +}" +`; + +exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = ` +"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const t1 = _template("

") + const t2 = _template("fine") + const t3 = _fragment() + const n0 = t3() + const n1 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.orNot), () => { + const n3 = t1() + return n3 + }, () => { + const n4 = t2() + return n4 + })) + _prepend(n0, n1) + return n0 +}" +`; + +exports[`compiler: v-if > v-if + v-else-if 1`] = ` +"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const t1 = _template("

") + const t2 = _fragment() + const n0 = t2() + const n1 = _createIf(() => (_ctx.ok), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.orNot), () => { + const n3 = t1() + return n3 + })) + _prepend(n0, n1) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts index b7f7b18864..7d6cd38a1d 100644 --- a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts @@ -1,11 +1,14 @@ import { makeCompile } from './_utils' import { + IRNodeTypes, + type IfIRNode, transformElement, transformInterpolation, transformOnce, transformVIf, transformVText, } from '../../src' +import { NodeTypes } from '@vue/compiler-core' const compileWithVIf = makeCompile({ nodeTransforms: [ @@ -21,15 +24,87 @@ const compileWithVIf = makeCompile({ describe('compiler: v-if', () => { test('basic v-if', () => { - const { code } = compileWithVIf(`
{{msg}}
`) + const { code, vaporHelpers, ir, helpers } = compileWithVIf( + `
{{msg}}
`, + ) + + expect(vaporHelpers).contains('createIf') + expect(helpers.size).toBe(0) + + expect(ir.template).lengthOf(2) + expect(ir.template).toMatchObject([ + { + template: '
', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + type: IRNodeTypes.FRAGMENT_FACTORY, + }, + ]) + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.IF, + id: 1, + condition: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'ok', + isStatic: false, + }, + positive: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 0, + }, + }, + { + type: IRNodeTypes.APPEND_NODE, + elements: [1], + parent: 0, + }, + ]) + + expect(ir.dynamic).toMatchObject({ + id: 0, + children: { 0: { id: 1 } }, + }) + + expect(ir.effect).toEqual([]) + expect((ir.operation[0] as IfIRNode).positive.effect).lengthOf(1) + expect(code).matchSnapshot() }) test('template v-if', () => { - const { code } = compileWithVIf( + const { code, ir } = compileWithVIf( ``, ) expect(code).matchSnapshot() + + expect(ir.template).lengthOf(2) + expect(ir.template[0]).toMatchObject({ + template: '
hello

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }) + + expect(ir.effect).toEqual([]) + expect((ir.operation[0] as IfIRNode).positive.effect).toMatchObject([ + { + operations: [ + { + type: IRNodeTypes.SET_TEXT, + element: 3, + value: { + content: 'msg', + type: NodeTypes.SIMPLE_EXPRESSION, + isStatic: false, + }, + }, + ], + }, + ]) + expect((ir.operation[0] as IfIRNode).positive.dynamic).toMatchObject({ + id: 2, + children: { 2: { id: 3 } }, + }) }) test('dedupe same template', () => { @@ -42,10 +117,185 @@ describe('compiler: v-if', () => { test.todo('v-if with v-once') test.todo('component v-if') - test.todo('v-if + v-else') - test.todo('v-if + v-else-if') - test.todo('v-if + v-else-if + v-else') - test.todo('comment between branches') + + test('v-if + v-else', () => { + const { code, ir, vaporHelpers, helpers } = compileWithVIf( + `

`, + ) + expect(code).matchSnapshot() + expect(ir.template).lengthOf(3) + expect(ir.template).toMatchObject([ + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + type: IRNodeTypes.FRAGMENT_FACTORY, + }, + ]) + + expect(vaporHelpers).contains('createIf') + expect(ir.effect).lengthOf(0) + expect(helpers).lengthOf(0) + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.IF, + id: 1, + condition: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'ok', + isStatic: false, + }, + positive: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 0, + }, + negative: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 1, + }, + }, + { + type: IRNodeTypes.PREPEND_NODE, + elements: [1], + parent: 0, + }, + ]) + }) + + test('v-if + v-else-if', () => { + const { code, ir } = compileWithVIf( + `

`, + ) + expect(code).matchSnapshot() + expect(ir.template).lengthOf(3) + expect(ir.template).toMatchObject([ + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { type: IRNodeTypes.FRAGMENT_FACTORY }, + ]) + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.IF, + id: 1, + condition: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'ok', + isStatic: false, + }, + positive: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 0, + }, + negative: { + type: IRNodeTypes.IF, + condition: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'orNot', + isStatic: false, + }, + positive: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 1, + }, + }, + }, + { + type: IRNodeTypes.PREPEND_NODE, + elements: [1], + parent: 0, + }, + ]) + }) + + test('v-if + v-else-if + v-else', () => { + const { code, ir } = compileWithVIf( + `

`, + ) + expect(code).matchSnapshot() + expect(ir.template).lengthOf(4) + expect(ir.template).toMatchObject([ + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + template: 'fine', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { type: IRNodeTypes.FRAGMENT_FACTORY }, + ]) + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.IF, + id: 1, + positive: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 0, + }, + negative: { + type: IRNodeTypes.IF, + positive: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 1, + }, + negative: { + type: IRNodeTypes.BLOCK_FUNCTION, + templateIndex: 2, + }, + }, + }, + { + type: IRNodeTypes.PREPEND_NODE, + elements: [1], + parent: 0, + }, + ]) + }) + + test('comment between branches', () => { + const { code, ir } = compileWithVIf(` +
+ +

+ + + `) + expect(code).matchSnapshot() + expect(ir.template).lengthOf(4) + expect(ir.template).toMatchObject([ + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + template: '

', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { + template: 'fine', + type: IRNodeTypes.TEMPLATE_FACTORY, + }, + { type: IRNodeTypes.FRAGMENT_FACTORY }, + ]) + }) + describe.todo('errors') describe.todo('codegen') test.todo('v-on with v-if') diff --git a/packages/compiler-vapor/src/generators/if.ts b/packages/compiler-vapor/src/generators/if.ts index 69ef737d93..0019273f82 100644 --- a/packages/compiler-vapor/src/generators/if.ts +++ b/packages/compiler-vapor/src/generators/if.ts @@ -1,12 +1,30 @@ import { type CodegenContext, genBlockFunctionContent } from '../generate' -import type { BlockFunctionIRNode, IfIRNode } from '../ir' +import { type BlockFunctionIRNode, IRNodeTypes, type IfIRNode } from '../ir' import { genExpression } from './expression' -export function genIf(oper: IfIRNode, context: CodegenContext) { - const { pushFnCall, vaporHelper, pushNewline, push, withIndent } = context +export function genIf( + oper: IfIRNode, + context: CodegenContext, + isNested = false, +) { + const { pushFnCall, vaporHelper, pushNewline, push } = context const { condition, positive, negative } = oper - pushNewline(`const n${oper.id} = `) + let positiveArg = () => genBlockFunction(positive, context) + let negativeArg: false | (() => void) = false + + if (negative) { + if (negative.type === IRNodeTypes.BLOCK_FUNCTION) { + negativeArg = () => genBlockFunction(negative, context) + } else { + negativeArg = () => { + push('() => ') + genIf(negative!, context, true) + } + } + } + + if (!isNested) pushNewline(`const n${oper.id} = `) pushFnCall( vaporHelper('createIf'), () => { @@ -14,15 +32,17 @@ export function genIf(oper: IfIRNode, context: CodegenContext) { genExpression(condition, context) push(')') }, - () => genBlockFunction(positive), - !!negative && (() => genBlockFunction(negative!)), + positiveArg, + negativeArg, ) +} - function genBlockFunction(oper: BlockFunctionIRNode) { - push('() => {') - withIndent(() => { - genBlockFunctionContent(oper, context) - }) - pushNewline('}') - } +function genBlockFunction(oper: BlockFunctionIRNode, context: CodegenContext) { + const { pushNewline, push, withIndent } = context + + push('() => {') + withIndent(() => { + genBlockFunctionContent(oper, context) + }) + pushNewline('}') } diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 09ca59fff6..7c0872a1bd 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -62,7 +62,7 @@ export interface IfIRNode extends BaseIRNode { id: number condition: IRExpression positive: BlockFunctionIRNode - negative?: BlockFunctionIRNode + negative?: BlockFunctionIRNode | IfIRNode } export interface TemplateFactoryIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index b5f66c1ca6..b8b3adf254 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -42,9 +42,9 @@ export type DirectiveTransform = ( // A structural directive transform is technically also a NodeTransform; // Only v-if and v-for fall into this category. export type StructuralDirectiveTransform = ( - node: RootNode | TemplateChildNode, + node: ElementNode, dir: VaporDirectiveNode, - context: TransformContext, + context: TransformContext, ) => void | (() => void) export type TransformOptions = HackOptions @@ -60,7 +60,7 @@ export interface TransformContext { > template: string - childrenTemplate: string[] + childrenTemplate: (string | null)[] dynamic: IRDynamicInfo inVOnce: boolean @@ -311,15 +311,12 @@ function transformNode( } if (context.node.type === NodeTypes.ROOT) - context.template += context.childrenTemplate.join('') + context.template += context.childrenTemplate.filter(Boolean).join('') } function transformChildren(ctx: TransformContext) { const { children } = ctx.node let i = 0 - // const nodeRemoved = () => { - // i-- - // } for (; i < children.length; i++) { const child = children[i] const childContext = createContext(child, ctx, i) @@ -405,7 +402,11 @@ export function createStructuralDirectiveTransform( const exitFns = [] for (const prop of props) { if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) { - const onExit = fn(node, prop as VaporDirectiveNode, context) + const onExit = fn( + node, + prop as VaporDirectiveNode, + context as TransformContext, + ) if (onExit) exitFns.push(onExit) } } diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts index 90d4a8c93c..25e1c4de8b 100644 --- a/packages/compiler-vapor/src/transforms/vIf.ts +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -1,4 +1,5 @@ import { + type ElementNode, ElementTypes, ErrorCodes, NodeTypes, @@ -15,6 +16,7 @@ import { import { type BlockFunctionIRNode, IRNodeTypes, + type OperationNode, type VaporDirectiveNode, } from '../ir' import { extend } from '@vue/shared' @@ -25,7 +27,7 @@ export const transformVIf = createStructuralDirectiveTransform( ) export function processIf( - node: RootNode | TemplateChildNode, + node: ElementNode, dir: VaporDirectiveNode, context: TransformContext, ) { @@ -40,7 +42,7 @@ export function processIf( if (dir.name === 'if') { const id = context.reference() context.dynamic.ghost = true - const [branch, onExit] = createIfBranch(node, dir, context) + const [branch, onExit] = createIfBranch(node, context) return () => { onExit() @@ -52,37 +54,100 @@ export function processIf( positive: branch, }) } + } else { + // check the adjacent v-if + const parent = context.parent! + const siblings = parent.node.children + const templates = parent.childrenTemplate + + const comments = [] + let sibling: TemplateChildNode | undefined + let i = siblings.indexOf(node) + while (i-- >= -1) { + sibling = siblings[i] + + if (sibling) { + if (sibling.type === NodeTypes.COMMENT) { + __DEV__ && comments.unshift(sibling) + templates[i] = null + continue + } else if ( + sibling.type === NodeTypes.TEXT && + !sibling.content.trim().length + ) { + templates[i] = null + continue + } + } + break + } + + const { operation } = context.block + let lastIfNode: OperationNode + if ( + // check if v-if is the sibling node + !sibling || + sibling.type !== NodeTypes.ELEMENT || + !sibling.props.some( + ({ type, name }) => + type === NodeTypes.DIRECTIVE && ['if', 'else-if'].includes(name), + ) || + // check if IFNode is the last operation and get the root IFNode + !(lastIfNode = operation[operation.length - 1]) || + lastIfNode.type !== IRNodeTypes.IF + ) { + context.options.onError( + createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc), + ) + return + } + + while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) { + lastIfNode = lastIfNode.negative + } + + // Check if v-else was followed by v-else-if + if (dir.name === 'else-if' && lastIfNode.negative) { + context.options.onError( + createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc), + ) + } + + // TODO ignore comments if the v-if is direct child of (PR #3622) + if (__DEV__ && comments.length) { + node = wrapTemplate(node) + context.node = node = extend({}, node, { + children: [...comments, ...node.children], + }) + } + + const [branch, onExit] = createIfBranch(node, context) + + if (dir.name === 'else') { + lastIfNode.negative = branch + } else { + lastIfNode.negative = { + type: IRNodeTypes.IF, + id: -1, + loc: dir.loc, + condition: dir.exp!, + positive: branch, + } + } + + return () => onExit() } } export function createIfBranch( - node: RootNode | TemplateChildNode, - dir: VaporDirectiveNode, + node: ElementNode, context: TransformContext, ): [BlockFunctionIRNode, () => void] { - if ( - node.type === NodeTypes.ELEMENT && - node.tagType !== ElementTypes.TEMPLATE - ) { - node = extend({}, node, { - type: NodeTypes.ELEMENT, - tag: 'template', - props: [], - tagType: ElementTypes.TEMPLATE, - children: [ - extend({}, node, { - props: node.props.filter( - p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if', - ), - } as TemplateChildNode), - ], - } as Partial) - context.node = node - } + context.node = node = wrapTemplate(node) const branch: BlockFunctionIRNode = { type: IRNodeTypes.BLOCK_FUNCTION, - loc: dir.loc, + loc: node.loc, node, templateIndex: -1, dynamic: { @@ -105,3 +170,22 @@ export function createIfBranch( } return [branch, onExit] } + +function wrapTemplate(node: ElementNode): TemplateNode { + if (node.tagType === ElementTypes.TEMPLATE) { + return node + } + return extend({}, node, { + type: NodeTypes.ELEMENT, + tag: 'template', + props: [], + tagType: ElementTypes.TEMPLATE, + children: [ + extend({}, node, { + props: node.props.filter( + p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if', + ), + } as TemplateChildNode), + ], + } as Partial) +}