Skip to content

Commit

Permalink
feat(compiler): support v-on
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiyuanzmj committed Jun 28, 2024
1 parent b953ec8 commit 0c2adfe
Show file tree
Hide file tree
Showing 15 changed files with 900 additions and 70 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"@vue-macros/common": "^1.10.4",
"@vue-vapor/compiler-vapor": "3.2024.0-63dbc26",
"@vue-vapor/vue": "3.2024.0-63dbc26",
"@vue/shared": "^3.4.31",
"html-tags": "^4.0.0",
"svg-tags": "^1.0.0",
"unplugin": "^1.10.1"
Expand Down
23 changes: 17 additions & 6 deletions playground/App.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
<script lang="tsx">
import { defineComponent, ref } from 'vue'
import Count2 from './Count.vue'
export default defineComponent({
setup() {
const count = ref(0)
setInterval(() => {
count.value++
}, 1000)
const count = ref(1)
const Count = () => <div>{count.value * 2}</div>
const Count = (props) => {
return <div>{props.value}</div>
}
const Count1 = ({ value }) => {
return <div>{value}</div>
}
return (
<>
<Count />
<input
value_prop={count.value}
onInput={(e) => (count.value = e.target.value)}
/>
<Count value={count.value} />
<Count1 value={count.value} />
<Count2 value={count.value} />
</>
)
},
Expand Down
13 changes: 12 additions & 1 deletion playground/Count.vue
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
<template>Count</template>
<script lang="tsx">
import { defineComponent, ref } from 'vue'

Check failure on line 2 in playground/Count.vue

View workflow job for this annotation

GitHub Actions / lint

'ref' is defined but never used
export default defineComponent({
props: {
value: String,
},
setup(props) {
return <div>{props.value}</div>
},
})
</script>
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/core/compiler/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from './ir'
import { transformText } from './transforms/transformText'
import { transformVBind } from './transforms/vBind'
import { transformVOn } from './transforms/vOn'
import type { JSXElement, JSXFragment, Program } from '@babel/types'

export interface VaporCodegenResult
Expand All @@ -50,8 +51,7 @@ export function compile(
}
}

const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
const prefixIdentifiers = !__BROWSER__ && options.prefixIdentifiers === true

if (options.scopeId && !isModuleMode) {
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
Expand Down Expand Up @@ -126,6 +126,7 @@ export function getBaseTransformPreset(
[transformText, transformElement, transformChildren],
{
bind: transformVBind,
on: transformVOn,
},
]
}
1 change: 1 addition & 0 deletions src/core/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export { transformText } from './transforms/transformText'
export { transformElement } from './transforms/transformElement'
export { transformChildren } from './transforms/transformChildren'
export { transformVBind } from './transforms/vBind'
export { transformVOn } from './transforms/vOn'
4 changes: 2 additions & 2 deletions src/core/compiler/transforms/transformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
type IRPropsDynamicAttribute,
type IRPropsStatic,
} from '../ir'
import { isComponent as _isComponent, resolveSimpleExpression } from '../utils'
import { isComponentNode, resolveSimpleExpression } from '../utils'
import { EMPTY_EXPRESSION } from './utils'
import type { SimpleExpressionNode } from '@vue/compiler-dom'
import type { JSXAttribute, JSXElement, JSXSpreadAttribute } from '@babel/types'
Expand Down Expand Up @@ -44,7 +44,7 @@ export const transformElement: NodeTransform = (node, context) => {
openingElement: { name },
} = node
const tag = name.type === 'JSXIdentifier' ? name.name : ''
const isComponent = _isComponent(node.openingElement.name)
const isComponent = isComponentNode(node)
const propsResult = buildProps(
node,
context as TransformContext<JSXElement>,
Expand Down
4 changes: 2 additions & 2 deletions src/core/compiler/transforms/transformText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '../ir'
import {
getLiteralExpressionValue,
isComponent,
isComponentNode,
isConstantExpression,
resolveExpression,
} from '../utils'
Expand All @@ -33,7 +33,7 @@ export const transformText: NodeTransform = (node, context) => {

if (
node.type === 'JSXElement' &&
!isComponent(node.openingElement) &&
!isComponentNode(node) &&
isAllTextLike(node.children)
) {
processTextLikeContainer(
Expand Down
38 changes: 6 additions & 32 deletions src/core/compiler/transforms/vBind.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,16 @@
import {
ErrorCodes,
type SimpleExpressionNode,
createCompilerError,
} from '@vue/compiler-dom'
import { camelize, extend } from '@vue/shared'
import {
resolveExpression,
resolveLocation,
resolveSimpleExpression,
} from '../utils'
import { resolveExpression, resolveSimpleExpression } from '../utils'
import { isReservedProp } from './transformElement'
import type { DirectiveTransform } from '../transform'

const __BROWSER__ = false
export const transformVBind: DirectiveTransform = (dir, node, context) => {
const { loc } = dir
if (!loc || dir.name.type === 'JSXNamespacedName') return
const { name, value, loc } = dir
if (!loc || name.type === 'JSXNamespacedName') return

const [name, ...modifiers] = dir.name.name.split('_')
const [nameString, ...modifiers] = name.name.split('_')

let exp: SimpleExpressionNode
if (!name.trim() && loc) {
if (!__BROWSER__) {
// #10280 only error against empty expression in non-browser build
// because :foo in in-DOM templates will be parsed into :foo="" by the
// browser
context.options.onError(
createCompilerError(
ErrorCodes.X_V_BIND_NO_EXPRESSION,
resolveLocation(loc, name),
),
)
}
exp = resolveSimpleExpression('', true, loc)
}

exp = resolveExpression(dir.value, context)
let arg = resolveExpression(dir.name, context)
const exp = resolveExpression(value, context)
let arg = resolveSimpleExpression(nameString, true, dir.name.loc)

if (arg.isStatic && isReservedProp(arg.content)) return

Expand Down
102 changes: 102 additions & 0 deletions src/core/compiler/transforms/vOn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
ErrorCodes,
createCompilerError,
resolveModifiers,
} from '@vue/compiler-dom'
import { extend, makeMap } from '@vue/shared'
import { IRNodeTypes, type KeyOverride, type SetEventIRNode } from '../ir'
import {
isComponentNode,
resolveExpression,
resolveLocation,
resolveSimpleExpression,
} from '../utils'
import { EMPTY_EXPRESSION } from './utils'
import type { DirectiveTransform } from '../transform'

const delegatedEvents = /*#__PURE__*/ makeMap(
'beforeinput,click,dblclick,contextmenu,focusin,focusout,input,keydown,' +
'keyup,mousedown,mousemove,mouseout,mouseover,mouseup,pointerdown,' +
'pointermove,pointerout,pointerover,pointerup,touchend,touchmove,' +
'touchstart',
)

export const transformVOn: DirectiveTransform = (dir, node, context) => {
const { name, loc, value } = dir
if (name.type === 'JSXNamespacedName') return
const isComponent = isComponentNode(node)

const [nameString, ...modifiers] = name.name
.replace(/^on([A-Z])/, (_, $1) => $1.toLowerCase())
.split('_')

if (!value && !modifiers.length) {
context.options.onError(
createCompilerError(
ErrorCodes.X_V_ON_NO_EXPRESSION,
resolveLocation(loc, context),
),
)
}

let arg = resolveSimpleExpression(nameString, true, dir.name.loc)
const exp = resolveExpression(dir.value, context)

const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
resolveModifiers(
arg.isStatic ? `on${nameString}` : arg,
modifiers,
null,
resolveLocation(loc, context),
)

let keyOverride: KeyOverride | undefined
const isStaticClick = arg.isStatic && arg.content.toLowerCase() === 'click'
const delegate =
arg.isStatic && !eventOptionModifiers.length && delegatedEvents(arg.content)

// normalize click.right and click.middle since they don't actually fire
if (nonKeyModifiers.includes('middle')) {
if (keyOverride) {
// TODO error here
}
if (isStaticClick) {
arg = extend({}, arg, { content: 'mouseup' })
} else if (!arg.isStatic) {
keyOverride = ['click', 'mouseup']
}
}
if (nonKeyModifiers.includes('right')) {
if (isStaticClick) {
arg = extend({}, arg, { content: 'contextmenu' })
} else if (!arg.isStatic) {
keyOverride = ['click', 'contextmenu']
}
}

if (isComponent) {
const handler = exp || EMPTY_EXPRESSION
return {
key: arg,
value: handler,
handler: true,
}
}

const operation: SetEventIRNode = {
type: IRNodeTypes.SET_EVENT,
element: context.reference(),
key: arg,
value: exp,
modifiers: {
keys: keyModifiers,
nonKeys: nonKeyModifiers,
options: eventOptionModifiers,
},
keyOverride,
delegate,
effect: !arg.isStatic,
}

context.registerEffect([arg], operation)
}
44 changes: 26 additions & 18 deletions src/core/compiler/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isGloballyAllowed } from '@vue/shared'
import { isGloballyAllowed, isString } from '@vue/shared'
import {
type AttributeNode,
type ElementNode,
Expand All @@ -19,9 +19,9 @@ import type {
BigIntLiteral,
Expression,
JSXAttribute,
JSXElement,
JSXIdentifier,
JSXText,
Node,
NumericLiteral,
SourceLocation,
StringLiteral,
Expand Down Expand Up @@ -128,31 +128,39 @@ export function resolveSimpleExpression(
return result
}

export function resolveLocation(location?: SourceLocation | null, source = '') {
return {
start: location
? {
export function resolveLocation(
location: SourceLocation | null | undefined,
context: TransformContext | string,
) {
return location
? {
start: {
line: location.start.line,
column: location.start.column + 1,
offset: location.start.index,
}
: { line: 1, column: 1, offset: 0 },
end: location
? {
},
end: {
line: location.end.line,
column: location.end.column + 1,
offset: location.end.index,
}
: { line: 1, column: 1, offset: 0 },
source,
}
},
source: isString(context)
? context
: context.ir.source.slice(location.start.index, location.end.index),
}
: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
source: '',
}
}

export function isComponent(node: Node) {
if (node.type === 'JSXIdentifier') {
const name = node.name
export function isComponentNode(node: JSXElement) {
const { openingElement } = node
if (openingElement.name.type === 'JSXIdentifier') {
const name = openingElement.name.name
return !htmlTags.includes(name as HtmlTags) && !svgTags.includes(name)
} else {
return node.type === 'JSXMemberExpression'
return openingElement.name.type === 'JSXMemberExpression'
}
}
2 changes: 1 addition & 1 deletion test/compile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function compile(template: string, options: CompilerOptions = {}) {
return _compile(template, {
...options,
mode: 'module',
prefixIdentifiers: false,
prefixIdentifiers: true,
})
}

Expand Down
Loading

0 comments on commit 0c2adfe

Please sign in to comment.