feat(compiler-vapor): `v-if` (#96)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Rizumu Ayaka 2024-01-28 01:31:20 +08:00 committed by GitHub
parent 2b5e8e4df6
commit ede6c29434
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 448 additions and 82 deletions

View File

@ -44,6 +44,10 @@ PR are welcome!
- #17
- needs #19 first
- [ ] `v-if` / `v-else` / `v-else-if`
- [x] `v-if`
- [ ] `v-else`
- [ ] `v-else-if`
- [ ] with `<template>`
- #9
- [ ] `v-for`
- #21

View File

@ -175,7 +175,6 @@ exports[`compile > dynamic root 1`] = `
export function render(_ctx) {
const t0 = _fragment()
const n0 = t0()
const n1 = _createTextNode(1)
const n2 = _createTextNode(2)
@ -217,7 +216,6 @@ export function render(_ctx) {
exports[`compile > expression parsing > interpolation 1`] = `
"(() => {
const t0 = _fragment()
const n0 = t0()
const n1 = _createTextNode(a + b.value)
_append(n0, n1)

View File

@ -0,0 +1,61 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-if > basic v-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
const { 0: [n3],} = _children(n2)
_renderEffect(() => {
_setText(n3, _ctx.msg)
})
return n2
})
_append(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';
export function render(_ctx) {
const t0 = _template("<div>hello</div>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
})
const n3 = _createIf(() => (_ctx.ok), () => {
const n4 = t0()
return n4
})
_append(n0, n1, n3)
return n0
}"
`;
exports[`compiler: v-if > template v-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>hello<p></p>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
const { 2: [n3],} = _children(n2)
_renderEffect(() => {
_setText(n3, _ctx.msg)
})
return n2
})
_append(n0, n1)
return n0
}"
`;

View File

@ -0,0 +1,52 @@
import { makeCompile } from './_utils'
import {
transformElement,
transformInterpolation,
transformOnce,
transformVIf,
transformVText,
} from '../../src'
const compileWithVIf = makeCompile({
nodeTransforms: [
transformOnce,
transformInterpolation,
transformVIf,
transformElement,
],
directiveTransforms: {
text: transformVText,
},
})
describe('compiler: v-if', () => {
test('basic v-if', () => {
const { code } = compileWithVIf(`<div v-if="ok">{{msg}}</div>`)
expect(code).matchSnapshot()
})
test('template v-if', () => {
const { code } = compileWithVIf(
`<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
)
expect(code).matchSnapshot()
})
test('dedupe same template', () => {
const { code, ir } = compileWithVIf(
`<div v-if="ok">hello</div><div v-if="ok">hello</div>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(2)
})
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')
describe.todo('errors')
describe.todo('codegen')
test.todo('v-on with v-if')
})

View File

@ -24,6 +24,7 @@ import { transformRef } from './transforms/transformRef'
import { transformInterpolation } from './transforms/transformInterpolation'
import type { HackOptions } from './ir'
import { transformVModel } from './transforms/vModel'
import { transformVIf } from './transforms/vIf'
export type CompilerOptions = HackOptions<BaseCompilerOptions>
@ -96,7 +97,13 @@ export function getBaseTransformPreset(
prefixIdentifiers?: boolean,
): TransformPreset {
return [
[transformOnce, transformRef, transformInterpolation, transformElement],
[
transformOnce,
transformRef,
transformInterpolation,
transformVIf,
transformElement,
],
{
bind: transformVBind,
on: transformVOn,

View File

@ -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,49 +273,11 @@ export function generate(
)
} else {
// fragment
pushNewline(
`const t0 = ${vaporHelper('fragment')}()\n`,
NewlineType.End,
)
pushNewline(`const t${i} = ${vaporHelper('fragment')}()`)
}
})
{
pushNewline(`const n${ir.dynamic.id} = t0()`)
const children = genChildren(ir.dynamic.children)
if (children) {
pushNewline(
`const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
)
}
const directiveOps = ir.operation.filter(
(oper): oper is WithDirectiveIRNode =>
oper.type === IRNodeTypes.WITH_DIRECTIVE,
)
for (const directives of groupDirective(directiveOps)) {
genWithDirective(directives, 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()
@ -396,6 +360,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
@ -404,6 +370,45 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
}
}
export function genBlockFunctionContent(
ir: BlockFunctionIRNode | RootIRNode,
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})`,
)
}
const directiveOps = ir.operation.filter(
(oper): oper is WithDirectiveIRNode =>
oper.type === IRNodeTypes.WITH_DIRECTIVE,
)
for (const directives of groupDirective(directiveOps)) {
genWithDirective(directives, 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}`)
}
function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] {
const directiveMap: Record<number, WithDirectiveIRNode[]> = {}
for (const oper of ops) {

View File

@ -0,0 +1,28 @@
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, withIndent } = context
const { condition, positive, negative } = oper
pushNewline(`const n${oper.id} = `)
pushFnCall(
vaporHelper('createIf'),
() => {
push('() => (')
genExpression(condition, context)
push(')')
},
() => genBlockFunction(positive),
!!negative && (() => genBlockFunction(negative!)),
)
function genBlockFunction(oper: BlockFunctionIRNode) {
push('() => {')
withIndent(() => {
genBlockFunctionContent(oper, context)
})
pushNewline('}')
}
}

