vue3-core/packages/compiler-vapor/src/transform.ts

418 lines
9.3 KiB
TypeScript
Raw Normal View History

import type {
NodeTypes,
2023-11-17 03:01:19 +08:00
RootNode,
2023-11-23 23:42:08 +08:00
Node,
2023-11-17 03:01:19 +08:00
TemplateChildNode,
ElementNode,
AttributeNode,
2023-11-20 14:16:36 +08:00
InterpolationNode,
2023-11-23 23:42:08 +08:00
TransformOptions,
DirectiveNode,
2023-11-24 15:25:34 +08:00
ExpressionNode,
2023-11-17 03:01:19 +08:00
} from '@vue/compiler-dom'
import {
type DynamicChildren,
2023-11-24 15:02:47 +08:00
type OperationNode,
type RootIRNode,
IRNodeTypes,
} from './ir'
2023-11-24 14:59:10 +08:00
import { isVoidTag } from '@vue/shared'
2023-11-23 23:42:08 +08:00
export interface TransformContext<T extends Node = Node> {
node: T
parent: TransformContext | null
root: TransformContext<RootNode>
index: number
options: TransformOptions
template: string
children: DynamicChildren
store: boolean
ghost: boolean
2023-11-24 15:25:34 +08:00
once: boolean
2023-11-26 03:08:35 +08:00
id: number | null
2023-11-23 23:42:08 +08:00
2023-11-26 03:08:35 +08:00
getId(): number
incraseId(): number
2023-11-23 23:42:08 +08:00
registerTemplate(): number
2023-11-24 15:25:34 +08:00
registerEffect(expr: string, operation: OperationNode): void
2023-11-24 15:02:47 +08:00
registerOpration(...oprations: OperationNode[]): void
helper(name: string): string
2023-11-23 23:42:08 +08:00
}
function createRootContext(
ir: RootIRNode,
node: RootNode,
options: TransformOptions,
): TransformContext<RootNode> {
2023-11-26 03:08:35 +08:00
let globalId = 0
2023-11-24 15:02:47 +08:00
const { effect, operation: operation, helpers, vaporHelpers } = ir
2023-11-23 23:42:08 +08:00
const ctx: TransformContext<RootNode> = {
node,
parent: null,
index: 0,
root: undefined as any, // set later
options,
children: {},
store: false,
ghost: false,
2023-11-24 15:25:34 +08:00
once: false,
2023-11-23 23:42:08 +08:00
2023-11-26 03:08:35 +08:00
id: null,
incraseId: () => globalId++,
getId() {
if (this.id !== null) return this.id
return (this.id = this.incraseId())
},
2023-11-24 15:25:34 +08:00
registerEffect(expr, operation) {
if (!effect[expr]) effect[expr] = []
2023-11-24 15:25:34 +08:00
effect[expr].push(operation)
2023-11-23 23:42:08 +08:00
},
template: '',
registerTemplate() {
if (!ctx.template) return -1
const idx = ir.template.findIndex((t) => t.template === ctx.template)
if (idx !== -1) return idx
ir.template.push({
type: IRNodeTypes.TEMPLATE_GENERATOR,
template: ctx.template,
loc: node.loc,
})
return ir.template.length - 1
},
registerOpration(...node) {
2023-11-24 15:02:47 +08:00
operation.push(...node)
},
// TODO not used yet
helper(name, vapor = true) {
;(vapor ? vaporHelpers : helpers).add(name)
return name
},
2023-11-23 23:42:08 +08:00
}
ctx.root = ctx
return ctx
}
function createContext<T extends TemplateChildNode>(
node: T,
parent: TransformContext,
index: number,
): TransformContext<T> {
const children = {}
const ctx: TransformContext<T> = {
...parent,
2023-11-26 03:08:35 +08:00
id: null,
2023-11-23 23:42:08 +08:00
node,
parent,
index,
get template() {
return parent.template
},
set template(t) {
parent.template = t
},
children,
store: false,
2023-11-24 15:25:34 +08:00
registerEffect(expr, operation) {
if (ctx.once) {
return ctx.registerOpration(operation)
}
2023-11-24 20:29:05 +08:00
return parent.registerEffect(expr, operation)
2023-11-24 15:25:34 +08:00
},
2023-11-23 23:42:08 +08:00
}
return ctx
}
2023-11-17 03:01:19 +08:00
// AST -> IR
export function transform(
root: RootNode,
2023-11-23 23:42:08 +08:00
options: TransformOptions = {},
2023-11-17 03:01:19 +08:00
): RootIRNode {
2023-11-23 23:42:08 +08:00
const ir: RootIRNode = {
2023-11-17 03:01:19 +08:00
type: IRNodeTypes.ROOT,
loc: root.loc,
2023-11-23 23:42:08 +08:00
template: [],
2023-11-26 02:13:59 +08:00
children: {} as any,
2023-11-23 23:42:08 +08:00
effect: Object.create(null),
2023-11-24 15:02:47 +08:00
operation: [],
helpers: new Set([]),
vaporHelpers: new Set([]),
2023-11-17 03:01:19 +08:00
}
2023-11-23 23:42:08 +08:00
const ctx = createRootContext(ir, root, options)
2023-11-26 03:08:35 +08:00
const rootId = ctx.getId()
2023-11-24 14:44:57 +08:00
// TODO: transform presets, see packages/compiler-core/src/transforms
transformChildren(ctx, true)
2023-11-26 02:13:59 +08:00
ir.children = {
id: rootId,
2023-11-26 03:08:35 +08:00
store: true,
ghost: false,
2023-11-26 02:13:59 +08:00
children: ctx.children,
}
2023-11-23 23:42:08 +08:00
return ir
2023-11-17 03:01:19 +08:00
}
2023-11-24 14:44:57 +08:00
function transformChildren(
ctx: TransformContext<RootNode | ElementNode>,
root?: boolean,
) {
2023-11-23 23:42:08 +08:00
const {
node: { children },
} = ctx
let index = 0
children.forEach((child, i) => walkNode(child, i))
2023-11-24 14:44:57 +08:00
if (root) ctx.registerTemplate()
2023-11-23 23:42:08 +08:00
function walkNode(node: TemplateChildNode, i: number) {
const child = createContext(node, ctx, index)
const isFirst = i === 0
const isLast = i === children.length - 1
2023-11-17 03:01:19 +08:00
switch (node.type) {
case 1 satisfies NodeTypes.ELEMENT: {
2023-11-23 23:42:08 +08:00
transformElement(child as TransformContext<ElementNode>)
2023-11-17 03:01:19 +08:00
break
}
2023-11-23 23:42:08 +08:00
case 2 satisfies NodeTypes.TEXT: {
ctx.template += node.content
2023-11-17 03:01:19 +08:00
break
2023-11-23 23:42:08 +08:00
}
case 3 satisfies NodeTypes.COMMENT: {
ctx.template += `<!--${node.content}-->`
2023-11-17 17:35:49 +08:00
break
2023-11-23 23:42:08 +08:00
}
case 5 satisfies NodeTypes.INTERPOLATION: {
transformInterpolation(
child as TransformContext<InterpolationNode>,
isFirst,
isLast,
)
2023-11-17 03:01:19 +08:00
break
2023-11-23 23:42:08 +08:00
}
2023-11-24 11:39:49 +08:00
case 12 satisfies NodeTypes.TEXT_CALL:
// never?
break
2023-11-23 23:42:08 +08:00
default: {
2023-11-24 11:39:49 +08:00
// TODO handle other types
// CompoundExpressionNode
// IfNode
// IfBranchNode
// ForNode
2023-11-23 23:42:08 +08:00
ctx.template += `[type: ${node.type}]`
}
2023-11-17 03:01:19 +08:00
}
2023-11-23 23:42:08 +08:00
if (Object.keys(child.children).length > 0 || child.store)
ctx.children[index] = {
2023-11-26 03:08:35 +08:00
id: child.store ? child.getId() : null,
2023-11-23 23:42:08 +08:00
store: child.store,
children: child.children,
2023-11-26 03:08:35 +08:00
ghost: child.ghost,
2023-11-23 23:42:08 +08:00
}
if (!child.ghost) index++
2023-11-17 03:01:19 +08:00
}
}
2023-11-23 23:42:08 +08:00
function transformElement(ctx: TransformContext<ElementNode>) {
const { node } = ctx
const { tag, props, children } = node
ctx.template += `<${tag}`
props.forEach((prop) => transformProp(prop, ctx))
2023-11-24 14:59:10 +08:00
ctx.template += `>`
2023-11-23 23:42:08 +08:00
2023-11-24 11:39:49 +08:00
if (children.length) transformChildren(ctx)
2023-11-24 14:59:10 +08:00
// TODO remove unnecessary close tag, e.g. if it's the last element of the template
if (!node.isSelfClosing || !isVoidTag(tag)) {
ctx.template += `</${tag}>`
}
2023-11-23 23:42:08 +08:00
}
function transformInterpolation(
ctx: TransformContext<InterpolationNode>,
isFirst: boolean,
isLast: boolean,
) {
const { node } = ctx
2023-11-17 03:01:19 +08:00
if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) {
2023-11-24 15:25:34 +08:00
const expr = processExpression(ctx, node.content)!
2023-11-23 23:42:08 +08:00
const parent = ctx.parent!
2023-11-26 03:08:35 +08:00
const parentId = parent.getId()
2023-11-23 23:42:08 +08:00
parent.store = true
if (isFirst && isLast) {
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: parentId,
2023-11-24 15:25:34 +08:00
value: expr,
2023-11-23 23:42:08 +08:00
})
} else {
let id: number
let anchor: number | 'first' | 'last'
if (!isFirst && !isLast) {
2023-11-26 03:08:35 +08:00
id = ctx.incraseId()
anchor = ctx.getId()
2023-11-23 23:42:08 +08:00
ctx.template += '<!>'
ctx.store = true
} else {
2023-11-26 03:08:35 +08:00
id = ctx.getId()
2023-11-23 23:42:08 +08:00
ctx.ghost = true
anchor = isFirst ? 'first' : 'last'
}
ctx.registerOpration(
2023-11-23 23:42:08 +08:00
{
type: IRNodeTypes.CREATE_TEXT_NODE,
2023-11-23 23:42:08 +08:00
loc: node.loc,
id,
2023-11-24 15:25:34 +08:00
value: expr,
2023-11-23 23:42:08 +08:00
},
{
type: IRNodeTypes.INSERT_NODE,
loc: node.loc,
element: id,
parent: parentId,
anchor,
},
)
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: id,
2023-11-24 15:25:34 +08:00
value: expr,
2023-11-23 23:42:08 +08:00
})
}
2023-11-24 11:39:49 +08:00
return
2023-11-17 03:01:19 +08:00
}
2023-11-24 11:39:49 +08:00
// TODO: CompoundExpressionNode: {{ count + 1 }}
2023-11-17 03:01:19 +08:00
}
2023-11-23 23:42:08 +08:00
function transformProp(
node: DirectiveNode | AttributeNode,
ctx: TransformContext<ElementNode>,
): void {
const { name } = node
2023-11-17 03:01:19 +08:00
2023-11-23 23:42:08 +08:00
if (node.type === (6 satisfies NodeTypes.ATTRIBUTE)) {
if (node.value) {
ctx.template += ` ${name}="${node.value.content}"`
} else {
ctx.template += ` ${name}`
}
return
2023-11-17 03:01:19 +08:00
}
2023-11-23 23:42:08 +08:00
ctx.store = true
2023-11-24 15:25:34 +08:00
const expr = processExpression(ctx, node.exp)
2023-11-24 14:44:57 +08:00
switch (name) {
case 'bind': {
2023-11-24 15:25:34 +08:00
if (expr === null) {
// TODO: Vue 3.4 supported shorthand syntax
// https://github.com/vuejs/core/pull/9451
return
} else if (!node.arg) {
2023-11-24 14:44:57 +08:00
// TODO support v-bind="{}"
return
} else if (
node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)
) {
// TODO support :[foo]="bar"
return
}
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_PROP,
loc: node.loc,
2023-11-26 03:08:35 +08:00
element: ctx.getId(),
2023-11-24 14:44:57 +08:00
name: node.arg.content,
2023-11-24 15:25:34 +08:00
value: expr,
2023-11-24 14:44:57 +08:00
})
break
}
case 'on': {
if (!node.arg) {
// TODO support v-on="{}"
return
} else if (
node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)
) {
// TODO support @[foo]="bar"
return
2023-11-24 15:25:34 +08:00
} else if (expr === null) {
// TODO: support @foo
// https://github.com/vuejs/core/pull/9451
return
2023-11-24 14:44:57 +08:00
}
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_EVENT,
loc: node.loc,
2023-11-26 03:08:35 +08:00
element: ctx.getId(),
2023-11-24 14:44:57 +08:00
name: node.arg.content,
2023-11-24 15:25:34 +08:00
value: expr,
2023-11-24 14:44:57 +08:00
})
break
}
2023-11-24 15:25:34 +08:00
case 'html': {
const value = expr || '""'
ctx.registerEffect(value, {
2023-11-24 14:44:57 +08:00
type: IRNodeTypes.SET_HTML,
loc: node.loc,
2023-11-26 03:08:35 +08:00
element: ctx.getId(),
2023-11-24 15:25:34 +08:00
value,
2023-11-24 14:44:57 +08:00
})
break
2023-11-24 15:25:34 +08:00
}
case 'text': {
const value = expr || '""'
ctx.registerEffect(value, {
2023-11-24 14:48:51 +08:00
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
2023-11-26 03:08:35 +08:00
element: ctx.getId(),
2023-11-24 15:25:34 +08:00
value,
2023-11-24 14:48:51 +08:00
})
break
2023-11-24 15:25:34 +08:00
}
case 'once': {
ctx.once = true
break
}
2023-11-24 15:40:38 +08:00
case 'cloak': {
// do nothing
break
}
2023-11-23 23:42:08 +08:00
}
2023-11-17 03:01:19 +08:00
}
2023-11-24 11:39:49 +08:00
// TODO: reuse packages/compiler-core/src/transforms/transformExpression.ts
2023-11-24 15:25:34 +08:00
function processExpression(
ctx: TransformContext,
expr: ExpressionNode | undefined,
): string | null {
if (!expr) return null
if (expr.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
// TODO
return ''
}
const { content } = expr
if (ctx.options.bindingMetadata?.[content] === 'setup-ref') {
return content + '.value'
2023-11-23 23:42:08 +08:00
}
2023-11-24 15:25:34 +08:00
return content
2023-11-17 03:01:19 +08:00
}