docs(compiler): ivy separate compilation design document (#22480)

PR Close #22480
This commit is contained in:
Chuck Jazdzewski 2018-02-27 10:15:23 -08:00 committed by Alex Eagle
parent 0e311e3918
commit 8bb2f5c71d
1 changed files with 835 additions and 0 deletions

View File

@ -0,0 +1,835 @@
# DESIGN DOC (Ivy): Separate Compilation
AUTHOR: chuckj@
## Background
### Angular 5 (Renderer2)
In 5.0 and prior versions of Angular the compiler performs whole program
analysis and generates template and injector definitions that are using
this global knowledge to flatten injector scope definitions, inline
directives into the component, pre-calculate queries, pre-calculate
content projection, etc. This global knowledge requires that module and
component factories are generated as a final global step when compiling
a module. If any of the transitive information changed then all factories
need to be regenerated.
Separate component and module compilation is supported only at the module
definition level and only from source. That is, npm packages must contain
the metadata necessary to generate the factories, they cannot contain,
themselves, the generated factories. This is because, if any of there
dependencies change, their factories would be invalid preventing them from
using version ranges in their dependencies. To support producing factories
from compiled source (already translated by TypeScript into JavaScript)
libraries include metadata that describe the content of the the Angular
decorators.
This document refers to this style of code generation as Renderer2 after the
name of the renderer class it uses at runtime.
### Angular Ivy
In Ivy, the runtime is crafted in a way that allows for separate compilation
by performing at runtime much of what was previously pre-calculated by
compiler. This allows the definition of components to change without
requiring modules and components that depend on them being recompiled.
The mental model of Ivy is that the decorator is the compiler. That is
the decorator can be thought of as parameters to a class transformer that
transforms the class by generating definitions based on the decorator
parameters. An `@Component` decorator transforms the class by adding
an `ngComponentDef` static property, `@Directive` adds `ngDirectiveDef`,
`@Pipe` adds `ngPipeDef`, etc. In most cases values supplied to the
decorator is sufficient to generate the definition. However, in the case of
interpreting the template, the compiler needs to know the selector defined for
each component, directive and pipe that are in scope of the template. The
purpose of this document is to define the information is needed by the
compiler and how that information is is serialized to be discovered and
used by subsequent calls to `ngc`.
This document refers to this style of code generation as ivy after the code
name of the project to create it. It would be more consistent to refer to it
as Renderer3 that looks too similar to Renderer2.
## Information needed
The information available across compilations in Angular 5 is represented in
the compiler by a summary description. For example, components and directives
are represented by the [`CompileDirectiveSummary`](https://github.com/angular/angular/blob/d3827a0017fd5ff5ac0f6de8a19692ce47bf91b4/packages/compiler/src/compile_metadata.ts#L257).
The following table shows where this information ends up in an ivy compiled
class:
### `CompileDirectiveSummary`
| field | destination |
|---------------------|-----------------------|
| `type` | implicit |
| `isComponent` | `ngComponentDef` |
| `selector` | `ngModuleScope` |
| `exportAs` | `ngDirectiveDef` |
| `inputs` | `ngDirectiveDef` |
| `outputs` | `ngDirectiveDef` |
| `hostListeners` | `ngDirectiveDef` |
| `hostProperties` | `ngDirectiveDef` |
| `hostAttributes` | `ngDirectiveDef` |
| `providers` | `ngInjectorDef` |
| `viewProviders` | `ngComponentDef` |
| `queries` | `ngDirectiveDef` |
| `guards` | not used |
| `viewQueries` | `ngComponentDef` |
| `entryComponents` | not used |
| `changeDetection` | `ngComponentDef` |
| `template` | `ngComponentDef` |
| `componentViewType` | not used |
| `renderType` | not used |
| `componentFactory` | not used |
Only one definition is generated per class. All components are directives so a
`ngComponentDef` contains all the `ngDirectiveDef` information. All directives
are injectable so `ngComponentDef` and `ngDirectiveDef` contain `ngInjectableDef`
information.
For `CompilePipeSummary` the table looks like:
#### `CompilePipeSummary`
| field | destination |
|---------------------|-----------------------|
| `type` | implicit |
| `name` | `ngModuleScope` |
| `pure` | `ngPipeDef` |
The only pieces of information that are not generated into the definition are
the directive selector and the pipe name as they go into the module scope.
The information needed to build an `ngModuleScope` needs to be communicated
from the directive and pipe to the module that declares them.
## Metadata
### Angular 5
Angular 5 uses `.metadata.json` files to store information that is directly
inferred from the `.ts` files and include value information that is not
included in the `.d.ts` file produced by TypeScript. Because only exports for
types are included in `.d.ts` files and might not include the exports necessary
for values, the metadata includes export clauses from the `.ts` file.
When a module is flattened into a FESM (Flat ECMAScript Module), a flat
metadata file is also produced which is the metadata for all symbols exported
from the module index. The metadata represents what the `.metadata.json` file
would look like if all the symbols were declared in the index instead of
reexported from the index.
### Angular Ivy
The metadata for a class in ivy is transformed to be what the metadata of the
transformed .js file produced by the ivy compiler would be. For example, a
component's `@Component` is removed by the compiler and replaced by a `ngComponentDef`.
The `.metadata.json` file is similarly transformed but the content of the
value assigned is elided (e.g. `"ngComponentDef": {}`). The compiler doesn't
record the selector declared for a component but it is needed to produce the
`ngModuleScope` so the information is recorded as if a static field
`ngSelector` was declared on class with the value of the `selector` field
from the `@Component` or `@Directive` decorator.
The following transformations are performed:
#### `@Component`
The metadata for a component is transformed by:
1. Removing the `@Component` directive.
2. Add `"ngComponentDef": {}` static field.
3. Add `"ngSelector": <selector-value>` static field.
##### Example
*my.component.ts*
```ts
@Component({
selector: 'my-comp',
template: `<h1>Hello, {{name}}!</h1>`
})
export class MyComponent {
@Input() name: string;
}
```
*my.component.js*
```js
export class MyComponent {
name: string;
static ngComponentDef = defineComponent({...});
}
```
*my.component.metadata.json*
```json
{
"__symbolic": "module",
"version": 4,
"metadata": {
"MyComponent": {
"__symbolic": "class",
"statics": {
"ngComponentDef": {},
"ngSelector": "my-comp"
}
}
}
}
```
Note that this is exactly what is produced if the transform had been done
manually or by some other compiler before `ngc` compiler is invoked. That is
this model has the advantage that there is no magic introduced by the compiler
as it treats classes annotated by `@Component` identically to those produced
manually.
#### `@Directive`
The metadata for a directive is transformed by:
1. Removing the `@Directive` directive.
2. Add `"ngDirectiveDef": {}` static field.
3. Add `"ngSelector": <selector-value>` static field.
##### example
*my.directive.ts*
```ts
@Directive({selector: '[my-dir]'})
export class MyDirective {
@HostBinding('id') dirId = 'some id';
}
```
*my.directive.js*
```js
export class MyDirective {
constructor() {
this.dirId = 'some id';
}
static ngDirectiveDef = defineDirective({...});
}
```
*my.directive.metadata.json*
```json
{
"__symbolic": "module",
"version": 4,
"metadata": {
"MyDirective": {
"__symbolic": "class",
"statics": {
"ngDirectiveDef": {},
"ngSelector": "[my-dir]"
}
}
}
}
```
#### `@Pipe`
The metadata for a pipe is transformed by:
1. Removing the `@Pipe` directive.
2. Add `"ngPipeDef": {}` static field.
3. Add `"ngSelector": <name-value>` static field.
##### example
*my.pipe.ts*
```ts
@Pipe({name: 'myPipe'})
export class MyPipe implements PipeTransform {
transform(...) ...
}
```
*my.pipe.js*
```js
export class MyPipe {
transform(...) ...
static ngPipeDef = definePipe({...});
}
```
*my.pipe.metadata.json*
```json
{
"__symbolic": "module",
"version": 4,
"metadata": {
"MyPipe": {
"__symbolic": "class",
"statics": {
"ngPipeDef": {},
"ngSelector": "myPipe"
}
}
}
}
```
#### `@NgModule`
The metadata for a module is transformed by:
1. Remove the `@NgModule` directive.
2. Add `"ngInjectorDef": {}` static field.
3. Add `"ngModuleScope": <module-scope>` static field.
The scope value is an array the following type:
```ts
export type ModuleScope = ModuleScopeEntry[];
export interface ModuleDirectiveEntry {
type: Type;
selector: string;
}
export interface ModulePipeEntry {
type: Type;
name: string;
isPipe: true;
}
export interface ModuleExportEntry {
type: Type;
isModule: true;
}
type ModuleScopeEntry = ModuleDirectiveEntry | ModulePipeEntry | ModuleExportEntry;
```
where the `type` values are generated as references.
##### example
*my.module.ts*
```ts
@NgModule({
imports: [CommonModule, UtilityModule],
declarations: [MyComponent, MyDirective, MyComponent],
exports: [MyComponent, MyDirective, MyPipe, UtilityModule],
providers: [{
provide: Service, useClass: ServiceImpl
}]
})
export class MyModule {}
```
*my.module.js*
```js
export class MyModule {
static ngInjectorDef = defineInjector(...);
}
```
*my.module.metadata.json*
```json
{
"__symbolic": "module",
"version": 4,
"metadata": {
"MyModule": {
"__symbolic": "class",
"statics": {
"ngInjectorDef": {},
"ngModuleScope": [
{
"type": {
"__symbolic": "reference",
"module": "./my.component",
"name": "MyComponent"
},
"selector": "my-comp"
},
{
"type": {
"__symbolic": "reference",
"module": "./my.directive",
"name": "MyDirective"
},
"selector": "[my-dir]"
},
{
"type": {
"__symbolic": "reference",
"module": "./my.pipe",
"name": "MyPipe"
},
"name": "myPipe",
"isPipe": true
},
{
"type": {
"__symbolic": "reference",
"module": "./utility.module",
"name": "UtilityModule"
},
"isModule": true
}
]
}
}
}
}
```
Note that this is identical to what would have been generated if the this was
manually written as:
```ts
export class MyModule {
static ngInjectorDef = defineInjector({
providers: [{
provide: Service, useClass: ServiceImpl
}],
imports: [CommonModule, UtilityModule]
});
static ngModuleScope = [{
type: MyComponent,
selector: 'my-comp'
}, {
type: MyDirective,
selector: '[my-dir]'
}, {
type: MyPipe,
name: 'myPipe'
}, {
type: UtilityModule,
isModule: true
}];
}
```
except for the call to `defineInjector` would generate a `{ __symbolic: 'error' }`
value which is ignored by the ivy compiler. This allows the system to ignore
the difference between manually and mechanically created module definitions.
## Manual Considerations
With this proposal, the compiler treats manually and mechanically generated
Angular definitions identically. This allows flexibility not only in the future
for how the declarations are mechanically produced it also allows alternative
mechanism to generate declarations be easily explored without altering the
compiler or dependent tool chain. It also allows third-party code generators
with possibly different component syntaxes to generate a component fully
understood by the compiler.
Unfortunately, however, manually generated modules contain references to
classes that might not be necessary at runtime. Manually or third-party
components can get the same payload properties of an Angular generated
component by annotating the `ngSelector` and `ngModuleScope` properties with
`// @__BUILD_OPTIMIZER_REMOVE_` comment which will cause the build optimizer
to remove the declaration.
##### example
For example the above manually created module would have better payload
properties by including a `// @__BUILD_OPTIMIZER_REMOVE_` comment:
```ts
export class MyModule {
static ngInjectorDef = defineInjector({
providers: [{
provide: Service, useClass: ServiceImpl
}],
imports: [CommonModule, UtilityModule]
});
// @__BUILD_OPTIMIZER_REMOVE_
static ngModuleScope = [{
type: MyComponent,
selector: 'my-comp'
}, {
type: MyDirective,
selector: '[my-dir]'
}, {
type: MyPipe,
name: 'myPipe'
}, {
type: UtilityModule,
isModule: true
}];
}
```
## `ngc` output (non-Bazel)
The cases that `ngc` handle are producing an application and producing a
reusable library used in an application.
### Application output
The output of the ivy compiler only optionally generates the factories
generated by the Renderer2 style output of Angular 5.0. In ivy, the information
that was generated in factories is now generated in Angular as a definition
that is generated as a static field on the Angular decorated class.
Renderer2 requires that, when building the final application, all factories for
all libraries also be generated. In ivy, the definitions are generated when
the library is compiled.
The ivy compile can adapt Renderer2 target libraries by generating the factories
for them and back-patching, at runtime, the static property into the class.
#### Back-patching module (`"renderer2BackPatching"`)
When an application contains Renderer2 target libraries the ivy definitions
need to be back-patch onto the component, directive, module, pipe, and
injectable classes.
If the Angular compiler option `"renderer2BackPatching"` is enabled, the
compiler will generate an `angular.back-patch` module in the to root output
directory of the project. If `"generateRenderer2Factories"` is set to `true`
then the default value for `"renderer2BackPatching"` is `true` and it is and
error for it to be `false`. `"renderer2BackPatching"` is ignored if `"enableIvy"`
is `false`.
`angular.back-patch` exports a function per `@NgModule` for the entire
application, including previously compiled libraries. The name of the function
is determined by name of the imported module with all non alphanumeric
character, including '`/`' and '`.`', replaced by '`_`'.
The back-patch functions will call the back-patch function of any module they
import. This means that only the application's module and lazy loaded modules
back-patching functions needs to be called. If using the Renderer2 module factory
instances, this is performed automatically when the first application module
instance is created.
#### Renderer2 Factories (`"generateRenderer2Factories"`)
`ngc` can generate an implementation of `NgModuleFactory` in the same location
that Angular 5.0 would generate it. This implementation of `NgModuleFactory`
will back-patch the Renderer2 style classes when the first module instance is
created by calling the correct back-patching function generated in the`angular.back-patch`
module.
Renderer2 style factories are created when the `"generateRenderer2Factories"`
Angular compiler option is `true`. Setting `"generateRenderer2Factories"` implies
`"renderer2BackPatching"` is also `true` and it is an error to explicitly set it
to `false`. `"generateRenderer2Factories"` is ignored if `"enableIvy"` is
`false`.
When this option is `true` a factory module is created with the same public API
at the same location as Angular 5.0 whenever Angular 5.0 would have generated a
factory.
### Recommended options
The recommended options for producing a ivy application are
| option | value | |
|--------------------------------|----------|-------------|
| `"enableIvy"` | `true` | required |
| `"generateRenderer2Factories"` | `true` | implied |
| `"renderer2BackPatching"` | `true` | implied |
| `"generateCodeForLibraries"` | `true` | default |
| `"annotationsAs"` | `remove` | implied |
| `"enableLegacyTemplate"` | `false` | default |
| `"preserveWhitespaces"` | `false` | default |
| `"skipMetadataEmit"` | `true` | default |
| `"strictMetadataEmit"` | `false` | implied |
| `"skipTemplateCodegen"` | | ignored |
The options marked "implied" are implied by other options having the
recommended value and do not need to be explicitly set. Options marked
"default" also do not need to be set explicitly.
## Library output
Building an ivy library with `ngc` differs from Renderer2 in that the
declarations are included in the generated output and should be included in the
package published to `npm`. The `.metadata.json` files still need to be
included but they are transformed as described below.
### Transforming metadata
As described above, when the compiler adds the declaration to the class it will
also transform the `.metadata.json` file to reflect the new static fields added
to the class.
Once the static fields are added to the metadata, the ivy compiler no longer
needs the the information in the decorator. When `"enableIvy"` is `true` this
information is removed from the `.metadata.json` file.
### Recommended options
The recommended options for producing a ivy library are:
| option | value | |
|--------------------------------|----------|-------------|
| `"enableIvy"` | `true` | required |
| `"generateRenderer2Factories"` | `false` | |
| `"renderer2BackPatching"` | `false` | default |
| `"generateCodeForLibraries"` | `false` | |
| `"annotationsAs"` | `remove` | implied |
| `"enableLegacyTemplate"` | `false` | default |
| `"preserveWhitespaces"` | `false` | default |
| `"skipMetadataEmit"` | `false` | |
| `"strictMetadataEmit"` | `true ` | |
| `"skipTemplateCodegen"` | | ignored |
The options marked "implied" are implied by other options having the
recommended value and do not need to be explicitly set. Options marked
"default" also do not need to be set explicitly.
## Simplified options
The default Angular Compiler options default to, mostly, the recommended set of
options but the options necessary to set for specific targets are not clear and
mixing them can produce nonsensical results. The `"target"` option can be used
to simplify the setting of the compiler options to the recommended values
depending on the target:
| target | option | value | |
|-------------------|--------------------------------|--------------|-------------|
| `"application"` | `"generateRenderer2Factories"` | `true` | enforced |
| | `"renderer2BackPatching"` | `true` | enforced |
| | `"generateCodeForLibraries"` | `true` | |
| | `"annotationsAs"` | `remove` | |
| | `"enableLegacyTemplate"` | `false` | |
| | `"preserveWhitespaces"` | `false` | |
| | `"skipMetadataEmit"` | `false` | |
| | `"strictMetadataEmit"` | `true` | |
| | `"skipTemplateCodegen"` | `false` | |
| | `"fullTemplateTypeCheck"` | `true` | |
| | `"enableLegacyTemplate"` | `false` | |
| | | | |
| `"library"` | `"generateRenderer2Factories"` | `false` | enforced |
| | `"renderer2BackPatching"` | `false` | enforced |
| | `"generateCodeForLibraries"` | `false` | enforced |
| | `"annotationsAs"` | `decorators` | |
| | `"enableLegacyTemplate"` | `false` | |
| | `"preserveWhitespaces"` | `false` | |
| | `"skipMetadataEmit"` | `false` | enforced |
| | `"strictMetadataEmit"` | `true` | |
| | `"skipTemplateCodegen"` | `false` | enforced |
| | `"fullTemplateTypeCheck"` | `true` | |
| | `"enableLegacyTemplate"` | `false` | |
| | | | |
| `"package"` | `"flatModuleOutFile"` | | required |
| | `"flatModuleId"` | | required |
| | `"enableIvy"` | `false` | enforced |
| | `"generateRenderer2Factories"` | `false` | enforced |
| | `"renderer2BackPatching"` | `false` | enforced |
| | `"generateCodeForLibraries"` | `false` | enforced |
| | `"annotationsAs"` | `remove` | |
| | `"enableLegacyTemplate"` | `false` | |
| | `"preserveWhitespaces"` | `false` | |
| | `"skipMetadataEmit"` | `false` | enforced |
| | `"strictMetadataEmit"` | `true` | |
| | `"skipTemplateCodegen"` | `false` | enforced |
| | `"fullTemplateTypeCheck"` | `true` | |
| | `"enableLegacyTemplate"` | `false` | |
Options that are marked "enforced" are reported as an error if they are
explicitly set to a value different from what is specified here. The options
marked "required" are required to be set and an error message is displayed if
no value is supplied but no default is provided.
The purpose of the "application" target is for the options used when the `ngc`
invocation contains the root application module. Lazy loaded modules should
also be considered "application" targets.
The purpose of the "library" target is for are all `ngc` invocations that do
not contain the root application module or a lazy loaded module.
The purpose of the "package" target is to produce a library package that will
be an entry point for an npm package. Each entry point should be separately
compiled using a "package" target.
##### example - application
To produce a Renderer2 application the options would look like,
```json
{
"compileOptions": {
...
},
"angularCompilerOptions": {
"target": "application"
}
}
```
alternately, since the recommended `"application"` options are the default
values, the `"angularCompilerOptions"` can be out.
##### example - library
To produce a Renderer2 library the options would look like,
```json
{
"compileOptions": {
...
},
"angularCompilerOptions": {
"target": "library"
}
}
```
##### example - package
To produce a Renderer2 package the options would look like,
```json
{
"compileOptions": {
...
},
"angularCompilerOptions": {
"target": "package"
}
}
```
##### example - ivy application
To produce an ivy application the options would look like,
```json
{
"compileOptions": {
...
},
"angularCompilerOptions": {
"target": "application",
"enableIvy": true
}
}
```
##### example - ivy library
To produce an ivy application the options would look like,
```json
{
"compileOptions": {
...
},
"angularCompilerOptions": {
"target": "library",
"enableIvy": true
}
}
```
##### example - ivy package
Ivy packages are not supported in Angular 6.0 as they are not recommended in
npm packages as they would only be usable if in ivy application where an ivy
application. Ivy application support Renderer2 libraries so npm packages
should all be Renderer2 libraries.
## `ng_module` output (Bazel)
The `ng_module` rule describes the source necessary to produce a Angular
library that is reusable and composable into an application.
### Angular 5.0
The `ng_module` rule invokes `ngc`[<sup>1<sup>](#ngc_wrapped) to produce
the Angular output. However, `ng_module` uses a feature, the `.ngsummary.json`
file, not normally used and is often difficult to configure correctly.
The `.ngsummary.json` describes all the information that is necessary for
the compiler to use a generated factory. It is produced by actions defined
in the `ng_module` rule and is consumed by actions defined by `ng_module`
rules that depend on other `ng_module` rules.
### Angular Ivy
The `ng_module` rule will still use `ngc` to produce the Angular output but,
when producing ivy output, it no longer will need the `.ngsummary.json` file.
#### `ng_experimental_ivy_srcs`
The `ng_experimental_ivy_srcs` can be used as use to cause the ivy versions of
files to be generated. It is intended the sole dependency of a `ts_dev_server`
rule and the `ts_dev_server` sources move to `ng_experimental_iv_srcs`.
#### `ng_module` ivy output
The `ng_module` is able to provide the ivy version of the `.js` files which
will be generated with as `.ivy.js` for the development sources and `.ivy.closure.js`
for the production sources.
The `ng_module` rule will also generate a `angular.back_patch.js` and `.closure.js`
files and a `module_scope.json` file. The type of the `module_scope.json` file will
be:
```ts
interface ModuleScopeSummary {
[moduleName: string]: ModuleScopeEntry[];
}
```
where `moduleName` is the name of the as it would appear in an import statement
in a `.ts` file at the same relative location in the source tree. All the
references in this file are also relative this location.
##### example
The following is a typical Angular application build in bazel:
*src/BUILD.bazel*
```py
ng_module(
name = "src",
srcs = glob(["*.ts"]),
deps= ["//common/component"],
)
ts_dev_server(
name = "server",
srcs = ":src",
)
```
To use produce an ivy version you would add:
```py
ng_experimental_ivy_srcs(
name = "ivy_srcs",
srcs = ":src",
)
ts_dev_server(
name = "server_ivy",
srcs = [":ivy_srcs"]
)
```
To serve the Renderer2 version, you would run:
```sh
bazel run :server
```
to serve the ivy version you would run
```sh
bazel run :server_ivy
```
The `ng_experimental_ivy_srcs` rule is only needed when ivy is experimental. Once ivy
is released the `ng_experimental_ivy_srcs`, dependent rules, can be removed.
---
<a name="myfootnote1"><sup>1</sup></a> More correctly, it calls `performCompilation`
from the `@angular/compiler-cli` which is what `ngc` does too.