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

@ -40,9 +40,7 @@ export interface CompilerFacade {
compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3BaseMetadataFacade):
any;
compileFactory(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
meta: R3PipeMetadataFacade|R3DirectiveMetadataFacade|R3ComponentMetadataFacade,
isPipe?: boolean): any;
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3FactoryDefMetadataFacade): any;
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
@ -94,7 +92,6 @@ export interface R3InjectableMetadataFacade {
name: string;
type: any;
typeArgumentCount: number;
ctorDeps: R3DependencyMetadataFacade[]|null;
providedIn: any;
useClass?: any;
useFactory?: any;
@ -164,6 +161,15 @@ export interface R3BaseMetadataFacade {
viewQueries?: R3QueryMetadataFacade[];
}
export interface R3FactoryDefMetadataFacade {
name: string;
type: any;
typeArgumentCount: number;
deps: R3DependencyMetadataFacade[]|null;
injectFn: 'directiveInject'|'inject';
isPipe: boolean;
}
export type ViewEncapsulation = number;
export type ChangeDetectionStrategy = number;

View File

@ -8,7 +8,9 @@
import {R3InjectableMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade';
import {Type} from '../../interface/type';
import {NG_FACTORY_DEF} from '../../render3/fields';
import {getClosureSafeProperty} from '../../util/property';
import {resolveForwardRef} from '../forward_ref';
import {Injectable} from '../injectable';
import {NG_INJECTABLE_DEF} from '../interface/defs';
import {ClassSansProvider, ExistingSansProvider, FactorySansProvider, ValueProvider, ValueSansProvider} from '../interface/provider';
@ -23,59 +25,45 @@ import {convertDependencies, reflectDependencies} from './util';
* `ngInjectableDef` onto the injectable type.
*/
export function compileInjectable(type: Type<any>, srcMeta?: Injectable): void {
let def: any = null;
let ngInjectableDef: any = null;
let ngFactoryDef: any = null;
// if NG_INJECTABLE_DEF is already defined on this class then don't overwrite it
if (type.hasOwnProperty(NG_INJECTABLE_DEF)) return;
Object.defineProperty(type, NG_INJECTABLE_DEF, {
get: () => {
if (def === null) {
// Allow the compilation of a class with a `@Injectable()` decorator without parameters
const meta: Injectable = srcMeta || {providedIn: null};
const hasAProvider = isUseClassProvider(meta) || isUseFactoryProvider(meta) ||
isUseValueProvider(meta) || isUseExistingProvider(meta);
const compilerMeta: R3InjectableMetadataFacade = {
name: type.name,
type: type,
typeArgumentCount: 0,
providedIn: meta.providedIn,
ctorDeps: reflectDependencies(type),
userDeps: undefined,
};
if ((isUseClassProvider(meta) || isUseFactoryProvider(meta)) && meta.deps !== undefined) {
compilerMeta.userDeps = convertDependencies(meta.deps);
if (!type.hasOwnProperty(NG_INJECTABLE_DEF)) {
Object.defineProperty(type, NG_INJECTABLE_DEF, {
get: () => {
if (ngInjectableDef === null) {
ngInjectableDef = getCompilerFacade().compileInjectable(
angularCoreDiEnv, `ng:///${type.name}/ngInjectableDef.js`,
getInjectableMetadata(type, srcMeta));
}
if (!hasAProvider) {
// In the case the user specifies a type provider, treat it as {provide: X, useClass: X}.
// The deps will have been reflected above, causing the factory to create the class by
// calling
// its constructor with injected deps.
compilerMeta.useClass = type;
} else if (isUseClassProvider(meta)) {
// The user explicitly specified useClass, and may or may not have provided deps.
compilerMeta.useClass = meta.useClass;
} else if (isUseValueProvider(meta)) {
// The user explicitly specified useValue.
compilerMeta.useValue = meta.useValue;
} else if (isUseFactoryProvider(meta)) {
// The user explicitly specified useFactory.
compilerMeta.useFactory = meta.useFactory;
} else if (isUseExistingProvider(meta)) {
// The user explicitly specified useExisting.
compilerMeta.useExisting = meta.useExisting;
} else {
// Can't happen - either hasAProvider will be false, or one of the providers will be set.
throw new Error(`Unreachable state.`);
return ngInjectableDef;
},
});
}
// if NG_FACTORY_DEF is already defined on this class then don't overwrite it
if (!type.hasOwnProperty(NG_FACTORY_DEF)) {
Object.defineProperty(type, NG_FACTORY_DEF, {
get: () => {
if (ngFactoryDef === null) {
const metadata = getInjectableMetadata(type, srcMeta);
ngFactoryDef = getCompilerFacade().compileFactory(
angularCoreDiEnv, `ng:///${type.name}/ngFactoryDef.js`, {
name: metadata.name,
type: metadata.type,
typeArgumentCount: metadata.typeArgumentCount,
deps: reflectDependencies(type),
injectFn: 'inject',
isPipe: false
});
}
def = getCompilerFacade().compileInjectable(
angularCoreDiEnv, `ng:///${type.name}/ngInjectableDef.js`, compilerMeta);
}
return def;
},
});
return ngFactoryDef;
},
// Leave this configurable so that the factories from directives or pipes can take precedence.
configurable: true
});
}
}
type UseClassProvider = Injectable & ClassSansProvider & {deps?: any[]};
@ -98,3 +86,32 @@ function isUseFactoryProvider(meta: Injectable): meta is Injectable&FactorySansP
function isUseExistingProvider(meta: Injectable): meta is Injectable&ExistingSansProvider {
return (meta as ExistingSansProvider).useExisting !== undefined;
}
function getInjectableMetadata(type: Type<any>, srcMeta?: Injectable): R3InjectableMetadataFacade {
// Allow the compilation of a class with a `@Injectable()` decorator without parameters
const meta: Injectable = srcMeta || {providedIn: null};
const compilerMeta: R3InjectableMetadataFacade = {
name: type.name,
type: type,
typeArgumentCount: 0,
providedIn: meta.providedIn,
userDeps: undefined,
};
if ((isUseClassProvider(meta) || isUseFactoryProvider(meta)) && meta.deps !== undefined) {
compilerMeta.userDeps = convertDependencies(meta.deps);
}
if (isUseClassProvider(meta)) {
// The user explicitly specified useClass, and may or may not have provided deps.
compilerMeta.useClass = resolveForwardRef(meta.useClass);
} else if (isUseValueProvider(meta)) {
// The user explicitly specified useValue.
compilerMeta.useValue = resolveForwardRef(meta.useValue);
} else if (isUseFactoryProvider(meta)) {
// The user explicitly specified useFactory.
compilerMeta.useFactory = meta.useFactory;
} else if (isUseExistingProvider(meta)) {
// The user explicitly specified useExisting.
compilerMeta.useExisting = resolveForwardRef(meta.useExisting);
}
return compilerMeta;
}

View File

@ -25,6 +25,7 @@ export {ɵɵinject} from './di/injector_compatibility';
export {ɵɵInjectableDef, ɵɵInjectorDef, ɵɵdefineInjectable, ɵɵdefineInjector} from './di/interface/defs';
export {NgModuleDef, ɵɵNgModuleDefWithMeta} from './metadata/ng_module';
export {ɵɵdefineNgModule} from './render3/definition';
export {ɵɵFactoryDef} from './render3/interfaces/definition';
export {setClassMetadata} from './render3/metadata';
export {NgModuleFactory} from './render3/ng_module_ref';

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,