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:
@ -171,8 +171,12 @@ export class DecorationAnalyzer {
|
||||
for (const {handler, analysis} of clazz.matches) {
|
||||
const result = handler.compile(clazz.declaration, analysis, constantPool);
|
||||
if (Array.isArray(result)) {
|
||||
compilations.push(...result);
|
||||
} else {
|
||||
result.forEach(current => {
|
||||
if (!compilations.some(compilation => compilation.name === current.name)) {
|
||||
compilations.push(current);
|
||||
}
|
||||
});
|
||||
} else if (!compilations.some(compilation => compilation.name === result.name)) {
|
||||
compilations.push(result);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
|
||||
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CycleAnalyzer} from '../../cycles';
|
||||
@ -486,7 +486,7 @@ export class ComponentDecoratorHandler implements
|
||||
CompileResult[] {
|
||||
const meta = analysis.meta;
|
||||
const res = compileComponentFromMetadata(meta, pool, makeBindingParser());
|
||||
const factoryRes = compileNgFactoryDefField(meta);
|
||||
const factoryRes = compileNgFactoryDefField({...meta, injectFn: Identifiers.directiveInject});
|
||||
if (analysis.metadataStmt !== null) {
|
||||
factoryRes.statements.push(analysis.metadataStmt);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ConstantPool, EMPTY_SOURCE_SPAN, Expression, ParseError, ParsedHostBindings, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings, verifyHostBindings} from '@angular/compiler';
|
||||
import {ConstantPool, EMPTY_SOURCE_SPAN, Expression, Identifiers, ParseError, ParsedHostBindings, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings, verifyHostBindings} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
@ -94,7 +94,7 @@ export class DirectiveDecoratorHandler implements
|
||||
CompileResult[] {
|
||||
const meta = analysis.meta;
|
||||
const res = compileDirectiveFromMetadata(meta, pool, makeBindingParser());
|
||||
const factoryRes = compileNgFactoryDefField(meta);
|
||||
const factoryRes = compileNgFactoryDefField({...meta, injectFn: Identifiers.directiveInject});
|
||||
if (analysis.metadataStmt !== null) {
|
||||
factoryRes.statements.push(analysis.metadataStmt);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, Statement, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler';
|
||||
import {Expression, Identifiers, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, Statement, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
@ -14,12 +14,15 @@ import {DefaultImportRecorder} from '../../imports';
|
||||
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
|
||||
import {compileNgFactoryDefField} from './factory';
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, validateConstructorDependencies} from './util';
|
||||
import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, unwrapForwardRef, validateConstructorDependencies} from './util';
|
||||
|
||||
export interface InjectableHandlerData {
|
||||
meta: R3InjectableMetadata;
|
||||
metadataStmt: Statement|null;
|
||||
ctorDeps: R3DependencyMetadata[]|'invalid'|null;
|
||||
needsFactory: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,41 +52,65 @@ export class InjectableDecoratorHandler implements
|
||||
}
|
||||
|
||||
analyze(node: ClassDeclaration, decorator: Decorator): AnalysisOutput<InjectableHandlerData> {
|
||||
const meta = extractInjectableMetadata(node, decorator, this.reflector);
|
||||
const decorators = this.reflector.getDecoratorsOfDeclaration(node);
|
||||
|
||||
return {
|
||||
analysis: {
|
||||
meta: extractInjectableMetadata(
|
||||
node, decorator, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||
meta,
|
||||
ctorDeps: extractInjectableCtorDeps(
|
||||
node, meta, decorator, this.reflector, this.defaultImportRecorder, this.isCore,
|
||||
this.strictCtorDeps),
|
||||
metadataStmt: generateSetClassMetadataCall(
|
||||
node, this.reflector, this.defaultImportRecorder, this.isCore),
|
||||
// Avoid generating multiple factories if a class has
|
||||
// more Angular decorators, apart from Injectable.
|
||||
needsFactory: !decorators ||
|
||||
decorators.every(current => !isAngularCore(current) || current.name === 'Injectable')
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
compile(node: ClassDeclaration, analysis: InjectableHandlerData): CompileResult {
|
||||
compile(node: ClassDeclaration, analysis: InjectableHandlerData): CompileResult[] {
|
||||
const res = compileIvyInjectable(analysis.meta);
|
||||
const statements = res.statements;
|
||||
if (analysis.metadataStmt !== null) {
|
||||
statements.push(analysis.metadataStmt);
|
||||
const results: CompileResult[] = [];
|
||||
|
||||
if (analysis.needsFactory) {
|
||||
const meta = analysis.meta;
|
||||
const factoryRes = compileNgFactoryDefField({
|
||||
name: meta.name,
|
||||
type: meta.type,
|
||||
typeArgumentCount: meta.typeArgumentCount,
|
||||
deps: analysis.ctorDeps,
|
||||
injectFn: Identifiers.inject
|
||||
});
|
||||
if (analysis.metadataStmt !== null) {
|
||||
factoryRes.statements.push(analysis.metadataStmt);
|
||||
}
|
||||
results.push(factoryRes);
|
||||
}
|
||||
return {
|
||||
|
||||
results.push({
|
||||
name: 'ngInjectableDef',
|
||||
initializer: res.expression, statements,
|
||||
type: res.type,
|
||||
};
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the input
|
||||
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the
|
||||
* input
|
||||
* metadata needed to run `compileIvyInjectable`.
|
||||
*
|
||||
* A `null` return value indicates this is @Injectable has invalid data.
|
||||
*/
|
||||
function extractInjectableMetadata(
|
||||
clazz: ClassDeclaration, decorator: Decorator, reflector: ReflectionHost,
|
||||
defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
|
||||
strictCtorDeps: boolean): R3InjectableMetadata {
|
||||
clazz: ClassDeclaration, decorator: Decorator,
|
||||
reflector: ReflectionHost): R3InjectableMetadata {
|
||||
const name = clazz.name.text;
|
||||
const type = new WrappedNodeExpr(clazz.name);
|
||||
const typeArgumentCount = reflector.getGenericArityOfClass(clazz) || 0;
|
||||
@ -92,53 +119,13 @@ function extractInjectableMetadata(
|
||||
ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called');
|
||||
}
|
||||
if (decorator.args.length === 0) {
|
||||
// Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be
|
||||
// subject to the same validation. However, existing Angular code abuses @Injectable, applying
|
||||
// it to things like abstract classes with constructors that were never meant for use with
|
||||
// Angular's DI.
|
||||
//
|
||||
// To deal with this, @Injectable() without an argument is more lenient, and if the constructor
|
||||
// signature does not work for DI then an ngInjectableDef that throws.
|
||||
let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null;
|
||||
if (strictCtorDeps) {
|
||||
ctorDeps = getValidConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
|
||||
} else {
|
||||
const possibleCtorDeps =
|
||||
getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
|
||||
if (possibleCtorDeps !== null) {
|
||||
if (possibleCtorDeps.deps !== null) {
|
||||
// This use of @Injectable has valid constructor dependencies.
|
||||
ctorDeps = possibleCtorDeps.deps;
|
||||
} else {
|
||||
// This use of @Injectable is technically invalid. Generate a factory function which
|
||||
// throws
|
||||
// an error.
|
||||
// TODO(alxhub): log warnings for the bad use of @Injectable.
|
||||
ctorDeps = 'invalid';
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
typeArgumentCount,
|
||||
providedIn: new LiteralExpr(null), ctorDeps,
|
||||
providedIn: new LiteralExpr(null),
|
||||
};
|
||||
} else if (decorator.args.length === 1) {
|
||||
const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
|
||||
let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null;
|
||||
|
||||
// rawCtorDeps will be null if the class has no constructor.
|
||||
if (rawCtorDeps !== null) {
|
||||
if (rawCtorDeps.deps !== null) {
|
||||
// A constructor existed and had valid dependencies.
|
||||
ctorDeps = rawCtorDeps.deps;
|
||||
} else {
|
||||
// A constructor existed but had invalid dependencies.
|
||||
ctorDeps = 'invalid';
|
||||
}
|
||||
}
|
||||
|
||||
const metaNode = decorator.args[0];
|
||||
// Firstly make sure the decorator argument is an inline literal - if not, it's illegal to
|
||||
// transport references from one location to another. This is the problem that lowering
|
||||
@ -170,27 +157,25 @@ function extractInjectableMetadata(
|
||||
name,
|
||||
type,
|
||||
typeArgumentCount,
|
||||
ctorDeps,
|
||||
providedIn,
|
||||
useValue: new WrappedNodeExpr(meta.get('useValue') !),
|
||||
useValue: new WrappedNodeExpr(unwrapForwardRef(meta.get('useValue') !, reflector)),
|
||||
};
|
||||
} else if (meta.has('useExisting')) {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
typeArgumentCount,
|
||||
ctorDeps,
|
||||
providedIn,
|
||||
useExisting: new WrappedNodeExpr(meta.get('useExisting') !),
|
||||
useExisting: new WrappedNodeExpr(unwrapForwardRef(meta.get('useExisting') !, reflector)),
|
||||
};
|
||||
} else if (meta.has('useClass')) {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
typeArgumentCount,
|
||||
ctorDeps,
|
||||
providedIn,
|
||||
useClass: new WrappedNodeExpr(meta.get('useClass') !), userDeps,
|
||||
useClass: new WrappedNodeExpr(unwrapForwardRef(meta.get('useClass') !, reflector)),
|
||||
userDeps,
|
||||
};
|
||||
} else if (meta.has('useFactory')) {
|
||||
// useFactory is special - the 'deps' property must be analyzed.
|
||||
@ -200,14 +185,10 @@ function extractInjectableMetadata(
|
||||
type,
|
||||
typeArgumentCount,
|
||||
providedIn,
|
||||
useFactory: factory, ctorDeps, userDeps,
|
||||
useFactory: factory, userDeps,
|
||||
};
|
||||
} else {
|
||||
if (strictCtorDeps) {
|
||||
// Since use* was not provided, validate the deps according to strictCtorDeps.
|
||||
validateConstructorDependencies(clazz, rawCtorDeps);
|
||||
}
|
||||
return {name, type, typeArgumentCount, providedIn, ctorDeps};
|
||||
return {name, type, typeArgumentCount, providedIn};
|
||||
}
|
||||
} else {
|
||||
throw new FatalDiagnosticError(
|
||||
@ -215,7 +196,69 @@ function extractInjectableMetadata(
|
||||
}
|
||||
}
|
||||
|
||||
function extractInjectableCtorDeps(
|
||||
clazz: ClassDeclaration, meta: R3InjectableMetadata, decorator: Decorator,
|
||||
reflector: ReflectionHost, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
|
||||
strictCtorDeps: boolean) {
|
||||
if (decorator.args === null) {
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called');
|
||||
}
|
||||
|
||||
let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null;
|
||||
|
||||
if (decorator.args.length === 0) {
|
||||
// Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be
|
||||
// subject to the same validation. However, existing Angular code abuses @Injectable, applying
|
||||
// it to things like abstract classes with constructors that were never meant for use with
|
||||
// Angular's DI.
|
||||
//
|
||||
// To deal with this, @Injectable() without an argument is more lenient, and if the
|
||||
// constructor
|
||||
// signature does not work for DI then an ngInjectableDef that throws.
|
||||
if (strictCtorDeps) {
|
||||
ctorDeps = getValidConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
|
||||
} else {
|
||||
const possibleCtorDeps =
|
||||
getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
|
||||
if (possibleCtorDeps !== null) {
|
||||
if (possibleCtorDeps.deps !== null) {
|
||||
// This use of @Injectable has valid constructor dependencies.
|
||||
ctorDeps = possibleCtorDeps.deps;
|
||||
} else {
|
||||
// This use of @Injectable is technically invalid. Generate a factory function which
|
||||
// throws
|
||||
// an error.
|
||||
// TODO(alxhub): log warnings for the bad use of @Injectable.
|
||||
ctorDeps = 'invalid';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctorDeps;
|
||||
} else if (decorator.args.length === 1) {
|
||||
const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
|
||||
|
||||
// rawCtorDeps will be null if the class has no constructor.
|
||||
if (rawCtorDeps !== null) {
|
||||
if (rawCtorDeps.deps !== null) {
|
||||
// A constructor existed and had valid dependencies.
|
||||
ctorDeps = rawCtorDeps.deps;
|
||||
} else {
|
||||
// A constructor existed but had invalid dependencies.
|
||||
ctorDeps = 'invalid';
|
||||
}
|
||||
}
|
||||
|
||||
if (strictCtorDeps && !meta.useValue && !meta.useExisting && !meta.useClass &&
|
||||
!meta.useFactory) {
|
||||
// Since use* was not provided, validate the deps according to strictCtorDeps.
|
||||
validateConstructorDependencies(clazz, rawCtorDeps);
|
||||
}
|
||||
}
|
||||
|
||||
return ctorDeps;
|
||||
}
|
||||
|
||||
function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata {
|
||||
const meta: R3DependencyMetadata = {
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler';
|
||||
import {Identifiers, R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
@ -109,7 +109,11 @@ export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, D
|
||||
compile(node: ClassDeclaration, analysis: PipeHandlerData): CompileResult[] {
|
||||
const meta = analysis.meta;
|
||||
const res = compilePipeFromMetadata(meta);
|
||||
const factoryRes = compileNgFactoryDefField({...meta, isPipe: true});
|
||||
const factoryRes = compileNgFactoryDefField({
|
||||
...meta,
|
||||
injectFn: Identifiers.directiveInject,
|
||||
isPipe: true,
|
||||
});
|
||||
if (analysis.metadataStmt !== null) {
|
||||
factoryRes.statements.push(analysis.metadataStmt);
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
|
||||
['ɵɵdefineNgModule', 'ɵɵdefineNgModule'],
|
||||
['ɵɵsetNgModuleScope', 'ɵɵsetNgModuleScope'],
|
||||
['ɵɵinject', 'ɵɵinject'],
|
||||
['ɵɵFactoryDef', 'ɵɵFactoryDef'],
|
||||
['ɵsetClassMetadata', 'setClassMetadata'],
|
||||
['ɵɵInjectableDef', 'ɵɵInjectableDef'],
|
||||
['ɵɵInjectorDef', 'ɵɵInjectorDef'],
|
||||
|
@ -353,10 +353,14 @@ export class IvyCompilation {
|
||||
const compileMatchRes =
|
||||
match.handler.compile(node as ClassDeclaration, match.analyzed.analysis, constantPool);
|
||||
this.perf.stop(compileSpan);
|
||||
if (!Array.isArray(compileMatchRes)) {
|
||||
if (Array.isArray(compileMatchRes)) {
|
||||
compileMatchRes.forEach(result => {
|
||||
if (!res.some(r => r.name === result.name)) {
|
||||
res.push(result);
|
||||
}
|
||||
});
|
||||
} else if (!res.some(result => result.name === compileMatchRes.name)) {
|
||||
res.push(compileMatchRes);
|
||||
} else {
|
||||
res.push(...compileMatchRes);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,4 +66,294 @@ describe('compiler compliance: dependency injection', () => {
|
||||
expectEmit(result.source, factory, 'Incorrect factory');
|
||||
});
|
||||
|
||||
it('should create a factory definition for an injectable', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
class MyDependency {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(dep: MyDependency) {}
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const factory = `
|
||||
MyService.ngFactoryDef = function MyService_Factory(t) {
|
||||
return new (t || MyService)($r3$.ɵɵinject(MyDependency));
|
||||
}`;
|
||||
|
||||
const def = `
|
||||
MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({
|
||||
token: MyService,
|
||||
factory: function(t) {
|
||||
return MyService.ngFactoryDef(t);
|
||||
},
|
||||
providedIn: null
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, factory, 'Incorrect factory definition');
|
||||
expectEmit(result.source, def, 'Incorrect injectable definition');
|
||||
});
|
||||
|
||||
it('should create a single ngFactoryDef if the class has more than one decorator', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Injectable, Pipe} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const result = compile(files, angularFiles).source;
|
||||
const matches = result.match(/MyPipe\.ngFactoryDef = function MyPipe_Factory/g);
|
||||
expect(matches ? matches.length : 0).toBe(1);
|
||||
});
|
||||
|
||||
it('should delegate directly to the alternate factory when setting `useFactory` without `deps`',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
class MyAlternateService {}
|
||||
|
||||
function alternateFactory() {
|
||||
return new MyAlternateService();
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
useFactory: alternateFactory
|
||||
})
|
||||
export class MyService {
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const def = `
|
||||
MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({
|
||||
token: MyService,
|
||||
factory: function() {
|
||||
return alternateFactory();
|
||||
},
|
||||
providedIn: null
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, def, 'Incorrect injectable definition');
|
||||
});
|
||||
|
||||
it('should not delegate directly to the alternate factory when setting `useFactory` with `deps`',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
class SomeDep {}
|
||||
class MyAlternateService {}
|
||||
|
||||
@Injectable({
|
||||
useFactory: () => new MyAlternateFactory(),
|
||||
deps: [SomeDep]
|
||||
})
|
||||
export class MyService {
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const def = `
|
||||
MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({
|
||||
token: MyService,
|
||||
factory: function MyService_Factory(t) {
|
||||
var r = null;
|
||||
if (t) {
|
||||
(r = new t());
|
||||
} else {
|
||||
(r = (() => new MyAlternateFactory())($r3$.ɵɵinject(SomeDep)));
|
||||
}
|
||||
return r;
|
||||
},
|
||||
providedIn: null
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, def, 'Incorrect injectable definition');
|
||||
});
|
||||
|
||||
it('should delegate directly to the alternate class factory when setting `useClass` without `deps`',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
class MyAlternateService {}
|
||||
|
||||
@Injectable({
|
||||
useClass: MyAlternateService
|
||||
})
|
||||
export class MyService {
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const factory = `
|
||||
MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({
|
||||
token: MyService,
|
||||
factory: function(t) {
|
||||
return MyAlternateService.ngFactoryDef(t);
|
||||
},
|
||||
providedIn: null
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, factory, 'Incorrect factory definition');
|
||||
});
|
||||
|
||||
it('should not delegate directly to the alternate class when setting `useClass` with `deps`',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
class SomeDep {}
|
||||
|
||||
@Injectable()
|
||||
class MyAlternateService {}
|
||||
|
||||
@Injectable({
|
||||
useClass: MyAlternateService,
|
||||
deps: [SomeDep]
|
||||
})
|
||||
export class MyService {
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const factory = `
|
||||
MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({
|
||||
token: MyService,
|
||||
factory: function MyService_Factory(t) {
|
||||
var r = null;
|
||||
if (t) {
|
||||
(r = new t());
|
||||
} else {
|
||||
(r = new MyAlternateService($r3$.ɵɵinject(SomeDep)));
|
||||
}
|
||||
return r;
|
||||
},
|
||||
providedIn: null
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, factory, 'Incorrect factory definition');
|
||||
});
|
||||
|
||||
it('should unwrap forward refs when delegating to a different class', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Injectable, forwardRef} from '@angular/core';
|
||||
|
||||
@Injectable({providedIn: 'root', useClass: forwardRef(() => SomeProviderImpl)})
|
||||
abstract class SomeProvider {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class SomeProviderImpl extends SomeProvider {
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const factory = `
|
||||
SomeProvider.ngInjectableDef = $r3$.ɵɵdefineInjectable({
|
||||
token: SomeProvider,
|
||||
factory: function(t) {
|
||||
return SomeProviderImpl.ngFactoryDef(t);
|
||||
},
|
||||
providedIn: 'root'
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, factory, 'Incorrect factory definition');
|
||||
});
|
||||
|
||||
it('should have the pipe factory take precedence over the injectable factory, if a class has multiple decorators',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, Pipe, PipeTransform, Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
class Service {}
|
||||
|
||||
@Injectable()
|
||||
@Pipe({name: 'myPipe'})
|
||||
export class MyPipe implements PipeTransform {
|
||||
constructor(service: Service) {}
|
||||
transform(value: any, ...args: any[]) { return value; }
|
||||
}
|
||||
|
||||
@Pipe({name: 'myOtherPipe'})
|
||||
@Injectable()
|
||||
export class MyOtherPipe implements PipeTransform {
|
||||
constructor(service: Service) {}
|
||||
transform(value: any, ...args: any[]) { return value; }
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: '{{0 | myPipe | myOtherPipe}}'
|
||||
})
|
||||
export class MyApp {}
|
||||
|
||||
@NgModule({declarations: [MyPipe, MyOtherPipe, MyApp], declarations: [Service]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
const source = result.source;
|
||||
|
||||
const MyPipeFactory = `
|
||||
MyPipe.ngFactoryDef = function MyPipe_Factory(t) { return new (t || MyPipe)($r3$.ɵɵdirectiveInject(Service)); };
|
||||
`;
|
||||
|
||||
const MyOtherPipeFactory = `
|
||||
MyOtherPipe.ngFactoryDef = function MyOtherPipe_Factory(t) { return new (t || MyOtherPipe)($r3$.ɵɵdirectiveInject(Service)); };
|
||||
`;
|
||||
|
||||
expectEmit(source, MyPipeFactory, 'Invalid pipe factory function');
|
||||
expectEmit(source, MyOtherPipeFactory, 'Invalid pipe factory function');
|
||||
expect(source.match(/MyPipe\.ngFactoryDef =/g) !.length).toBe(1);
|
||||
expect(source.match(/MyOtherPipe\.ngFactoryDef =/g) !.length).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -67,6 +67,8 @@ runInEachFileSystem(os => {
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef<Service>;');
|
||||
});
|
||||
|
||||
it('should compile Injectables with a generic service', () => {
|
||||
@ -83,6 +85,7 @@ runInEachFileSystem(os => {
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('Store.ngInjectableDef =');
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef<Store<any>>;');
|
||||
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef<Store<any>>;');
|
||||
});
|
||||
|
||||
@ -106,11 +109,15 @@ runInEachFileSystem(os => {
|
||||
expect(jsContents).toContain('Dep.ngInjectableDef =');
|
||||
expect(jsContents).toContain('Service.ngInjectableDef =');
|
||||
expect(jsContents)
|
||||
.toContain('return new (t || Service)(i0.ɵɵinject(Dep)); }, providedIn: \'root\' });');
|
||||
.toContain(
|
||||
'Service.ngFactoryDef = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };');
|
||||
expect(jsContents).toContain('providedIn: \'root\' })');
|
||||
expect(jsContents).not.toContain('__decorate');
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef<Service>;');
|
||||
});
|
||||
|
||||
it('should compile Injectables with providedIn and factory without errors', () => {
|
||||
@ -128,13 +135,14 @@ runInEachFileSystem(os => {
|
||||
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('Service.ngInjectableDef =');
|
||||
expect(jsContents).toContain('(r = new t());');
|
||||
expect(jsContents).toContain('(r = (function () { return new Service(); })());');
|
||||
expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {');
|
||||
expect(jsContents).toContain('return r; }, providedIn: \'root\' });');
|
||||
expect(jsContents)
|
||||
.toContain('factory: function () { return (function () { return new Service(); })(); }');
|
||||
expect(jsContents).toContain('Service_Factory(t) { return new (t || Service)(); }');
|
||||
expect(jsContents).toContain(', providedIn: \'root\' });');
|
||||
expect(jsContents).not.toContain('__decorate');
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef<Service>;');
|
||||
});
|
||||
|
||||
it('should compile Injectables with providedIn and factory with deps without errors', () => {
|
||||
@ -156,13 +164,14 @@ runInEachFileSystem(os => {
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('Service.ngInjectableDef =');
|
||||
expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {');
|
||||
expect(jsContents).toContain('(r = new t(i0.ɵɵinject(Dep)));');
|
||||
expect(jsContents).toContain('return new (t || Service)(i0.ɵɵinject(Dep));');
|
||||
expect(jsContents)
|
||||
.toContain('(r = (function (dep) { return new Service(dep); })(i0.ɵɵinject(Dep)));');
|
||||
expect(jsContents).toContain('return r; }, providedIn: \'root\' });');
|
||||
expect(jsContents).not.toContain('__decorate');
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef<Service>;');
|
||||
});
|
||||
|
||||
it('should compile @Injectable with an @Optional dependency', () => {
|
||||
@ -1282,7 +1291,7 @@ runInEachFileSystem(os => {
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toMatch(/if \(t\).*throw new Error.* else .* '42'/ms);
|
||||
expect(jsContents).toMatch(/function Test_Factory\(t\) { throw new Error\(/ms);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1290,33 +1299,35 @@ runInEachFileSystem(os => {
|
||||
it('should compile an @Injectable on a class with a non-injectable constructor', () => {
|
||||
env.tsconfig({strictInjectionParameters: false});
|
||||
env.write('test.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
@Injectable()
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error(');
|
||||
expect(jsContents)
|
||||
.toContain('Test.ngFactoryDef = function Test_Factory(t) { throw new Error(');
|
||||
});
|
||||
|
||||
it('should compile an @Injectable provided in the root on a class with a non-injectable constructor',
|
||||
() => {
|
||||
env.tsconfig({strictInjectionParameters: false});
|
||||
env.write('test.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
import {Injectable} from '@angular/core';
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error(');
|
||||
expect(jsContents)
|
||||
.toContain('Test.ngFactoryDef = function Test_Factory(t) { throw new Error(');
|
||||
});
|
||||
|
||||
});
|
||||
|
Reference in New Issue
Block a user