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

@ -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);
});
});

View File

@ -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(');
});
});