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

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

440 lines
11 KiB
TypeScript
Raw Normal View History

2023-11-29 22:09:46 +08:00
import {
2023-12-01 07:34:18 +08:00
type AllNode,
2024-01-31 17:00:19 +08:00
type AttributeNode,
2023-12-29 22:05:33 +08:00
type TransformOptions as BaseTransformOptions,
2023-12-01 08:05:43 +08:00
type CompilerCompatOptions,
2024-01-31 17:00:19 +08:00
type DirectiveNode,
2023-12-29 22:05:33 +08:00
type ElementNode,
ElementTypes,
NodeTypes,
2023-12-29 22:05:33 +08:00
type RootNode,
type SimpleExpressionNode,
2023-12-29 22:05:33 +08:00
type TemplateChildNode,
2024-01-31 17:00:19 +08:00
type TemplateNode,
createSimpleExpression,
2023-12-01 08:05:43 +08:00
defaultOnError,
defaultOnWarn,
isVSlot,
2023-11-17 03:01:19 +08:00
} from '@vue/compiler-dom'
import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
import {
2024-01-29 22:08:57 +08:00
type BlockIRNode,
DynamicFlag,
type HackOptions,
2023-12-01 23:30:21 +08:00
type IRDynamicInfo,
IRNodeTypes,
2023-12-29 22:05:33 +08:00
type OperationNode,
type RootIRNode,
2024-01-29 22:08:57 +08:00
type VaporDirectiveNode,
} from './ir'
import { isConstantExpression } from './utils'
2023-11-23 23:42:08 +08:00
2023-12-01 07:34:18 +08:00
export type NodeTransform = (
node: RootNode | TemplateChildNode,
2023-12-02 16:59:43 +08:00
context: TransformContext<RootNode | TemplateChildNode>,
2023-12-01 07:34:18 +08:00
) => void | (() => void) | (() => void)[]
2023-12-03 01:40:26 +08:00
export type DirectiveTransform = (
2023-12-06 00:15:57 +08:00
dir: VaporDirectiveNode,
2023-12-03 01:40:26 +08:00
node: ElementNode,
context: TransformContext<ElementNode>,
) => DirectiveTransformResult | void
export interface DirectiveTransformResult {
key: SimpleExpressionNode
value: SimpleExpressionNode
modifier?: '.' | '^'
runtimeCamelize?: boolean
}
2023-12-03 01:40:26 +08:00
// A structural directive transform is technically also a NodeTransform;
// Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = (
node: ElementNode,
dir: VaporDirectiveNode,
context: TransformContext<ElementNode>,
) => void | (() => void)
2023-12-01 07:34:18 +08:00
export type TransformOptions = HackOptions<BaseTransformOptions>
export interface TransformContext<T extends AllNode = AllNode> {
2023-11-23 23:42:08 +08:00
node: T
parent: TransformContext<RootNode | ElementNode> | null
2023-11-23 23:42:08 +08:00
root: TransformContext<RootNode>
index: number
block: BlockIRNode
2023-12-01 07:34:18 +08:00
options: Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>
2023-11-27 05:16:21 +08:00
2023-11-23 23:42:08 +08:00
template: string
childrenTemplate: (string | null)[]
2023-12-01 23:30:21 +08:00
dynamic: IRDynamicInfo
2023-11-27 05:16:21 +08:00
2023-12-01 07:34:18 +08:00
inVOnce: boolean
2023-11-23 23:42:08 +08:00
enterBlock(ir: TransformContext['block']): () => void
2023-11-27 05:16:21 +08:00
reference(): number
increaseId(): number
2023-11-23 23:42:08 +08:00
registerTemplate(): number
2023-12-01 23:30:21 +08:00
registerEffect(
expressions: SimpleExpressionNode[],
2023-12-01 23:30:21 +08:00
operation: OperationNode[],
): void
registerOperation(...operations: OperationNode[]): void
}
const defaultOptions = {
filename: '',
prefixIdentifiers: false,
hoistStatic: false,
hmr: false,
cacheHandlers: false,
nodeTransforms: [],
directiveTransforms: {},
transformHoist: null,
isBuiltInComponent: NOOP,
isCustomElement: NOOP,
expressionPlugins: [],
scopeId: null,
slotted: true,
ssr: false,
inSSR: false,
ssrCssVars: ``,
bindingMetadata: EMPTY_OBJ,
inline: false,
isTS: false,
onError: defaultOnError,
onWarn: defaultOnWarn,
2023-11-23 23:42:08 +08:00
}
2024-01-29 22:08:57 +08:00
export const genDefaultDynamic = (): IRDynamicInfo => ({
id: null,
flags: DynamicFlag.NONE,
anchor: null,
children: [],
2024-01-29 22:08:57 +08:00
})
2023-12-01 07:34:18 +08:00
// TODO use class for better perf
2023-11-23 23:42:08 +08:00
function createRootContext(
root: RootIRNode,
2023-11-23 23:42:08 +08:00
node: RootNode,
2023-12-01 07:34:18 +08:00
options: TransformOptions = {},
2023-11-23 23:42:08 +08:00
): TransformContext<RootNode> {
2023-11-26 03:08:35 +08:00
let globalId = 0
2023-11-23 23:42:08 +08:00
const context: TransformContext<RootNode> = {
2023-11-23 23:42:08 +08:00
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: root.dynamic,
2023-12-01 07:34:18 +08:00
inVOnce: false,
2023-11-23 23:42:08 +08:00
increaseId: () => globalId++,
2023-11-27 05:16:21 +08:00
reference() {
if (this.dynamic.id !== null) return this.dynamic.id
this.dynamic.flags |= DynamicFlag.REFERENCED
return (this.dynamic.id = this.increaseId())
2023-11-26 03:08:35 +08:00
},
2023-12-01 23:30:21 +08:00
registerEffect(expressions, operations) {
expressions = expressions.filter(exp => !isConstantExpression(exp))
if (this.inVOnce || expressions.length === 0) {
2023-12-01 23:30:21 +08:00
return this.registerOperation(...operations)
2023-11-27 05:16:21 +08:00
}
2024-01-29 03:11:30 +08:00
const existing = this.block.effect.find(e =>
isSameExpression(e.expressions, expressions),
2023-12-07 01:09:03 +08:00
)
if (existing) {
existing.operations.push(...operations)
} else {
this.block.effect.push({
expressions,
2023-12-07 01:09:03 +08:00
operations,
})
}
function isSameExpression(
a: SimpleExpressionNode[],
b: SimpleExpressionNode[],
) {
2023-12-07 01:09:03 +08:00
if (a.length !== b.length) return false
return a.every((exp, i) => exp.content === b[i].content)
2023-12-07 01:09:03 +08:00
}
2023-11-23 23:42:08 +08:00
},
template: '',
2023-12-02 16:59:43 +08:00
childrenTemplate: [],
2023-11-23 23:42:08 +08:00
registerTemplate() {
this.template += this.childrenTemplate.filter(Boolean).join('')
if (!this.template) {
return -1
}
2023-11-23 23:42:08 +08:00
const existing = root.template.findIndex(
t => t.template === this.template,
)
if (existing !== -1) {
return (this.block.templateIndex = existing)
}
2023-11-23 23:42:08 +08:00
root.template.push({
type: IRNodeTypes.TEMPLATE_FACTORY,
template: this.template,
})
return (this.block.templateIndex = root.template.length - 1)
2023-11-23 23:42:08 +08:00
},
registerOperation(...node) {
this.block.operation.push(...node)
},
2023-11-23 23:42:08 +08:00
}
context.root = context
context.reference()
return context
2023-11-23 23:42:08 +08:00
}
function createContext<T extends TemplateChildNode>(
node: T,
parent: TransformContext<RootNode | ElementNode>,
2023-11-23 23:42:08 +08:00
index: number,
): TransformContext<T> {
return extend({}, parent, {
2023-11-23 23:42:08 +08:00
node,
parent,
index,
2023-11-27 05:16:21 +08:00
template: '',
2023-12-02 16:59:43 +08:00
childrenTemplate: [],
2024-01-29 22:08:57 +08:00
dynamic: genDefaultDynamic(),
} satisfies Partial<TransformContext<T>>) satisfies TransformContext<T>
2023-11-23 23:42:08 +08:00
}
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,
2023-12-01 23:30:21 +08:00
node: root,
2023-12-01 22:12:19 +08:00
source: root.source,
2023-11-23 23:42:08 +08:00
template: [],
templateIndex: -1,
2024-01-29 22:08:57 +08:00
dynamic: extend(genDefaultDynamic(), {
flags: DynamicFlag.REFERENCED,
2024-01-29 22:08:57 +08:00
} satisfies Partial<IRDynamicInfo>),
2023-12-01 23:30:21 +08:00
effect: [],
2023-11-24 15:02:47 +08:00
operation: [],
2023-11-17 03:01:19 +08:00
}
const context = createRootContext(ir, root, options)
2023-12-02 16:59:43 +08:00
transformNode(context)
2023-11-23 23:42:08 +08:00
return ir
2023-11-17 03:01:19 +08:00
}
2023-12-01 07:34:18 +08:00
function transformNode(
context: TransformContext<RootNode | TemplateChildNode>,
2023-11-24 14:44:57 +08:00
) {
let { node } = context
2023-12-01 07:34:18 +08:00
// apply transform plugins
const { nodeTransforms } = context.options
const exitFns = []
for (const nodeTransform of nodeTransforms) {
2023-12-02 16:59:43 +08:00
const onExit = nodeTransform(node, context)
2023-12-01 07:34:18 +08:00
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
if (!context.node) {
// node was removed
return
} else {
// node may have been replaced
node = context.node
}
}
2024-01-29 22:08:57 +08:00
2023-12-01 07:34:18 +08:00
switch (node.type) {
2023-12-02 16:59:43 +08:00
case NodeTypes.ROOT:
2023-12-01 07:34:18 +08:00
case NodeTypes.ELEMENT: {
2023-12-02 16:59:43 +08:00
transformChildren(context as TransformContext<RootNode | ElementNode>)
2023-12-01 07:34:18 +08:00
break
}
case NodeTypes.COMMENT: {
context.template += `<!--${node.content}-->`
break
}
}
// exit transforms
context.node = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
2023-12-02 16:59:43 +08:00
if (context.node.type === NodeTypes.ROOT) {
context.registerTemplate()
}
2023-12-01 07:34:18 +08:00
}
function transformChildren(context: TransformContext<RootNode | ElementNode>) {
const { children } = context.node
let referencedCount = 0
for (const [i, child] of children.entries()) {
const childContext = createContext(child, context, i)
2023-12-01 07:34:18 +08:00
transformNode(childContext)
context.childrenTemplate.push(childContext.template)
context.dynamic.children[i] = childContext.dynamic
if (childContext.dynamic.flags & DynamicFlag.REFERENCED) {
referencedCount++
}
}
if (referencedCount > 1) {
context.reference()
2023-11-27 14:16:05 +08:00
}
2023-12-02 16:59:43 +08:00
processDynamicChildren(context)
2023-11-17 03:01:19 +08:00
}
function processDynamicChildren(
context: TransformContext<RootNode | ElementNode>,
) {
let prevDynamics: IRDynamicInfo[] = []
let hasStaticTemplate = false
const children = context.dynamic.children
const isFragment = context.block.node === context.node
const allNonTemplate = children.every(
child => child.flags & DynamicFlag.NON_TEMPLATE,
)
// all non-template: don't gen fragment but return array directly
if (isFragment && allNonTemplate) {
context.block.returns = children
.filter(child => child.flags & DynamicFlag.INSERT)
.map(child => child.id!)
return
}
2023-12-02 16:59:43 +08:00
// mixed: insert with anchor
for (const [index, child] of children.entries()) {
if (child.flags & DynamicFlag.INSERT) {
prevDynamics.push(child)
}
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
if (prevDynamics.length) {
if (hasStaticTemplate) {
context.childrenTemplate[index - prevDynamics.length] = `<!>`
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
const anchor = (prevDynamics[0].anchor = context.increaseId())
2023-11-23 23:42:08 +08:00
context.registerOperation({
2023-12-02 16:59:43 +08:00
type: IRNodeTypes.INSERT_NODE,
element: prevDynamics.map(child => child.id!),
parent: context.reference(),
2023-12-02 16:59:43 +08:00
anchor,
})
} else {
context.registerOperation({
2023-12-02 16:59:43 +08:00
type: IRNodeTypes.PREPEND_NODE,
elements: prevDynamics.map(child => child.id!),
parent: context.reference(),
2023-12-02 16:59:43 +08:00
})
}
prevDynamics = []
2023-12-02 16:59:43 +08:00
}
hasStaticTemplate = true
2023-12-02 16:59:43 +08:00
}
}
2023-11-23 23:42:08 +08:00
if (prevDynamics.length) {
context.registerOperation({
type: IRNodeTypes.APPEND_NODE,
elements: prevDynamics.map(child => child.id!),
parent: context.reference(),
})
2023-11-24 14:59:10 +08:00
}
2023-11-23 23:42:08 +08:00
}
export function createStructuralDirectiveTransform(
name: string | string[],
fn: StructuralDirectiveTransform,
): NodeTransform {
const matches = (n: string) =>
isString(name) ? n === name : name.includes(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 = []
2024-01-28 03:28:27 +08:00
for (const prop of props) {
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
const onExit = fn(
node,
prop as VaporDirectiveNode,
context as TransformContext<ElementNode>,
)
if (onExit) exitFns.push(onExit)
}
}
return exitFns
}
}
}
2024-01-31 17:00:19 +08:00
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
if (node.tagType === ElementTypes.TEMPLATE) {
return node
}
const reserved: Array<AttributeNode | DirectiveNode> = []
const pass: Array<AttributeNode | DirectiveNode> = []
node.props.forEach(prop => {
if (prop.type === NodeTypes.DIRECTIVE && dirs.includes(prop.name)) {
reserved.push(prop)
} else {
pass.push(prop)
}
})
return extend({}, node, {
type: NodeTypes.ELEMENT,
tag: 'template',
props: reserved,
tagType: ElementTypes.TEMPLATE,
children: [extend({}, node, { props: pass } as TemplateChildNode)],
} as Partial<TemplateNode>)
}
export const EMPTY_EXPRESSION = createSimpleExpression('', true)