feat: change @Injectable() to support tree-shakeable tokens (#22005)

This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.

Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".

Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.

Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.

Additionally, this commit adds several unit and integration tests of various
flavors to test this change.

PR Close #22005
This commit is contained in:
Alex Rickabaugh 2018-02-02 10:33:48 -08:00 committed by Miško Hevery
parent 2d5e7d1b52
commit 235a235fab
58 changed files with 1753 additions and 228 deletions

View File

@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"inline": 1447,
"main": 154185,
"main": 159944,
"polyfills": 59179
}
}
@ -11,7 +11,7 @@
"hello_world__closure": {
"master": {
"uncompressed": {
"bundle": 101744
"bundle": 105779
}
}
},

View File

@ -0,0 +1,29 @@
{
"name": "angular-integration",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/http": "file:../../dist/packages-dist/http",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@types/node": "^9.4.0",
"rxjs": "file:../../node_modules/rxjs",
"typescript": "file:../../node_modules/typescript",
"zone.js": "file:../../node_modules/zone.js"
},
"devDependencies": {
"@types/jasmine": "2.5.41",
"concurrently": "3.4.0",
"lite-server": "2.2.2",
"protractor": "file:../../node_modules/protractor"
},
"scripts": {
"test": "./test.sh"
}
}

View File

@ -0,0 +1,21 @@
import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {ServerModule} from '@angular/platform-server';
import {Lib2Module} from 'lib2_built';
@Component({
selector: 'test-app',
template: '<test-cmp></test-cmp>',
})
export class TestApp {}
@NgModule({
declarations: [TestApp],
bootstrap: [TestApp],
imports: [
Lib2Module,
BrowserModule.withServerTransition({appId: 'appId'}),
ServerModule,
],
})
export class AppModule {}

View File

@ -0,0 +1,10 @@
import {Injectable, NgModule} from '@angular/core';
@NgModule({})
export class Lib1Module {}
@Injectable({scope: Lib1Module})
export class Service {
static instance = 0;
readonly instance = Service.instance++;
}

View File

@ -0,0 +1,23 @@
import {Component, Injector, NgModule} from '@angular/core';
import {Lib1Module, Service} from 'lib1_built';
@Component({
selector: 'test-cmp',
template: '{{instance1}}:{{instance2}}',
})
export class TestCmp {
instance1: number;
instance2: number;
constructor(service: Service, injector: Injector) {
this.instance1 = service.instance;
this.instance2 = injector.get(Service).instance;
}
}
@NgModule({
declarations: [TestCmp],
exports: [TestCmp],
imports: [Lib1Module],
})
export class Lib2Module {}

View File

@ -0,0 +1,21 @@
import 'zone.js/dist/zone-node';
import {enableProdMode} from '@angular/core';
import {renderModuleFactory} from '@angular/platform-server';
import {AppModuleNgFactory} from './app.ngfactory';
enableProdMode();
renderModuleFactory(AppModuleNgFactory, {
document: '<test-app></test-app>',
url: '/',
}).then(html => {
if (/>0:0</.test(html)) {
process.exit(0);
} else {
console.error('html was', html);
process.exit(1);
}
}).catch(err => {
console.error(err);
process.exit(2);
})

View File

@ -0,0 +1,7 @@
{
"name": "lib1_built",
"version": "0.0.0",
"license": "MIT",
"main": "./src/index.js",
"typings": "./src/index.d.ts"
}

View File

@ -0,0 +1,7 @@
{
"name": "lib2_built",
"version": "0.0.0",
"license": "MIT",
"main": "./src/index.js",
"typings": "./src/index.d.ts"
}

View File

@ -0,0 +1,17 @@
#!/bin/bash
set -e -x
NPM_BIN=$(npm bin)
PATH="$PATH:${NPM_BIN}"
rm -rf node_modules/lib1_built node_modules/lib2_built dist/
ngc -p tsconfig-lib1.json
cp src/package-lib1.json node_modules/lib1_built/package.json
ngc -p tsconfig-lib2.json
cp src/package-lib2.json node_modules/lib2_built/package.json
ngc -p tsconfig-app.json
node ./dist/src/main.js

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["es2015", "dom"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "dist",
"types": ["node"],
"rootDir": "."
},
"files": [
"src/app.ts",
"src/main.ts"
]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"moduleResolution": "node",
"lib": ["es2015", "dom"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "node_modules/lib1_built",
"types": [],
"rootDir": "."
},
"files": [
"src/lib1.ts"
],
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "lib1_built"
}
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"moduleResolution": "node",
"lib": ["es2015", "dom"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "node_modules/lib2_built",
"types": [],
"rootDir": "."
},
"files": [
"src/lib2.ts"
],
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"flatModuleId": "lib2_built",
"flatModuleOutFile": "index.js"
}
}

View File

@ -0,0 +1,21 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ng_module", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ng_module(
name = "app",
srcs = glob(
[
"src/**/*.ts",
],
),
module_name = "app_built",
deps = [
"//packages/compiler-cli/integrationtest/bazel/injectable_def/lib2",
"//packages/core",
"//packages/platform-browser",
"//packages/platform-server",
"@rxjs",
],
)

View File

