diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap index 08c0b3e9..76560269 100644 --- a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap @@ -76,9 +76,9 @@ return function render() { exports[`compiler: codegen ifNode 1`] = ` "return function render() { with (this) { - return (foo) + return foo ? \\"foo\\" - : (bar) + : (a + b) ? toString(bye) : createVNode(Comment, 0, \\"foo\\") } @@ -88,9 +88,9 @@ exports[`compiler: codegen ifNode 1`] = ` exports[`compiler: codegen ifNode with no v-else 1`] = ` "return function render() { with (this) { - return (foo) + return foo ? \\"foo\\" - : (bar) + : (a + b) ? toString(bye) : null } diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index 38ef6b06..43d5d0b4 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -191,7 +191,7 @@ describe('compiler: codegen', () => { }, { type: NodeTypes.IF_BRANCH, - condition: createExpression('bar', false, mockLoc), + condition: createExpression('a + b', false, mockLoc), loc: mockLoc, isRoot: true, children: [createExpression(`bye`, false, mockLoc, true)] @@ -215,9 +215,9 @@ describe('compiler: codegen', () => { }) ) expect(code).toMatch(` - return (foo) + return foo ? "foo" - : (bar) + : (a + b) ? ${TO_STRING}(bye) : ${CREATE_VNODE}(${COMMENT}, 0, "foo")`) expect(code).toMatchSnapshot() @@ -248,7 +248,7 @@ describe('compiler: codegen', () => { }, { type: NodeTypes.IF_BRANCH, - condition: createExpression('bar', false, mockLoc), + condition: createExpression('a + b', false, mockLoc), loc: mockLoc, isRoot: true, children: [createExpression(`bye`, false, mockLoc, true)] @@ -259,9 +259,9 @@ describe('compiler: codegen', () => { }) ) expect(code).toMatch(` - return (foo) + return foo ? "foo" - : (bar) + : (a + b) ? ${TO_STRING}(bye) : null`) expect(code).toMatchSnapshot() diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts index b85b5151..41159d97 100644 --- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts @@ -1 +1,125 @@ -test.todo('v-on') +import { + parse, + transform, + ElementNode, + ObjectExpression, + CompilerOptions, + ErrorCodes +} from '../../src' +import { transformOn } from '../../src/transforms/vOn' +import { transformElement } from '../../src/transforms/transformElement' +import { transformExpression } from '../../src/transforms/transformExpression' + +function parseWithVOn( + template: string, + options: CompilerOptions = {} +): ElementNode { + const ast = parse(template) + transform(ast, { + nodeTransforms: [transformExpression, transformElement], + directiveTransforms: { + on: transformOn + }, + ...options + }) + return ast.children[0] as ElementNode +} + +describe('compiler: transform v-bind', () => { + test('basic', () => { + const node = parseWithVOn(`
`) + const props = node.codegenNode!.arguments[1] as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `onClick`, + isStatic: true, + loc: { + start: { + line: 1, + column: 11 + }, + end: { + line: 1, + column: 16 + } + } + }, + value: { + content: `onClick`, + isStatic: false, + loc: { + start: { + line: 1, + column: 17 + }, + end: { + line: 1, + column: 26 + } + } + }, + loc: { + start: { + line: 1, + column: 6 + }, + end: { + line: 1, + column: 26 + } + } + }) + }) + + test('dynamic arg', () => { + const node = parseWithVOn(``) + const props = node.codegenNode!.arguments[1] as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `"on" + event`, + isStatic: false + }, + value: { + content: `handler`, + isStatic: false + } + }) + }) + + test('dynamic arg with prefixing', () => { + const node = parseWithVOn(``, { + prefixIdentifiers: true + }) + const props = node.codegenNode!.arguments[1] as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + isStatic: false, + children: [`"on" + `, `_ctx.`, { content: `event` }] + }, + value: { + content: `handler`, + isStatic: false + } + }) + }) + + test('should error if no expression', () => { + const onError = jest.fn() + parseWithVOn(``, { 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: 10 + } + } + }) + }) + + test.todo('.once modifier') +}) diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 559e3942..0706a150 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -15,7 +15,11 @@ import { IfBranchNode } from './ast' import { SourceMapGenerator, RawSourceMap } from 'source-map' -import { advancePositionWithMutation, assert } from './utils' +import { + advancePositionWithMutation, + assert, + isSimpleIdentifier +} from './utils' import { isString, isArray } from '@vue/shared' import { RENDER_LIST, @@ -325,7 +329,7 @@ function genExpressionAsPropertyKey( push(`]`) } else if (isStatic) { // only quote keys if necessary - const text = /^\d|[^\w]/.test(content) ? JSON.stringify(content) : content + const text = isSimpleIdentifier(content) ? content : JSON.stringify(content) push(text, node) } else { push(`[${content}]`, node) @@ -366,9 +370,10 @@ function genIfBranch( if (condition) { // v-if or v-else-if const { push, indent, deindent, newline } = context - push(`(`) + const needsQuote = !isSimpleIdentifier(condition.content) + needsQuote && push(`(`) genExpression(condition, context) - push(`)`) + needsQuote && push(`)`) indent() context.indentLevel++ push(`? `) diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 127b29c2..cc77e8bc 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -17,22 +17,21 @@ export function compile( template: string | RootNode, options: CompilerOptions = {} ): CodegenResult { - const ast = isString(template) ? parse(template, options) : template - const prefixIdentifiers = !__BROWSER__ && options.prefixIdentifiers === true - - if (__BROWSER__ && options.prefixIdentifiers === false) { + if (__BROWSER__ && options.prefixIdentifiers) { ;(options.onError || defaultOnError)( createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED) ) } + const ast = isString(template) ? parse(template, options) : template + transform(ast, { ...options, - prefixIdentifiers, + prefixIdentifiers: !__BROWSER__ && options.prefixIdentifiers === true, nodeTransforms: [ transformIf, transformFor, - ...(prefixIdentifiers ? [transformExpression] : []), + transformExpression, transformElement, ...(options.nodeTransforms || []) // user transforms ], diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index feee1942..12dbf515 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -56,6 +56,9 @@ export function processExpression( node: ExpressionNode, context: TransformContext ) { + if (!context.prefixIdentifiers) { + return + } // lazy require dependencies so that they don't end up in rollup's dep graph // and thus can be tree-shaken in browser builds. const parseScript = @@ -155,6 +158,9 @@ export function processExpression( }) if (children.length) { + // mark it empty so that it's more noticeable in case another transform or + // codegen forget to handle `.children` first. + node.content = __DEV__ ? `[[REMOVED]]` : `` node.children = children } } diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index a7e38b99..db3b51ee 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -1,14 +1,33 @@ import { DirectiveTransform } from '../transform' -import { createObjectProperty, createExpression } from '../ast' +import { createObjectProperty, createExpression, ExpressionNode } from '../ast' import { capitalize } from '@vue/shared' +import { createCompilerError, ErrorCodes } from '../errors' +import { isSimpleIdentifier } from '../utils' // v-on without arg is handled directly in ./element.ts due to it affecting // codegen for the entire props object. This transform here is only for v-on // *with* args. -export const transformOn: DirectiveTransform = ({ arg, exp, loc }) => { - const eventName = arg!.isStatic - ? createExpression(`on${capitalize(arg!.content)}`, true, arg!.loc) - : createExpression(`'on' + (${arg!.content})`, false, arg!.loc) +export const transformOn: DirectiveTransform = ({ arg, exp, loc }, context) => { + if (!exp) { + context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc)) + } + const { content, children, isStatic, loc: argLoc } = arg! + let eventName: ExpressionNode + if (isStatic) { + // static arg + eventName = createExpression(`on${capitalize(content)}`, true, argLoc) + } else if (!children) { + // dynamic arg with no rewrite + eventName = createExpression( + `"on" + ${isSimpleIdentifier(content) ? content : `(${content})`}`, + false, + argLoc + ) + } else { + // dynamic arg with ctx prefixing + eventName = arg! + children.unshift(`"on" + `) + } // TODO .once modifier handling since it is platform agnostic // other modifiers are handled in compiler-dom return { diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index 59704a2d..f857f629 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -1,5 +1,8 @@ import { SourceLocation, Position } from './ast' +export const isSimpleIdentifier = (name: string): boolean => + !/^\d|[^\w]/.test(name) + export function getInnerRange( loc: SourceLocation, offset: number,