- #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
export function render(_ctx) {
const t0 = _fragment()
-
const n0 = t0()
const n1 = _createTextNode(1)
const n2 = _createTextNode(2)
exports[`compile > expression parsing > interpolation 1`] = `
"(() => {
const t0 = _fragment()
-
const n0 = t0()
const n1 = _createTextNode(a + b.value)
_append(n0, n1)
--- /dev/null
+// 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
+}"
+`;
--- /dev/null
+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')
+})
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>
prefixIdentifiers?: boolean,
): TransformPreset {
return [
- [transformOnce, transformRef, transformInterpolation, transformElement],
+ [
+ transformOnce,
+ transformRef,
+ transformInterpolation,
+ transformVIf,
+ transformElement,
+ ],
{
bind: transformVBind,
on: transformVOn,
locStub,
} from '@vue/compiler-dom'
import {
+ type BlockFunctionIRNode,
type IRDynamicChildren,
IRNodeTypes,
type OperationNode,
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[]
)
} 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()
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
}
}
+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) {
--- /dev/null
+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('}')
+ }
+}
export { transformOnce } from './transforms/vOnce'
export { transformVShow } from './transforms/vShow'
export { transformVText } from './transforms/vText'
+export { transformVIf } from './transforms/vIf'
RootNode,
SimpleExpressionNode,
SourceLocation,
+ TemplateChildNode,
} from '@vue/compiler-dom'
import type { Prettify } from '@vue/shared'
import type { DirectiveTransform, NodeTransform } from './transform'
CREATE_TEXT_NODE,
WITH_DIRECTIVE,
+
+ IF,
+ BLOCK_FUNCTION,
}
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
| PrependNodeIRNode
| AppendNodeIRNode
| WithDirectiveIRNode
+ | IfIRNode
+
+export type BlockIRNode = RootIRNode | BlockFunctionIRNode
export interface IRDynamicInfo {
id: number | null
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,
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,
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> {
parent: TransformContext<ParentNode> | null
root: TransformContext<RootNode>
index: number
+ block: BlockIRNode
options: Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>
inVOnce: boolean
+ enterBlock(ir: TransformContext['block']): () => void
reference(): number
increaseId(): number
registerTemplate(): number
// 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++,
) {
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,
})
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
source: root.source,
loc: root.loc,
template: [],
+ templateIndex: -1,
dynamic: {
id: null,
referenced: true,
}
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
}
node = context.node
}
}
-
switch (node.type) {
case NodeTypes.ROOT:
case NodeTypes.ELEMENT: {
}
}
}
+
+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
+ }
+ }
+}
--- /dev/null
+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]
+}
// }
}
-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)
}
}
-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)