@ -0,0 +1,31 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {ServerModule} from '@angular/platform-server';
import {Lib2Module} from 'lib2_built/module';
@Component({
selector: 'id-app',
template: '<lib2-cmp></lib2-cmp>',
})
export class AppComponent {
}
@NgModule({
imports: [
Lib2Module,
BrowserModule.withServerTransition({appId: 'id-app'}),
ServerModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class BasicAppModule {
}

View File

@ -0,0 +1,44 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Injectable, NgModule, Optional, Self} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {ServerModule} from '@angular/platform-server';
@Injectable()
export class Service {
}
@Component({
selector: 'hierarchy-app',
template: '<child-cmp></child-cmp>',
providers: [Service],
})
export class AppComponent {
}
@Component({
selector: 'child-cmp',
template: '{{found}}',
})
export class ChildComponent {
found: boolean;
constructor(@Optional() @Self() service: Service|null) { this.found = !!service; }
}
@NgModule({
imports: [
BrowserModule.withServerTransition({appId: 'hierarchy-app'}),
ServerModule,
],
declarations: [AppComponent, ChildComponent],
bootstrap: [AppComponent],
})
export class HierarchyAppModule {
}

View File

@ -0,0 +1,41 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Injectable, NgModule, Optional, Self} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {ServerModule} from '@angular/platform-server';
@Injectable()
export class NormalService {
constructor(@Optional() @Self() readonly shakeable: ShakeableService|null) {}
}
@Component({
selector: 'self-app',
template: '{{found}}',
})
export class AppComponent {
found: boolean;
constructor(service: NormalService) { this.found = !!service.shakeable; }
}
@NgModule({
imports: [
BrowserModule.withServerTransition({appId: 'id-app'}),
ServerModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: [NormalService],
})
export class SelfAppModule {
}
@Injectable({scope: SelfAppModule})
export class ShakeableService {
}

View File

@ -0,0 +1,42 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Inject, InjectionToken, NgModule, forwardRef} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {ServerModule} from '@angular/platform-server';
export interface IService { readonly data: string; }
@Component({
selector: 'token-app',
template: '{{data}}',
})
export class AppComponent {
data: string;
constructor(@Inject(TOKEN) service: IService) { this.data = service.data; }
}
@NgModule({
imports: [
BrowserModule.withServerTransition({appId: 'id-app'}),
ServerModule,
],
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: [],
});

View File

@ -0,0 +1,30 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ts_library(
name = "test_lib",
testonly = 1,
srcs = glob(
[
"**/*.ts",
],
),
deps = [
"//packages/compiler-cli/integrationtest/bazel/injectable_def/app",
"//packages/core",
"//packages/platform-server",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_spec.js"],
deps = [
":test_lib",
"//packages/platform-server",
"//packages/platform-server/testing",
"//tools/testing:node",
],
)

View File

@ -0,0 +1,58 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {enableProdMode} from '@angular/core';
import {renderModuleFactory} from '@angular/platform-server';
import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory';
import {HierarchyAppModuleNgFactory} from 'app_built/src/hierarchy.ngfactory';
import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory';
import {TokenAppModuleNgFactory} from 'app_built/src/token.ngfactory';
enableProdMode();
describe('ngInjectableDef Bazel Integration', () => {
it('works in AOT', done => {
renderModuleFactory(BasicAppModuleNgFactory, {
document: '<id-app></id-app>',
url: '/',
}).then(html => {
expect(html).toMatch(/>0:0<\//);
done();
});
});
it('@Self() works in component hierarchies', done => {
renderModuleFactory(HierarchyAppModuleNgFactory, {
document: '<hierarchy-app></hierarchy-app>',
url: '/',
}).then(html => {
expect(html).toMatch(/>false<\//);
done();
});
});
it('@Optional() Self() resolves to @Injectable() scoped service', done => {
renderModuleFactory(SelfAppModuleNgFactory, {
document: '<self-app></self-app>',
url: '/',
}).then(html => {
expect(html).toMatch(/>true<\//);
done();
});
});
it('InjectionToken ngInjectableDef works', done => {
renderModuleFactory(TokenAppModuleNgFactory, {
document: '<token-app></token-app>',
url: '/',
}).then(html => {
expect(html).toMatch(/>fromToken<\//);
done();
});
});
});

View File

@ -0,0 +1,17 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ng_module")
ng_module(
name = "lib1",
srcs = glob(
[
"**/*.ts",
],
),
module_name = "lib1_built",
deps = [
"//packages/core",
"@rxjs",
],
)

View File

@ -0,0 +1,21 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable, NgModule} from '@angular/core';
@NgModule({})
export class Lib1Module {
}
@Injectable({
scope: Lib1Module,
})
export class Service {
static instanceCount = 0;
instance = Service.instanceCount++;
}

View File

@ -0,0 +1,18 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ng_module")
ng_module(
name = "lib2",
srcs = glob(
[
"**/*.ts",
],
),
module_name = "lib2_built",
deps = [
"//packages/compiler-cli/integrationtest/bazel/injectable_def/lib1",
"//packages/core",
"@rxjs",
],
)

View File

@ -0,0 +1,32 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Injector, NgModule} from '@angular/core';
import {Lib1Module, Service} from 'lib1_built/module';
@Component({
selector: 'lib2-cmp',
template: '{{instance1}}:{{instance2}}',
})
export class Lib2Cmp {
instance1: number = -1;
instance2: number = -1;
constructor(service: Service, injector: Injector) {
this.instance1 = service.instance;
this.instance2 = injector.get(Service).instance;
}
}
@NgModule({
declarations: [Lib2Cmp],
exports: [Lib2Cmp],
imports: [Lib1Module],
})
export class Lib2Module {
}

View File

@ -7,7 +7,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
@ -22,7 +22,7 @@ import {MetadataCache, MetadataTransformer} from './metadata_cache';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
import {getAngularClassTransformerFactory} from './r3_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
@ -62,6 +62,7 @@ class AngularCompilerProgram implements Program {
private _hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter;
private _tsProgram: ts.Program;
private _analyzedModules: NgAnalyzedModules|undefined;
private _analyzedInjectables: NgAnalyzedFileWithInjectables[]|undefined;
private _structuralDiagnostics: Diagnostic[]|undefined;
private _programWithStubs: ts.Program|undefined;
private _optionsDiagnostics: Diagnostic[] = [];
@ -191,13 +192,15 @@ class AngularCompilerProgram implements Program {
}
return Promise.resolve()
.then(() => {
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
return this.compiler.loadFilesAsync(sourceFiles).then(analyzedModules => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronously');
}
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
});
const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
return this.compiler.loadFilesAsync(sourceFiles, tsFiles)
.then(({analyzedModules, analyzedInjectables}) => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronously');
}
this._updateProgramWithTypeCheckStubs(
tmpProgram, analyzedModules, analyzedInjectables, rootNames);
});
})
.catch(e => this._createProgramOnError(e));
}
@ -304,8 +307,12 @@ class AngularCompilerProgram implements Program {
}
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
};
const tsCustomTransformers = this.calculateTransforms(
genFileByFileName, /* partialModules */ undefined, customTransformers);
const modules = this._analyzedInjectables &&
this.compiler.emitAllPartialModules2(this._analyzedInjectables);
const tsCustomTransformers =
this.calculateTransforms(genFileByFileName, modules, customTransformers);
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
// Restore the original references before we emit so TypeScript doesn't emit
// a reference to the .d.ts file.
@ -491,9 +498,11 @@ class AngularCompilerProgram implements Program {
return;
}
try {
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
const analyzedModules = this.compiler.loadFilesSync(sourceFiles);
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
const {analyzedModules, analyzedInjectables} =
this.compiler.loadFilesSync(sourceFiles, tsFiles);
this._updateProgramWithTypeCheckStubs(
tmpProgram, analyzedModules, analyzedInjectables, rootNames);
} catch (e) {
this._createProgramOnError(e);
}
@ -520,6 +529,7 @@ class AngularCompilerProgram implements Program {
tmpProgram: ts.Program,
rootNames: string[],
sourceFiles: string[],
tsFiles: string[],
} {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initialized!`);
@ -553,17 +563,23 @@ class AngularCompilerProgram implements Program {
const tmpProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, oldTsProgram);
const sourceFiles: string[] = [];
const tsFiles: string[] = [];
tmpProgram.getSourceFiles().forEach(sf => {
if (this.hostAdapter.isSourceFile(sf.fileName)) {
sourceFiles.push(sf.fileName);
}
if (TS.test(sf.fileName) && !DTS.test(sf.fileName)) {
tsFiles.push(sf.fileName);
}
});
return {tmpProgram, sourceFiles, rootNames};
return {tmpProgram, sourceFiles, tsFiles, rootNames};
}
private _updateProgramWithTypeCheckStubs(
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules, rootNames: string[]) {
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules,
analyzedInjectables: NgAnalyzedFileWithInjectables[], rootNames: string[]) {
this._analyzedModules = analyzedModules;
this._analyzedInjectables = analyzedInjectables;
tmpProgram.getSourceFiles().forEach(sf => {
if (sf.fileName.endsWith('.ngfactory.ts')) {
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName);

View File

@ -26,7 +26,7 @@ export function getAngularClassTransformerFactory(modules: PartialModule[]): Tra
return function(context: ts.TransformationContext) {
return function(sourceFile: ts.SourceFile): ts.SourceFile {
const module = moduleMap.get(sourceFile.fileName);
if (module) {
if (module && module.statements.length > 0) {
const [newSourceFile] = updateSourceFile(sourceFile, module, context);
return newSourceFile;
}

View File

@ -14,6 +14,7 @@ import {CompilerOptions, DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from './api';
export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
export const DTS = /\.d\.ts$/;
export const TS = /^(?!.*\.d\.ts$).*\.ts$/;
export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2}

View File

@ -1942,4 +1942,157 @@ describe('ngc transformer command-line', () => {
expect(emittedFile('hello-world.js')).toContain('ngComponentDef');
});
});
describe('tree shakeable services', () => {
function compileService(source: string): string {
write('service.ts', source);
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
expect(exitCode).toEqual(0);
const servicePath = path.resolve(outDir, 'service.js');
return fs.readFileSync(servicePath, 'utf8');
}
beforeEach(() => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"files": ["service.ts"]
}`);
write('module.ts', `
import {NgModule} from '@angular/core';
@NgModule({})
export class Module {}
`);
});
describe(`doesn't break existing injectables`, () => {
it('on simple services', () => {
const source = compileService(`
import {Injectable, NgModule} from '@angular/core';
@Injectable()
export class Service {
constructor(public param: string) {}
}
@NgModule({
providers: [{provide: Service, useValue: new Service('test')}],
})
export class ServiceModule {}
`);
expect(source).not.toMatch(/ngInjectableDef/);
});
it('on a service with a base class service', () => {
const source = compileService(`
import {Injectable, NgModule} from '@angular/core';
@Injectable()
export class Dep {}
export class Base {
constructor(private dep: Dep) {}
}
@Injectable()
export class Service extends Base {}
@NgModule({
providers: [Service],
})
export class ServiceModule {}
`);
expect(source).not.toMatch(/ngInjectableDef/);
});
});
it('compiles a basic InjectableDef', () => {
const source = compileService(`
import {Injectable} from '@angular/core';
import {Module} from './module';
@Injectable({
scope: Module,
})
export class Service {}
`);
expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/);
expect(source).toMatch(/ngInjectableDef.*token: Service/);
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
});
it('compiles a useValue InjectableDef', () => {
const source = compileService(`
import {Injectable} from '@angular/core';
import {Module} from './module';
export const CONST_SERVICE: Service = null;
@Injectable({
scope: Module,
useValue: CONST_SERVICE
})
export class Service {}
`);
expect(source).toMatch(/ngInjectableDef.*return CONST_SERVICE/);
});
it('compiles a useExisting InjectableDef', () => {
const source = compileService(`
import {Injectable} from '@angular/core';
import {Module} from './module';
@Injectable()
export class Existing {}
@Injectable({
scope: Module,
useExisting: Existing,
})
export class Service {}
`);
expect(source).toMatch(/ngInjectableDef.*return ..\.inject\(Existing\)/);
});
it('compiles a useFactory InjectableDef with optional dep', () => {
const source = compileService(`
import {Injectable, Optional} from '@angular/core';
import {Module} from './module';
@Injectable()
export class Existing {}
@Injectable({
scope: Module,
useFactory: (existing: Existing|null) => new Service(existing),
deps: [[new Optional(), Existing]],
})
export class Service {
constructor(e: Existing|null) {}
}
`);
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, null, 0\)/);
});
it('compiles a useFactory InjectableDef with skip-self dep', () => {
const source = compileService(`
import {Injectable, SkipSelf} from '@angular/core';
import {Module} from './module';
@Injectable()
export class Existing {}
@Injectable({
scope: Module,
useFactory: (existing: Existing) => new Service(existing),
deps: [[new SkipSelf(), Existing]],
})
export class Service {
constructor(e: Existing) {}
}
`);
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, undefined, 1\)/);
});
});
});

View File

