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:
parent
5dd2b5135d
commit
f755db78dc
|
@ -3,7 +3,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"inline": 1447,
|
||||
"main": 159944,
|
||||
"main": 155112,
|
||||
"polyfills": 59179
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'; }
|
|
@ -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);
|
||||
|
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue