Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(compiler): v-if
Browse files Browse the repository at this point in the history
LittleSound authored and sxzz committed Jan 21, 2024
1 parent 32604cf commit 08fc9e9
Showing 7 changed files with 300 additions and 69 deletions.
9 changes: 8 additions & 1 deletion packages/compiler-vapor/src/compile.ts
Original file line number Diff line number Diff line change
@@ -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<BaseCompilerOptions>

@@ -97,7 +98,13 @@ export function getBaseTransformPreset(
prefixIdentifiers?: boolean,
): TransformPreset {
return [
[transformOnce, transformRef, transformInterpolation, transformElement],
[
transformOnce,
transformRef,
transformInterpolation,
transformIf,
transformElement,
],
{
bind: transformVBind,
on: transformVOn,
78 changes: 43 additions & 35 deletions packages/compiler-vapor/src/generate.ts
Original file line number Diff line number Diff line change
@@ -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,10 +354,50 @@ 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
default:
return checkNever(oper)
}
}

export function genBlockFunctionContent(
ir: Omit<BlockFunctionIRNode, 'type'>,
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}`)
}
32 changes: 32 additions & 0 deletions packages/compiler-vapor/src/generators/if.ts
Original file line number Diff line number Diff line change
@@ -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<BlockFunctionIRNode, 'type'>,
context: CodegenContext,
) {
const { push, pushNewline, withIndent } = context
push('() => {')
withIndent(() => {
genBlockFunctionContent(oper, context)
})
pushNewline('}')
}
27 changes: 23 additions & 4 deletions packages/compiler-vapor/src/ir.ts
Original file line number Diff line number Diff line change
@@ -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<TemplateFactoryIRNode | FragmentFactoryIRNode>
node: RootNode | TemplateChildNode
templateIndex: number
dynamic: IRDynamicInfo
effect: IREffect[]
operation: OperationNode[]
}

export interface RootIRNode extends Omit<BlockFunctionIRNode, 'type'> {
type: IRNodeTypes.ROOT
node: RootNode
template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
helpers: Set<string>
vaporHelpers: Set<VaporHelper>
}

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
131 changes: 103 additions & 28 deletions packages/compiler-vapor/src/transform.ts
Original file line number Diff line number Diff line change
@@ -3,22 +3,29 @@ 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,
IRNodeTypes,
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,13 +38,22 @@ export type DirectiveTransform = (
context: TransformContext<ElementNode>,
) => 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<RootNode | TemplateChildNode>,
) => void | (() => void)

export type TransformOptions = HackOptions<BaseTransformOptions>

export interface TransformContext<T extends AllNode = AllNode> {
node: T
parent: TransformContext<ParentNode> | null
root: TransformContext<RootNode>
index: number
blockFnIR: Omit<BlockFunctionIRNode, 'type'>
options: Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>
@@ -48,9 +64,10 @@ export interface TransformContext<T extends AllNode = AllNode> {

inVOnce: boolean

replaceBlockFnIR(ir: TransformContext['blockFnIR']): () => void
reference(): number
increaseId(): number
registerTemplate(): number
pushTemplate(): number
registerEffect(
expressions: Array<IRExpression | null | undefined>,
operation: OperationNode[],
@@ -61,18 +78,35 @@ export interface TransformContext<T extends AllNode = AllNode> {

// TODO use class for better perf
function createRootContext(
ir: RootIRNode,
rootIR: RootIRNode,
node: RootNode,
options: TransformOptions = {},
): TransformContext<RootNode> {
let globalId = 0
const { effect, operation: operation, helpers, vaporHelpers } = ir
const { helpers, vaporHelpers } = rootIR

const ctx: TransformContext<RootNode> = {
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<RootNode | ElementNode>) {
}
}
}

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
}
}
}
75 changes: 75 additions & 0 deletions packages/compiler-vapor/src/transforms/vIf.ts
Original file line number Diff line number Diff line change
@@ -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<RootNode | TemplateChildNode>,
) {
// 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<RootNode | TemplateChildNode>,
): [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]
}
17 changes: 16 additions & 1 deletion packages/runtime-vapor/src/dom.ts
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 08fc9e9

Please sign in to comment.