refactor(ivy): generate ngFactoryDef for injectables (#32433)

With #31953 we moved the factories for components, directives and pipes into a new field called `ngFactoryDef`, however I decided not to do it for injectables, because they needed some extra logic. These changes set up the `ngFactoryDef` for injectables as well.

For reference, the extra logic mentioned above is that for injectables we have two code paths:

1. For injectables that don't configure how they should be instantiated, we create a `factory` that proxies to `ngFactoryDef`:

```
// Source
@Injectable()
class Service {}

// Output
class Service {
  static ngInjectableDef = defineInjectable({
    factory: () => Service.ngFactoryFn(),
  });

  static ngFactoryFn: (t) => new (t || Service)();
}
```

2. For injectables that do configure how they're created, we keep the `ngFactoryDef` and generate the factory based on the metadata:

```
// Source
@Injectable({
  useValue: DEFAULT_IMPL,
})
class Service {}

// Output
export class Service {
  static ngInjectableDef = defineInjectable({
    factory: () => DEFAULT_IMPL,
  });

  static ngFactoryFn: (t) => new (t || Service)();
}
```

PR Close #32433
This commit is contained in:
crisbeto
2019-09-01 12:26:04 +02:00
committed by atscott
parent 2729747225
commit 4e35e348af
33 changed files with 695 additions and 295 deletions

View File

@ -753,11 +753,11 @@ export function getBaseDef<T>(type: any): ɵɵBaseDef<T>|null {
export function getFactoryDef<T>(type: any, throwNotFound: true): FactoryFn<T>;
export function getFactoryDef<T>(type: any): FactoryFn<T>|null;
export function getFactoryDef<T>(type: any, throwNotFound?: boolean): FactoryFn<T>|null {
const factoryFn = type[NG_FACTORY_DEF] || null;
if (!factoryFn && throwNotFound === true && ngDevMode) {
const hasFactoryDef = type.hasOwnProperty(NG_FACTORY_DEF);
if (!hasFactoryDef && throwNotFound === true && ngDevMode) {
throw new Error(`Type ${stringify(type)} does not have 'ngFactoryDef' property.`);
}
return factoryFn;
return hasFactoryDef ? type[NG_FACTORY_DEF] : null;
}
export function getNgModuleDef<T>(type: any, throwNotFound: true): NgModuleDef<T>;

View File

@ -16,7 +16,7 @@ import {Type} from '../interface/type';
import {assertDefined, assertEqual} from '../util/assert';
import {getFactoryDef} from './definition';
import {NG_ELEMENT_ID} from './fields';
import {NG_ELEMENT_ID, NG_FACTORY_DEF} from './fields';
import {DirectiveDef, FactoryFn} from './interfaces/definition';
import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags, TNODE, isFactory} from './interfaces/injector';
import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node';
@ -642,9 +642,11 @@ export function ɵɵgetFactoryOf<T>(type: Type<any>): FactoryFn<T>|null {
}) as any;
}
// TODO(crisbeto): unify injectable factories with getFactory.
const def = getInjectableDef<T>(typeAny) || getInjectorDef<T>(typeAny);
const factory = def && def.factory || getFactoryDef<T>(typeAny);
let factory = getFactoryDef<T>(typeAny);
if (factory === null) {
const injectorDef = getInjectorDef<T>(typeAny);
factory = injectorDef && injectorDef.factory;
}
return factory || null;
}
@ -653,7 +655,7 @@ export function ɵɵgetFactoryOf<T>(type: Type<any>): FactoryFn<T>|null {
*/
export function ɵɵgetInheritedFactory<T>(type: Type<any>): (type: Type<T>) => T {
const proto = Object.getPrototypeOf(type.prototype).constructor as Type<any>;
const factory = ɵɵgetFactoryOf<T>(proto);
const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf<T>(proto);
if (factory !== null) {
return factory;
} else {

View File

@ -7,9 +7,8 @@
*/
import {R3DirectiveMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade';
import {CompilerFacade, R3BaseMetadataFacade, R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface';
import {R3BaseMetadataFacade, R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface';
import {resolveForwardRef} from '../../di/forward_ref';
import {compileInjectable} from '../../di/jit/injectable';
import {getReflect, reflectDependencies} from '../../di/jit/util';
import {Type} from '../../interface/type';
import {Query} from '../../metadata/di';
@ -43,31 +42,52 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
(typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
let ngComponentDef: any = null;
let ngFactoryDef: any = null;
// Metadata may have resources which need to be resolved.
maybeQueueResolutionOfComponentResources(type, metadata);
Object.defineProperty(type, NG_FACTORY_DEF, {
get: () => {
if (ngFactoryDef === null) {
const compiler = getCompilerFacade();
const meta = getComponentMetadata(compiler, type, metadata);
ngFactoryDef = compiler.compileFactory(
angularCoreEnv, `ng:///${type.name}/ngFactory.js`, meta.metadata);
}
return ngFactoryDef;
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});
// Note that we're using the same function as `Directive`, because that's only subset of metadata
// that we need to create the ngFactoryDef. We're avoiding using the component metadata
// because we'd have to resolve the asynchronous templates.
addDirectiveFactoryDef(type, metadata);
Object.defineProperty(type, NG_COMPONENT_DEF, {
get: () => {
if (ngComponentDef === null) {
const compiler = getCompilerFacade();
const meta = getComponentMetadata(compiler, type, metadata);
ngComponentDef = compiler.compileComponent(angularCoreEnv, meta.templateUrl, meta.metadata);
if (componentNeedsResolution(metadata)) {
const error = [`Component '${type.name}' is not resolved:`];
if (metadata.templateUrl) {
error.push(` - templateUrl: ${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'));
}
const templateUrl = metadata.templateUrl || `ng:///${type.name}/template.html`;
const meta: R3ComponentMetadataFacade = {
...directiveMetadata(type, metadata),
typeSourceSpan: compiler.createParseSourceSpan('Component', type.name, templateUrl),
template: metadata.template || '',
preserveWhitespaces: metadata.preserveWhitespaces || false,
styles: metadata.styles || EMPTY_ARRAY,
animations: metadata.animations,
directives: [],
changeDetection: metadata.changeDetection,
pipes: new Map(),
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
interpolation: metadata.interpolation,
viewProviders: metadata.viewProviders || null,
};
if (meta.usesInheritance) {
addBaseDefToUndecoratedParents(type);
}
ngComponentDef = compiler.compileComponent(angularCoreEnv, templateUrl, meta);
// When NgModule decorator executed, we enqueued the module definition such that
// it would only dequeue and add itself as module scope to all of its declarations,
@ -90,45 +110,6 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});
// Add ngInjectableDef so components are reachable through the module injector by default
// This is mostly to support injecting components in tests. In real application code,
// components should be retrieved through the node injector, so this isn't a problem.
compileInjectable(type);
}
function getComponentMetadata(compiler: CompilerFacade, type: Type<any>, metadata: Component) {
if (componentNeedsResolution(metadata)) {
const error = [`Component '${type.name}' is not resolved:`];
if (metadata.templateUrl) {
error.push(` - templateUrl: ${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'));
}
const templateUrl = metadata.templateUrl || `ng:///${type.name}/template.html`;
const meta: R3ComponentMetadataFacade = {
...directiveMetadata(type, metadata),
typeSourceSpan: compiler.createParseSourceSpan('Component', type.name, templateUrl),
template: metadata.template || '',
preserveWhitespaces: metadata.preserveWhitespaces || false,
styles: metadata.styles || EMPTY_ARRAY,
animations: metadata.animations,
directives: [],
changeDetection: metadata.changeDetection,
pipes: new Map(),
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
interpolation: metadata.interpolation,
viewProviders: metadata.viewProviders || null,
};
if (meta.usesInheritance) {
addBaseDefToUndecoratedParents(type);
}
return {metadata: meta, templateUrl};
}
function hasSelectorScope<T>(component: Type<T>): component is Type<T>&
@ -145,23 +126,8 @@ function hasSelectorScope<T>(component: Type<T>): component is Type<T>&
*/
export function compileDirective(type: Type<any>, directive: Directive | null): void {
let ngDirectiveDef: any = null;
let ngFactoryDef: any = null;
Object.defineProperty(type, NG_FACTORY_DEF, {
get: () => {
if (ngFactoryDef === null) {
// `directive` can be null in the case of abstract directives as a base class
// that use `@Directive()` with no selector. In that case, pass empty object to the
// `directiveMetadata` function instead of null.
const meta = getDirectiveMetadata(type, directive || {});
ngFactoryDef = getCompilerFacade().compileFactory(
angularCoreEnv, `ng:///${type.name}/ngFactory.js`, meta.metadata);
}
return ngFactoryDef;
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});
addDirectiveFactoryDef(type, directive || {});
Object.defineProperty(type, NG_DIRECTIVE_DEF, {
get: () => {
@ -178,11 +144,6 @@ export function compileDirective(type: Type<any>, directive: Directive | null):
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});
// Add ngInjectableDef so directives are reachable through the module injector by default
// This is mostly to support injecting directives in tests. In real application code,
// directives should be retrieved through the node injector, so this isn't a problem.
compileInjectable(type);
}
function getDirectiveMetadata(type: Type<any>, metadata: Directive) {
@ -197,6 +158,24 @@ function getDirectiveMetadata(type: Type<any>, metadata: Directive) {
return {metadata: facade, sourceMapUrl};
}
function addDirectiveFactoryDef(type: Type<any>, metadata: Directive | Component) {
let ngFactoryDef: any = null;
Object.defineProperty(type, NG_FACTORY_DEF, {
get: () => {
if (ngFactoryDef === null) {
const meta = getDirectiveMetadata(type, metadata);
ngFactoryDef = getCompilerFacade().compileFactory(
angularCoreEnv, `ng:///${type.name}/ngFactoryDef.js`,
{...meta.metadata, injectFn: 'directiveInject', isPipe: false});
}
return ngFactoryDef;
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});
}
export function extendsDirectlyFromObject(type: Type<any>): boolean {
return Object.getPrototypeOf(type.prototype) === Object.prototype;
}
@ -225,7 +204,7 @@ export function directiveMetadata(type: Type<any>, metadata: Directive): R3Direc
usesInheritance: !extendsDirectlyFromObject(type),
exportAs: extractExportAs(metadata.exportAs),
providers: metadata.providers || null,
viewQueries: extractQueriesMetadata(type, propMetadata, isViewQuery),
viewQueries: extractQueriesMetadata(type, propMetadata, isViewQuery)
};
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getCompilerFacade} from '../../compiler/compiler_facade';
import {R3PipeMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade';
import {reflectDependencies} from '../../di/jit/util';
import {Type} from '../../interface/type';
import {Pipe} from '../../metadata/directives';
@ -23,7 +23,8 @@ export function compilePipe(type: Type<any>, meta: Pipe): void {
if (ngFactoryDef === null) {
const metadata = getPipeMetadata(type, meta);
ngFactoryDef = getCompilerFacade().compileFactory(
angularCoreEnv, `ng:///${metadata.name}/ngFactory.js`, metadata, true);
angularCoreEnv, `ng:///${metadata.name}/ngFactoryDef.js`,
{...metadata, injectFn: 'directiveInject', isPipe: true});
}
return ngFactoryDef;
},
@ -45,7 +46,7 @@ export function compilePipe(type: Type<any>, meta: Pipe): void {
});
}
function getPipeMetadata(type: Type<any>, meta: Pipe) {
function getPipeMetadata(type: Type<any>, meta: Pipe): R3PipeMetadataFacade {
return {
type: type,
typeArgumentCount: 0,