Skip to content

fix(angular-compiler): hostDirectives, emitExpr safety, TDZ hoisting, misc compilation fixes#2255

Merged
brandonroberts merged 1 commit into
betafrom
feat/host-directives-emitexpr
Apr 7, 2026
Merged

fix(angular-compiler): hostDirectives, emitExpr safety, TDZ hoisting, misc compilation fixes#2255
brandonroberts merged 1 commit into
betafrom
feat/host-directives-emitexpr

Conversation

@brandonroberts

Copy link
Copy Markdown
Member

PR Checklist

Affected scope

  • Primary scope: angular-compiler
  • Secondary scopes: vite-plugin-angular

Recommended merge strategy for maintainer [optional]

  • Squash merge
  • Rebase merge
  • Other

What is the new behavior?

Fix multiple angular-compiler bugs:

  • hostDirectives metadata extraction
  • emitExpr crash on production builds
  • TDZ errors from static initializers referencing forward declarations
  • Angular version detection for local builds, empty router-outlet with hasDirectiveDependencies
  • type-elision incorrectly removing imports used in as-casts.

Validated analog-compiler works with angular.dev application

Test plan

  • npx vitest run packages/angular-compiler/src/lib/integration.spec.ts — 70 tests passing (8 new)
  • ANGULAR_SOURCE_DIR=.angular-conformance npx vitest run packages/angular-compiler/src/lib/conformance.spec.ts — 160 passed, 0 failures (Angular v21.2.7)
  • pnpm build
  • pnpm test

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

@netlify

netlify Bot commented Apr 7, 2026

Copy link
Copy Markdown

Deploy Preview for analog-blog ready!

Name Link
🔨 Latest commit 8645225
🔍 Latest deploy log https://app.netlify.com/projects/analog-blog/deploys/69d4792cab7efc00083f09c3
😎 Deploy Preview https://deploy-preview-2255--analog-blog.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Apr 7, 2026

Copy link
Copy Markdown

Deploy Preview for analog-docs ready!

Name Link
🔨 Latest commit 8645225
🔍 Latest deploy log https://app.netlify.com/projects/analog-docs/deploys/69d4792c12fc710008c4786e
😎 Deploy Preview https://deploy-preview-2255--analog-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Apr 7, 2026

Copy link
Copy Markdown

Deploy Preview for analog-app ready!

Name Link
🔨 Latest commit 8645225
🔍 Latest deploy log https://app.netlify.com/projects/analog-app/deploys/69d4792c12fc710008c4786c
😎 Deploy Preview https://deploy-preview-2255--analog-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 117561b2-1f6c-479b-b159-9b378cab38d2

📥 Commits

Reviewing files that changed from the base of the PR and between c9b06c9 and 8645225.

📒 Files selected for processing (6)
  • packages/angular-compiler/COMPILER.md
  • packages/angular-compiler/src/lib/compile.ts
  • packages/angular-compiler/src/lib/integration.spec.ts
  • packages/angular-compiler/src/lib/js-emitter.ts
  • packages/angular-compiler/src/lib/metadata.ts
  • packages/angular-compiler/src/lib/type-elision.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/angular-compiler/src/lib/js-emitter.ts
  • packages/angular-compiler/src/lib/compile.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/angular-compiler/src/lib/type-elision.ts

📝 Walkthrough

Walkthrough

Updates extend compiler behavior and tests: documentation adds hostDirectives coverage; Angular major detection fallback updated to 22; component metadata now marks hasDirectiveDependencies when meta.imports is non-empty; a new MagicString pass hoists non-exported post-class variable statements and function declarations before the first class; hostDirectives array literals are parsed (bare ids, object form with inputs/outputs, forwardRef); call-site emitters now route argument emission through emitExpr; type-elision treats TSAsExpression/TSSatisfiesExpression expressions as value-context subtrees; integration tests expanded to cover hoisting, hostDirectives, template expression routing, and type-elision.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Key observations

  • Regression risk: the new hoisting pass must preserve evaluation order and avoid changing semantics (TDZ fixes expected for non-exported declarations); verify source maps and side-effecting top-level statements remain correct.
  • API/compatibility: no exported API or signature changes; documentation added for hostDirectives only.
  • Tests: significant integration test additions increase confidence; review for deterministic expectations (ordering, static modifiers, forwardRef/unwrapping).
  • Release impact: Angular-major fallback changed to 22 — confirm this aligns with release policy and CI platform expectations.
  • Monorepo consistency: ensure the MagicString edits and hoisting logic are consistently applied across module outputs and do not conflict with other emitter passes.
  • Type-elision: verify as/satisfies handling preserves value usages and avoids eliding needed imports.
  • BREAKING CHANGE: none.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows Conventional Commit style with scope and clearly summarizes the main fixes in the changeset.
Description check ✅ Passed The PR description provides detailed context about the bugs fixed, test validation, and affected scopes, all directly related to the changeset.

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


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 and usage tips.

@github-actions github-actions Bot added the scope:angular-compiler Changes in @analogjs/angular-compiler label Apr 7, 2026

@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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/angular-compiler/src/lib/compile.ts (1)

791-818: Consider narrowing the hoisting scope to only declarations actually referenced by Ivy initializers.