@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, flatten, identifierName, templateSourceUrl, tokenReference} from '../compile_metadata';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileInjectableMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, flatten, identifierName, templateSourceUrl, tokenReference} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {ConstantPool} from '../constant_pool';
import {ViewEncapsulation} from '../core';
import {MessageBundle} from '../i18n/message_bundle';
import {Identifiers, createTokenForExternalReference} from '../identifiers';
import {InjectableCompiler} from '../injectable_compiler';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
@ -49,6 +50,7 @@ export class AotCompiler {
private _templateAstCache =
new Map<StaticSymbol, {template: TemplateAst[], pipes: CompilePipeSummary[]}>();
private _analyzedFiles = new Map<string, NgAnalyzedFile>();
private _analyzedFilesForInjectables = new Map<string, NgAnalyzedFileWithInjectables>();
constructor(
private _config: CompilerConfig, private _options: AotCompilerOptions,
@ -56,7 +58,7 @@ export class AotCompiler {
private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser,
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _outputEmitter: OutputEmitter,
private _injectableCompiler: InjectableCompiler, private _outputEmitter: OutputEmitter,
private _summaryResolver: SummaryResolver<StaticSymbol>,
private _symbolResolver: StaticSymbolResolver) {}
@ -91,6 +93,16 @@ export class AotCompiler {
return analyzedFile;
}
private _analyzeFileForInjectables(fileName: string): NgAnalyzedFileWithInjectables {
let analyzedFile = this._analyzedFilesForInjectables.get(fileName);
if (!analyzedFile) {
analyzedFile = analyzeFileForInjectables(
this._host, this._symbolResolver, this._metadataResolver, fileName);
this._analyzedFilesForInjectables.set(fileName, analyzedFile);
}
return analyzedFile;
}
findGeneratedFileNames(fileName: string): string[] {
const genFileNames: string[] = [];
const file = this._analyzeFile(fileName);
@ -174,7 +186,8 @@ export class AotCompiler {
null;
}
loadFilesAsync(fileNames: string[]): Promise<NgAnalyzedModules> {
loadFilesAsync(fileNames: string[], tsFiles: string[]): Promise<
{analyzedModules: NgAnalyzedModules, analyzedInjectables: NgAnalyzedFileWithInjectables[]}> {
const files = fileNames.map(fileName => this._analyzeFile(fileName));
const loadingPromises: Promise<NgAnalyzedModules>[] = [];
files.forEach(
@ -182,16 +195,25 @@ export class AotCompiler {
ngModule =>
loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, false))));
return Promise.all(loadingPromises).then(_ => mergeAndValidateNgFiles(files));
const analyzedInjectables = tsFiles.map(tsFile => this._analyzeFileForInjectables(tsFile));
return Promise.all(loadingPromises).then(_ => ({
analyzedModules: mergeAndValidateNgFiles(files),
analyzedInjectables: analyzedInjectables,
}));
}
loadFilesSync(fileNames: string[]): NgAnalyzedModules {
loadFilesSync(fileNames: string[], tsFiles: string[]):
{analyzedModules: NgAnalyzedModules, analyzedInjectables: NgAnalyzedFileWithInjectables[]} {
const files = fileNames.map(fileName => this._analyzeFile(fileName));
files.forEach(
file => file.ngModules.forEach(
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, true)));
return mergeAndValidateNgFiles(files);
const analyzedInjectables = tsFiles.map(tsFile => this._analyzeFileForInjectables(tsFile));
return {
analyzedModules: mergeAndValidateNgFiles(files),
analyzedInjectables: analyzedInjectables,
};
}
private _createNgFactoryStub(
@ -320,7 +342,7 @@ export class AotCompiler {
private _emitPartialModule(
fileName: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[],
injectables: StaticSymbol[]): PartialModule[] {
injectables: CompileInjectableMetadata[]): PartialModule[] {
const classes: o.ClassStmt[] = [];
const context = this._createOutputContext(fileName);
@ -342,7 +364,29 @@ export class AotCompiler {
}
});
if (context.statements) {
injectables.forEach(injectable => this._injectableCompiler.compile(injectable, context));
if (context.statements && context.statements.length > 0) {
return [{fileName, statements: [...context.constantPool.statements, ...context.statements]}];
}
return [];
}
emitAllPartialModules2(files: NgAnalyzedFileWithInjectables[]): PartialModule[] {
// Using reduce like this is a select many pattern (where map is a select pattern)
return files.reduce<PartialModule[]>((r, file) => {
r.push(...this._emitPartialModule2(file.fileName, file.injectables));
return r;
}, []);
}
private _emitPartialModule2(fileName: string, injectables: CompileInjectableMetadata[]):
PartialModule[] {
const context = this._createOutputContext(fileName);
injectables.forEach(injectable => this._injectableCompiler.compile(injectable, context));
if (context.statements && context.statements.length > 0) {
return [{fileName, statements: [...context.constantPool.statements, ...context.statements]}];
}
return [];
@ -360,7 +404,7 @@ export class AotCompiler {
private _compileImplFile(
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[],
injectables: StaticSymbol[]): GeneratedFile[] {
injectables: CompileInjectableMetadata[]): GeneratedFile[] {
const fileSuffix = normalizeGenFileSuffix(splitTypescriptSuffix(srcFileUrl, true)[1]);
const generatedFiles: GeneratedFile[] = [];
@ -414,7 +458,7 @@ export class AotCompiler {
private _createSummary(
srcFileName: string, directives: StaticSymbol[], pipes: StaticSymbol[],
ngModules: CompileNgModuleMetadata[], injectables: StaticSymbol[],
ngModules: CompileNgModuleMetadata[], injectables: CompileInjectableMetadata[],
ngFactoryCtx: OutputContext): GeneratedFile[] {
const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileName)
.map(symbol => this._symbolResolver.resolveSymbol(symbol));
@ -437,10 +481,11 @@ export class AotCompiler {
summary: this._metadataResolver.getPipeSummary(ref) !,
metadata: this._metadataResolver.getPipeMetadata(ref) !
})),
...injectables.map(ref => ({
summary: this._metadataResolver.getInjectableSummary(ref) !,
metadata: this._metadataResolver.getInjectableSummary(ref) !.type
}))
...injectables.map(
ref => ({
summary: this._metadataResolver.getInjectableSummary(ref.symbol) !,
metadata: this._metadataResolver.getInjectableSummary(ref.symbol) !.type
}))
];
const forJitOutputCtx = this._options.enableSummariesForJit ?
this._createOutputContext(summaryForJitFileName(srcFileName, true)) :
@ -682,12 +727,17 @@ export interface NgAnalyzedModules {
symbolsMissingModule?: StaticSymbol[];
}
export interface NgAnalyzedFileWithInjectables {
fileName: string;
injectables: CompileInjectableMetadata[];
}
export interface NgAnalyzedFile {
fileName: string;
directives: StaticSymbol[];
pipes: StaticSymbol[];
ngModules: CompileNgModuleMetadata[];
injectables: StaticSymbol[];
injectables: CompileInjectableMetadata[];
exportsNonSourceFiles: boolean;
}
@ -747,7 +797,7 @@ export function analyzeFile(
metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFile {
const directives: StaticSymbol[] = [];
const pipes: StaticSymbol[] = [];
const injectables: StaticSymbol[] = [];
const injectables: CompileInjectableMetadata[] = [];
const ngModules: CompileNgModuleMetadata[] = [];
const hasDecorators = staticSymbolResolver.hasDecorators(fileName);
let exportsNonSourceFiles = false;
@ -779,7 +829,10 @@ export function analyzeFile(
}
} else if (metadataResolver.isInjectable(symbol)) {
isNgSymbol = true;
injectables.push(symbol);
const injectable = metadataResolver.getInjectableMetadata(symbol, null, false);
if (injectable) {
injectables.push(injectable);
}
}
}
if (!isNgSymbol) {
@ -793,6 +846,32 @@ export function analyzeFile(
};
}
export function analyzeFileForInjectables(
host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver,
metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFileWithInjectables {
const injectables: CompileInjectableMetadata[] = [];
if (staticSymbolResolver.hasDecorators(fileName)) {
staticSymbolResolver.getSymbolsOf(fileName).forEach((symbol) => {
const resolvedSymbol = staticSymbolResolver.resolveSymbol(symbol);
const symbolMeta = resolvedSymbol.metadata;
if (!symbolMeta || symbolMeta.__symbolic === 'error') {
return;
}
let isNgSymbol = false;
if (symbolMeta.__symbolic === 'class') {
if (metadataResolver.isInjectable(symbol)) {
isNgSymbol = true;
const injectable = metadataResolver.getInjectableMetadata(symbol, null, false);
if (injectable) {
injectables.push(injectable);
}
}
}
});
}
return {fileName, injectables};
}
function isValueExportingNonSourceFile(host: NgAnalyzeModulesHost, metadata: any): boolean {
let exportsNonSourceFiles = false;

View File

@ -13,6 +13,7 @@ import {DirectiveResolver} from '../directive_resolver';
import {Lexer} from '../expression_parser/lexer';
import {Parser} from '../expression_parser/parser';
import {I18NHtmlParser} from '../i18n/i18n_html_parser';
import {InjectableCompiler} from '../injectable_compiler';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {NgModuleCompiler} from '../ng_module_compiler';
@ -90,7 +91,7 @@ export function createAotCompiler(
const compiler = new AotCompiler(
config, options, compilerHost, staticReflector, resolver, tmplParser,
new StyleCompiler(urlResolver), viewCompiler, typeCheckCompiler,
new NgModuleCompiler(staticReflector), new TypeScriptEmitter(), summaryResolver,
symbolResolver);
new NgModuleCompiler(staticReflector), new InjectableCompiler(staticReflector),
new TypeScriptEmitter(), summaryResolver, symbolResolver);
return {compiler, reflector: staticReflector};
}

View File

@ -124,6 +124,16 @@ export class StaticReflector implements CompileReflector {
return symbol;
}
public tryAnnotations(type: StaticSymbol): any[] {
const originalRecorder = this.errorRecorder;
this.errorRecorder = (error: any, fileName: string) => {};
try {
return this.annotations(type);
} finally {
this.errorRecorder = originalRecorder;
}
}
public annotations(type: StaticSymbol): any[] {
let annotations = this.annotationCache.get(type);
if (!annotations) {
@ -331,6 +341,8 @@ export class StaticReflector implements CompileReflector {
}
private initializeConversionMap(): void {
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'Injectable'), createInjectable);
this.injectionToken = this.findDeclaration(ANGULAR_CORE, 'InjectionToken');
this.opaqueToken = this.findDeclaration(ANGULAR_CORE, 'OpaqueToken');
this.ROUTES = this.tryFindDeclaration(ANGULAR_ROUTER, 'ROUTES');
@ -338,8 +350,6 @@ export class StaticReflector implements CompileReflector {
this.findDeclaration(ANGULAR_CORE, 'ANALYZE_FOR_ENTRY_COMPONENTS');
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), createHost);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'Injectable'), createInjectable);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), createSelf);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf);

View File

@ -12,6 +12,9 @@ import {ValueTransformer, visitValue} from '../util';
import {StaticSymbol, StaticSymbolCache} from './static_symbol';
import {isGeneratedFile, stripSummaryForJitFileSuffix, stripSummaryForJitNameSuffix, summaryForJitFileName, summaryForJitName} from './util';
const DTS = /\.d\.ts$/;
const TS = /^(?!.*\.d\.ts$).*\.ts$/;
export class ResolvedStaticSymbol {
constructor(public symbol: StaticSymbol, public metadata: any) {}
}
@ -374,7 +377,8 @@ export class StaticSymbolResolver {
// (e.g. their constructor parameters).
// We do this to prevent introducing deep imports
// as we didn't generate .ngfactory.ts files with proper reexports.
if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath) && metadata &&
const isTsFile = TS.test(sourceSymbol.filePath);
if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath) && !isTsFile && metadata &&
metadata['__symbolic'] === 'class') {
const transformedMeta = {__symbolic: 'class', arity: metadata.arity};
return new ResolvedStaticSymbol(sourceSymbol, transformedMeta);

View File

@ -136,6 +136,19 @@ export interface CompileTokenMetadata {
identifier?: CompileIdentifierMetadata|CompileTypeMetadata;
}
export interface CompileInjectableMetadata {
symbol: StaticSymbol;
type: CompileTypeMetadata;
module?: StaticSymbol;
useValue?: any;
useClass?: StaticSymbol;
useExisting?: StaticSymbol;
useFactory?: StaticSymbol;
deps?: any[];
}
/**
* Metadata regarding compilation of a type.
*/

View File

@ -15,6 +15,7 @@ import * as o from './output/output_ast';
export abstract class CompileReflector {
abstract parameters(typeOrFunc: /*Type*/ any): any[][];
abstract annotations(typeOrFunc: /*Type*/ any): any[];
abstract tryAnnotations(typeOrFunc: /*Type*/ any): any[];
abstract propMetadata(typeOrFunc: /*Type*/ any): {[key: string]: any[]};
abstract hasLifecycleHook(type: any, lcProperty: string): boolean;
abstract guards(typeOrFunc: /* Type */ any): {[key: string]: any};

View File

@ -14,8 +14,8 @@
export interface Inject { token: any; }
export const createInject = makeMetadataFactory<Inject>('Inject', (token: any) => ({token}));
export const createInjectionToken =
makeMetadataFactory<object>('InjectionToken', (desc: string) => ({_desc: desc}));
export const createInjectionToken = makeMetadataFactory<object>(
'InjectionToken', (desc: string) => ({_desc: desc, ngInjectableDef: undefined}));
export interface Attribute { attributeName?: string; }
export const createAttribute =
@ -126,7 +126,16 @@ export interface ModuleWithProviders {
ngModule: Type;
providers?: Provider[];
}
export interface Injectable {
scope?: Type|any;
useClass?: Type|any;
useExisting?: Type|any;
useValue?: any;
useFactory?: Type|any;
deps?: Array<Type|any[]>;
}
export const createInjectable =
makeMetadataFactory('Injectable', (injectable: Injectable = {}) => injectable);
export interface SchemaMetadata { name: string; }
export const CUSTOM_ELEMENTS_SCHEMA: SchemaMetadata = {
@ -138,7 +147,6 @@ export const NO_ERRORS_SCHEMA: SchemaMetadata = {
};
export const createOptional = makeMetadataFactory('Optional');
export const createInjectable = makeMetadataFactory('Injectable');
export const createSelf = makeMetadataFactory('Self');
export const createSkipSelf = makeMetadataFactory('SkipSelf');
export const createHost = makeMetadataFactory('Host');
@ -205,7 +213,18 @@ export const enum DepFlags {
None = 0,
SkipSelf = 1 << 0,
Optional = 1 << 1,
Value = 2 << 2,
Self = 1 << 2,
Value = 1 << 3,
}
/** Injection flags for DI. */
export const enum InjectFlags {
Default = 0,
/** Skip the node that is requesting injection. */
SkipSelf = 1 << 0,
/** Don't descend into ancestors of the node requesting injection. */
Self = 1 << 1,
}
export const enum ArgumentType {Inline = 0, Dynamic = 1}

View File

@ -61,7 +61,9 @@ export class Identifiers {
moduleName: CORE,
};
static inject: o.ExternalReference = {name: 'inject', moduleName: CORE};
static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE};
static defineInjectable: o.ExternalReference = {name: 'defineInjectable', moduleName: CORE};
static ViewEncapsulation: o.ExternalReference = {
name: 'ViewEncapsulation',
moduleName: CORE,

View File

@ -0,0 +1,111 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompileInjectableMetadata, CompileNgModuleMetadata, CompileProviderMetadata, identifierName} from './compile_metadata';
import {CompileReflector} from './compile_reflector';
import {InjectFlags, NodeFlags} from './core';
import {Identifiers} from './identifiers';
import * as o from './output/output_ast';
import {convertValueToOutputAst} from './output/value_util';
import {typeSourceSpan} from './parse_util';
import {NgModuleProviderAnalyzer} from './provider_analyzer';
import {OutputContext} from './util';
import {componentFactoryResolverProviderDef, depDef, providerDef} from './view_compiler/provider_compiler';
type MapEntry = {
key: string,
quoted: boolean,
value: o.Expression
};
type MapLiteral = MapEntry[];
function mapEntry(key: string, value: o.Expression): MapEntry {
return {key, value, quoted: false};
}
export class InjectableCompiler {
constructor(private reflector: CompileReflector) {}
private depsArray(deps: any[], ctx: OutputContext): o.Expression[] {
return deps.map(dep => {
let token = dep;
let defaultValue = undefined;
let args = [token];
let flags: InjectFlags = InjectFlags.Default;
if (Array.isArray(dep)) {
for (let i = 0; i < dep.length; i++) {
const v = dep[i];
if (v) {
if (v.ngMetadataName === 'Optional') {
defaultValue = null;
} else if (v.ngMetadataName === 'SkipSelf') {
flags |= InjectFlags.SkipSelf;
} else if (v.ngMetadataName === 'Self') {
flags |= InjectFlags.Self;
} else if (v.ngMetadataName === 'Inject') {
throw new Error('@Inject() is not implemented');
} else {
token = v;
}
}
}
args = [ctx.importExpr(token), o.literal(defaultValue), o.literal(flags)];
} else {
args = [ctx.importExpr(token)];
}
return o.importExpr(Identifiers.inject).callFn(args);
});
}
private factoryFor(injectable: CompileInjectableMetadata, ctx: OutputContext): o.Expression {
let retValue: o.Expression;
if (injectable.useExisting) {
retValue = o.importExpr(Identifiers.inject).callFn([ctx.importExpr(injectable.useExisting)]);
} else if (injectable.useFactory) {
const deps = injectable.deps || [];
if (deps.length > 0) {
retValue = ctx.importExpr(injectable.useFactory).callFn(this.depsArray(deps, ctx));
} else {
return ctx.importExpr(injectable.useFactory);
}
} else if (injectable.useValue) {
retValue = convertValueToOutputAst(ctx, injectable.useValue);
} else {
const clazz = injectable.useClass || injectable.symbol;
const depArgs = this.depsArray(this.reflector.parameters(clazz), ctx);
retValue = new o.InstantiateExpr(ctx.importExpr(clazz), depArgs);
}
return o.fn(
[], [new o.ReturnStatement(retValue)], undefined, undefined,
injectable.symbol.name + '_Factory');
}
injectableDef(injectable: CompileInjectableMetadata, ctx: OutputContext): o.Expression {
const def: MapLiteral = [
mapEntry('factory', this.factoryFor(injectable, ctx)),
mapEntry('token', ctx.importExpr(injectable.type.reference)),
mapEntry('scope', ctx.importExpr(injectable.module !)),
];
return o.importExpr(Identifiers.defineInjectable).callFn([o.literalMap(def)]);
}
compile(injectable: CompileInjectableMetadata, ctx: OutputContext): void {
if (injectable.module) {
const className = identifierName(injectable.type) !;
const clazz = new o.ClassStmt(
className, null,
[
new o.ClassField(
'ngInjectableDef', o.INFERRED_TYPE, [o.StmtModifier.Static],
this.injectableDef(injectable, ctx)),
],
[], new o.ClassMethod(null, [], []), []);
ctx.statements.push(clazz);
}
}
}

View File

@ -12,7 +12,7 @@ import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions';
import * as cpl from './compile_metadata';
import {CompileReflector} from './compile_reflector';
import {CompilerConfig} from './config';
import {ChangeDetectionStrategy, Component, Directive, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, ViewEncapsulation, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core';
import {ChangeDetectionStrategy, Component, Directive, Injectable, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, ViewEncapsulation, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core';
import {DirectiveNormalizer} from './directive_normalizer';
import {DirectiveResolver} from './directive_resolver';
import {Identifiers} from './identifiers';
@ -771,7 +771,7 @@ export class CompileMetadataResolver {
}
isInjectable(type: any): boolean {
const annotations = this._reflector.annotations(type);
const annotations = this._reflector.tryAnnotations(type);
return annotations.some(ann => createInjectable.isTypeOf(ann));
}
@ -782,13 +782,32 @@ export class CompileMetadataResolver {
};
}
private _getInjectableMetadata(type: Type, dependencies: any[]|null = null):
cpl.CompileTypeMetadata {
getInjectableMetadata(
type: any, dependencies: any[]|null = null,
throwOnUnknownDeps: boolean = true): cpl.CompileInjectableMetadata|null {
const typeSummary = this._loadSummary(type, cpl.CompileSummaryKind.Injectable);
if (typeSummary) {
return typeSummary.type;
const typeMetadata = typeSummary ?
typeSummary.type :
this._getTypeMetadata(type, dependencies, throwOnUnknownDeps);
const annotations: Injectable[] =
this._reflector.annotations(type).filter(ann => createInjectable.isTypeOf(ann));
if (annotations.length === 0) {
return null;
}
return this._getTypeMetadata(type, dependencies);
const meta = annotations[annotations.length - 1];
return {
symbol: type,
type: typeMetadata,
module: meta.scope || undefined,
useValue: meta.useValue,
useClass: meta.useClass,
useExisting: meta.useExisting,
useFactory: meta.useFactory,
deps: meta.deps,
};
}
private _getTypeMetadata(type: Type, dependencies: any[]|null = null, throwOnUnknownDeps = true):
@ -1042,6 +1061,15 @@ export class CompileMetadataResolver {
return null;
}
private _getInjectableTypeMetadata(type: Type, dependencies: any[]|null = null):
cpl.CompileTypeMetadata {
const typeSummary = this._loadSummary(type, cpl.CompileSummaryKind.Injectable);
if (typeSummary) {
return typeSummary.type;
}
return this._getTypeMetadata(type, dependencies);
}
getProviderMetadata(provider: cpl.ProviderMeta): cpl.CompileProviderMetadata {
let compileDeps: cpl.CompileDiDependencyMetadata[] = undefined !;
let compileTypeMetadata: cpl.CompileTypeMetadata = null !;
@ -1049,7 +1077,8 @@ export class CompileMetadataResolver {
let token: cpl.CompileTokenMetadata = this._getTokenMetadata(provider.token);
if (provider.useClass) {
compileTypeMetadata = this._getInjectableMetadata(provider.useClass, provider.dependencies);
compileTypeMetadata =
this._getInjectableTypeMetadata(provider.useClass, provider.dependencies);
compileDeps = compileTypeMetadata.diDeps;
if (provider.token === provider.useClass) {
// use the compileTypeMetadata as it contains information about lifecycleHooks...

View File

@ -294,7 +294,7 @@ export class ProviderElementContext {
this.viewContext.viewProviders.get(tokenReference(dep.token !)) != null) {
result = dep;
} else {
result = dep.isOptional ? result = {isValue: true, value: null} : null;
result = dep.isOptional ? {isValue: true, value: null} : null;
}
}
}
@ -321,11 +321,12 @@ export class NgModuleProviderAnalyzer {
const ngModuleProvider = {token: {identifier: ngModuleType}, useClass: ngModuleType};
_resolveProviders(
[ngModuleProvider], ProviderAstType.PublicService, true, sourceSpan, this._errors,
this._allProviders, true);
this._allProviders, /* isModule */ true);
});
_resolveProviders(
ngModule.transitiveModule.providers.map(entry => entry.provider).concat(extraProviders),
ProviderAstType.PublicService, false, sourceSpan, this._errors, this._allProviders, false);
ProviderAstType.PublicService, false, sourceSpan, this._errors, this._allProviders,
/* isModule */ false);
}
parse(): ProviderAst[] {
@ -415,16 +416,7 @@ export class NgModuleProviderAnalyzer {
foundLocal = true;
}
}
let result: CompileDiDependencyMetadata = dep;
if (dep.isSelf && !foundLocal) {
if (dep.isOptional) {
result = {isValue: true, value: null};
} else {
this._errors.push(
new ProviderError(`No provider for ${tokenName(dep.token!)}`, requestorSourceSpan));
}
}
return result;
return dep;
}
}
@ -461,7 +453,7 @@ function _resolveProvidersFromDirectives(
_resolveProviders(
[dirProvider],
directive.isComponent ? ProviderAstType.Component : ProviderAstType.Directive, true,
sourceSpan, targetErrors, providersByToken, false);
sourceSpan, targetErrors, providersByToken, /* isModule */ false);
});
// Note: directives need to be able to overwrite providers of a component!
@ -470,10 +462,10 @@ function _resolveProvidersFromDirectives(
directivesWithComponentFirst.forEach((directive) => {
_resolveProviders(
directive.providers, ProviderAstType.PublicService, false, sourceSpan, targetErrors,
providersByToken, false);
providersByToken, /* isModule */ false);
_resolveProviders(
directive.viewProviders, ProviderAstType.PrivateService, false, sourceSpan, targetErrors,
providersByToken, false);
providersByToken, /* isModule */ false);
});
return providersByToken;
}

View File

@ -129,7 +129,7 @@ function tokenExpr(ctx: OutputContext, tokenMeta: CompileTokenMetadata): o.Expre
export function depDef(ctx: OutputContext, dep: CompileDiDependencyMetadata): o.Expression {
// Note: the following fields have already been normalized out by provider_analyzer:
// - isAttribute, isSelf, isHost
// - isAttribute, isHost
const expr = dep.isValue ? convertValueToOutputAst(ctx, dep.value) : tokenExpr(ctx, dep.token !);
let flags = DepFlags.None;
if (dep.isSkipSelf) {
@ -138,6 +138,9 @@ export function depDef(ctx: OutputContext, dep: CompileDiDependencyMetadata): o.
if (dep.isOptional) {
flags |= DepFlags.Optional;
}
if (dep.isSelf) {
flags |= DepFlags.Self;
}
if (dep.isValue) {
flags |= DepFlags.Value;
}

View File

@ -531,6 +531,7 @@ const minCoreIndex = `
export * from './src/change_detection';
export * from './src/metadata';
export * from './src/di/metadata';
export * from './src/di/injectable';
export * from './src/di/injector';
export * from './src/di/injection_token';
export * from './src/linker';

View File

@ -148,6 +148,10 @@ import * as core from '@angular/core';
expect(compilerCore.DepFlags.Optional).toBe(core.ɵDepFlags.Optional);
expect(compilerCore.DepFlags.Value).toBe(core.ɵDepFlags.Value);
expect(compilerCore.InjectFlags.Default).toBe(core.InjectFlags.Default);
expect(compilerCore.InjectFlags.SkipSelf).toBe(core.InjectFlags.SkipSelf);
expect(compilerCore.InjectFlags.Self).toBe(core.InjectFlags.Self);
expect(compilerCore.ArgumentType.Inline).toBe(core.ɵArgumentType.Inline);
expect(compilerCore.ArgumentType.Dynamic).toBe(core.ɵArgumentType.Dynamic);

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Optional, SkipSelf, StaticProvider} from '../../di';
import {Optional, SkipSelf} from '../../di/metadata';
import {StaticProvider} from '../../di/provider';
/**

View File

@ -13,10 +13,11 @@
*/
export * from './di/metadata';
export {defineInjectable, Injectable, InjectableDecorator, InjectableProvider, InjectableType} from './di/injectable';
export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref';
export {Injector} from './di/injector';
export {InjectFlags, Injector} from './di/injector';
export {ReflectiveInjector} from './di/reflective_injector';
export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider';
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider';

View File

@ -0,0 +1,143 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ReflectionCapabilities} from '../reflection/reflection_capabilities';
import {Type} from '../type';
import {makeDecorator, makeParamDecorator} from '../util/decorators';
import {getClosureSafeProperty} from '../util/property';
import {inject, injectArgs} from './injector';
import {ClassSansProvider, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, StaticClassProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} from './provider';
const GET_PROPERTY_NAME = {} as any;
const USE_VALUE = getClosureSafeProperty<ValueProvider>(
{provide: String, useValue: GET_PROPERTY_NAME}, GET_PROPERTY_NAME);
/**
* Injectable providers used in `@Injectable` decorator.
*
* @experimental
*/
export type InjectableProvider = ValueSansProvider | ExistingSansProvider |
StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider;
/**
* Type of the Injectable decorator / constructor function.
*
* @stable
*/
export interface InjectableDecorator {
/**
* @whatItDoes A marker metadata that marks a class as available to {@link Injector} for creation.
* @howToUse
* ```
* @Injectable()
* class Car {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/metadata_spec.ts region='Injectable'}
*
* {@link Injector} will throw an error when trying to instantiate a class that
* does not have `@Injectable` marker, as shown in the example below.
*
* {@example core/di/ts/metadata_spec.ts region='InjectableThrows'}
*
* @stable
*/
(): any;
(options?: {scope: Type<any>}&InjectableProvider): any;
new (): Injectable;
new (options?: {scope: Type<any>}&InjectableProvider): Injectable;
}
/**
* Type of the Injectable metadata.
*
* @experimental
*/
export interface Injectable {
scope?: Type<any>;
factory: () => any;
}
const EMPTY_ARRAY: any[] = [];
export function convertInjectableProviderToFactory(
type: Type<any>, provider?: InjectableProvider): () => any {
if (!provider) {
const reflectionCapabilities = new ReflectionCapabilities();
const deps = reflectionCapabilities.parameters(type);
// TODO - convert to flags.
return () => new type(...injectArgs(deps as any[]));
}
if (USE_VALUE in provider) {
const valueProvider = (provider as ValueSansProvider);
return () => valueProvider.useValue;
} else if ((provider as ExistingSansProvider).useExisting) {
const existingProvider = (provider as ExistingSansProvider);
return () => inject(existingProvider.useExisting);
} else if ((provider as FactorySansProvider).useFactory) {
const factoryProvider = (provider as FactorySansProvider);
return () => factoryProvider.useFactory(...injectArgs(factoryProvider.deps || EMPTY_ARRAY));
} else if ((provider as StaticClassSansProvider | ClassSansProvider).useClass) {
const classProvider = (provider as StaticClassSansProvider | ClassSansProvider);
let deps = (provider as StaticClassSansProvider).deps;
if (!deps) {
const reflectionCapabilities = new ReflectionCapabilities();
deps = reflectionCapabilities.parameters(type);
}
return () => new classProvider.useClass(...injectArgs(deps));
} else {
let deps = (provider as ConstructorSansProvider).deps;
if (!deps) {
const reflectionCapabilities = new ReflectionCapabilities();
deps = reflectionCapabilities.parameters(type);
}
return () => new type(...injectArgs(deps !));
}
}
/**
* Define injectable
*
* @experimental
*/
export function defineInjectable(opts: Injectable): Injectable {
return opts;
}
/**
* Injectable decorator and metadata.
*
* @stable
* @Annotation
*/
export const Injectable: InjectableDecorator = makeDecorator(
'Injectable', undefined, undefined, undefined,
(injectableType: Type<any>, options: {scope: Type<any>} & InjectableProvider) => {
if (options && options.scope) {
(injectableType as InjectableType<any>).ngInjectableDef = defineInjectable({
scope: options.scope,
factory: convertInjectableProviderToFactory(injectableType, options)
});
}
});
/**
* Type representing injectable service.
*
* @experimental
*/
export interface InjectableType<T> extends Type<T> { ngInjectableDef?: Injectable; }

View File

@ -6,6 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
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;
/**
* Creates a token that can be used in a DI Provider.
*
@ -32,7 +40,18 @@ export class InjectionToken<T> {
/** @internal */
readonly ngMetadataName = 'InjectionToken';
constructor(protected _desc: string) {}
readonly ngInjectableDef: Injectable|undefined;
constructor(protected _desc: string, options?: {scope: Type<any>}&InjectionTokenProvider) {
if (options !== undefined) {
this.ngInjectableDef = defineInjectable({
scope: options.scope,
factory: convertInjectableProviderToFactory(this as any, options),
});
} else {
this.ngInjectableDef = undefined;
}
}
toString(): string { return `InjectionToken ${this._desc}`; }
}

View File

@ -57,7 +57,7 @@ export abstract class Injector {
* Injector.THROW_IF_NOT_FOUND is given
* - Returns the `notFoundValue` otherwise
*/
abstract get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T): T;
abstract get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
/**
* @deprecated from v4.0.0 use Type<T> or InjectionToken<T>
* @suppress {duplicate}
@ -130,12 +130,12 @@ export class StaticInjector implements Injector {
recursivelyProcessProviders(records, providers);
}
get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T): T;
get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
get(token: any, notFoundValue?: any): any;
get(token: any, notFoundValue?: any): any {
get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any {
const record = this._records.get(token);
try {
return tryResolveToken(token, record, this._records, this.parent, notFoundValue);
return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags);
} catch (e) {
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
if (token[SOURCE]) {
@ -253,9 +253,9 @@ function recursivelyProcessProviders(records: Map<any, Record>, provider: Static
function tryResolveToken(
token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector,
notFoundValue: any): any {
notFoundValue: any, flags: InjectFlags): any {
try {
return resolveToken(token, record, records, parent, notFoundValue);
return resolveToken(token, record, records, parent, notFoundValue, flags);
} catch (e) {
// ensure that 'e' is of type Error.
if (!(e instanceof Error)) {
@ -273,9 +273,9 @@ function tryResolveToken(
function resolveToken(
token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector,
notFoundValue: any): any {
notFoundValue: any, flags: InjectFlags): any {
let value;
if (record) {
if (record && !(flags & InjectFlags.SkipSelf)) {
// If we don't have a record, this implies that we don't own the provider hence don't know how
// to resolve it.
value = record.value;
@ -306,13 +306,14 @@ function resolveToken(
// If we don't know how to resolve dependency and we should not check parent for it,
// than pass in Null injector.
!childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent,
options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND));
options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND,
InjectFlags.Default));
}
}
record.value = value = useNew ? new (fn as any)(...deps) : fn.apply(obj, deps);
}
} else {
value = parent.get(token, notFoundValue);
} else if (!(flags & InjectFlags.Self)) {
value = parent.get(token, notFoundValue, InjectFlags.Default);
}
return value;
}
@ -386,3 +387,73 @@ function getClosureSafeProperty<T>(objWithPropertyToExtract: T): string {
}
throw Error('!prop');
}
/**
* Injection flags for DI.
*
* @stable
*/
export const enum InjectFlags {
Default = 0,
/** Skip the node that is requesting injection. */
SkipSelf = 1 << 0,
/** Don't descend into ancestors of the node requesting injection. */
Self = 1 << 1,
}
let _currentInjector: Injector|null = null;
export function setCurrentInjector(injector: Injector | null): Injector|null {
const former = _currentInjector;
_currentInjector = injector;
return former;
}
export function inject<T>(
token: Type<T>| InjectionToken<T>, notFoundValue?: undefined, flags?: InjectFlags): T;
export function inject<T>(
token: Type<T>| InjectionToken<T>, notFoundValue: T | null, flags?: InjectFlags): T|null;
export function inject<T>(
token: Type<T>| InjectionToken<T>, notFoundValue?: T | null, flags = InjectFlags.Default): T|
null {
if (_currentInjector === null) {
throw new Error(`inject() must be called from an injection context`);
}
return _currentInjector.get(token, notFoundValue, flags);
}
export function injectArgs(types: (Type<any>| InjectionToken<any>| any[])[]): any[] {
const args: any[] = [];
for (let i = 0; i < types.length; i++) {
const arg = types[i];
if (Array.isArray(arg)) {
if (arg.length === 0) {
throw new Error('Arguments array must have arguments.');
}
let type: Type<any>|undefined = undefined;
let defaultValue: null|undefined = undefined;
let flags: InjectFlags = InjectFlags.Default;
for (let j = 0; j < arg.length; j++) {
const meta = arg[j];
if (meta instanceof Optional || meta.__proto__.ngMetadataName === 'Optional') {
defaultValue = null;
} else if (meta instanceof SkipSelf || meta.__proto__.ngMetadataName === 'SkipSelf') {
flags |= InjectFlags.SkipSelf;
} else if (meta instanceof Self || meta.__proto__.ngMetadataName === 'Self') {
flags |= InjectFlags.Self;
} else if (meta instanceof Inject) {
type = meta.token;
} else {
type = meta;
}
}
args.push(inject(type !, defaultValue, InjectFlags.Default));
} else {
args.push(inject(arg));
}
}
return args;
}

View File

@ -6,7 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ClassSansProvider, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, StaticClassProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} from '../di/provider';
import {ReflectionCapabilities} from '../reflection/reflection_capabilities';
import {Type} from '../type';
import {makeDecorator, makeParamDecorator} from '../util/decorators';
import {EMPTY_ARRAY} from '../view/util';
/**
@ -106,53 +110,6 @@ export interface Optional {}
*/
export const Optional: OptionalDecorator = makeParamDecorator('Optional');
/**
* Type of the Injectable decorator / constructor function.
*
* @stable
*/
export interface InjectableDecorator {
/**
* @whatItDoes A marker metadata that marks a class as available to {@link Injector} for creation.
* @howToUse
* ```
* @Injectable()
* class Car {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/metadata_spec.ts region='Injectable'}
*
* {@link Injector} will throw an error when trying to instantiate a class that
* does not have `@Injectable` marker, as shown in the example below.
*
* {@example core/di/ts/metadata_spec.ts region='InjectableThrows'}
*
* @stable
*/
(): any;
new (): Injectable;
}
/**
* Type of the Injectable metadata.
*
* @stable
*/
export interface Injectable {}
/**
* Injectable decorator and metadata.
*
* @stable
* @Annotation
*/
export const Injectable: InjectableDecorator = makeDecorator('Injectable');
/**
* Type of the Self decorator / constructor function.
*

View File

@ -8,6 +8,30 @@
import {Type} from '../type';
/**
* @whatItDoes Configures the {@link Injector} to return a value for a token.
* @howToUse
* ```
* @Injectable(SomeModule, {useValue: 'someValue'})
* class SomeClass {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/provider_spec.ts region='ValueSansProvider'}
*
* @experimental
*/
export interface ValueSansProvider {
/**
* The value to inject.
*/
useValue: any;
}
/**
* @whatItDoes Configures the {@link Injector} to return a value for a token.
* @howToUse
@ -24,17 +48,12 @@ import {Type} from '../type';
*
* @stable
*/
export interface ValueProvider {
export interface ValueProvider extends ValueSansProvider {
/**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/
provide: any;
/**
* The value to inject.
*/
useValue: any;
/**
* If true, then injector returns an array of instances. This is useful to allow multiple
* providers spread across many files to provide configuration information to a common token.
@ -46,6 +65,37 @@ export interface ValueProvider {
multi?: boolean;
}
/**
* @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token.
* @howToUse
* ```
* @Injectable(SomeModule, {useClass: MyService, deps: []})
* class MyService {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/provider_spec.ts region='StaticClassSansProvider'}
*
* @experimental
*/
export interface StaticClassSansProvider {
/**
* An optional class to instantiate for the `token`. (If not provided `provide` is assumed to be a
* class to instantiate)
*/
useClass: Type<any>;
/**
* A list of `token`s which need to be resolved by the injector. The list of values is then
* used as arguments to the `useClass` constructor.
*/
deps: any[];
}
/**
* @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token.
* @howToUse
@ -68,25 +118,12 @@ export interface ValueProvider {
*
* @stable
*/
export interface StaticClassProvider {
export interface StaticClassProvider extends StaticClassSansProvider {
/**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/
provide: any;
/**
* An optional class to instantiate for the `token`. (If not provided `provide` is assumed to be a
* class to
* instantiate)
*/
useClass: Type<any>;
/**
* A list of `token`s which need to be resolved by the injector. The list of values is then
* used as arguments to the `useClass` constructor.
*/
deps: any[];
/**
* If true, then injector returns an array of instances. This is useful to allow multiple
* providers spread across many files to provide configuration information to a common token.
@ -98,6 +135,31 @@ export interface StaticClassProvider {
multi?: boolean;
}
/**
* @whatItDoes Configures the {@link Injector} to return an instance of a token.
* @howToUse
* ```
* @Injectable(SomeModule, {deps: []})
* class MyService {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/provider_spec.ts region='ConstructorSansProvider'}
*
* @experimental
*/
export interface ConstructorSansProvider {
/**
* A list of `token`s which need to be resolved by the injector. The list of values is then
* used as arguments to the `useClass` constructor.
*/
deps?: any[];
}
/**
* @whatItDoes Configures the {@link Injector} to return an instance of a token.
* @howToUse
@ -117,18 +179,12 @@ export interface StaticClassProvider {
*
* @stable
*/
export interface ConstructorProvider {
export interface ConstructorProvider extends ConstructorSansProvider {
/**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/
provide: Type<any>;
/**
* A list of `token`s which need to be resolved by the injector. The list of values is then
* used as arguments to the `useClass` constructor.
*/
deps: any[];
/**
* If true, then injector returns an array of instances. This is useful to allow multiple
* providers spread across many files to provide configuration information to a common token.
@ -140,6 +196,30 @@ export interface ConstructorProvider {
multi?: boolean;
}
/**
* @whatItDoes Configures the {@link Injector} to return a value of another `useExisting` token.
* @howToUse
* ```
* @Injectable(SomeModule, {useExisting: 'someOtherToken'})
* class SomeClass {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/provider_spec.ts region='ExistingSansProvider'}
*
* @stable
*/
export interface ExistingSansProvider {
/**
* Existing `token` to return. (equivalent to `injector.get(useExisting)`)
*/
useExisting: any;
}
/**
* @whatItDoes Configures the {@link Injector} to return a value of another `useExisting` token.
* @howToUse
@ -156,17 +236,12 @@ export interface ConstructorProvider {
*
* @stable
*/
export interface ExistingProvider {
export interface ExistingProvider extends ExistingSansProvider {
/**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/
provide: any;
/**
* Existing `token` to return. (equivalent to `injector.get(useExisting)`)
*/
useExisting: any;
/**
* If true, then injector returns an array of instances. This is useful to allow multiple
* providers spread across many files to provide configuration information to a common token.
@ -178,6 +253,40 @@ export interface ExistingProvider {
multi?: boolean;
}
/**
* @whatItDoes Configures the {@link Injector} to return a value by invoking a `useFactory`
* function.
* @howToUse
* ```
* function serviceFactory() { ... }
*
* @Injectable(SomeModule, {useFactory: serviceFactory, deps: []})
* class SomeClass {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/provider_spec.ts region='FactorySansProvider'}
*
* @experimental
*/
export interface FactorySansProvider {
/**
* A function to invoke to create a value for this `token`. The function is invoked with
* resolved values of `token`s in the `deps` field.
*/
useFactory: Function;
/**
* A list of `token`s which need to be resolved by the injector. The list of values is then
* used as arguments to the `useFactory` function.
*/
deps?: any[];
}
/**
* @whatItDoes Configures the {@link Injector} to return a value by invoking a `useFactory`
* function.
@ -200,24 +309,12 @@ export interface ExistingProvider {
*
* @stable
*/
export interface FactoryProvider {
export interface FactoryProvider extends FactorySansProvider {
/**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/
provide: any;
/**
* A function to invoke to create a value for this `token`. The function is invoked with
* resolved values of `token`s in the `deps` field.
*/
useFactory: Function;
/**
* A list of `token`s which need to be resolved by the injector. The list of values is then
* used as arguments to the `useFactory` function.
*/
deps?: any[];
/**
* If true, then injector returns an array of instances. This is useful to allow multiple
* providers spread across many files to provide configuration information to a common token.
@ -270,6 +367,34 @@ export type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvi
*/
export interface TypeProvider extends Type<any> {}
/**
* @whatItDoes Configures the {@link Injector} to return a value by invoking a `useClass`
* function.
* @howToUse
* ```
*
* class SomeClassImpl {}
*
* @Injectable(SomeModule, {useClass: SomeClassImpl})
* class SomeClass {}
* ```
*
* @description
* For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}.
*
* ### Example
*
* {@example core/di/ts/provider_spec.ts region='ClassSansProvider'}
*
* @experimental
*/
export interface ClassSansProvider {
/**
* Class to instantiate for the `token`.
*/
useClass: Type<any>;
}
/**
* @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token.
* @howToUse
@ -292,17 +417,12 @@ export interface TypeProvider extends Type<any> {}
*
* @stable
*/
export interface ClassProvider {
export interface ClassProvider extends ClassSansProvider {
/**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/
provide: any;
/**
* Class to instantiate for the `token`.
*/
useClass: Type<any>;
/**
* If true, then injector returns an array of instances. This is useful to allow multiple
* providers spread across many files to provide configuration information to a common token.

View File

@ -43,18 +43,19 @@ export const PROP_METADATA = '__prop__metadata__';
*/
export function makeDecorator(
name: string, props?: (...args: any[]) => any, parentClass?: any,
chainFn?: (fn: Function) => void):
chainFn?: (fn: Function) => void, typeFn?: (type: Type<any>, ...args: any[]) => void):
{new (...args: any[]): any; (...args: any[]): any; (...args: any[]): (cls: any) => any;} {
const metaCtor = makeMetadataCtor(props);
function DecoratorFactory(objOrType: any): (cls: any) => any {
function DecoratorFactory(...args: any[]): (cls: any) => any {
if (this instanceof DecoratorFactory) {
metaCtor.call(this, objOrType);
metaCtor.call(this, ...args);
return this;
}
const annotationInstance = new (<any>DecoratorFactory)(objOrType);
const annotationInstance = new (<any>DecoratorFactory)(...args);
const TypeDecorator: TypeDecorator = <TypeDecorator>function TypeDecorator(cls: Type<any>) {
typeFn && typeFn(cls, ...args);
// Use of Object.defineProperty is important since it creates non-enumerable property which
// prevents the property is copied during subclassing.
const annotations = cls.hasOwnProperty(ANNOTATIONS) ?

View File

@ -0,0 +1,16 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export function getClosureSafeProperty<T>(objWithPropertyToExtract: T, target: any): string {
for (let key in objWithPropertyToExtract) {
if (objWithPropertyToExtract[key] === target) {
return key;
}
}
throw Error('Could not find renamed property on target object.');
}

View File

@ -7,11 +7,11 @@
*/
import {resolveForwardRef} from '../di/forward_ref';
import {Injector} from '../di/injector';
import {InjectFlags, Injector, setCurrentInjector} from '../di/injector';
import {NgModuleRef} from '../linker/ng_module_factory';
import {stringify} from '../util';
import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types';
import {DepDef, DepFlags, InjectableDef, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types';
import {splitDepsDsl, tokenKey} from './util';
const UNDEFINED_VALUE = new Object();
@ -19,6 +19,12 @@ const UNDEFINED_VALUE = new Object();
const InjectorRefTokenKey = tokenKey(Injector);
const NgModuleRefTokenKey = tokenKey(NgModuleRef);
export function injectableDef(scope: any, factory: () => any): InjectableDef {
return {
scope, factory,
};
}
export function moduleProvideDef(
flags: NodeFlags, token: any, value: any,
deps: ([DepFlags, any] | any)[]): NgModuleProviderDef {
@ -90,10 +96,32 @@ export function resolveNgModuleDep(
_createProviderInstance(data, providerDef);
}
return providerInstance === UNDEFINED_VALUE ? undefined : providerInstance;
} else if (depDef.token.ngInjectableDef && targetsModule(data, depDef.token.ngInjectableDef)) {
const injectableDef = depDef.token.ngInjectableDef as InjectableDef;
const key = tokenKey;
const index = data._providers.length;
data._def.providersByKey[depDef.tokenKey] = {
flags: NodeFlags.TypeFactoryProvider | NodeFlags.LazyProvider,
value: injectableDef.factory,
deps: [], index,
token: depDef.token,
};
const former = setCurrentInjector(data);
try {
data._providers[index] = UNDEFINED_VALUE;
return (
data._providers[index] =
_createProviderInstance(data, data._def.providersByKey[depDef.tokenKey]));
} finally {
setCurrentInjector(former);
}
}
return data._parent.get(depDef.token, notFoundValue);
}
function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean {
return def.scope != null && ngModule._def.modules.indexOf(def.scope) > -1;
}
function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any {
let injectable: any;

View File

@ -346,50 +346,56 @@ export function resolveDep(
elDef = elDef.parent !;
}
while (view) {
let searchView: ViewData|null = view;
while (searchView) {
if (elDef) {
switch (tokenKey) {
case RendererV1TokenKey: {
const compView = findCompView(view, elDef, allowPrivateServices);
const compView = findCompView(searchView, elDef, allowPrivateServices);
return createRendererV1(compView);
}
case Renderer2TokenKey: {
const compView = findCompView(view, elDef, allowPrivateServices);
const compView = findCompView(searchView, elDef, allowPrivateServices);
return compView.renderer;
}
case ElementRefTokenKey:
return new ElementRef(asElementData(view, elDef.nodeIndex).renderElement);
return new ElementRef(asElementData(searchView, elDef.nodeIndex).renderElement);
case ViewContainerRefTokenKey:
return asElementData(view, elDef.nodeIndex).viewContainer;
return asElementData(searchView, elDef.nodeIndex).viewContainer;
case TemplateRefTokenKey: {
if (elDef.element !.template) {
return asElementData(view, elDef.nodeIndex).template;
return asElementData(searchView, elDef.nodeIndex).template;
}
break;
}
case ChangeDetectorRefTokenKey: {
let cdView = findCompView(view, elDef, allowPrivateServices);
let cdView = findCompView(searchView, elDef, allowPrivateServices);
return createChangeDetectorRef(cdView);
}
case InjectorRefTokenKey:
return createInjector(view, elDef);
return createInjector(searchView, elDef);
default:
const providerDef =
(allowPrivateServices ? elDef.element !.allProviders :
elDef.element !.publicProviders) ![tokenKey];
if (providerDef) {
let providerData = asProviderData(view, providerDef.nodeIndex);
let providerData = asProviderData(searchView, providerDef.nodeIndex);
if (!providerData) {
providerData = {instance: _createProviderInstance(view, providerDef)};
view.nodes[providerDef.nodeIndex] = providerData as any;
providerData = {instance: _createProviderInstance(searchView, providerDef)};
searchView.nodes[providerDef.nodeIndex] = providerData as any;
}
return providerData.instance;
}
}
}
allowPrivateServices = isComponentView(view);
elDef = viewParentEl(view) !;
view = view.parent !;
allowPrivateServices = isComponentView(searchView);
elDef = viewParentEl(searchView) !;
searchView = searchView.parent !;
if (depDef.flags & DepFlags.Self) {
searchView = null;
}
}
const value = startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);

View File

@ -8,7 +8,7 @@
import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef} from '../change_detection/change_detection';
import {Injector} from '../di/injector';
import {InjectFlags, Injector} from '../di/injector';
import {ComponentFactory, ComponentRef} from '../linker/component_factory';
import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver';
import {ElementRef} from '../linker/element_ref';
@ -480,6 +480,7 @@ class NgModuleRef_ implements NgModuleData, InternalNgModuleRef<any> {
private _destroyed: boolean = false;
/** @internal */
_providers: any[];
/** @internal */
_modules: any[];
readonly injector: Injector = this;
@ -490,9 +491,16 @@ class NgModuleRef_ implements NgModuleData, InternalNgModuleRef<any> {
initNgModule(this);
}
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
injectFlags: InjectFlags = InjectFlags.Default): any {
let flags = DepFlags.None;
if (injectFlags & InjectFlags.SkipSelf) {
flags |= DepFlags.SkipSelf;
} else if (injectFlags & InjectFlags.Self) {
flags |= DepFlags.Self;
}
return resolveNgModuleDep(
this, {token: token, tokenKey: tokenKey(token), flags: DepFlags.None}, notFoundValue);
this, {token: token, tokenKey: tokenKey(token), flags: flags}, notFoundValue);
}
get instance() { return this.get(this._moduleType); }

View File

@ -292,7 +292,8 @@ export const enum DepFlags {
None = 0,
SkipSelf = 1 << 0,
Optional = 1 << 1,
Value = 2 << 2,
Self = 1 << 2,
Value = 1 << 3,
}
export interface InjectableDef {

View File

@ -945,15 +945,6 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(inj.get(Car)).toBeAnInstanceOf(Car);
});
it('should throw when not requested provider on self', () => {
expect(() => createInjector([{
provide: Car,
useFactory: (e: Engine) => new Car(e),
deps: [[Engine, new Self()]]
}]))
.toThrowError(/No provider for Engine/g);
});
});
describe('default', () => {

View File

@ -0,0 +1,149 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModuleRef} from '@angular/core';
import {InjectFlags, Injector, inject} from '@angular/core/src/di/injector';
import {makePropDecorator} from '@angular/core/src/util/decorators';
import {InjectableDef, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from '@angular/core/src/view';
import {moduleDef, moduleProvideDef, resolveNgModuleDep} from '@angular/core/src/view/ng_module';
import {createNgModuleRef} from '@angular/core/src/view/refs';
import {tokenKey} from '@angular/core/src/view/util';
class Foo {}
class MyModule {}
class MyChildModule {}
class NotMyModule {}
class Bar {
static ngInjectableDef: InjectableDef = {
factory: () => new Bar(),
scope: MyModule,
};
}
class Baz {
static ngInjectableDef: InjectableDef = {
factory: () => new Baz(),
scope: NotMyModule,
};
}
class HasNormalDep {
constructor(public foo: Foo) {}
static ngInjectableDef: InjectableDef = {
factory: () => new HasNormalDep(inject(Foo)),
scope: MyModule,
};
}
class HasDefinedDep {
constructor(public bar: Bar) {}
static ngInjectableDef: InjectableDef = {
factory: () => new HasDefinedDep(inject(Bar)),
scope: MyModule,
};
}
class HasOptionalDep {
constructor(public baz: Baz|null) {}
static ngInjectableDef: InjectableDef = {
factory: () => new HasOptionalDep(inject(Baz, null)),
scope: MyModule,
};
}
class ChildDep {
static ngInjectableDef: InjectableDef = {
factory: () => new ChildDep(),
scope: MyChildModule,
};
}
class FromChildWithOptionalDep {
constructor(public baz: Baz|null) {}
static ngInjectableDef: InjectableDef = {
factory: () => new FromChildWithOptionalDep(inject(Baz, null, InjectFlags.Default)),
scope: MyChildModule,
};
}
class FromChildWithSkipSelfDep {
constructor(public depFromParent: ChildDep|null, public depFromChild: Bar|null) {}
static ngInjectableDef: InjectableDef = {
factory: () => new FromChildWithSkipSelfDep(
inject(ChildDep, null, InjectFlags.SkipSelf), inject(Bar, null, InjectFlags.Self)),
scope: MyChildModule,
};
}
function makeProviders(classes: any[], modules: any[]): NgModuleDefinition {
const providers =
classes.map((token, index) => ({
index,
deps: [],
flags: NodeFlags.TypeClassProvider | NodeFlags.LazyProvider, token,
value: token,
}));
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
providers.forEach(provider => providersByKey[tokenKey(provider.token)] = provider);
return {factory: null, providers, providersByKey, modules};
}
describe('NgModuleRef_ injector', () => {
let ref: NgModuleRef<any>;
let childRef: NgModuleRef<any>;
beforeEach(() => {
ref =
createNgModuleRef(MyModule, Injector.NULL, [], makeProviders([MyModule, Foo], [MyModule]));
childRef = createNgModuleRef(
MyChildModule, ref.injector, [], makeProviders([MyChildModule], [MyChildModule]));
});
it('injects a provided value',
() => { expect(ref.injector.get(Foo) instanceof Foo).toBeTruthy(); });
it('injects an InjectableDef value',
() => { expect(ref.injector.get(Bar) instanceof Bar).toBeTruthy(); });
it('caches InjectableDef values',
() => { expect(ref.injector.get(Bar)).toBe(ref.injector.get(Bar)); });
it('injects provided deps properly', () => {
const instance = ref.injector.get(HasNormalDep);
expect(instance instanceof HasNormalDep).toBeTruthy();
expect(instance.foo).toBe(ref.injector.get(Foo));
});
it('injects defined deps properly', () => {
const instance = ref.injector.get(HasDefinedDep);
expect(instance instanceof HasDefinedDep).toBeTruthy();
expect(instance.bar).toBe(ref.injector.get(Bar));
});
it('injects optional deps properly', () => {
const instance = ref.injector.get(HasOptionalDep);
expect(instance instanceof HasOptionalDep).toBeTruthy();
expect(instance.baz).toBeNull();
});
it('injects skip-self and self deps across injectors properly', () => {
const instance = childRef.injector.get(FromChildWithSkipSelfDep);
expect(instance instanceof FromChildWithSkipSelfDep).toBeTruthy();
expect(instance.depFromParent).toBeNull();
expect(instance.depFromChild instanceof Bar).toBeTruthy();
});
it('does not inject something not scoped to the module',
() => { expect(ref.injector.get(Baz, null)).toBeNull(); });
});

View File

@ -33,6 +33,7 @@ export class JitReflector implements CompileReflector {
parameters(typeOrFunc: /*Type*/ any): any[][] {
return this.reflectionCapabilities.parameters(typeOrFunc);
}
tryAnnotations(typeOrFunc: /*Type*/ any): any[] { return this.annotations(typeOrFunc); }
annotations(typeOrFunc: /*Type*/ any): any[] {
return this.reflectionCapabilities.annotations(typeOrFunc);
}

View File

@ -164,10 +164,9 @@ export declare abstract class ChangeDetectorRef {
}
/** @stable */
export interface ClassProvider {
export interface ClassProvider extends ClassSansProvider {
multi?: boolean;
provide: any;
useClass: Type<any>;
}
/** @deprecated */
@ -344,6 +343,9 @@ export declare class DefaultIterableDiffer<V> implements IterableDiffer<V>, Iter
onDestroy(): void;
}
/** @experimental */
export declare function defineInjectable(opts: Injectable): Injectable;
/** @experimental */
export declare function destroyPlatform(): void;
@ -390,18 +392,15 @@ export declare class EventEmitter<T> extends Subject<T> {
}
/** @stable */
export interface ExistingProvider {
export interface ExistingProvider extends ExistingSansProvider {
multi?: boolean;
provide: any;
useExisting: any;
}
/** @stable */
export interface FactoryProvider {
deps?: any[];
export interface FactoryProvider extends FactorySansProvider {
multi?: boolean;
provide: any;
useFactory: Function;
}
/** @experimental */
@ -454,7 +453,21 @@ export declare const Injectable: InjectableDecorator;
/** @stable */
export interface InjectableDecorator {
/** @stable */ (): any;
(options?: {
scope: Type<any>;
} & InjectableProvider): any;
new (): Injectable;
new (options?: {
scope: Type<any>;
} & InjectableProvider): Injectable;
}
/** @experimental */
export declare type InjectableProvider = ValueSansProvider | ExistingSansProvider | StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider;
/** @experimental */
export interface InjectableType<T> extends Type<T> {
ngInjectableDef?: Injectable;
}
/** @stable */
@ -463,16 +476,26 @@ export interface InjectDecorator {
new (token: any): Inject;
}
/** @stable */
export declare const enum InjectFlags {
Default = 0,
SkipSelf = 1,
Self = 2,
}
/** @stable */
export declare class InjectionToken<T> {
protected _desc: string;
constructor(_desc: string);
readonly ngInjectableDef: Injectable | undefined;
constructor(_desc: string, options?: {
scope: Type<any>;
} & InjectionTokenProvider);
toString(): string;
}
/** @stable */
export declare abstract class Injector {
abstract get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T): T;
abstract get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
/** @deprecated */ abstract get(token: any, notFoundValue?: any): any;
static NULL: Injector;
static THROW_IF_NOT_FOUND: Object;
@ -1016,10 +1039,9 @@ export interface TypeProvider extends Type<any> {
}
/** @stable */
export interface ValueProvider {
export interface ValueProvider extends ValueSansProvider {
multi?: boolean;
provide: any;
useValue: any;
}
/** @stable */