angular/packages/compiler-cli/src/ngtsc/core
JoostK bc54687c7b fix(compiler-cli): exclude abstract classes from strictInjectionParameters requirement (#44615)
In AOT compilations, the `strictInjectionParameters` compiler option can
be enabled to report errors when an `@Injectable` annotated class has a
constructor with parameters that do not provide an injection token, e.g.
only a primitive type or interface.

Since Ivy it's become required that any class with Angular behavior
(e.g. the `ngOnDestroy` lifecycle hook) is decorated using an Angular
decorator, which meant that `@Injectable()` may need to have been added
to abstract base classes. Doing so would then report an error if
`strictInjectionParameters` is enabled, if the abstract class has an
incompatible constructor for DI purposes. This may be fine though, as
a subclass may call the constructor explicitly without relying on
Angular's DI mechanism.

Therefore, this commit excludes abstract classes from the
`strictInjectionParameters` check. This avoids an error from being
reported at compile time. If the constructor ends up being used by
Angular's DI system at runtime, then the factory function of the
abstract class will throw an error by means of the `ɵɵinvalidFactory`
instruction.

In addition to the runtime error, this commit also analyzes the inheritance
chain of an injectable without a constructor to verify that their inherited
constructor is valid.

BREAKING CHANGE: Invalid constructors for DI may now report compilation errors

When a class inherits its constructor from a base class, the compiler may now
report an error when that constructor cannot be used for DI purposes. This may
either be because the base class is missing an Angular decorator such as
`@Injectable()` or `@Directive()`, or because the constructor contains parameters
which do not have an associated token (such as primitive types like `string`).
These situations used to behave unexpectedly at runtime, where the class may be
constructed without any of its constructor parameters, so this is now reported
as an error during compilation.

Any new errors that may be reported because of this change can be resolved either
by decorating the base class from which the constructor is inherited, or by adding
an explicit constructor to the class for which the error is reported.

Closes #37914

PR Close #44615
2022-10-10 21:46:25 +00:00
..
api refactor(compiler-cli): remove enableIvy options (#47346) 2022-09-06 11:33:48 -07:00
src fix(compiler-cli): exclude abstract classes from strictInjectionParameters requirement (#44615) 2022-10-10 21:46:25 +00:00
test build: switch devmode output to es2015 (#44505) 2022-01-05 23:20:20 +00:00
BUILD.bazel fix(compiler-cli): exclude abstract classes from strictInjectionParameters requirement (#44615) 2022-10-10 21:46:25 +00:00
index.ts ci: add lint error for files with missing trailing new-line (#42478) 2021-06-04 13:31:03 -07:00
README.md refactor(compiler-cli): introduce CompilationTicket system for NgCompiler (#40561) 2021-01-27 10:45:57 -08:00

What is the 'core' package?

This package contains the core functionality of the Angular compiler. It provides APIs for the implementor of a TypeScript compiler to provide Angular compilation as well.

It supports the 'ngc' command-line tool and the Angular CLI (via the NgtscProgram), as well as an experimental integration with tsc_wrapped and the ts_library Bazel rule via NgTscPlugin.

Angular compilation

Angular compilation involves the translation of Angular decorators into static definition fields. At build time, this is done during the overall process of TypeScript compilation, where TypeScript code is type-checked and then downleveled to JavaScript code. Along the way, diagnostics specific to Angular can also be produced.

Compilation flow

Any use of the TypeScript compiler APIs follows a multi-step process:

  1. A ts.CompilerHost is created.
  2. That ts.CompilerHost, plus a set of "root files", is used to create a ts.Program.
  3. The ts.Program is used to gather various kinds of diagnostics.
  4. Eventually, the ts.Program is asked to emit, and JavaScript code is produced.

A compiler which integrates Angular compilation into this process follows a very similar flow, with a few extra steps:

  1. A ts.CompilerHost is created.
  2. That ts.CompilerHost is wrapped in an NgCompilerHost, which adds Angular specific files to the compilation.
  3. A ts.Program is created from the NgCompilerHost and its augmented set of root files.
  4. A CompilationTicket is created, optionally incorporating any state from a previous compilation run.
  5. An NgCompiler is created using the CompilationTicket.
  6. Diagnostics can be gathered from the ts.Program as normal, as well as from the NgCompiler.
  7. Prior to emit, NgCompiler.prepareEmit is called to retrieve the Angular transformers which need to be fed to ts.Program.emit.
  8. emit is called on the ts.Program with the Angular transformers from above, which produces JavaScript code with Angular extensions.

NgCompiler and incremental compilation

The Angular compiler is capable of incremental compilation, where information from a previous compilation is used to accelerate the next compilation. During compilation, the compiler produces two major kinds of information: local information (such as component and directive metadata) and global information (such as reified NgModule scopes). Incremental compilation is managed in two ways:

  1. For most changes, a new NgCompiler can selectively inherit local information from a previous instance, and only needs to recompute it where an underlying TypeScript file has change. Global information is always recomputed from scratch in this case.

  2. For specific changes, such as those in component resources, an NgCompiler can be reused in its entirety, and updated to incorporate the effects of such changes without needing to recompute any other information.

Note that these two modes differ in terms of whether a new NgCompiler instance is needed or whether a previous one can reused. To prevent leaking this implementation complexity and shield consumers from having to manage the lifecycle of NgCompiler so specifically, this process is abstracted via CompilationTickets. Consumers first obtain a CompilationTicket (depending on the nature of the incoming change), and then use this ticket to retrieve an NgCompiler instance. In creating the CompilationTicket, the compiler can decide whether to reuse an old NgCompiler instance or to create a new one.

Asynchronous compilation

In some compilation environments (such as the Webpack-driven compilation inside the Angular CLI), various inputs to the compilation are only producible in an asynchronous fashion. For example, SASS compilation of styleUrls that link to SASS files requires spawning a child Webpack compilation. To support this, Angular has an asynchronous interface for loading such resources.

If this interface is used, an additional asynchronous step after NgCompiler creation is to call NgCompiler.analyzeAsync and await its Promise. After this operation completes, all resources have been loaded and the rest of the NgCompiler API can be used synchronously.

Wrapping the ts.CompilerHost

Angular compilation generates a number of synthetic files (files which did not exist originally as inputs), depending on configuration. Such files can include:

  • .ngfactory shim files, if requested.
  • .ngsummary shim files, if requested.
  • A flat module index file, if requested.
  • The __ng_typecheck__.ts file, which supports template type-checking code.

These files don't exist on disk, but need to appear as such to the ts.Program. This is accomplished by wrapping the ts.CompilerHost (which abstracts the outside world to the ts.Program) in an implementation which provides these synthetic files. This is the primary function of NgCompilerHost.

API definitions

The core package contains separate API definitions, which are used across the compiler. Of note is the interface NgCompilerOptions, which unifies various supported compilation options across Angular and TypeScript itself. It's assignable to ts.CompilerOptions, and implemented by the legacy CompilerOptions type in //packages/compiler-cli/src/transformers/api.ts.

The various types of options are split out into distinct interfaces according to their purpose and level of support.