View File

@ -12,3 +12,4 @@ export { transformVOn } from './transforms/vOn'
export { transformOnce } from './transforms/vOnce'
export { transformVShow } from './transforms/vShow'
export { transformVText } from './transforms/vText'
export { transformVIf } from './transforms/vIf'

View File

@ -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,16 +41,30 @@ export interface BaseIRNode {
// TODO refactor
export type VaporHelper = keyof typeof import('../../runtime-vapor/src')
export interface RootIRNode extends BaseIRNode {
type: IRNodeTypes.ROOT
source: string
node: RootNode
template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
export interface BlockFunctionIRNode extends BaseIRNode {
type: IRNodeTypes.BLOCK_FUNCTION
node: RootNode | TemplateChildNode
templateIndex: number
dynamic: IRDynamicInfo
effect: IREffect[]
operation: OperationNode[]
}
export interface RootIRNode extends Omit<BlockFunctionIRNode, 'type'> {
type: IRNodeTypes.ROOT
node: RootNode
source: string
template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
}
export interface IfIRNode extends BaseIRNode {
type: IRNodeTypes.IF
id: number
condition: IRExpression
positive: BlockFunctionIRNode
negative?: BlockFunctionIRNode
}
export interface TemplateFactoryIRNode extends BaseIRNode {
type: IRNodeTypes.TEMPLATE_FACTORY
template: string
@ -158,6 +176,9 @@ export type OperationNode =
| PrependNodeIRNode
| AppendNodeIRNode
| WithDirectiveIRNode
| IfIRNode
export type BlockIRNode = RootIRNode | BlockFunctionIRNode
export interface IRDynamicInfo {
id: number | null

View File

@ -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,13 @@ import {
type OperationNode,
type RootIRNode,
} from './ir'
import type { HackOptions, VaporDirectiveNode } from './ir'
import type {
BlockIRNode,
FragmentFactoryIRNode,
HackOptions,
TemplateFactoryIRNode,
VaporDirectiveNode,
} from './ir'
export type NodeTransform = (
node: RootNode | TemplateChildNode,
@ -31,6 +39,14 @@ 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> {
@ -38,6 +54,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
parent: TransformContext<ParentNode> | null
root: TransformContext<RootNode>
index: number
block: BlockIRNode
options: Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>
@ -48,6 +65,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
inVOnce: boolean
enterBlock(ir: TransformContext['block']): () => void
reference(): number
increaseId(): number
registerTemplate(): number
@ -84,20 +102,34 @@ const defaultOptions = {
// TODO use class for better perf
function createRootContext(
ir: RootIRNode,
root: RootIRNode,
node: RootNode,
options: TransformOptions = {},
): TransformContext<RootNode> {
let globalId = 0
const { effect, operation: operation } = ir
const ctx: TransformContext<RootNode> = {
node,
parent: null,
index: 0,
root: null!, // set later
block: root,
enterBlock(ir) {
const { block, template, dynamic, childrenTemplate } = this
this.block = ir
this.dynamic = ir.dynamic
this.template = ''
this.childrenTemplate = []
return () => {
// exit
this.block = block
this.template = template
this.dynamic = dynamic
this.childrenTemplate = childrenTemplate
}
},
options: extend({}, defaultOptions, options),
dynamic: ir.dynamic,
dynamic: root.dynamic,
inVOnce: false,
increaseId: () => globalId++,
@ -113,13 +145,13 @@ function createRootContext(
) {
return this.registerOperation(...operations)
}
const existing = effect.find((e) =>
const existing = this.block.effect.find((e) =>
isSameExpression(e.expressions, expressions as IRExpression[]),
)
if (existing) {
existing.operations.push(...operations)
} else {
effect.push({
this.block.effect.push({
expressions: expressions as IRExpression[],
operations,
})
@ -140,24 +172,34 @@ function createRootContext(
template: '',
childrenTemplate: [],
registerTemplate() {
if (!ctx.template) return -1
let templateNode: TemplateFactoryIRNode | FragmentFactoryIRNode
const idx = ir.template.findIndex(
(t) =>
t.type === IRNodeTypes.TEMPLATE_FACTORY &&
t.template === ctx.template,
)
if (idx !== -1) return idx
if (this.template) {
const idx = root.template.findIndex(
(t) =>
t.type === IRNodeTypes.TEMPLATE_FACTORY &&
t.template === this.template,
)
if (idx !== -1) {
return (this.block.templateIndex = idx)
}
ir.template.push({
type: IRNodeTypes.TEMPLATE_FACTORY,
template: ctx.template,
loc: node.loc,
})
return ir.template.length - 1
templateNode = {
type: IRNodeTypes.TEMPLATE_FACTORY,
template: this.template,
loc: node.loc,
}
} else {
templateNode = {
type: IRNodeTypes.FRAGMENT_FACTORY,
loc: node.loc,
}
}
root.template.push(templateNode)
return (this.block.templateIndex = root.template.length - 1)
},
registerOperation(...node) {
operation.push(...node)
this.block.operation.push(...node)
},
}
ctx.root = ctx
@ -199,6 +241,7 @@ export function transform(
source: root.source,
loc: root.loc,
template: [],
templateIndex: -1,
dynamic: {
id: null,
referenced: true,
@ -211,17 +254,9 @@ export function transform(
}
const ctx = createRootContext(ir, root, options)
transformNode(ctx)
if (ctx.node.type === NodeTypes.ROOT) {
ctx.registerTemplate()
}
if (ir.template.length === 0) {
ir.template.push({
type: IRNodeTypes.FRAGMENT_FACTORY,
loc: root.loc,
})
}
transformNode(ctx)
ctx.registerTemplate()
return ir
}
@ -251,7 +286,6 @@ function transformNode(
node = context.node
}
}
switch (node.type) {
case NodeTypes.ROOT:
case NodeTypes.ELEMENT: {
@ -351,3 +385,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
}
}
}

View File

@ -0,0 +1,91 @@
import {
ElementTypes,
NodeTypes,
type RootNode,
type TemplateChildNode,
type TemplateNode,
} 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 transformVIf = 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.block,
})
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!,
positive: branch,
}
parentContext.registerOperation(operation)
return onExit
}
}
export function createIfBranch(
node: RootNode | TemplateChildNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>,
): [BlockFunctionIRNode, () => void] {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType !== ElementTypes.TEMPLATE
) {
node = extend({}, node, {
tagType: ElementTypes.TEMPLATE,
children: [node],
} as TemplateNode)
context.node = node
}
const branch: BlockFunctionIRNode = {
type: IRNodeTypes.BLOCK_FUNCTION,
loc: dir.loc,
node: node,
templateIndex: -1,
dynamic: {
id: null,
referenced: true,
ghost: true,
placeholder: null,
children: {},
},
effect: [],
operation: [],
}
const exitBlock = context.enterBlock(branch)
context.reference()
const onExit = () => {
context.template += context.childrenTemplate.join('')
context.registerTemplate()
exitBlock()
}
return [branch, onExit]
}

View File

@ -18,7 +18,22 @@ export function insert(block: Block, parent: Node, anchor: Node | null = null) {
// }
}
export function prepend(parent: ParentBlock, ...nodes: Node[]) {
export function prepend(parent: ParentBlock, ...blocks: Block[]) {
const nodes: Node[] = []
for (const block of blocks) {
if (block instanceof Node) {
nodes.push(block)
} else if (isArray(block)) {
prepend(parent, ...block)
} else {
prepend(parent, block.nodes)
block.anchor && prepend(parent, block.anchor)
}
}
if (!nodes.length) return
if (parent instanceof Node) {
// TODO use insertBefore for better performance https://jsbench.me/rolpg250hh/1
parent.prepend(...nodes)
@ -27,7 +42,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)