diff --git a/aio/content/guide/metadata.md b/aio/content/guide/metadata.md new file mode 100644 index 0000000000..f7f42cdc8a --- /dev/null +++ b/aio/content/guide/metadata.md @@ -0,0 +1,874 @@ +# Angular Metadata and AOT + +The Angular **AOT compiler** turns your TypeScript source code into runnable JavaScript. +As part of that process, the compiler extracts and interprets **metadata** about the parts of the application that Angular is supposed to manage. + +You write metadata in a _subset_ of TypeScript. This guide explains why a subset is necessary, describes the subset constraints, and what happens when you step outside of those constraints. + +## Angular metadata +Angular metadata tells Angular how to construct instances of your application classes and interact with them at runtime. + +You specify the metadata with **decorators** such as `@Component()` and `@Input()`. +You also specify metadata implicitly in the constructor declarations of these decorated classes. + +In the following example, the `@Component()` metadata object and the class constructor tell Angular how to create and display an instance of `TypicalComponent`. + +```typescript +@Component({ + selector: 'app-typical', + template: 'div>A typical component for {{data.name}} +)} +export class TypicalComponent { + @Input() data: TypicalData; + constructor(private someService: SomeService) { ... } +} +``` + +The Angular compiler extracts the metadata _once_ and generates a _factory_ for `TypicalComponent`. +When it needs to create a `TypicalComponent` instance, Angular calls the factory, which produces a new visual element, bound to a new instance of the component class with its injected dependency. + +## Compile ahead-of-time (AOT) + +You should use AOT to compile an application that must launch quickly. +With AOT, there is no runtime compile step. +The client doesn't need the compiler library at all and excluding it significantly reduces the total payload. +The browser downloads a smaller set of safely-compiled, application module(s) and libraries that it can parse quickly and run almost immediately. + +The AOT compiler produces a number of files, including the application JavaScript that ultimately runs in the browser. It then statically analyzes your source code and interprets the Angular metadata without actually running the application. + +To compile the app, run the `ngc` stand-alone tool as part of your build process. +When using the CLI, run the `ng build` command. + +For more information on AOT, see [Ahead-of-Time Compilation](guide/aot-compiler). + +## Metadata restrictions + +Angular metadata expressions must conform to the following general constraints: + +1. Limit [expression syntax](#expression-syntax) to the supported subset of JavaScript. +2. Only reference exported symbols after [code folding](#folding). +3. Only call [functions supported](#supported-functions) by the compiler. +4. Decorated and data-bound class members must be public. + +The next sections elaborate on these points. + +## How AOT works + +It helps to think of the AOT compiler as having two phases: a code analysis phase in which it simply records a representation of the source; and a code generation phase in which the compiler's `StaticReflector` handles the interpretation as well as places restrictions on what it interprets. + +## Phase 1: analysis + +The TypeScript compiler does some of the analytic work of the first phase. It emits the `.d.ts` _type definition files_ with type information that the AOT compiler needs to generate application code. + +At the same time, the AOT **_collector_** analyzes the metadata recorded in the Angular decorators and outputs metadata information in **`.metadata.json`** files, one per `.d.ts` file. + +You can think of `.metadata.json` as a diagram of the overall structure of a decorator's metadata, represented as an [abstract syntax tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree). + +
+ +Angular's [schema.ts](https://github.com/angular/angular/blob/master/packages/tsc-wrapped/src/schema.ts) +describes the JSON format as a collection of TypeScript interfaces. + +
+ +{@a expression-syntax} +### Expression syntax + +The _collector_ only understands a subset of JavaScript. +Define metadata objects with the following limited syntax: + +Syntax | Example +-----------------------------------|----------------------------------- +Literal object | `{cherry: true, apple: true, mincemeat: false}` +Literal array | `['cherries', 'flour', 'sugar']` +Spread in literal array | `['apples', 'flour', ...the_rest]` +Calls | `bake(ingredients)` +New | `new Oven()` +Property access | `pie.slice` +Array index | `ingredients[0]` +Identifier reference | `Component` +A template string | `pie is ${multiplier} times better than cake` +Literal string | `'pi'` +Literal number | `3.14153265` +Literal boolean | `true` +Literal null | `null` +Supported prefix operator | `!cake` +Supported Binary operator | `a + b` +Conditional operator | `a ? b : c` +Parentheses | `(a + b)` + +If an expression uses unsupported syntax, the _collector_ writes an error node to the `.metadata.json` file. The compiler later reports the error if it needs that +piece of metadata to generate the application code. + +
+ + If you want `ngc` to report syntax errors immediately rather than produce a `.metadata.json` file with errors, set the `strictMetadataEmit` option in `tsconfig`. + +``` + "angularCompilerOptions": { + ... + "strictMetadataEmit" : true + } + ``` + +Angular libraries have this option to ensure that all Angular `.metadata.json` files are clean and it is a best practice to do the same when building your own libraries. + +
+ +{@a function-expression} +{@a arror-functions} +### No arrow functions + +The AOT compiler does not support [function expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function) +and [arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions), also called _lambda_ functions. + +Consider the following component decorator: + +```ts +@Component({ + ... + providers: [{provide: server, useFactory: () => new Server()}] +}) +``` + +The AOT _collector_ does not support the arrow function, `() => new Server()`, in a metadata expression. +It generates an error node in place of the function. + +When the compiler later interprets this node, it reports an error that invites you to turn the arrow function into an _exported function_. + +You can fix the error by converting to this: + +```ts +export function serverFactory() { + return new Server(); +} + +@Component({ + ... + providers: [{provide: server, useFactory: serverFactory}] +}) +``` + +### Limited function calls + +The _collector_ can represent a function call or object creation with `new` as long as the syntax is valid. The _collector_ only cares about proper syntax. + +But beware. The compiler may later refuse to generate a call to a _particular_ function or creation of a _particular_ object. +The compiler only supports calls to a small set of functions and will use `new` for only a few designated classes. These functions and classes are in a table of [below](#supported-functions). + + +### Folding +{@a exported-symbols} +The compiler can only resolve references to **_exported_** symbols. +Fortunately, the _collector_ enables limited use of non-exported symbols through _folding_. + +The _collector_ may be able to evaluate an expression during collection and record the result in the `.metadata.json` instead of the original expression. + +For example, the _collector_ can evaluate the expression `1 + 2 + 3 + 4` and replace it with the result, `10`. + +This process is called _folding_. An expression that can be reduced in this manner is _foldable_. + +{@a var-declaration} +The collector can evaluate references to +module-local `const` declarations and initialized `var` and `let` declarations, effectively removing them from the `.metadata.json` file. + +Consider the following component definition: + +```ts +const template = '
{{hero.name}}
'; + +@Component({ + selector: 'app-hero', + template: template +}) +class HeroComponent { + @Input() hero: Hero; +} +``` + +The compiler could not refer to the `template` constant because it isn't exported. + +But the _collector_ can _fold_ the `template` constant into the metadata definition by inlining its contents. +The effect is the same as if you had written: + +```TypeScript +@Component({ + selector: 'app-hero', + template: '
{{hero.name}}
' +}) +class HeroComponent { + @Input() hero: Hero; +} +``` + +There is no longer a reference to `template` and, therefore, nothing to trouble the compiler when it later interprets the _collector's_ output in `.metadata.json`. + +You can take this example a step further by including the `template` constant in another expression: + +```TypeScript +const template = '
{{hero.name}}
'; + +@Component({ + selector: 'app-hero', + template: template + '
{{hero.title}}
' +}) +class HeroComponent { + @Input() hero: Hero; +} +``` + +The _collector_ reduces this expression to its equivalent _folded_ string: + +`'
{{hero.name}}
{{hero.title}}
'`. + +#### Foldable syntax + +The following table describes which expressions the _collector_ can and cannot fold: + +Syntax | Foldable +-----------------------------------|----------------------------------- +Literal object | yes +Literal array | yes +Spread in literal array | no +Calls | no +New | no +Property access | yes, if target is foldable +Array index | yes, if target and index are foldable +Identifier reference | yes, if it is a reference to a local +A template with no substitutions | yes +A template with substitutions | yes, if the substitutions are foldable +Literal string | yes +Literal number | yes +Literal boolean | yes +Literal null | yes +Supported prefix operator | yes, if operand is foldable +Supported binary operator | yes, if both left and right are foldable +Conditional operator | yes, if condition is foldable +Parentheses | yes, if the expression is foldable + +If an expression is not foldable, the collector writes it to `.metadata.json` as an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree) for the compiler to resolve. + + +## Phase 2: code generation + +The _collector_ makes no attempt to understand the metadata that it collects and outputs to `.metadata.json`. It represents the metadata as best it can and records errors when it detects a metadata syntax violation. + +It's the compiler's job to interpret the `.metadata.json` in the code generation phase. + +The compiler understands all syntax forms that the _collector_ supports, but it may reject _syntactically_ correct metadata if the _semantics_ violate compiler rules. + +The compiler can only reference _exported symbols_. + +Decorated component class members must be public. You cannot make an `@Input()` property private or internal. + +Data bound properties must also be public. + +```TypeScript +// BAD CODE - title is private +@Component({ + selector: 'app-root', + template: '

