Skip to content

fix(angular-compiler): preserve constructor di token imports from elision#2270

Merged
brandonroberts merged 1 commit into
betafrom
fix/constructor-di-type-elision
Apr 9, 2026
Merged

fix(angular-compiler): preserve constructor di token imports from elision#2270
brandonroberts merged 1 commit into
betafrom
fix/constructor-di-type-elision

Conversation

@brandonroberts

Copy link
Copy Markdown
Member

PR Checklist

Closes #

When a service is imported only for use as a constructor DI token, the analog compiler's single-file type-elision pass marks the import as type-only and erases it. The constructor DI extractor then sees the missing token and emits ɵɵinvalidFactory(), causing Error: This constructor was not compatible with Dependency Injection at runtime.

Affected scope

  • Primary scope: angular-compiler
  • Secondary scopes:

Recommended merge strategy for maintainer [optional]

  • Squash merge
  • Rebase merge
  • Other

Commit preservation note [optional]

What is the new behavior?

type-elision.ts runs a pre-pass that walks every decorated class declaration, finds its constructor, and marks parameter type names as value-referenced before the type-elision walk. The import is preserved, extractConstructorDeps sees the runtime token, and ɵfac correctly emits i0.ɵɵdirectiveInject(MyService).

Before

import { MyService } from './my-service';
@Component({...})
export class FooComponent {
  constructor(private svc: MyService) {}
}

Compiled to:

// import erased
export class FooComponent {
  constructor(private svc: MyService) {}
  static ɵfac = function FooComponent_Factory(...) { i0.ɵɵinvalidFactory(); };
  static ɵcmp = ...;
}

Runtime: Error: This constructor was not compatible with Dependency Injection

After

import { MyService } from './my-service';
export class FooComponent {
  constructor(private svc: MyService) {}
  static ɵfac = (__ngFactoryType__) => new (__ngFactoryType__ || FooComponent)(i0.ɵɵdirectiveInject(MyService));
  static ɵcmp = ...;
}

Edge cases handled

  • Plain constructor(svc: Foo) parameters
  • TSParameterProperty (constructor(private svc: Foo))
  • Generic type arguments (constructor(svc: Service<Bar>))
  • Union/intersection types
  • Non-decorated classes still get correct type-only elision (no false negatives)

Test plan

  • npx vitest run packages/angular-compiler/src/lib/type-elision.spec.ts — 48 tests pass (3 new)
  • npx vitest run packages/angular-compiler — 534 tests pass, 0 failures
  • npx nx e2e analog-app-e2e — 11/11 pass
  • pnpm build
  • pnpm test

Does this PR introduce a breaking change?

  • Yes
  • No

This only adds preservation — no imports that were previously preserved are now elided. Pre-existing behavior for non-decorated classes is unchanged.

Other information

The bug only affects projects using the older constructor DI pattern (constructor(private svc: MyService)). Modern Angular code that uses inject() in field initializers wasn't affected because MyService appears in a value position (call argument) and was correctly preserved. This is why the bug didn't surface in analog-app (which uses inject() everywhere).

🤖 Generated with Claude Code

…sion

Constructor parameter type annotations are TypeScript type positions, but
for decorated classes they double as runtime DI tokens that ɵfac uses to
emit ɵɵdirectiveInject calls. The single-file type-elision pass treated
them as type-only and erased the imports, which then caused
extractConstructorDeps to mark the deps as invalid and emit
ɵɵinvalidFactory().

Pre-collect constructor parameter type names from decorated classes
before the type-elision walk so those imports are preserved as
value-referenced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Apr 9, 2026

Copy link
Copy Markdown

Deploy Preview for analog-blog ready!

Name Link
🔨 Latest commit 563d24a
🔍 Latest deploy log https://app.netlify.com/projects/analog-blog/deploys/69d79dc8f822bb0008555f01
😎 Deploy Preview https://deploy-preview-2270--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 9, 2026

Copy link
Copy Markdown

Deploy Preview for analog-app ready!

Name Link
🔨 Latest commit 563d24a
🔍 Latest deploy log https://app.netlify.com/projects/analog-app/deploys/69d79dc7983c9b0008d4ba5a
😎 Deploy Preview https://deploy-preview-2270--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.

@netlify

netlify Bot commented Apr 9, 2026

Copy link
Copy Markdown

Deploy Preview for analog-docs ready!

Name Link
🔨 Latest commit 563d24a
🔍 Latest deploy log https://app.netlify.com/projects/analog-docs/deploys/69d79dc7f822bb0008555efc
😎 Deploy Preview https://deploy-preview-2270--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.

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c11384cf-397d-4840-8440-64b6261ecdb2

📥 Commits

Reviewing files that changed from the base of the PR and between 694d4b9 and 563d24a.

📒 Files selected for processing (2)
  • packages/angular-compiler/src/lib/type-elision.spec.ts
  • packages/angular-compiler/src/lib/type-elision.ts

📝 Walkthrough

Walkthrough

This pull request adds test coverage and implementation logic to prevent DI token imports from being elided when used in decorated Angular class constructors. The implementation introduces a pre-pass in analyzeTypeOnlyImports via new functions collectConstructorDiTokens and collectTypeReferenceNames to scan decorated class constructors, extract parameter type annotations, and recursively collect identifier names from imported type references. Three new test cases verify that constructor parameter types in decorated classes are preserved as runtime-referenced values, while maintaining existing behavior for non-decorated classes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Key observations

Logic impact: The changes introduce a new pre-pass that intercepts imports before the existing elision logic executes. This modifies core behavior around what gets classified as type-only vs. runtime-referenced.

Recursive type traversal: The collectTypeReferenceNames function recursively handles generics, unions, intersections, and type parameter instantiations—areas prone to edge cases or missed constructs.

Test coverage: Three test cases validate the decorated class path, parameter property shorthand, and the non-decorated control case, providing reasonable baseline coverage for the feature.

Consideration: Given that import elision is a sensitive optimization that directly affects bundle output, the interaction between the new pre-pass and downstream applyElisionEdits logic warrants careful verification that no regressions are introduced for existing type-only scenarios.


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 9, 2026
@brandonroberts brandonroberts merged commit 9de43fa into beta Apr 9, 2026
22 of 26 checks passed
@brandonroberts brandonroberts deleted the fix/constructor-di-type-elision branch April 9, 2026 12:39
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