2023-11-24 11:07:31 +08:00
|
|
|
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'
|
2023-11-24 11:07:31 +08:00
|
|
|
import {
|
|
|
|
type DynamicChildren,
|
2023-11-24 15:02:47 +08:00
|
|
|
type OperationNode,
|
2023-11-24 11:07:31 +08:00
|
|
|
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
|
2023-11-24 11:07:31 +08:00
|
|
|
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) {
|
2023-11-24 11:07:31 +08:00
|
|
|
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
|
|
|
|
},
|
2023-11-24 11:07:31 +08:00
|
|
|
registerOpration(...node) {
|
2023-11-24 15:02:47 +08:00
|
|
|
operation.push(...node)
|
2023-11-24 11:07:31 +08:00
|
|
|
},
|
|
|
|
// 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: [],
|
2023-11-24 11:07:31 +08:00
|
|
|
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'
|
|
|
|
}
|
|
|
|
|
2023-11-24 11:07:31 +08:00
|
|
|
ctx.registerOpration(
|
2023-11-23 23:42:08 +08:00
|
|
|
{
|
|
|
|
type: IRNodeTypes.TEXT_NODE,
|
|
|
|
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
|
|
|
}
|