diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts index d271cc1bb..cec124f70 100644 --- a/packages/compiler-vapor/src/compile.ts +++ b/packages/compiler-vapor/src/compile.ts @@ -25,6 +25,7 @@ import { transformRef } from './transforms/transformRef' import { transformInterpolation } from './transforms/transformInterpolation' import type { HackOptions } from './ir' import { transformVModel } from './transforms/vModel' +import { transformIf } from './transforms/vIf' export type CompilerOptions = HackOptions @@ -97,7 +98,13 @@ export function getBaseTransformPreset( prefixIdentifiers?: boolean, ): TransformPreset { return [ - [transformOnce, transformRef, transformInterpolation, transformElement], + [ + transformOnce, + transformRef, + transformInterpolation, + transformIf, + transformElement, + ], { bind: transformVBind, on: transformVOn, diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index bba27e325..8afb5d27d 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -8,6 +8,7 @@ import { locStub, } from '@vue/compiler-dom' import { + type BlockFunctionIRNode, type IRDynamicChildren, IRNodeTypes, type OperationNode, @@ -26,6 +27,7 @@ import { genSetRef } from './generators/ref' import { genSetModelValue } from './generators/modelValue' import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom' import { genWithDirective } from './generators/directive' +import { genIf } from './generators/if' interface CodegenOptions extends BaseCodegenOptions { expressionPlugins?: ParserPlugin[] @@ -271,41 +273,7 @@ export function generate( } }) - { - pushNewline(`const n${ir.dynamic.id} = t0()`) - - const children = genChildren(ir.dynamic.children) - if (children) { - pushNewline( - `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`, - ) - } - - for (const oper of ir.operation.filter( - (oper): oper is WithDirectiveIRNode => - oper.type === IRNodeTypes.WITH_DIRECTIVE, - )) { - genWithDirective(oper, ctx) - } - - for (const operation of ir.operation) { - genOperation(operation, ctx) - } - - for (const { operations } of ir.effect) { - pushNewline(`${vaporHelper('renderEffect')}(() => {`) - withIndent(() => { - for (const operation of operations) { - genOperation(operation, ctx) - } - }) - pushNewline('})') - } - - // TODO multiple-template - // TODO return statement in IR - pushNewline(`return n${ir.dynamic.id}`) - } + genBlockFunctionContent(ir, ctx) }) newline() @@ -386,6 +354,8 @@ function genOperation(oper: OperationNode, context: CodegenContext) { return genPrependNode(oper, context) case IRNodeTypes.APPEND_NODE: return genAppendNode(oper, context) + case IRNodeTypes.IF: + return genIf(oper, context) case IRNodeTypes.WITH_DIRECTIVE: // generated, skip return @@ -393,3 +363,41 @@ function genOperation(oper: OperationNode, context: CodegenContext) { return checkNever(oper) } } + +export function genBlockFunctionContent( + ir: Omit, + ctx: CodegenContext, +) { + const { pushNewline, withIndent, vaporHelper } = ctx + pushNewline(`const n${ir.dynamic.id} = t${ir.templateIndex}()`) + + const children = genChildren(ir.dynamic.children) + if (children) { + pushNewline( + `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`, + ) + } + + for (const oper of ir.operation.filter( + (oper): oper is WithDirectiveIRNode => + oper.type === IRNodeTypes.WITH_DIRECTIVE, + )) { + genWithDirective(oper, ctx) + } + + for (const operation of ir.operation) { + genOperation(operation, ctx) + } + + for (const { operations } of ir.effect) { + pushNewline(`${vaporHelper('renderEffect')}(() => {`) + withIndent(() => { + for (const operation of operations) { + genOperation(operation, ctx) + } + }) + pushNewline('})') + } + + pushNewline(`return n${ir.dynamic.id}`) +} diff --git a/packages/compiler-vapor/src/generators/if.ts b/packages/compiler-vapor/src/generators/if.ts new file mode 100644 index 000000000..05cd31d0c --- /dev/null +++ b/packages/compiler-vapor/src/generators/if.ts @@ -0,0 +1,32 @@ +import { type CodegenContext, genBlockFunctionContent } from '../generate' +import type { BlockFunctionIRNode, IfIRNode } from '../ir' +import { genExpression } from './expression' + +export function genIf(oper: IfIRNode, context: CodegenContext) { + const { pushFnCall, vaporHelper, pushNewline, push } = context + const { condition, truthyBranch, falsyBranch } = oper + + pushNewline(`const n${oper.id} = `) + pushFnCall( + vaporHelper('createIf'), + () => { + push('() => (') + genExpression(condition, context) + push(')') + }, + () => genBlockFunction(truthyBranch, context), + !!falsyBranch && (() => genBlockFunction(falsyBranch!, context)), + ) +} + +export function genBlockFunction( + oper: Omit, + context: CodegenContext, +) { + const { push, pushNewline, 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 00fcdb587..ee5fe68a7 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -5,6 +5,7 @@ import type { RootNode, SimpleExpressionNode, SourceLocation, + TemplateChildNode, } from '@vue/compiler-dom' import type { Prettify } from '@vue/shared' import type { DirectiveTransform, NodeTransform } from './transform' @@ -27,6 +28,9 @@ export enum IRNodeTypes { CREATE_TEXT_NODE, WITH_DIRECTIVE, + + IF, + BLOCK_FUNCTION, } export interface BaseIRNode { @@ -37,18 +41,32 @@ export interface BaseIRNode { // TODO refactor export type VaporHelper = keyof typeof import('../../runtime-vapor/src') -export interface RootIRNode extends BaseIRNode { - type: IRNodeTypes.ROOT +export interface BlockFunctionIRNode extends BaseIRNode { + type: IRNodeTypes.BLOCK_FUNCTION source: string - node: RootNode - template: Array + node: RootNode | TemplateChildNode + templateIndex: number dynamic: IRDynamicInfo effect: IREffect[] operation: OperationNode[] +} + +export interface RootIRNode extends Omit { + type: IRNodeTypes.ROOT + node: RootNode + template: Array helpers: Set vaporHelpers: Set } +export interface IfIRNode extends BaseIRNode { + type: IRNodeTypes.IF + id: number + condition: IRExpression + truthyBranch: BlockFunctionIRNode + falsyBranch?: BlockFunctionIRNode +} + export interface TemplateFactoryIRNode extends BaseIRNode { type: IRNodeTypes.TEMPLATE_FACTORY template: string @@ -160,6 +178,7 @@ export type OperationNode = | PrependNodeIRNode | AppendNodeIRNode | WithDirectiveIRNode + | IfIRNode export interface IRDynamicInfo { id: number | null diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index eb230da76..3899fbfba 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -3,14 +3,16 @@ import { type TransformOptions as BaseTransformOptions, type CompilerCompatOptions, type ElementNode, + ElementTypes, NodeTypes, type ParentNode, type RootNode, type TemplateChildNode, defaultOnError, defaultOnWarn, + isVSlot, } from '@vue/compiler-dom' -import { EMPTY_OBJ, NOOP, extend, isArray } from '@vue/shared' +import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared' import { type IRDynamicInfo, type IRExpression, @@ -18,7 +20,12 @@ import { type OperationNode, type RootIRNode, } from './ir' -import type { HackOptions, VaporDirectiveNode } from './ir' +import type { + BlockFunctionIRNode, + HackOptions, + TemplateFactoryIRNode, + VaporDirectiveNode, +} from './ir' export type NodeTransform = ( node: RootNode | TemplateChildNode, @@ -31,6 +38,14 @@ export type DirectiveTransform = ( context: TransformContext, ) => void +// 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, + dir: VaporDirectiveNode, + context: TransformContext, +) => void | (() => void) + export type TransformOptions = HackOptions export interface TransformContext { @@ -38,6 +53,7 @@ export interface TransformContext { parent: TransformContext | null root: TransformContext index: number + blockFnIR: Omit options: Required< Omit > @@ -48,9 +64,10 @@ export interface TransformContext { inVOnce: boolean + replaceBlockFnIR(ir: TransformContext['blockFnIR']): () => void reference(): number increaseId(): number - registerTemplate(): number + pushTemplate(): number registerEffect( expressions: Array, operation: OperationNode[], @@ -61,18 +78,35 @@ export interface TransformContext { // TODO use class for better perf function createRootContext( - ir: RootIRNode, + rootIR: RootIRNode, node: RootNode, options: TransformOptions = {}, ): TransformContext { let globalId = 0 - const { effect, operation: operation, helpers, vaporHelpers } = ir + const { helpers, vaporHelpers } = rootIR const ctx: TransformContext = { node, parent: null, index: 0, root: null!, // set later + blockFnIR: rootIR, + replaceBlockFnIR(ir) { + const currentIR = this.blockFnIR + const currentTemplate = this.template + const currentChildrenTemplate = this.childrenTemplate + const currentDynamic = this.dynamic + this.blockFnIR = ir + this.dynamic = ir.dynamic + this.template = '' + this.childrenTemplate = [] + return () => { + this.blockFnIR = currentIR + this.dynamic = currentDynamic + this.template = currentTemplate + this.childrenTemplate = currentChildrenTemplate + } + }, options: extend( {}, { @@ -100,7 +134,7 @@ function createRootContext( }, options, ), - dynamic: ir.dynamic, + dynamic: rootIR.dynamic, inVOnce: false, increaseId: () => globalId++, @@ -116,13 +150,13 @@ function createRootContext( ) { return this.registerOperation(...operations) } - const existing = effect.find((e) => + const existing = this.blockFnIR.effect.find((e) => isSameExpression(e.expressions, expressions as IRExpression[]), ) if (existing) { existing.operations.push(...operations) } else { - effect.push({ + this.blockFnIR.effect.push({ expressions: expressions as IRExpression[], operations, }) @@ -142,25 +176,26 @@ function createRootContext( template: '', childrenTemplate: [], - registerTemplate() { - if (!ctx.template) return -1 - - const idx = ir.template.findIndex( - (t) => - t.type === IRNodeTypes.TEMPLATE_FACTORY && - t.template === ctx.template, - ) - if (idx !== -1) return idx + pushTemplate() { + // update template + if (this.blockFnIR.templateIndex !== -1) { + const templateFactory = rootIR.template[ + this.blockFnIR.templateIndex + ] as TemplateFactoryIRNode + templateFactory.template = this.template + return this.blockFnIR.templateIndex + } - ir.template.push({ + // register template + rootIR.template.push({ type: IRNodeTypes.TEMPLATE_FACTORY, - template: ctx.template, + template: this.template, loc: node.loc, }) - return ir.template.length - 1 + return (this.blockFnIR.templateIndex = rootIR.template.length - 1) }, registerOperation(...node) { - operation.push(...node) + this.blockFnIR.operation.push(...node) }, // TODO not used yet helper(name, vapor = true) { @@ -207,6 +242,7 @@ export function transform( source: root.source, loc: root.loc, template: [], + templateIndex: -1, dynamic: { id: null, referenced: true, @@ -221,17 +257,22 @@ export function transform( } const ctx = createRootContext(ir, root, options) - transformNode(ctx) if (ctx.node.type === NodeTypes.ROOT) { - ctx.registerTemplate() + ctx.pushTemplate() } - if (ir.template.length === 0) { - ir.template.push({ - type: IRNodeTypes.FRAGMENT_FACTORY, - loc: root.loc, - }) + transformNode(ctx) + if (ctx.node.type === NodeTypes.ROOT) { + ctx.pushTemplate() } + ir.template.forEach((template, index) => { + if ('template' in template && template.template === '') { + ir.template[index] = { + type: IRNodeTypes.FRAGMENT_FACTORY, + loc: root.loc, + } + } + }) return ir } @@ -361,3 +402,37 @@ function processDynamicChildren(ctx: TransformContext) { } } } + +export function createStructuralDirectiveTransform( + name: string | RegExp, + fn: StructuralDirectiveTransform, +): NodeTransform { + const matches = isString(name) + ? (n: string) => n === name + : (n: string) => name.test(n) + + return (node, context) => { + if (node.type === NodeTypes.ELEMENT) { + const { props } = node + // structural directive transforms are not concerned with slots + // as they are handled separately in vSlot.ts + if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) { + return + } + const exitFns = [] + for (let i = 0; i < props.length; i++) { + const prop = props[i] + if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) { + // structural directives are removed to avoid infinite recursion + // also we remove them *before* applying so that it can further + // traverse itself in case it moves the node around + props.splice(i, 1) + i-- + const onExit = fn(node, prop as VaporDirectiveNode, context) + if (onExit) exitFns.push(onExit) + } + } + return exitFns + } + } +} diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts new file mode 100644 index 000000000..8ef2e4fc7 --- /dev/null +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -0,0 +1,75 @@ +import type { RootNode, TemplateChildNode } from '@vue/compiler-dom' +import { + type TransformContext, + createStructuralDirectiveTransform, +} from '../transform' +import { + type BlockFunctionIRNode, + IRNodeTypes, + type IfIRNode, + type VaporDirectiveNode, +} from '../ir' +import { extend } from '@vue/shared' + +export const transformIf = createStructuralDirectiveTransform( + /^(if|else|else-if)$/, + processIf, +) + +export function processIf( + node: RootNode | TemplateChildNode, + dir: VaporDirectiveNode, + context: TransformContext, +) { + // TODO refactor this + const parentContext = extend({}, context, { + currentScopeIR: context.blockFnIR, + }) + + if (dir.name === 'if') { + const id = context.reference() + context.dynamic.ghost = true + const [branch, onExit] = createIfBranch(node, dir, context) + const operation: IfIRNode = { + type: IRNodeTypes.IF, + id, + loc: dir.loc, + condition: dir.exp!, + truthyBranch: branch, + } + parentContext.registerOperation(operation) + return onExit + } +} + +export function createIfBranch( + node: RootNode | TemplateChildNode, + dir: VaporDirectiveNode, + context: TransformContext, +): [BlockFunctionIRNode, () => void] { + const branch: BlockFunctionIRNode = { + type: IRNodeTypes.BLOCK_FUNCTION, + loc: dir.loc, + source: (node as any)?.source || '', + node: node, + templateIndex: -1, + dynamic: { + id: null, + referenced: false, + ghost: false, + placeholder: null, + children: {}, + }, + effect: [], + operation: [], + } + + const reset = context.replaceBlockFnIR(branch) + context.reference() + context.pushTemplate() + const onExit = () => { + context.pushTemplate() + reset() + } + return [branch, onExit] +} diff --git a/packages/runtime-vapor/src/dom.ts b/packages/runtime-vapor/src/dom.ts index 44540940e..88939edce 100644 --- a/packages/runtime-vapor/src/dom.ts +++ b/packages/runtime-vapor/src/dom.ts @@ -27,7 +27,22 @@ export function prepend(parent: ParentBlock, ...nodes: Node[]) { } } -export function append(parent: ParentBlock, ...nodes: Node[]) { +export function append(parent: ParentBlock, ...blocks: Block[]) { + const nodes: Node[] = [] + + for (const block of blocks) { + if (block instanceof Node) { + nodes.push(block) + } else if (isArray(block)) { + append(parent, ...block) + } else { + append(parent, block.nodes) + block.anchor && append(parent, block.anchor) + } + } + + if (!nodes.length) return + if (parent instanceof Node) { // TODO use insertBefore for better performance parent.append(...nodes)