{{title}}

' +}) +class AppComponent { + private title = 'My App'; // Bad +} +``` + +{@a supported-functions} +Most importantly, the compiler only generates code to create instances of certain classes, support certain decorators, and call certain functions from the following lists. + + +### New instances + +The compiler only allows metadata that create instances of these Angular classes. + +Class | Module +-----------------|-------------- +`OpaqueToken` | `@angular/core` +`InjectionToken` | `@angular/core` + + +### Annotations/Decorators + +The compiler only supports metadata for these Angular decorators. + +Decorator | Module +------------------|-------------- +`Attribute` | `@angular/core` +`Component` | `@angular/core` +`ContentChild` | `@angular/core` +`ContentChildren` | `@angular/core` +`Directive` | `@angular/core` +`Host` | `@angular/core` +`HostBinding` | `@angular/core` +`HostListener` | `@angular/core` +`Inject` | `@angular/core` +`Injectable` | `@angular/core` +`Input` | `@angular/core` +`NgModule` | `@angular/core` +`Optional` | `@angular/core` +`Output` | `@angular/core` +`Pipe` | `@angular/core` +`Self` | `@angular/core` +`SkipSelf` | `@angular/core` +`ViewChild` | `@angular/core` + + +### Macro-functions and macro-static methods + +The compiler also supports _macros_ in the form of functions or static +methods that return an expression. + +For example, consider the following function: + +```TypeScript +export function wrapInArray(value: T): T[] { + return [value]; +} +``` + +You can call the `wrapInArray` in a metadata definition because it returns the value of an expression that conforms to the compiler's restrictive JavaScript subset. + +You might use `wrapInArray()` like this: + +```TypeScript +@NgModule({ + declarations: wrapInArray(TypicalComponent) +}) +class TypicalModule {} +``` + +The compiler treats this usage as if you had written: + +```TypeScript +@NgModule({ + declarations: [TypicalComponent] +}) +class TypicalModule {} +``` + +The collector is simplistic in its determination of what qualifies as a macro +function; it can only contain a single `return` statement. + +The Angular [`RouterModule`](api/router/RouterModule) exports two macro static methods, `forRoot` and `forChild`, to help declare root and child routes. +Review the [source code](https://github.com/angular/angular/blob/master/packages/router/src/router_module.ts#L139 "RouterModule.forRoot source code") +for these methods to see how macros can simplify configuration of complex Angular modules. + +## Metadata Errors + +The following are metadata errors you may encounter, with explanations and suggested corrections. + +[Expression form not supported](#expression-form-not-supported)
+[Reference to a local (non-exported) symbol](#reference-to-a-local-symbol)
+[Only initialized variables and constants](#only-initialized-variables)
+[Reference to a non-exported class](#reference-to-a-non-exported-class)
+[Reference to a non-exported function](#reference-to-a-non-exported-function)
+[Function calls are not supported](#function-calls-not-supported)
+[Destructured variable or constant not supported](#destructured-variable-not-supported)
+[Could not resolve type](#could-not-resolve-type)
+[Name expected](#name-expected)
+[Unsupported enum member name](#unsupported-enum-member-name)
+[Tagged template expressions are not supported](#tagged-template-expressions-not-supported)
+[Symbol reference expected](#symbol-reference-expected)
+ +
+ +

Expression form not supported

+ +The compiler encountered an expression it didn't understand while evalutating Angular metadata. + +Language features outside of the compiler's [restricted expression syntax](#expression-syntax) +can produce this error, as seen in the following example: + +``` +// ERROR +export class Fooish { ... } +... +const prop = typeof Fooish; // typeof is not valid in metadata + ... + // bracket notation is not valid in metadata + { provide: 'token', useValue: { [prop]: 'value' } }; + ... +``` + +You can use `typeof` and bracket notation in normal application code. +You just can't use those features within expressions that define Angular metadata. + +Avoid this error by sticking to the compiler's [restricted expression syntax](#expression-syntax) +when writing Angular metadata +and be wary of new or unusual TypeScript features. + +
+ +{@a reference-to-a-local-symbol} +

Reference to a local (non-exported) symbol

+ +
+ +_Reference to a local (non-exported) symbol 'symbol name'. Consider exporting the symbol._ + +
+ +The compiler encountered a referenced to a locally defined symbol that either wasn't exported or wasn't initialized. + +Here's a `provider` example of the problem. + +``` +// ERROR +let foo: number; // neither exported nor initialized + +@Component({ + selector: 'my-component', + template: ... , + providers: [ + { provide: Foo, useValue: foo } + ] +}) +export class MyComponent {} +``` +The compiler generates the component factory, which includes the `useValue` provider code, in a separate module. _That_ factory module can't reach back to _this_ source module to access the local (non-exported) `foo` variable. + +You could fix the problem by initializing `foo`. + +``` +let foo = 42; // initialized +``` + +The compiler will [fold](#folding) the expression into the provider as if you had written this. + +``` + providers: [ + { provide: Foo, useValue: 42 } + ] +``` + +Alternatively, you can fix it by exporting `foo` with the expectation that `foo` will be assigned at runtime when you actually know its value. + +``` +// CORRECTED +export let foo: number; // exported + +@Component({ + selector: 'my-component', + template: ... , + providers: [ + { provide: Foo, useValue: foo } + ] +}) +export class MyComponent {} +``` + +Adding `export` often works for variables referenced in metadata such as `providers` and `animations` because the compiler can generate _references_ to the exported variables in these expressions. It doesn't need the _values_ of those variables. + +Adding `export` doesn't work when the compiler needs the _actual value_ +in order to generate code. +For example, it doesn't work for the `template` property. + +``` +// ERROR +export let someTemplate: string; // exported but not initialized + +@Component({ + selector: 'my-component', + template: someTemplate +}) +export class MyComponent {} +``` + +The compiler needs the value of the `template` property _right now_ to generate the component factory. +The variable reference alone is insufficient. +Prefixing the declaration with `export` merely produces a new error, "[`Only initialized variables and constants can be referenced`](#only-initialized-variables)". + +
+ +{@a only-initialized-variables} +

Only initialized variables and constants

+ +
+ +_Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler._ + +
+ +The compiler found a reference to an exported variable or static field that wasn't initialized. +It needs the value of that variable to generate code. + +The following example tries to set the component's `template` property to the value of +the exported `someTemplate` variable which is declared but _unassigned_. + +``` +// ERROR +export let someTemplate: string; + +@Component({ + selector: 'my-component', + template: someTemplate +}) +export class MyComponent {} +``` + +You'd also get this error if you imported `someTemplate` from some other module and neglected to initialize it there. + +``` +// ERROR - not initialized there either +import { someTemplate } from './config'; + +@Component({ + selector: 'my-component', + template: someTemplate +}) +export class MyComponent {} +``` + +The compiler cannot wait until runtime to get the template information. +It must statically derive the value of the `someTemplate` variable from the source code +so that it can generate the component factory, which includes +instructions for building the element based on the template. + +To correct this error, provide the initial value of the variable in an initializer clause _on the same line_. + +``` +// CORRECTED +export let someTemplate = '

Greetings from Angular

'; + +@Component({ + selector: 'my-component', + template: someTemplate +}) +export class MyComponent {} +``` + +
+ +

Reference to a non-exported class

+ +
+ +_Reference to a non-exported class . Consider exporting the class._ + +
+ +Metadata referenced a class that wasn't exported. + +For example, you may have defined a class and used it as an injection token in a providers array +but neglected to export that class. + +``` +// ERROR +abstract class MyStrategy { } + + ... + providers: [ + { provide: MyStrategy, useValue: ... } + ] + ... +``` + +Angular generates a class factory in a separate module and that +factory [can only access exported classes](#exported-symbols). +To correct this error, export the referenced class. + +``` +// CORRECTED +export abstract class MyStrategy { } + + ... + providers: [ + { provide: MyStrategy, useValue: ... } + ] + ... +``` +
+ +

Reference to a non-exported function

+ +Metadata referenced a function that wasn't exported. + +For example, you may have set a providers `useFactory` property to a locally defined function that you neglected to export. + +``` +// ERROR +function myStrategy() { ... } + + ... + providers: [ + { provide: MyStrategy, useFactory: myStrategy } + ] + ... +``` + +Angular generates a class factory in a separate module and that +factory [can only access exported functions](#exported-symbols). +To correct this error, export the function. + +``` +// CORRECTED +export function myStrategy() { ... } + + ... + providers: [ + { provide: MyStrategy, useFactory: myStrategy } + ] + ... +``` +
+ +{@a function-calls-not-supported} +

Function calls are not supported

+ +
+ +_Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function._ + +
+ +The compiler does not currently support [function expressions or lambda functions](#function-expression). +For example, you cannot set a provider's `useFactory` to an anonymous function or arrow function like this. + +``` +// ERROR + ... + providers: [ + { provide: MyStrategy, useFactory: function() { ... } }, + { provide: OtherStrategy, useFactory: () => { ... } } + ] + ... +``` +You also get this error if you call a function or method in a provider's `useValue`. +``` +// ERROR +import { calculateValue } from './utilities'; + + ... + providers: [ + { provide: SomeValue, useValue: calculateValue() } + ] + ... +``` + +To correct this error, export a function from the module and refer to the function in a `useFactory` provider instead. + + +// CORRECTED +import { calculateValue } from './utilities'; + +export function myStrategy() { ... } +export function otherStrategy() { ... } +export function someValueFactory() { + return calculateValue(); +} + ... + providers: [ + { provide: MyStrategy, useFactory: myStrategy }, + { provide: OtherStrategy, useFactory: otherStrategy }, + { provide: SomeValue, useFactory: someValueFactory } + ] + ... + + +
+ +{@a destructured-variable-not-supported} +

Destructured variable or constant not supported

+ +
+ +_Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring._ + +
+ +The compiler does not support references to variables assigned by [destructuring](https://www.typescriptlang.org/docs/handbook/variable-declarations.html#destructuring). + +For example, you cannot write something like this: + + +// ERROR +import { configuration } from './configuration'; + +// destructured assignment to foo and bar +const {foo, bar} = configuration; + ... + providers: [ + {provide: Foo, useValue: foo}, + {provide: Bar, useValue: bar}, + ] + ... + + +To correct this error, refer to non-destructured values. + + +// CORRECTED +import { configuration } from './configuration'; + ... + providers: [ + {provide: Foo, useValue: configuration.foo}, + {provide: Bar, useValue: configuration.bar}, + ] + ... + + +
+ +

Could not resolve type

+ +The compiler encountered a type and can't determine which module exports that type. + +This can happen if you refer to an ambient type. +For example, the `Window` type is an ambiant type declared in the global `.d.ts` file. + +You'll get an error if you reference it in the component constructor, +which the compiler must statically analyze. + +``` +// ERROR +@Component({ }) +export class MyComponent { + constructor (private win: Window) { ... } +} +``` +TypeScript understands ambiant types so you don't import them. +The Angular compiler does not understand a type that you neglect to export or import. + +In this case, the compiler doesn't understand how to inject something with the `Window` token. + +Do not refer to ambient types in metadata expressions. + +If you must inject an instance of an ambiant type, +you can finesse the problem in four steps: + +1. Create an injection token for an instance of the ambiant type. +1. Create a factory function that returns that instance. +1. Add a `useFactory` provider with that factory function. +1. Use `@Inject` to inject the instance. + +Here's an illustrative example. + + +// CORRECTED +import { Inject } from '@angular/core'; + +export const WINDOW = new InjectionToken('Window'); +export function _window() { return window; } + +@Component({ + ... + providers: [ + { provide: WINDOW, useFactory: _window } + ] +}) +export class MyComponent { + constructor (@Inject(WINDOW) private win: Window) { ... } +} + + +The `Window` type in the constructor is no longer a problem for the compiler because it +uses the `@Inject(WINDOW)` to generate the injection code. + +Angular does something similar with the `DOCUMENT` token so you can inject the browser's `document` object (or an abstraction of it, depending upon the platform in which the application runs). + + +import { Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/platform-browser'; + +@Component({ ... }) +export class MyComponent { + constructor (@Inject(DOCUMENT) private doc: Document) { ... } +} + +
+ +

Name expected

+ +The compiler expected a name in an expression it was evaluating. +This can happen if you use a number as a property name as in the following example. + +``` +// ERROR +provider: [{ provide: Foo, useValue: { 0: 'test' } }] +``` + +Change the name of the property to something non-numeric. + +``` +// CORRECTED +provider: [{ provide: Foo, useValue: { '0': 'test' } }] +``` + +
+ +

Unsupported enum member name

+ +Angular couldn't determine the value of the [enum member](https://www.typescriptlang.org/docs/handbook/enums.html) +that you referenced in metadata. + +The compiler can understand simple enum values but not complex values such as those derived from computed properties. + + +// ERROR +enum Colors { + Red = 1, + White, + Blue = "Blue".length // computed +} + + ... + providers: [ + { provide: BaseColor, useValue: Colors.White } // ok + { provide: DangerColor, useValue: Colors.Red } // ok + { provide: StrongColor, useValue: Colors.Blue } // bad + ] + ... + + +Avoid referring to enums with complicated initializers or computed properties. + +
+ +{@a tagged-template-expressions-not-supported} +

Tagged template expressions are not supported

+ +
+ +_Tagged template expressions are not supported in metadata._ + +
+ +The compiler encountered a JavaScript ES2015 [tagged template expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_template_literals) such as, +``` +// ERROR +const expression = 'funky'; +const raw = String.raw`A tagged template ${expression} string`; + ... + template: '
' + raw + '
' + ... +``` +[`String.raw()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw) +is a _tag function_ native to JavaScript ES2015. + +The AOT compiler does not support tagged template expressions; avoid them in metadata expressions. + +
+ +

Symbol reference expected

+ +The compiler expected a reference to a symbol at the location specified in the error message. + +This error can occur if you use an expression in the `extends` clause of a class. + + + +## Conclusion + +This page covered: + +* What the AOT compiler does. +* Why metadata must be written in a subset of JavaScript. +* What that subset is. +* Other restrictions on metadata definition. +* Macro-functions and macro-static methods. +* Compiler errors related to metadata. diff --git a/aio/content/navigation.json b/aio/content/navigation.json index fa127350b4..715d247467 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -338,6 +338,11 @@ "title": "Ahead-of-Time Compilation", "tooltip": "Learn why and how to use the Ahead-of-Time (AOT) compiler." }, + { + "url": "guide/metadata", + "title": "Metadata and AOT", + "tooltip": "How Angular handles metadata with AOT." + }, { "url": "guide/deployment", "title": "Deployment",