Skip to content

fix(compiler-core): avoid codegen crash on template with invalid structural directive - #14955

Open
sarathfrancis90 wants to merge 2 commits into
vuejs:mainfrom
sarathfrancis90:fix/codegen-orphan-template-crash
Open

sarathfrancis90 wants to merge 2 commits into
vuejs:mainfrom
sarathfrancis90:fix/codegen-orphan-template-crash

Conversation

@sarathfrancis90

@sarathfrancis90 sarathfrancis90 commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

While fuzzing baseCompile with a non-throwing onError, I hit a raw internal crash on a few valid-to-parse templates.

A <template> that carries a structural directive which is invalid in its position — a misplaced v-slot/#slot, or a v-else/v-else-if with no adjacent v-if — has its structural transform report a proper compiler error and bail, but the element node is left in the tree without a codegenNode. The default onError throws, so this never surfaces; but onError is a public option and tooling commonly supplies a collecting (non-throwing) handler. In that case the orphaned node reaches codegen, and genNode throws Error: Codegen node is missing for element/if/for node (a TypeError in production builds where the assert is stripped) instead of degrading after the already-reported error.

Minimal repro:

import { baseCompile } from '@vue/compiler-core'
baseCompile('<template v-else>x</template>', { onError: () => {} }) // throws "Codegen node is missing..."

Other triggers: <template v-else-if="a">x</template>, <template #slot>x</template>, <div><template #slot>x</template></div>.

The fix is in genNode: when an element/if/for node has no codegenNode (which only happens after such an error was reported and swallowed), emit a null placeholder rather than throwing. The underlying compiler error is still reported through onError.

Added integration tests in compile.spec.ts covering the trigger cases and asserting the underlying error is still reported. Full compiler-core + compiler-dom suites, typecheck and lint pass locally.

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling in template compilation. The compiler now gracefully reports errors for invalid structural directives instead of crashing during code generation.
  • Tests

    • Added integration tests verifying proper error reporting for invalid template directives.

…ctural directive

A <template> that carries a structural directive which is invalid in its
position (a misplaced v-slot, or a v-else/v-else-if with no adjacent v-if)
has its directive transform report a compiler error and bail, leaving the
node in the tree without a codegenNode. With a non-throwing onError the
node still reaches codegen, which threw an internal "Codegen node is
missing" error (and a TypeError in production builds).

Emit a null placeholder for an element/if/for node whose codegenNode is
absent so the compiler degrades gracefully after the reported error
instead of throwing a raw JS error.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a code-generation crash that occurs when Vue templates use invalid structural directive positioning (e.g., v-else without an adjacent v-if). The compiler now emits a null placeholder when a node's codegenNode is missing and reports the error via callback instead of crashing. Integration tests verify the graceful failure and error reporting.

Changes

Invalid Structural Directive Handling

Layer / File(s) Summary
Codegen null-check guard
packages/compiler-core/src/codegen.ts
genNode checks for node.codegenNode == null in the ELEMENT/IF/FOR branch and emits null as a placeholder instead of asserting and crashing.
Integration tests for structural directive errors
packages/compiler-core/__tests__/compile.spec.ts
ErrorCodes is imported and a new parameterized test suite verifies that invalid v-else positioning does not crash compilation, returns code with return statements, and reports ErrorCodes.X_V_ELSE_NO_ADJACENT_IF via onError callback.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • vuejs/core#13699: Adds the integration test expecting ErrorCodes.X_V_ELSE_NO_ADJACENT_IF and updates codegen.ts to avoid crashing when invalid structural directive transforms leave node.codegenNode unset.

Suggested labels

ready to merge, scope: compiler, :hammer: p3-minor-bug