The pass unconditionally hoists all non-exported post-class declarations before the first class. While this prevents TDZ errors, it also changes module evaluation order for unrelated side effects. Function declarations are already hoisted by JavaScript and don't need moving. If blanket reordering is necessary, add an explicit note in the commit explaining why selective hoisting isn't feasible, and verify no real-world code depends on post-class declaration order.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/src/lib/compile.ts` around lines 791 - 818, The
current hoisting loop in compile.ts moves all non-exported post-class variable
and function declarations, which can reorder unrelated side effects; narrow this
to only move variable declarations actually referenced by Ivy initializers: use
classResults to collect identifier names used by the static ɵcmp/initializer
code (or other classResults fields that contain initializer AST/text), then when
iterating origSourceFile.statements only hoist ts.isVariableStatement stmts that
declare one or more of those referenced names (skip function declarations
entirely since JS already hoists them), using hasExportModifier to avoid
exported symbols and ms.appendLeft/ms.remove to perform the move; if selective
hoisting cannot cover all cases add a clear comment/commit note explaining why
blanket hoisting is required and document the risk to evaluation order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/angular-compiler/src/lib/integration.spec.ts`:
- Around line 1675-1716: Add a focused spec that reproduces the regression where
an argument can be emitted as undefined so compilation doesn't crash: inside the
existing "emitExpr routing for function/method args" suite add a test that calls
a component method with an undefined argument (for example via a literal
`undefined` or a property that is `undefined`) in the template, compile it
(using the existing compile(...) helper) and assert the compilation succeeds
(expectCompiles(result)) and the generated output contains the expected call
site (e.g., 'ctx.format' or the method name), so this directly exercises the
emitExpr path changed in js-emitter.ts and guards against the undefined-arg
crash.
- Around line 1623-1650: Update the test that uses compile/expectCompiles for
HostComponent/ColorDirective to assert the aliased input/output names are
exposed, not just that "hostDirectives" exists: modify the hostDirectives object
in the test to use inputs: ['color: appColor'] and outputs: ['colorChange:
appColorChange'] and then assert the compiled result contains the aliased
identifiers (e.g. "appColor" and "appColorChange" or the specific generated
mapping for hostDirectives), so the normalization logic in compile for
inputs/outputs is validated.

In `@packages/angular-compiler/src/lib/metadata.ts`:
- Around line 181-199: The inputs/outputs parsing currently stores the raw
string; update the loop in metadata.ts (the blocks handling
prop.initializer.elements where inputs and outputs are populated) to parse
ts.isStringLiteral(e). For each e.text, if it contains a colon split on ':' and
trim both sides so you assign the directive property as the key and the public
alias as the value (e.g., let [dirProp, alias] = text.split(':').map(s =>
s.trim()); inputs![dirProp] = alias ?? dirProp), otherwise preserve the original
name for key and value; apply the same fix for outputs and add a unit test that
exercises an aliased binding like inputs: ['color: appColor'] to prevent
regressions.

---

Nitpick comments:
In `@packages/angular-compiler/src/lib/compile.ts`:
- Around line 791-818: The current hoisting loop in compile.ts moves all
non-exported post-class variable and function declarations, which can reorder
unrelated side effects; narrow this to only move variable declarations actually
referenced by Ivy initializers: use classResults to collect identifier names
used by the static ɵcmp/initializer code (or other classResults fields that
contain initializer AST/text), then when iterating origSourceFile.statements
only hoist ts.isVariableStatement stmts that declare one or more of those
referenced names (skip function declarations entirely since JS already hoists
them), using hasExportModifier to avoid exported symbols and
ms.appendLeft/ms.remove to perform the move; if selective hoisting cannot cover
all cases add a clear comment/commit note explaining why blanket hoisting is
required and document the risk to evaluation order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c6d585b8-8430-4df2-b5a8-a13289694bbd

📥 Commits

Reviewing files that changed from the base of the PR and between 76ad554 and c9b06c9.

📒 Files selected for processing (6)
  • packages/angular-compiler/COMPILER.md
  • packages/angular-compiler/src/lib/compile.ts
  • packages/angular-compiler/src/lib/integration.spec.ts
  • packages/angular-compiler/src/lib/js-emitter.ts
  • packages/angular-compiler/src/lib/metadata.ts
  • packages/angular-compiler/src/lib/type-elision.ts

Comment thread packages/angular-compiler/src/lib/integration.spec.ts
Comment thread packages/angular-compiler/src/lib/integration.spec.ts
Comment thread packages/angular-compiler/src/lib/metadata.ts
… type-elision and version detection

- Route function/method/instantiate args through emitExpr() to handle
  undefined args gracefully (fixes visitExpression crash on production build)
- Parse hostDirectives decorator metadata into R3HostDirectiveMetadata
  (bare identifiers, object form with inputs/outputs, forwardRef)
- Hoist non-exported post-class const/function declarations before the
  first class to avoid TDZ errors in static initializers
- Fix ANGULAR_MAJOR fallback: treat version 0.0.0 (local builds) as 22
- Set hasDirectiveDependencies when meta.imports is non-empty even if
  registry declarations are empty (fixes empty router-outlet)
- Fix type-elision: TSAsExpression/TSSatisfiesExpression expression
  child is a value position, not a type position (fixes erroneous
  elision of imports used in as-casts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@brandonroberts brandonroberts force-pushed the feat/host-directives-emitexpr branch from c9b06c9 to 8645225 Compare April 7, 2026 03:25
@brandonroberts brandonroberts merged commit 796e3e0 into beta Apr 7, 2026
27 checks passed
@brandonroberts brandonroberts deleted the feat/host-directives-emitexpr branch April 7, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope:angular-compiler Changes in @analogjs/angular-compiler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant