2018-05-09 08:35:25 -07:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2018-10-29 10:07:40 +01:00
|
|
|
import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseHostBindings, parseTemplate} from '@angular/compiler';
|
2018-05-09 08:35:25 -07:00
|
|
|
|
2018-10-29 10:07:40 +01:00
|
|
|
import {Query} from '../../metadata/di';
|
2018-06-12 16:58:09 -07:00
|
|
|
import {Component, Directive, HostBinding, HostListener, Input, Output} from '../../metadata/directives';
|
2018-06-22 19:05:31 -07:00
|
|
|
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
|
2018-07-31 11:14:06 -07:00
|
|
|
import {ViewEncapsulation} from '../../metadata/view';
|
2018-05-09 08:35:25 -07:00
|
|
|
import {Type} from '../../type';
|
2018-06-22 19:05:31 -07:00
|
|
|
import {stringify} from '../../util';
|
2018-08-29 16:34:44 -07:00
|
|
|
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields';
|
2018-05-09 08:35:25 -07:00
|
|
|
|
|
|
|
import {angularCoreEnv} from './environment';
|
2018-08-06 14:09:38 -07:00
|
|
|
import {patchComponentDefWithScope, transitiveScopesFor} from './module';
|
2018-05-21 08:15:19 -07:00
|
|
|
import {getReflect, reflectDependencies} from './util';
|
2018-05-09 08:35:25 -07:00
|
|
|
|
2018-06-12 16:58:09 -07:00
|
|
|
type StringMap = {
|
|
|
|
[key: string]: string
|
|
|
|
};
|
|
|
|
|
2018-05-09 08:35:25 -07:00
|
|
|
/**
|
|
|
|
* Compile an Angular component according to its decorator metadata, and patch the resulting
|
|
|
|
* ngComponentDef onto the component type.
|
|
|
|
*
|
|
|
|
* Compilation may be asynchronous (due to the need to resolve URLs for the component template or
|
|
|
|
* other resources, for example). In the event that compilation is not immediate, `compileComponent`
|
2018-06-22 19:05:31 -07:00
|
|
|
* will enqueue resource resolution into a global queue and will fail to return the `ngComponentDef`
|
|
|
|
* until the global queue has been resolved with a call to `resolveComponentResources`.
|
2018-05-09 08:35:25 -07:00
|
|
|
*/
|
2018-06-22 19:05:31 -07:00
|
|
|
export function compileComponent(type: Type<any>, metadata: Component): void {
|
2018-08-06 14:09:38 -07:00
|
|
|
let ngComponentDef: any = null;
|
2018-06-22 19:05:31 -07:00
|
|
|
// Metadata may have resources which need to be resolved.
|
|
|
|
maybeQueueResolutionOfComponentResources(metadata);
|
2018-06-06 11:23:38 -07:00
|
|
|
Object.defineProperty(type, NG_COMPONENT_DEF, {
|
2018-05-21 08:15:19 -07:00
|
|
|
get: () => {
|
2018-08-06 14:09:38 -07:00
|
|
|
if (ngComponentDef === null) {
|
2018-06-22 19:05:31 -07:00
|
|
|
if (componentNeedsResolution(metadata)) {
|
|
|
|
const error = [`Component '${stringify(type)}' is not resolved:`];
|
|
|
|
if (metadata.templateUrl) {
|
|
|
|
error.push(` - templateUrl: ${stringify(metadata.templateUrl)}`);
|
|
|
|
}
|
|
|
|
if (metadata.styleUrls && metadata.styleUrls.length) {
|
|
|
|
error.push(` - styleUrls: ${JSON.stringify(metadata.styleUrls)}`);
|
|
|
|
}
|
|
|
|
error.push(`Did you run and wait for 'resolveComponentResources()'?`);
|
|
|
|
throw new Error(error.join('\n'));
|
|
|
|
}
|
2018-05-21 08:15:19 -07:00
|
|
|
// The ConstantPool is a requirement of the JIT'er.
|
|
|
|
const constantPool = new ConstantPool();
|
|
|
|
|
|
|
|
// Parse the template and check for errors.
|
2018-08-27 11:11:02 -07:00
|
|
|
const template = parseTemplate(
|
|
|
|
metadata.template !, `ng://${stringify(type)}/template.html`, {
|
2018-06-22 19:05:31 -07:00
|
|
|
preserveWhitespaces: metadata.preserveWhitespaces || false,
|
2018-08-27 11:11:02 -07:00
|
|
|
},
|
|
|
|
'');
|
2018-05-21 08:15:19 -07:00
|
|
|
if (template.errors !== undefined) {
|
|
|
|
const errors = template.errors.map(err => err.toString()).join(', ');
|
2018-06-22 19:05:31 -07:00
|
|
|
throw new Error(
|
|
|
|
`Errors during JIT compilation of template for ${stringify(type)}: ${errors}`);
|
2018-05-21 08:15:19 -07:00
|
|
|
}
|
|
|
|
|
2018-10-09 10:45:27 -07:00
|
|
|
const animations =
|
|
|
|
metadata.animations !== null ? new WrappedNodeExpr(metadata.animations) : null;
|
|
|
|
|
2018-05-21 08:15:19 -07:00
|
|
|
// Compile the component metadata, including template, into an expression.
|
2018-05-31 14:18:24 -07:00
|
|
|
const res = compileR3Component(
|
2018-05-21 08:15:19 -07:00
|
|
|
{
|
|
|
|
...directiveMetadata(type, metadata),
|
|
|
|
template,
|
|
|
|
directives: new Map(),
|
|
|
|
pipes: new Map(),
|
2018-10-29 10:07:40 +01:00
|
|
|
viewQueries: extractQueriesMetadata(getReflect().propMetadata(type), isViewQuery),
|
2018-10-25 11:13:14 -07:00
|
|
|
wrapDirectivesAndPipesInClosure: false,
|
2018-07-31 11:14:06 -07:00
|
|
|
styles: metadata.styles || [],
|
2018-10-09 10:45:27 -07:00
|
|
|
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, animations,
|
2018-10-18 09:23:18 +02:00
|
|
|
viewProviders: metadata.viewProviders ? new WrappedNodeExpr(metadata.viewProviders) :
|
|
|
|
null
|
2018-05-21 08:15:19 -07:00
|
|
|
},
|
|
|
|
constantPool, makeBindingParser());
|
2018-07-16 16:36:31 -07:00
|
|
|
const preStatements = [...constantPool.statements, ...res.statements];
|
2018-05-21 08:15:19 -07:00
|
|
|
|
2018-08-06 14:09:38 -07:00
|
|
|
ngComponentDef = jitExpression(
|
2018-07-16 16:36:31 -07:00
|
|
|
res.expression, angularCoreEnv, `ng://${type.name}/ngComponentDef.js`, preStatements);
|
2018-06-06 11:23:38 -07:00
|
|
|
|
|
|
|
// If component compilation is async, then the @NgModule annotation which declares the
|
|
|
|
// component may execute and set an ngSelectorScope property on the component type. This
|
2018-08-20 15:20:12 +02:00
|
|
|
// allows the component to patch itself with directiveDefs from the module after it
|
|
|
|
// finishes compiling.
|
2018-06-06 11:23:38 -07:00
|
|
|
if (hasSelectorScope(type)) {
|
2018-08-06 14:09:38 -07:00
|
|
|
const scopes = transitiveScopesFor(type.ngSelectorScope);
|
|
|
|
patchComponentDefWithScope(ngComponentDef, scopes);
|
2018-06-06 11:23:38 -07:00
|
|
|
}
|
2018-05-21 08:15:19 -07:00
|
|
|
}
|
2018-08-06 14:09:38 -07:00
|
|
|
return ngComponentDef;
|
2018-05-21 08:15:19 -07:00
|
|
|
},
|
2018-08-06 14:09:38 -07:00
|
|
|
// Make the property configurable in dev mode to allow overriding in tests
|
|
|
|
configurable: !!ngDevMode,
|
2018-05-21 08:15:19 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-06-06 11:23:38 -07:00
|
|
|
function hasSelectorScope<T>(component: Type<T>): component is Type<T>&
|
|
|
|
{ngSelectorScope: Type<any>} {
|
|
|
|
return (component as{ngSelectorScope?: any}).ngSelectorScope !== undefined;
|
|
|
|
}
|
|
|
|
|
2018-05-21 08:15:19 -07:00
|
|
|
/**
|
|
|
|
* Compile an Angular directive according to its decorator metadata, and patch the resulting
|
|
|
|
* ngDirectiveDef onto the component type.
|
|
|
|
*
|
|
|
|
* In the event that compilation is not immediate, `compileDirective` will return a `Promise` which
|
|
|
|
* will resolve when compilation completes and the directive becomes usable.
|
|
|
|
*/
|
2018-06-22 19:05:31 -07:00
|
|
|
export function compileDirective(type: Type<any>, directive: Directive): void {
|
2018-08-06 14:09:38 -07:00
|
|
|
let ngDirectiveDef: any = null;
|
2018-06-06 11:23:38 -07:00
|
|
|
Object.defineProperty(type, NG_DIRECTIVE_DEF, {
|
2018-05-21 08:15:19 -07:00
|
|
|
get: () => {
|
2018-08-06 14:09:38 -07:00
|
|
|
if (ngDirectiveDef === null) {
|
2018-05-21 08:15:19 -07:00
|
|
|
const constantPool = new ConstantPool();
|
|
|
|
const sourceMapUrl = `ng://${type && type.name}/ngDirectiveDef.js`;
|
2018-05-31 14:18:24 -07:00
|
|
|
const res = compileR3Directive(
|
2018-05-21 08:15:19 -07:00
|
|
|
directiveMetadata(type, directive), constantPool, makeBindingParser());
|
2018-07-16 16:36:31 -07:00
|
|
|
const preStatements = [...constantPool.statements, ...res.statements];
|
2018-08-06 14:09:38 -07:00
|
|
|
ngDirectiveDef = jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements);
|
2018-05-21 08:15:19 -07:00
|
|
|
}
|
2018-08-06 14:09:38 -07:00
|
|
|
return ngDirectiveDef;
|
2018-05-21 08:15:19 -07:00
|
|
|
},
|
2018-08-06 14:09:38 -07:00
|
|
|
// Make the property configurable in dev mode to allow overriding in tests
|
|
|
|
configurable: !!ngDevMode,
|
2018-05-21 08:15:19 -07:00
|
|
|
});
|
2018-05-09 08:35:25 -07:00
|
|
|
}
|
|
|
|
|
2018-06-18 08:05:06 -07:00
|
|
|
export function extendsDirectlyFromObject(type: Type<any>): boolean {
|
|
|
|
return Object.getPrototypeOf(type.prototype) === Object.prototype;
|
|
|
|
}
|
|
|
|
|
2018-05-21 08:15:19 -07:00
|
|
|
/**
|
|
|
|
* Extract the `R3DirectiveMetadata` for a particular directive (either a `Directive` or a
|
|
|
|
* `Component`).
|
|
|
|
*/
|
|
|
|
function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMetadata {
|
|
|
|
// Reflect inputs and outputs.
|
2018-06-12 16:58:09 -07:00
|
|
|
const propMetadata = getReflect().propMetadata(type);
|
|
|
|
|
|
|
|
const host = extractHostBindings(metadata, propMetadata);
|
2018-05-21 08:15:19 -07:00
|
|
|
|
2018-06-15 15:43:22 -07:00
|
|
|
const inputsFromMetadata = parseInputOutputs(metadata.inputs || []);
|
|
|
|
const outputsFromMetadata = parseInputOutputs(metadata.outputs || []);
|
|
|
|
|
2018-10-25 23:05:15 -07:00
|
|
|
const inputsFromType: {[key: string]: string | string[]} = {};
|
2018-06-15 15:43:22 -07:00
|
|
|
const outputsFromType: StringMap = {};
|
2018-06-18 08:05:06 -07:00
|
|
|
for (const field in propMetadata) {
|
|
|
|
if (propMetadata.hasOwnProperty(field)) {
|
|
|
|
propMetadata[field].forEach(ann => {
|
|
|
|
if (isInput(ann)) {
|
2018-10-25 23:05:15 -07:00
|
|
|
inputsFromType[field] =
|
|
|
|
ann.bindingPropertyName ? [ann.bindingPropertyName, field] : field;
|
2018-06-18 08:05:06 -07:00
|
|
|
} else if (isOutput(ann)) {
|
|
|
|
outputsFromType[field] = ann.bindingPropertyName || field;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2018-05-21 08:15:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: type.name,
|
|
|
|
type: new WrappedNodeExpr(type),
|
2018-07-13 14:49:01 -07:00
|
|
|
typeArgumentCount: 0,
|
2018-05-21 08:15:19 -07:00
|
|
|
selector: metadata.selector !,
|
2018-06-15 15:43:22 -07:00
|
|
|
deps: reflectDependencies(type), host,
|
|
|
|
inputs: {...inputsFromMetadata, ...inputsFromType},
|
|
|
|
outputs: {...outputsFromMetadata, ...outputsFromType},
|
2018-10-29 10:07:40 +01:00
|
|
|
queries: extractQueriesMetadata(propMetadata, isContentQuery),
|
2018-05-21 08:15:19 -07:00
|
|
|
lifecycle: {
|
|
|
|
usesOnChanges: type.prototype.ngOnChanges !== undefined,
|
|
|
|
},
|
|
|
|
typeSourceSpan: null !,
|
2018-06-18 08:05:06 -07:00
|
|
|
usesInheritance: !extendsDirectlyFromObject(type),
|
2018-08-06 09:56:43 +02:00
|
|
|
exportAs: metadata.exportAs || null,
|
2018-10-18 09:23:18 +02:00
|
|
|
providers: metadata.providers ? new WrappedNodeExpr(metadata.providers) : null
|
2018-05-21 08:15:19 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-06-12 16:58:09 -07:00
|
|
|
function extractHostBindings(metadata: Directive, propMetadata: {[key: string]: any[]}): {
|
|
|
|
attributes: StringMap,
|
|
|
|
listeners: StringMap,
|
|
|
|
properties: StringMap,
|
|
|
|
} {
|
|
|
|
// First parse the declarations from the metadata.
|
|
|
|
const {attributes, listeners, properties, animations} = parseHostBindings(metadata.host || {});
|
|
|
|
|
|
|
|
if (Object.keys(animations).length > 0) {
|
|
|
|
throw new Error(`Animation bindings are as-of-yet unsupported in Ivy`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Next, loop over the properties of the object, looking for @HostBinding and @HostListener.
|
2018-06-18 08:05:06 -07:00
|
|
|
for (const field in propMetadata) {
|
|
|
|
if (propMetadata.hasOwnProperty(field)) {
|
|
|
|
propMetadata[field].forEach(ann => {
|
|
|
|
if (isHostBinding(ann)) {
|
|
|
|
properties[ann.hostPropertyName || field] = field;
|
|
|
|
} else if (isHostListener(ann)) {
|
|
|
|
listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2018-06-12 16:58:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return {attributes, listeners, properties};
|
|
|
|
}
|
|
|
|
|
2018-10-29 10:07:40 +01:00
|
|
|
function convertToR3QueryPredicate(selector: any): Expression|string[] {
|
|
|
|
return typeof selector === 'string' ? splitByComma(selector) : new WrappedNodeExpr(selector);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function convertToR3QueryMetadata(propertyName: string, ann: Query): R3QueryMetadata {
|
|
|
|
return {
|
|
|
|
propertyName: propertyName,
|
|
|
|
predicate: convertToR3QueryPredicate(ann.selector),
|
|
|
|
descendants: ann.descendants,
|
|
|
|
first: ann.first,
|
|
|
|
read: ann.read ? new WrappedNodeExpr(ann.read) : null
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function extractQueriesMetadata(
|
|
|
|
propMetadata: {[key: string]: any[]},
|
|
|
|
isQueryAnn: (ann: any) => ann is Query): R3QueryMetadata[] {
|
|
|
|
const queriesMeta: R3QueryMetadata[] = [];
|
|
|
|
|
|
|
|
for (const field in propMetadata) {
|
|
|
|
if (propMetadata.hasOwnProperty(field)) {
|
|
|
|
propMetadata[field].forEach(ann => {
|
|
|
|
if (isQueryAnn(ann)) {
|
|
|
|
queriesMeta.push(convertToR3QueryMetadata(field, ann));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return queriesMeta;
|
|
|
|
}
|
|
|
|
|
2018-05-21 08:15:19 -07:00
|
|
|
function isInput(value: any): value is Input {
|
|
|
|
return value.ngMetadataName === 'Input';
|
|
|
|
}
|
|
|
|
|
|
|
|
function isOutput(value: any): value is Output {
|
|
|
|
return value.ngMetadataName === 'Output';
|
|
|
|
}
|
2018-06-12 16:58:09 -07:00
|
|
|
|
|
|
|
function isHostBinding(value: any): value is HostBinding {
|
|
|
|
return value.ngMetadataName === 'HostBinding';
|
|
|
|
}
|
|
|
|
|
|
|
|
function isHostListener(value: any): value is HostListener {
|
|
|
|
return value.ngMetadataName === 'HostListener';
|
|
|
|
}
|
2018-06-15 15:43:22 -07:00
|
|
|
|
2018-10-29 10:07:40 +01:00
|
|
|
function isContentQuery(value: any): value is Query {
|
|
|
|
const name = value.ngMetadataName;
|
|
|
|
return name === 'ContentChild' || name === 'ContentChildren';
|
|
|
|
}
|
|
|
|
|
|
|
|
function isViewQuery(value: any): value is Query {
|
|
|
|
const name = value.ngMetadataName;
|
|
|
|
return name === 'ViewChild' || name === 'ViewChildren';
|
|
|
|
}
|
|
|
|
|
|
|
|
function splitByComma(value: string): string[] {
|
|
|
|
return value.split(',').map(piece => piece.trim());
|
|
|
|
}
|
|
|
|
|
2018-06-15 15:43:22 -07:00
|
|
|
function parseInputOutputs(values: string[]): StringMap {
|
|
|
|
return values.reduce(
|
|
|
|
(map, value) => {
|
2018-10-29 10:07:40 +01:00
|
|
|
const [field, property] = splitByComma(value);
|
2018-06-15 15:43:22 -07:00
|
|
|
map[field] = property || field;
|
|
|
|
return map;
|
|
|
|
},
|
|
|
|
{} as StringMap);
|
|
|
|
}
|