Suggested reviewers

  • KazariEX
  • sxzz
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main fix: preventing codegen crashes when templates contain invalid structural directives, which matches the core change in codegen.ts and the test coverage in compile.spec.ts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 109 kB 41.3 kB 37 kB
vue.global.prod.js 168 kB (+76 B) 61.5 kB (+17 B) 54.7 kB (+112 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 50.8 kB 19.6 kB 18 kB
createApp 58.9 kB 22.7 kB 20.8 kB
createSSRApp 63.7 kB 24.7 kB 22.5 kB
defineCustomElement 65.2 kB 24.6 kB 22.5 kB
overall 73.5 kB 28 kB 25.5 kB

@pkg-pr-new

pkg-pr-new Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@14955
npm i https://pkg.pr.new/@vue/compiler-core@14955
yarn add https://pkg.pr.new/@vue/compiler-core@14955.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@14955
npm i https://pkg.pr.new/@vue/compiler-dom@14955
yarn add https://pkg.pr.new/@vue/compiler-dom@14955.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@14955
npm i https://pkg.pr.new/@vue/compiler-sfc@14955
yarn add https://pkg.pr.new/@vue/compiler-sfc@14955.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@14955
npm i https://pkg.pr.new/@vue/compiler-ssr@14955
yarn add https://pkg.pr.new/@vue/compiler-ssr@14955.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@14955
npm i https://pkg.pr.new/@vue/reactivity@14955
yarn add https://pkg.pr.new/@vue/reactivity@14955.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@14955
npm i https://pkg.pr.new/@vue/runtime-core@14955
yarn add https://pkg.pr.new/@vue/runtime-core@14955.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@14955
npm i https://pkg.pr.new/@vue/runtime-dom@14955
yarn add https://pkg.pr.new/@vue/runtime-dom@14955.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@14955
npm i https://pkg.pr.new/@vue/server-renderer@14955
yarn add https://pkg.pr.new/@vue/server-renderer@14955.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@14955
npm i https://pkg.pr.new/@vue/shared@14955
yarn add https://pkg.pr.new/@vue/shared@14955.tgz

vue

pnpm add https://pkg.pr.new/vue@14955
npm i https://pkg.pr.new/vue@14955
yarn add https://pkg.pr.new/vue@14955.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@14955
npm i https://pkg.pr.new/@vue/compat@14955
yarn add https://pkg.pr.new/@vue/compat@14955.tgz

commit: 60c58ce

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/compiler-core/__tests__/compile.spec.ts (2)

286-294: ⚡ Quick win

Verify error codes for all test cases, not just v-else.

The test suite includes four different invalid structural directive patterns (v-else, v-else-if, #slot standalone, #slot nested), but only the v-else case is verified to report the correct error code (X_V_ELSE_NO_ADJACENT_IF). Each pattern may report a different error code (e.g., v-else-if might report X_V_ELSE_NO_ADJACENT_IF, but #slot cases would report a different error code for misplaced slots).

Consider expanding this test to verify each case reports its appropriate error code.

💡 Example expansion
-    test('still reports the underlying error', () => {
+    test.each([
+      { source: `<template v-else>x</template>`, code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF },
+      { source: `<template v-else-if="a">x</template>`, code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF },
+      { source: `<template `#slot`>x</template>`, code: ErrorCodes.X_V_SLOT_MISPLACED },
+      { source: `<div><template `#slot`>x</template></div>`, code: ErrorCodes.X_V_SLOT_MISPLACED },
+    ])('reports correct error for $source', ({ source, code: expectedCode }) => {
       const onError = vi.fn()
-      expect(() =>
-        compile(`<template v-else>x</template>`, { onError }),
-      ).not.toThrow()
+      expect(() => compile(source, { onError })).not.toThrow()
       expect(onError).toHaveBeenCalledWith(
-        expect.objectContaining({ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF }),
+        expect.objectContaining({ code: expectedCode }),
       )
     })

Note: Verify the actual error code names for slot-related errors in the ErrorCodes enum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/compiler-core/__tests__/compile.spec.ts` around lines 286 - 294, The
test currently only asserts the error code for the v-else case; update the test
around the compile(...) calls to verify each invalid structural directive
reports its specific error code by invoking compile with the four inputs and
asserting onError was called with expect.objectContaining({ code:
ErrorCodes.X_V_ELSE_NO_ADJACENT_IF }) for the v-else and v-else-if cases (or the
correct enum if v-else-if differs), and the appropriate slot-related ErrorCodes
for the standalone and nested `#slot` cases (use the ErrorCodes enum names for
misplaced slot errors); keep using the existing onError spy and expect(...)
.not.toThrow() pattern so each compile invocation checks the corresponding
ErrorCodes entry.

272-284: ⚡ Quick win

Consider strengthening the generated code assertion.

The assertion expect(code).toContain('return') only verifies that the string 'return' appears somewhere in the generated code. This is a weak check that could pass even if the code structure is broken.

Consider adding assertions that verify the generated code is syntactically valid JavaScript or has the expected function structure.

💡 Example of stronger assertion
       expect(() => {
         code = compile(source, { onError }).code
       }).not.toThrow()
-      expect(code).toContain(`return`)
+      expect(code).toContain(`return`)
+      expect(code).toMatch(/function\s+render\s*\([^)]*\)\s*\{[\s\S]*return[\s\S]*\}/)

This regex verifies the code contains a render function with a return statement inside it, providing stronger validation of the generated structure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/compiler-core/__tests__/compile.spec.ts` around lines 272 - 284, The
current test uses a weak assertion expect(code).toContain('return'); update the
assertion to validate actual generated function structure and syntactic
validity: after obtaining code from compile(source, { onError }), either (a)
assert with a regex that the render function exists and contains a return (e.g.
match /function\s+render\([^)]*\)\s*{[\s\S]*return[\s\S]*}/ or the emitted
arrow/const render pattern used by compile), or (b) execute a syntactic check by
wrapping the emitted code in new Function(code) and asserting it does not throw;
apply this change in the test.each block (referencing compile, onError and code)
so the test verifies both structure and valid JS instead of just the substring
"return".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/compiler-core/__tests__/compile.spec.ts`:
- Around line 286-294: The test currently only asserts the error code for the
v-else case; update the test around the compile(...) calls to verify each
invalid structural directive reports its specific error code by invoking compile
with the four inputs and asserting onError was called with
expect.objectContaining({ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF }) for the
v-else and v-else-if cases (or the correct enum if v-else-if differs), and the
appropriate slot-related ErrorCodes for the standalone and nested `#slot` cases
(use the ErrorCodes enum names for misplaced slot errors); keep using the
existing onError spy and expect(...) .not.toThrow() pattern so each compile
invocation checks the corresponding ErrorCodes entry.
- Around line 272-284: The current test uses a weak assertion
expect(code).toContain('return'); update the assertion to validate actual
generated function structure and syntactic validity: after obtaining code from
compile(source, { onError }), either (a) assert with a regex that the render
function exists and contains a return (e.g. match
/function\s+render\([^)]*\)\s*{[\s\S]*return[\s\S]*}/ or the emitted arrow/const
render pattern used by compile), or (b) execute a syntactic check by wrapping
the emitted code in new Function(code) and asserting it does not throw; apply
this change in the test.each block (referencing compile, onError and code) so
the test verifies both structure and valid JS instead of just the substring
"return".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02d5878f-6b2c-4470-a093-fd08c1141330

📥 Commits

Reviewing files that changed from the base of the PR and between 478e3e8 and 211db37.

📒 Files selected for processing (2)
  • packages/compiler-core/__tests__/compile.spec.ts
  • packages/compiler-core/src/codegen.ts

@haoku123

Copy link
Copy Markdown
Contributor

Hi @sarathfrancis90 — I picked this fix up and re-implemented it on current main in #15297 (the codegen change is equivalent, tests are the same four cases). The previous e2e failure on this PR should be resolved by the rebase. Thanks for the original analysis — it made the pickup straightforward.

@haoku123

Copy link
Copy Markdown
Contributor

Hi @sarathfrancis90 — I investigated why this PR has been stuck, and I believe it is CI-flaky, not a code problem.

What I verified

The e2e failure from the 6-12 CI run is:

FAIL e2e packages/vue/__tests__/e2e/TransitionGroup.spec.ts > e2e: TransitionGroup > events
AssertionError: expected ... to be ... // transition timing assertion

That is an animation-timing test in runtime-dom territory. The codegen change here only emits a null placeholder when an invalid structural directive leaves a node without a codegenNode — a path the TransitionGroup e2e test never hits.

To confirm, I applied this PR's diff onto current main (a2b40db) and ran the suites locally:

  • git apply — the diff applies cleanly, no rebase conflicts
  • npx vitest run packages/compiler-core/__tests__/compile.spec.ts — 8/8 passing, including the new crash tests
  • built vue -f global and ran npx vitest --project e2e-browser run packages/vue/__tests__/e2e/TransitionGroup.spec.ts — 15/15 passing

So the fix is healthy on current main and the only red check is a stale flaky e2e run.

Suggested next steps

  1. Rebase onto main (the branch base is ~2 months old; the diff still applies cleanly).
  2. Rerun CI — the e2e job should pass; if it flakes again, a rerun should clear it since unit + dts are green.

I had opened #15297 as a re-implementation of this fix on current main; it was closed as a duplicate of this PR, so it would be great to get this one over the line. Happy to help with the rebase if useful.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants