feat: DirectiveTransform

This commit is contained in:
三咲智子 Kevin Deng 2023-12-03 01:40:26 +08:00
parent 03344eea7e
commit 5a424218f1
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
8 changed files with 138 additions and 79 deletions

View File

@ -16,9 +16,9 @@ PR are welcome! However, please create an issue before you start to work on it,
- [x] simple bindings
- [x] simple events
- [ ] TODO-MVC App
- [ ] transform
- [x] transform
- [x] NodeTransform
- [ ] DirectiveTransform
- [x] DirectiveTransform
- [ ] directives
- [x] `v-once`
- [x] `v-html`

View File

@ -172,8 +172,12 @@ describe('compile', () => {
})
test('no expression', async () => {
const code = await compile(`<div v-text></div>`)
const onError = vi.fn()
const code = await compile(`<div v-text></div>`, { onError })
expect(code).matchSnapshot()
expect(onError.mock.calls).toMatchObject([
[{ code: DOMErrorCodes.X_V_TEXT_NO_EXPRESSION }],
])
})
})

View File

@ -2,18 +2,19 @@ import {
type CodegenResult,
type CompilerOptions as BaseCompilerOptions,
type RootNode,
type DirectiveTransform,
parse,
defaultOnError,
createCompilerError,
ErrorCodes,
} from '@vue/compiler-dom'
import { extend, isString } from '@vue/shared'
import { NodeTransform, transform } from './transform'
import { DirectiveTransform, NodeTransform, transform } from './transform'
import { generate } from './generate'
import { HackOptions } from './hack'
import { transformOnce } from './transforms/vOnce'
import { transformElement } from './transforms/transformElement'
import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText'
export type CompilerOptions = HackOptions<BaseCompilerOptions>
@ -85,5 +86,11 @@ export type TransformPreset = [
export function getBaseTransformPreset(
prefixIdentifiers?: boolean,
): TransformPreset {
return [[transformOnce, transformElement], {}]
return [
[transformOnce, transformElement],
{
html: transformVHtml,
text: transformVText,
},
]
}

View File

@ -1,9 +1,15 @@
import type { Prettify } from '@vue/shared'
import type { NodeTransform } from './transform'
import type { DirectiveTransform, NodeTransform } from './transform'
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> &
Pick<U, Extract<keyof U, keyof T>>
export type HackOptions<T> = Prettify<
Overwrite<T, { nodeTransforms?: NodeTransform[] }>
Overwrite<
T,
{
nodeTransforms?: NodeTransform[]
directiveTransforms?: Record<string, DirectiveTransform | undefined>
}
>
>

View File

@ -10,6 +10,7 @@ import {
NodeTypes,
defaultOnError,
defaultOnWarn,
DirectiveNode,
} from '@vue/compiler-dom'
import { EMPTY_OBJ, NOOP, isArray } from '@vue/shared'
import {
@ -26,6 +27,15 @@ export type NodeTransform = (
context: TransformContext<RootNode | TemplateChildNode>,
) => void | (() => void) | (() => void)[]
export type DirectiveTransform = (
dir: DirectiveNode,
node: ElementNode,
context: TransformContext,
// a platform specific compiler can import the base transform and augment
// it by passing in this optional argument.
// augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult,
) => void
export type TransformOptions = HackOptions<BaseTransformOptions>
export interface TransformContext<T extends AllNode = AllNode> {

View File

@ -5,8 +5,6 @@ import {
NodeTypes,
ErrorCodes,
createCompilerError,
DOMErrorCodes,
createDOMCompilerError,
ElementTypes,
} from '@vue/compiler-dom'
import { isVoidTag } from '@vue/shared'
@ -28,13 +26,18 @@ export const transformElement: NodeTransform = (node, ctx) => {
}
const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
ctx.template += `<${tag}`
props.forEach((prop) =>
transformProp(prop, ctx as TransformContext<ElementNode>),
)
ctx.template += `>`
ctx.template += ctx.childrenTemplate.join('')
if (props.length) {
buildProps(
node,
ctx as TransformContext<ElementNode>,
undefined,
isComponent,
)
}
ctx.template += `>` + ctx.childrenTemplate.join('')
// TODO remove unnecessary close tag, e.g. if it's the last element of the template
if (!isVoidTag(tag)) {
@ -43,22 +46,35 @@ export const transformElement: NodeTransform = (node, ctx) => {
}
}
function transformProp(
node: DirectiveNode | AttributeNode,
ctx: TransformContext<ElementNode>,
): void {
const { name } = node
function buildProps(
node: ElementNode,
context: TransformContext<ElementNode>,
props: ElementNode['props'] = node.props,
isComponent: boolean,
) {
for (const prop of props) {
transformProp(prop, node, context)
}
}
if (node.type === NodeTypes.ATTRIBUTE) {
if (node.value) {
ctx.template += ` ${name}="${node.value.content}"`
} else {
ctx.template += ` ${name}`
}
function transformProp(
prop: DirectiveNode | AttributeNode,
node: ElementNode,
context: TransformContext<ElementNode>,
): void {
const { name } = prop
if (prop.type === NodeTypes.ATTRIBUTE) {
context.template += ` ${name}`
if (prop.value) context.template += `="${prop.value.content}"`
return
}
const { arg, exp, loc, modifiers } = node
const { arg, exp, loc, modifiers } = prop
const directiveTransform = context.options.directiveTransforms[name]
if (directiveTransform) {
directiveTransform(prop, node, context)
}
switch (name) {
case 'bind': {
@ -66,7 +82,7 @@ function transformProp(
!exp ||
(exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
) {
ctx.options.onError(
context.options.onError(
createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc),
)
return
@ -81,13 +97,13 @@ function transformProp(
return
}
ctx.registerEffect(
context.registerEffect(
[exp],
[
{
type: IRNodeTypes.SET_PROP,
loc: node.loc,
element: ctx.reference(),
loc: prop.loc,
element: context.reference(),
name: arg,
value: exp,
},
@ -97,7 +113,7 @@ function transformProp(
}
case 'on': {
if (!exp && !modifiers.length) {
ctx.options.onError(
context.options.onError(
createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc),
)
return
@ -112,13 +128,13 @@ function transformProp(
return
}
ctx.registerEffect(
context.registerEffect(
[exp],
[
{
type: IRNodeTypes.SET_EVENT,
loc: node.loc,
element: ctx.reference(),
loc: prop.loc,
element: context.reference(),
name: arg,
value: exp,
modifiers,
@ -127,49 +143,5 @@ function transformProp(
)
break
}
case 'html': {
if (!exp) {
ctx.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
)
}
if (ctx.node.children.length) {
ctx.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc),
)
ctx.childrenTemplate.length = 0
}
ctx.registerEffect(
[exp],
[
{
type: IRNodeTypes.SET_HTML,
loc: node.loc,
element: ctx.reference(),
value: exp || '""',
},
],
)
break
}
case 'text': {
ctx.registerEffect(
[exp],
[
{
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: ctx.reference(),
value: exp || '""',
},
],
)
break
}
case 'cloak': {
// do nothing
break
}
}
}

View File

@ -0,0 +1,30 @@
import { IRNodeTypes } from '../ir'
import { DirectiveTransform } from '../transform'
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
export const transformVHtml: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (!exp) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
)
}
if (node.children.length) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc),
)
context.childrenTemplate.length = 0
}
context.registerEffect(
[exp],
[
{
type: IRNodeTypes.SET_HTML,
loc: dir.loc,
element: context.reference(),
value: exp || '""',
},
],
)
}

View File

@ -0,0 +1,30 @@
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir'
export const transformVText: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (!exp) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc),
)
}
if (node.children.length) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc),
)
node.children.length = 0
}
context.registerEffect(
[exp],
[
{
type: IRNodeTypes.SET_TEXT,
loc: dir.loc,
element: context.reference(),
value: exp || '""',
},
],
)
}