fix(core): require factory to be provided for shakeable InjectionToken (#22207)

InjectionToken can be created with an ngInjectableDef, and previously
this allowed the full expressiveness of @Injectable. However, this
requires a runtime reflection system in order to generate factories
from expressed provider declarations.

Instead, this change requires scoped InjectionTokens to provide the
factory directly (likely using inject() for the arguments), bypassing
the need for a reflection system.

Fixes #22205

PR Close #22207
This commit is contained in:
Alex Rickabaugh 2018-02-13 12:51:21 -08:00 committed by Victor Berchet
parent 5dd2b5135d
commit f755db78dc
7 changed files with 40 additions and 21 deletions

View File

@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"inline": 1447,
"main": 159944,
"main": 155112,
"polyfills": 59179
}
}

View File

@ -12,6 +12,16 @@ import {ServerModule} from '@angular/platform-server';
export interface IService { readonly data: string; }
@NgModule({})
export class TokenModule {
}
export const TOKEN = new InjectionToken('test', {
scope: TokenModule,
factory: () => new Service(),
});
@Component({
selector: 'token-app',
template: '{{data}}',
@ -25,18 +35,12 @@ export class AppComponent {
imports: [
BrowserModule.withServerTransition({appId: 'id-app'}),
ServerModule,
TokenModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: [{provide: forwardRef(() => TOKEN), useClass: forwardRef(() => Service)}]
})
export class TokenAppModule {
}
export class Service { readonly data = 'fromToken'; }
export const TOKEN = new InjectionToken('test', {
scope: TokenAppModule,
useClass: Service,
deps: [],
});
export class Service { readonly data = 'fromToken'; }

View File

@ -381,9 +381,6 @@ export class Evaluator {
case ts.SyntaxKind.NewExpression:
const newExpression = <ts.NewExpression>node;
const newArgs = arrayOrEmpty(newExpression.arguments).map(arg => this.evaluateNode(arg));
if (!this.options.verboseInvalidExpression && newArgs.some(isMetadataError)) {
return recordEntry(newArgs.find(isMetadataError), node);
}
const newTarget = this.evaluateNode(newExpression.expression);
if (isMetadataError(newTarget)) {
return recordEntry(newTarget, node);

View File

@ -2094,5 +2094,24 @@ describe('ngc transformer command-line', () => {
`);
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, undefined, 1\)/);
});
it('compiles a service that depends on a token', () => {
const source = compileService(`
import {Inject, Injectable, InjectionToken} from '@angular/core';
import {Module} from './module';
export const TOKEN = new InjectionToken('desc', {scope: Module, factory: () => true});
@Injectable({
scope: Module,
})
export class Service {
constructor(@Inject(TOKEN) value: boolean) {}
}
`);
expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/);
expect(source).toMatch(/ngInjectableDef.*token: Service/);
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
});
});
});

View File

@ -48,12 +48,14 @@ export class InjectableCompiler {
} else if (v.ngMetadataName === 'Self') {
flags |= InjectFlags.Self;
} else if (v.ngMetadataName === 'Inject') {
throw new Error('@Inject() is not implemented');
token = v.token;
} else {
token = v;
}
}
}
}
if (flags !== InjectFlags.Default || defaultValue !== undefined) {
args = [ctx.importExpr(token), o.literal(defaultValue), o.literal(flags)];
} else {
args = [ctx.importExpr(token)];

View File

@ -8,11 +8,7 @@
import {Type} from '../type';
import {Injectable, convertInjectableProviderToFactory, defineInjectable} from './injectable';
import {ClassSansProvider, ExistingSansProvider, FactorySansProvider, StaticClassSansProvider, ValueSansProvider} from './provider';
export type InjectionTokenProvider = ValueSansProvider | ExistingSansProvider |
FactorySansProvider | ClassSansProvider | StaticClassSansProvider;
import {Injectable, defineInjectable} from './injectable';
/**
* Creates a token that can be used in a DI Provider.
@ -42,11 +38,11 @@ export class InjectionToken<T> {
readonly ngInjectableDef: Injectable|undefined;
constructor(protected _desc: string, options?: {scope: Type<any>}&InjectionTokenProvider) {
constructor(protected _desc: string, options?: {scope: Type<any>, factory: () => T}) {
if (options !== undefined) {
this.ngInjectableDef = defineInjectable({
scope: options.scope,
factory: convertInjectableProviderToFactory(this as any, options),
factory: options.factory,
});
} else {
this.ngInjectableDef = undefined;

View File

@ -492,7 +492,8 @@ export declare class InjectionToken<T> {
readonly ngInjectableDef: Injectable | undefined;
constructor(_desc: string, options?: {
scope: Type<any>;
} & InjectionTokenProvider);
factory: () => T;
});
toString(): string;
}