fix(angular-compiler): preserve constructor di token imports from elision#2270
Conversation
…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>
✅ Deploy Preview for analog-blog ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for analog-app ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for analog-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis 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 Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Key observationsLogic 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 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 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. Comment |
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(), causingError: This constructor was not compatible with Dependency Injectionat runtime.Affected scope
angular-compilerRecommended merge strategy for maintainer [optional]
Commit preservation note [optional]
What is the new behavior?
type-elision.tsruns 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,extractConstructorDepssees the runtime token, andɵfaccorrectly emitsi0.ɵɵdirectiveInject(MyService).Before
Compiled to:
Runtime:
Error: This constructor was not compatible with Dependency InjectionAfter
Edge cases handled
constructor(svc: Foo)parametersTSParameterProperty(constructor(private svc: Foo))constructor(svc: Service<Bar>))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 failuresnpx nx e2e analog-app-e2e— 11/11 passpnpm buildpnpm testDoes this PR introduce a breaking change?
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 usesinject()in field initializers wasn't affected becauseMyServiceappears in a value position (call argument) and was correctly preserved. This is why the bug didn't surface inanalog-app(which usesinject()everywhere).🤖 Generated with Claude Code