mirror of https://github.com/vuejs/core.git
feat: DirectiveTransform
This commit is contained in:
parent
03344eea7e
commit
5a424218f1
|
@ -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`
|
||||
|
|
|
@ -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 }],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
>
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 || '""',
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
|
@ -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 || '""',
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue