unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
vuejs/core
mid7/10
compiler-internalserror-handling

fix(compiler-vapor): wrap custom directive value in parens

Project: vuejs/core | Language: TypeScript | Concepts: compiler-internals, error-handling | Difficulty: mid
Link: https://github.com/vuejs/core/pull/15258

The problem

When compiling templates using the vapor codegen, custom directive values were wrapped as arrow functions without parentheses. If the directive value was an inline object literal (e.g. { value: msg }), the generated code looked like () => { value: msg }. In JavaScript, this is interpreted as an arrow function with a block body, not an object literal, which leads to unexpected behavior (returns undefined or throws a SyntaxError if there are multiple properties).

Why it's non-trivial

A naive fix might simply add parentheses in some places, but the codegen must ensure that all possible directive expressions—including both simple expressions and object literals—are always wrapped correctly, and this must work in both standard and inline compilation modes. The fix also needs to be consistent with how other vapor getters are generated, to avoid introducing subtle bugs or inconsistencies.

The approach

The core fix is in the directive code generator. Previously, the value for a custom directive was generated like this:

-    const value = dir.exp && ['() => ', ...genExpression(dir.exp, context)]

This would produce, for example, () => _ctx.msg or () => { value: _ctx.msg }.

The PR changes it to:

+    const value = dir.exp && [
+      '() => (',
+      ...genExpression(dir.exp, context),
+      ')',
+    ]

Now, every directive value is wrapped in parentheses, so the output becomes () => (_ctx.msg) or () => ({ value: _ctx.msg }). This ensures that object literals are parsed as expressions, not block bodies.

The tests were updated and expanded to cover these cases. For example, a new test for an object literal binding value:

test('object literal binding value', () => {
  const code = compile(
    `<div v-example="{ value: msg, other: 1 }"></div>`,
    {
      bindingMetadata: {
        msg: BindingTypes.SETUP_REF,
        vExample: BindingTypes.SETUP_CONST,
      },
    },
  )
  expect(code).matchSnapshot()
  expect(code).contains('() => ({ value: _ctx.msg, other: 1 })')
})

This ensures that the generated code always uses the correct parentheses.

The corresponding snapshot output now shows:

-  _withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg]])
+  _withVaporDirectives(n0, [[_ctx.vExample, () => (_ctx.msg)]])

and for object literals:

+  _withVaporDirectives(n0, [[_ctx.vExample, () => ({ value: _ctx.msg, other: 1 })]])

From the review thread

No notable review discussion on this PR.

Takeaways

  • Always wrap object literal expressions in parentheses when generating arrow functions in JavaScript to ensure they are parsed as expressions, not block bodies.
  • When fixing a code generation bug, update and expand tests to cover both the broken and the fixed cases, including edge cases (like object literals).
  • Consistency in code generation (e.g., always wrapping expressions in the same way) helps prevent subtle bugs and makes maintenance easier.