diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap index df47f1598..b1c8449cc 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap @@ -1,5 +1,31 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`v-on > case conversion for kebab-case events 1`] = ` +"import { template as _template, children as _children, on as _on } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "fooBar", (...args) => (_ctx.onMount && _ctx.onMount(...args))) + return n0 +}" +`; + +exports[`v-on > dynamic arg 1`] = ` +"import { template as _template, children as _children, effect as _effect, on as _on } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _effect(() => { + _on(n1, _ctx.event, (...args) => (_ctx.handler && _ctx.handler(...args))) + }) + return n0 +}" +`; + exports[`v-on > event modifier 1`] = ` "import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor'; @@ -33,6 +59,131 @@ export function render(_ctx) { }" `; +exports[`v-on > should not wrap keys guard if no key modifier is present 1`] = ` +"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "keyup", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["exact"])) + return n0 +}" +`; + +exports[`v-on > should support multiple events and modifiers options w/ prefixIdentifiers: true 1`] = ` +"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "click", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop"])) + _on(n1, "keyup", _withKeys((...args) => (_ctx.test && _ctx.test(...args)), ["enter"])) + return n0 +}" +`; + +exports[`v-on > should support multiple modifiers and event options w/ prefixIdentifiers: true 1`] = ` +"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "click", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop", "prevent"]), { capture: true, once: true }) + return n0 +}" +`; + +exports[`v-on > should transform click.middle 1`] = ` +"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "mouseup", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["middle"])) + return n0 +}" +`; + +exports[`v-on > should transform click.middle 2`] = ` +"import { template as _template, children as _children, effect as _effect, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _effect(() => { + _on(n1, (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["middle"])) + }) + return n0 +}" +`; + +exports[`v-on > should transform click.right 1`] = ` +"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "contextmenu", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["right"])) + return n0 +}" +`; + +exports[`v-on > should transform click.right 2`] = ` +"import { template as _template, children as _children, effect as _effect, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _effect(() => { + _on(n1, (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["right"]), ["right"])) + }) + return n0 +}" +`; + +exports[`v-on > should wrap as function if expression is inline statement 1`] = ` +"import { template as _template, children as _children, on as _on } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "click", (...args) => (_ctx.i++ && _ctx.i++(...args))) + return n0 +}" +`; + +exports[`v-on > should wrap keys guard for keyboard events or dynamic events 1`] = ` +"import { template as _template, children as _children, on as _on, withKeys as _withKeys, withModifiers as _withModifiers } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "keydown", _withKeys(_withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop", "ctrl"]), ["a"]), { capture: true }) + return n0 +}" +`; + +exports[`v-on > should wrap keys guard for static key event w/ left/right modifiers 1`] = ` +"import { template as _template, children as _children, on as _on, withKeys as _withKeys } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _on(n1, "keyup", _withKeys((...args) => (_ctx.test && _ctx.test(...args)), ["left"])) + return n0 +}" +`; + exports[`v-on > simple expression 1`] = ` "import { template as _template, children as _children, on as _on } from 'vue/vapor'; diff --git a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts index 0e38f9e45..c9c8d8132 100644 --- a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts @@ -80,6 +80,9 @@ describe('v-html', () => { expect(ir.vaporHelpers).contains('setHtml') expect(ir.helpers.size).toBe(0) + // children should have been removed + expect(ir.template).toMatchObject([{ template: '' }]) + expect(ir.operation).toEqual([]) expect(ir.effect).toMatchObject([ { @@ -109,6 +112,8 @@ describe('v-html', () => { ]) expect(code).matchSnapshot() + // children should have been removed + expect(code).contains('template("")') }) test('should raise error if has no expression', () => { diff --git a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts index 353396ba1..c731f30d0 100644 --- a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts @@ -1,45 +1,72 @@ -import { type RootNode, BindingTypes, ErrorCodes } from '@vue/compiler-dom' -import { type CompilerOptions, compile as _compile } from '../../src' +import { BindingTypes, ErrorCodes, parse, NodeTypes } from '@vue/compiler-dom' +import { + type CompilerOptions, + compile as _compile, + RootIRNode, + transform, + generate, + IRNodeTypes, +} from '../../src' -function compile(template: string | RootNode, options: CompilerOptions = {}) { - let { code } = _compile(template, { - ...options, - mode: 'module', +import { transformVOn } from '../../src/transforms/vOn' +import { transformElement } from '../../src/transforms/transformElement' + +function compileWithVOn( + template: string, + options: CompilerOptions = {}, +): { + ir: RootIRNode + code: string +} { + const ast = parse(template, { prefixIdentifiers: true, ...options }) + const ir = transform(ast, { + nodeTransforms: [transformElement], + directiveTransforms: { + on: transformVOn, + }, prefixIdentifiers: true, + ...options, }) - return code + const { code } = generate(ir, { prefixIdentifiers: true, ...options }) + return { ir, code } } describe('v-on', () => { test('simple expression', () => { - const code = compile(``, { + const { code, ir } = compileWithVOn(``, { bindingMetadata: { handleClick: BindingTypes.SETUP_CONST, }, }) + + expect(ir.vaporHelpers).contains('on') + expect(ir.helpers.size).toBe(0) + expect(ir.effect).toEqual([]) + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + element: 1, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'click', + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'handleClick', + isStatic: false, + }, + modifiers: { keys: [], nonKeys: [], options: [] }, + keyOverride: undefined, + }, + ]) + expect(code).matchSnapshot() }) - test('should error if no expression AND no modifier', () => { - const onError = vi.fn() - compile(``, { onError }) - expect(onError.mock.calls[0][0]).toMatchObject({ - code: ErrorCodes.X_V_ON_NO_EXPRESSION, - loc: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 16, - }, - }, - }) - }) - test('event modifier', () => { - const code = compile( + const { code } = compileWithVOn( ` @@ -70,4 +97,364 @@ describe('v-on', () => { ) expect(code).matchSnapshot() }) + + test('dynamic arg', () => { + const { code, ir } = compileWithVOn(``) + + expect(ir.vaporHelpers).contains('on') + expect(ir.vaporHelpers).contains('effect') + expect(ir.helpers.size).toBe(0) + expect(ir.operation).toEqual([]) + + expect(ir.effect[0].operations[0]).toMatchObject({ + type: IRNodeTypes.SET_EVENT, + element: 1, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'event', + isStatic: false, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'handler', + isStatic: false, + }, + }) + + expect(code).matchSnapshot() + }) + + test.todo('dynamic arg with prefixing') + test.todo('dynamic arg with complex exp prefixing') + test.todo('should wrap as function if expression is inline statement') + test.todo('should handle multiple inline statement') + test.todo('should handle multi-line statement') + test.todo('inline statement w/ prefixIdentifiers: true') + test.todo('multiple inline statements w/ prefixIdentifiers: true') + test.todo( + 'should NOT wrap as function if expression is already function expression', + ) + test.todo( + 'should NOT wrap as function if expression is already function expression (with Typescript)', + ) + test.todo( + 'should NOT wrap as function if expression is already function expression (with newlines)', + ) + test.todo( + 'should NOT wrap as function if expression is already function expression (with newlines + function keyword)', + ) + test.todo( + 'should NOT wrap as function if expression is complex member expression', + ) + test.todo('complex member expression w/ prefixIdentifiers: true') + test.todo('function expression w/ prefixIdentifiers: true') + + test('should error if no expression AND no modifier', () => { + const onError = vi.fn() + compileWithVOn(``, { onError }) + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_ON_NO_EXPRESSION, + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 16, + }, + }, + }) + }) + + test('should NOT error if no expression but has modifier', () => { + const onError = vi.fn() + compileWithVOn(``, { onError }) + expect(onError).not.toHaveBeenCalled() + }) + + test('case conversion for kebab-case events', () => { + const { ir, code } = compileWithVOn(``) + + expect(ir.vaporHelpers).contains('on') + expect(ir.helpers.size).toBe(0) + expect(ir.effect).toEqual([]) + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + element: 1, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'fooBar', + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'onMount', + isStatic: false, + }, + }, + ]) + + expect(code).matchSnapshot() + expect(code).contains('fooBar') + }) + + test('error for vnode hooks', () => { + const onError = vi.fn() + compileWithVOn(``, { onError }) + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_VNODE_HOOKS, + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 24, + }, + }, + }) + }) + + test.todo('vue: prefixed events') + + test('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => { + const { code, ir } = compileWithVOn( + ``, + { + prefixIdentifiers: true, + }, + ) + + expect(ir.vaporHelpers).contains('on') + expect(ir.vaporHelpers).contains('withModifiers') + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'test', + isStatic: false, + }, + modifiers: { + keys: [], + nonKeys: ['stop', 'prevent'], + options: ['capture', 'once'], + }, + keyOverride: undefined, + }, + ]) + + expect(code).matchSnapshot() + expect(code).contains('_withModifiers') + expect(code).contains('["stop", "prevent"]') + expect(code).contains('{ capture: true, once: true }') + }) + + test('should support multiple events and modifiers options w/ prefixIdentifiers: true', () => { + const { code, ir } = compileWithVOn( + ``, + { + prefixIdentifiers: true, + }, + ) + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'click', + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'test', + isStatic: false, + }, + modifiers: { + keys: [], + nonKeys: ['stop'], + options: [], + }, + }, + { + type: IRNodeTypes.SET_EVENT, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'keyup', + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'test', + isStatic: false, + }, + modifiers: { + keys: ['enter'], + nonKeys: [], + options: [], + }, + }, + ]) + + expect(code).matchSnapshot() + expect(code).contains( + '_on(n1, "click", _withModifiers((...args) => (_ctx.test && _ctx.test(...args)), ["stop"]))', + ) + expect(code).contains( + '_on(n1, "keyup", _withKeys((...args) => (_ctx.test && _ctx.test(...args)), ["enter"]))', + ) + }) + + test('should wrap keys guard for keyboard events or dynamic events', () => { + const { code, ir } = compileWithVOn( + ``, + { + prefixIdentifiers: true, + }, + ) + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + element: 1, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'keydown', + isStatic: true, + }, + value: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'test', + isStatic: false, + }, + modifiers: { + keys: ['a'], + nonKeys: ['stop', 'ctrl'], + options: ['capture'], + }, + }, + ]) + + expect(code).matchSnapshot() + }) + + test('should not wrap keys guard if no key modifier is present', () => { + const { code, ir } = compileWithVOn(``, { + prefixIdentifiers: true, + }) + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + modifiers: { nonKeys: ['exact'] }, + }, + ]) + + expect(code).matchSnapshot() + }) + + test('should wrap keys guard for static key event w/ left/right modifiers', () => { + const { code, ir } = compileWithVOn(``, { + prefixIdentifiers: true, + }) + + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + modifiers: { keys: ['left'] }, + }, + ]) + + expect(code).matchSnapshot() + }) + + test.todo('should wrap both for dynamic key event w/ left/right modifiers') + + test('should transform click.right', () => { + const { code, ir } = compileWithVOn(``) + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'contextmenu', + isStatic: true, + }, + modifiers: { nonKeys: ['right'] }, + keyOverride: undefined, + }, + ]) + + expect(code).matchSnapshot() + expect(code).contains('"contextmenu"') + + // dynamic + const { code: code2, ir: ir2 } = compileWithVOn( + ``, + ) + expect(ir2.effect[0].operations).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'event', + isStatic: false, + }, + modifiers: { nonKeys: ['right'] }, + keyOverride: ['click', 'contextmenu'], + }, + ]) + + expect(code2).matchSnapshot() + expect(code2).contains( + '(_ctx.event) === "click" ? "contextmenu" : (_ctx.event)', + ) + }) + + test('should transform click.middle', () => { + const { code, ir } = compileWithVOn(``) + expect(ir.operation).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'mouseup', + isStatic: true, + }, + modifiers: { nonKeys: ['middle'] }, + keyOverride: undefined, + }, + ]) + + expect(code).matchSnapshot() + expect(code).contains('"mouseup"') + + // dynamic + const { code: code2, ir: ir2 } = compileWithVOn( + ``, + ) + + expect(ir2.effect[0].operations).toMatchObject([ + { + type: IRNodeTypes.SET_EVENT, + key: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'event', + isStatic: false, + }, + modifiers: { nonKeys: ['middle'] }, + keyOverride: ['click', 'mouseup'], + }, + ]) + + expect(code2).matchSnapshot() + expect(code2).contains( + '(_ctx.event) === "click" ? "mouseup" : (_ctx.event)', + ) + }) }) diff --git a/packages/compiler-vapor/src/transforms/vOn.ts b/packages/compiler-vapor/src/transforms/vOn.ts index de5fc38a9..363a49788 100644 --- a/packages/compiler-vapor/src/transforms/vOn.ts +++ b/packages/compiler-vapor/src/transforms/vOn.ts @@ -1,7 +1,12 @@ -import { createCompilerError, ErrorCodes } from '@vue/compiler-core' +import { + createCompilerError, + ElementTypes, + ErrorCodes, +} from '@vue/compiler-core' import type { DirectiveTransform } from '../transform' -import { IRNodeTypes, KeyOverride } from '../ir' +import { IRNodeTypes, KeyOverride, SetEventIRNode } from '../ir' import { resolveModifiers } from '@vue/compiler-dom' +import { camelize } from '@vue/shared' export const transformVOn: DirectiveTransform = (dir, node, context) => { let { arg, exp, loc, modifiers } = dir @@ -16,6 +21,23 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => { return } + if (arg.isStatic) { + let rawName = arg.content + if (__DEV__ && rawName.startsWith('vnode')) { + context.options.onError( + createCompilerError(ErrorCodes.X_VNODE_HOOKS, arg.loc), + ) + } + + if ( + node.tagType !== ElementTypes.ELEMENT || + rawName.startsWith('vnode') || + !/[A-Z]/.test(rawName) + ) { + arg.content = camelize(arg.content) + } + } + const { keyModifiers, nonKeyModifiers, eventOptionModifiers } = resolveModifiers( arg.isStatic ? `on${arg.content}` : arg, @@ -48,7 +70,7 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => { } } - context.registerOperation({ + const operation: SetEventIRNode = { type: IRNodeTypes.SET_EVENT, loc, element: context.reference(), @@ -60,5 +82,11 @@ export const transformVOn: DirectiveTransform = (dir, node, context) => { options: eventOptionModifiers, }, keyOverride, - }) + } + + if (arg.isStatic) { + context.registerOperation(operation) + } else { + context.registerEffect([arg], [operation]) + } }