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": { "master": {
"uncompressed": { "uncompressed": {
"inline": 1447, "inline": 1447,
"main": 154185, "main": 159944,
"polyfills": 59179 "polyfills": 59179
} }
} }
@ -11,7 +11,7 @@
"hello_world__closure": { "hello_world__closure": {
"master": { "master": {
"uncompressed": { "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 * 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 fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -22,7 +22,7 @@ import {MetadataCache, MetadataTransformer} from './metadata_cache';
import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
import {getAngularClassTransformerFactory} from './r3_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 _hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter;
private _tsProgram: ts.Program; private _tsProgram: ts.Program;
private _analyzedModules: NgAnalyzedModules|undefined; private _analyzedModules: NgAnalyzedModules|undefined;
private _analyzedInjectables: NgAnalyzedFileWithInjectables[]|undefined;
private _structuralDiagnostics: Diagnostic[]|undefined; private _structuralDiagnostics: Diagnostic[]|undefined;
private _programWithStubs: ts.Program|undefined; private _programWithStubs: ts.Program|undefined;
private _optionsDiagnostics: Diagnostic[] = []; private _optionsDiagnostics: Diagnostic[] = [];
@ -191,13 +192,15 @@ class AngularCompilerProgram implements Program {
} }
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs(); const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
return this.compiler.loadFilesAsync(sourceFiles).then(analyzedModules => { return this.compiler.loadFilesAsync(sourceFiles, tsFiles)
if (this._analyzedModules) { .then(({analyzedModules, analyzedInjectables}) => {
throw new Error('Angular structure loaded both synchronously and asynchronously'); if (this._analyzedModules) {
} throw new Error('Angular structure loaded both synchronously and asynchronously');
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames); }
}); this._updateProgramWithTypeCheckStubs(
tmpProgram, analyzedModules, analyzedInjectables, rootNames);
});
}) })
.catch(e => this._createProgramOnError(e)); .catch(e => this._createProgramOnError(e));
} }
@ -304,8 +307,12 @@ class AngularCompilerProgram implements Program {
} }
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); 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; const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
// Restore the original references before we emit so TypeScript doesn't emit // Restore the original references before we emit so TypeScript doesn't emit
// a reference to the .d.ts file. // a reference to the .d.ts file.
@ -491,9 +498,11 @@ class AngularCompilerProgram implements Program {
return; return;
} }
try { try {
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs(); const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
const analyzedModules = this.compiler.loadFilesSync(sourceFiles); const {analyzedModules, analyzedInjectables} =
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames); this.compiler.loadFilesSync(sourceFiles, tsFiles);
this._updateProgramWithTypeCheckStubs(
tmpProgram, analyzedModules, analyzedInjectables, rootNames);
} catch (e) { } catch (e) {
this._createProgramOnError(e); this._createProgramOnError(e);
} }
@ -520,6 +529,7 @@ class AngularCompilerProgram implements Program {
tmpProgram: ts.Program, tmpProgram: ts.Program,
rootNames: string[], rootNames: string[],
sourceFiles: string[], sourceFiles: string[],
tsFiles: string[],
} { } {
if (this._analyzedModules) { if (this._analyzedModules) {
throw new Error(`Internal Error: already initialized!`); 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 tmpProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, oldTsProgram);
const sourceFiles: string[] = []; const sourceFiles: string[] = [];
const tsFiles: string[] = [];
tmpProgram.getSourceFiles().forEach(sf => { tmpProgram.getSourceFiles().forEach(sf => {
if (this.hostAdapter.isSourceFile(sf.fileName)) { if (this.hostAdapter.isSourceFile(sf.fileName)) {
sourceFiles.push(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( private _updateProgramWithTypeCheckStubs(
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules, rootNames: string[]) { tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules,
analyzedInjectables: NgAnalyzedFileWithInjectables[], rootNames: string[]) {
this._analyzedModules = analyzedModules; this._analyzedModules = analyzedModules;
this._analyzedInjectables = analyzedInjectables;
tmpProgram.getSourceFiles().forEach(sf => { tmpProgram.getSourceFiles().forEach(sf => {
if (sf.fileName.endsWith('.ngfactory.ts')) { if (sf.fileName.endsWith('.ngfactory.ts')) {
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName); 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(context: ts.TransformationContext) {
return function(sourceFile: ts.SourceFile): ts.SourceFile { return function(sourceFile: ts.SourceFile): ts.SourceFile {
const module = moduleMap.get(sourceFile.fileName); const module = moduleMap.get(sourceFile.fileName);
if (module) { if (module && module.statements.length > 0) {
const [newSourceFile] = updateSourceFile(sourceFile, module, context); const [newSourceFile] = updateSourceFile(sourceFile, module, context);
return newSourceFile; 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 GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
export const DTS = /\.d\.ts$/; export const DTS = /\.d\.ts$/;
export const TS = /^(?!.*\.d\.ts$).*\.ts$/;
export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2} 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'); 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 * 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 {CompilerConfig} from '../config';
import {ConstantPool} from '../constant_pool'; import {ConstantPool} from '../constant_pool';
import {ViewEncapsulation} from '../core'; import {ViewEncapsulation} from '../core';
import {MessageBundle} from '../i18n/message_bundle'; import {MessageBundle} from '../i18n/message_bundle';
import {Identifiers, createTokenForExternalReference} from '../identifiers'; import {Identifiers, createTokenForExternalReference} from '../identifiers';
import {InjectableCompiler} from '../injectable_compiler';
import {CompileMetadataResolver} from '../metadata_resolver'; import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser'; import {HtmlParser} from '../ml_parser/html_parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {InterpolationConfig} from '../ml_parser/interpolation_config';
@ -49,6 +50,7 @@ export class AotCompiler {
private _templateAstCache = private _templateAstCache =
new Map<StaticSymbol, {template: TemplateAst[], pipes: CompilePipeSummary[]}>(); new Map<StaticSymbol, {template: TemplateAst[], pipes: CompilePipeSummary[]}>();
private _analyzedFiles = new Map<string, NgAnalyzedFile>(); private _analyzedFiles = new Map<string, NgAnalyzedFile>();
private _analyzedFilesForInjectables = new Map<string, NgAnalyzedFileWithInjectables>();
constructor( constructor(
private _config: CompilerConfig, private _options: AotCompilerOptions, private _config: CompilerConfig, private _options: AotCompilerOptions,
@ -56,7 +58,7 @@ export class AotCompiler {
private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser, private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser,
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler, private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _outputEmitter: OutputEmitter, private _injectableCompiler: InjectableCompiler, private _outputEmitter: OutputEmitter,
private _summaryResolver: SummaryResolver<StaticSymbol>, private _summaryResolver: SummaryResolver<StaticSymbol>,
private _symbolResolver: StaticSymbolResolver) {} private _symbolResolver: StaticSymbolResolver) {}
@ -91,6 +93,16 @@ export class AotCompiler {
return analyzedFile; 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[] { findGeneratedFileNames(fileName: string): string[] {
const genFileNames: string[] = []; const genFileNames: string[] = [];
const file = this._analyzeFile(fileName); const file = this._analyzeFile(fileName);
@ -174,7 +186,8 @@ export class AotCompiler {
null; 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 files = fileNames.map(fileName => this._analyzeFile(fileName));
const loadingPromises: Promise<NgAnalyzedModules>[] = []; const loadingPromises: Promise<NgAnalyzedModules>[] = [];
files.forEach( files.forEach(
@ -182,16 +195,25 @@ export class AotCompiler {
ngModule => ngModule =>
loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, false)))); 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)); const files = fileNames.map(fileName => this._analyzeFile(fileName));
files.forEach( files.forEach(
file => file.ngModules.forEach( file => file.ngModules.forEach(
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, true))); ngModule.type.reference, true)));
return mergeAndValidateNgFiles(files); const analyzedInjectables = tsFiles.map(tsFile => this._analyzeFileForInjectables(tsFile));
return {
analyzedModules: mergeAndValidateNgFiles(files),
analyzedInjectables: analyzedInjectables,
};
} }
private _createNgFactoryStub( private _createNgFactoryStub(
@ -320,7 +342,7 @@ export class AotCompiler {
private _emitPartialModule( private _emitPartialModule(
fileName: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>, fileName: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[],
injectables: StaticSymbol[]): PartialModule[] { injectables: CompileInjectableMetadata[]): PartialModule[] {
const classes: o.ClassStmt[] = []; const classes: o.ClassStmt[] = [];
const context = this._createOutputContext(fileName); 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 [{fileName, statements: [...context.constantPool.statements, ...context.statements]}];
} }
return []; return [];
@ -360,7 +404,7 @@ export class AotCompiler {
private _compileImplFile( private _compileImplFile(
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>, srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[],
injectables: StaticSymbol[]): GeneratedFile[] { injectables: CompileInjectableMetadata[]): GeneratedFile[] {
const fileSuffix = normalizeGenFileSuffix(splitTypescriptSuffix(srcFileUrl, true)[1]); const fileSuffix = normalizeGenFileSuffix(splitTypescriptSuffix(srcFileUrl, true)[1]);
const generatedFiles: GeneratedFile[] = []; const generatedFiles: GeneratedFile[] = [];
@ -414,7 +458,7 @@ export class AotCompiler {
private _createSummary( private _createSummary(
srcFileName: string, directives: StaticSymbol[], pipes: StaticSymbol[], srcFileName: string, directives: StaticSymbol[], pipes: StaticSymbol[],
ngModules: CompileNgModuleMetadata[], injectables: StaticSymbol[], ngModules: CompileNgModuleMetadata[], injectables: CompileInjectableMetadata[],
ngFactoryCtx: OutputContext): GeneratedFile[] { ngFactoryCtx: OutputContext): GeneratedFile[] {
const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileName) const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileName)
.map(symbol => this._symbolResolver.resolveSymbol(symbol)); .map(symbol => this._symbolResolver.resolveSymbol(symbol));
@ -437,10 +481,11 @@ export class AotCompiler {
summary: this._metadataResolver.getPipeSummary(ref) !, summary: this._metadataResolver.getPipeSummary(ref) !,
metadata: this._metadataResolver.getPipeMetadata(ref) ! metadata: this._metadataResolver.getPipeMetadata(ref) !
})), })),
...injectables.map(ref => ({ ...injectables.map(
summary: this._metadataResolver.getInjectableSummary(ref) !, ref => ({
metadata: this._metadataResolver.getInjectableSummary(ref) !.type summary: this._metadataResolver.getInjectableSummary(ref.symbol) !,
})) metadata: this._metadataResolver.getInjectableSummary(ref.symbol) !.type
}))
]; ];
const forJitOutputCtx = this._options.enableSummariesForJit ? const forJitOutputCtx = this._options.enableSummariesForJit ?
this._createOutputContext(summaryForJitFileName(srcFileName, true)) : this._createOutputContext(summaryForJitFileName(srcFileName, true)) :
@ -682,12 +727,17 @@ export interface NgAnalyzedModules {
symbolsMissingModule?: StaticSymbol[]; symbolsMissingModule?: StaticSymbol[];
} }
export interface NgAnalyzedFileWithInjectables {
fileName: string;
injectables: CompileInjectableMetadata[];
}
export interface NgAnalyzedFile { export interface NgAnalyzedFile {
fileName: string; fileName: string;
directives: StaticSymbol[]; directives: StaticSymbol[];
pipes: StaticSymbol[]; pipes: StaticSymbol[];
ngModules: CompileNgModuleMetadata[]; ngModules: CompileNgModuleMetadata[];
injectables: StaticSymbol[]; injectables: CompileInjectableMetadata[];
exportsNonSourceFiles: boolean; exportsNonSourceFiles: boolean;
} }
@ -747,7 +797,7 @@ export function analyzeFile(
metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFile { metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFile {
const directives: StaticSymbol[] = []; const directives: StaticSymbol[] = [];
const pipes: StaticSymbol[] = []; const pipes: StaticSymbol[] = [];
const injectables: StaticSymbol[] = []; const injectables: CompileInjectableMetadata[] = [];
const ngModules: CompileNgModuleMetadata[] = []; const ngModules: CompileNgModuleMetadata[] = [];
const hasDecorators = staticSymbolResolver.hasDecorators(fileName); const hasDecorators = staticSymbolResolver.hasDecorators(fileName);
let exportsNonSourceFiles = false; let exportsNonSourceFiles = false;
@ -779,7 +829,10 @@ export function analyzeFile(
} }
} else if (metadataResolver.isInjectable(symbol)) { } else if (metadataResolver.isInjectable(symbol)) {
isNgSymbol = true; isNgSymbol = true;
injectables.push(symbol); const injectable = metadataResolver.getInjectableMetadata(symbol, null, false);
if (injectable) {
injectables.push(injectable);
}
} }
} }
if (!isNgSymbol) { 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 { function isValueExportingNonSourceFile(host: NgAnalyzeModulesHost, metadata: any): boolean {
let exportsNonSourceFiles = false; let exportsNonSourceFiles = false;

View File

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

View File

@ -124,6 +124,16 @@ export class StaticReflector implements CompileReflector {
return symbol; 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[] { public annotations(type: StaticSymbol): any[] {
let annotations = this.annotationCache.get(type); let annotations = this.annotationCache.get(type);
if (!annotations) { if (!annotations) {
@ -331,6 +341,8 @@ export class StaticReflector implements CompileReflector {
} }
private initializeConversionMap(): void { private initializeConversionMap(): void {
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'Injectable'), createInjectable);
this.injectionToken = this.findDeclaration(ANGULAR_CORE, 'InjectionToken'); this.injectionToken = this.findDeclaration(ANGULAR_CORE, 'InjectionToken');
this.opaqueToken = this.findDeclaration(ANGULAR_CORE, 'OpaqueToken'); this.opaqueToken = this.findDeclaration(ANGULAR_CORE, 'OpaqueToken');
this.ROUTES = this.tryFindDeclaration(ANGULAR_ROUTER, 'ROUTES'); 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.findDeclaration(ANGULAR_CORE, 'ANALYZE_FOR_ENTRY_COMPONENTS');
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), createHost); 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, 'Self'), createSelf);
this._registerDecoratorOrConstructor( this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf); this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf);

View File

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

View File

@ -136,6 +136,19 @@ export interface CompileTokenMetadata {
identifier?: CompileIdentifierMetadata|CompileTypeMetadata; 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. * Metadata regarding compilation of a type.
*/ */

View File

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

View File

@ -14,8 +14,8 @@
export interface Inject { token: any; } export interface Inject { token: any; }
export const createInject = makeMetadataFactory<Inject>('Inject', (token: any) => ({token})); export const createInject = makeMetadataFactory<Inject>('Inject', (token: any) => ({token}));
export const createInjectionToken = export const createInjectionToken = makeMetadataFactory<object>(
makeMetadataFactory<object>('InjectionToken', (desc: string) => ({_desc: desc})); 'InjectionToken', (desc: string) => ({_desc: desc, ngInjectableDef: undefined}));
export interface Attribute { attributeName?: string; } export interface Attribute { attributeName?: string; }
export const createAttribute = export const createAttribute =
@ -126,7 +126,16 @@ export interface ModuleWithProviders {
ngModule: Type; ngModule: Type;
providers?: Provider[]; 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 interface SchemaMetadata { name: string; }
export const CUSTOM_ELEMENTS_SCHEMA: SchemaMetadata = { export const CUSTOM_ELEMENTS_SCHEMA: SchemaMetadata = {
@ -138,7 +147,6 @@ export const NO_ERRORS_SCHEMA: SchemaMetadata = {
}; };
export const createOptional = makeMetadataFactory('Optional'); export const createOptional = makeMetadataFactory('Optional');
export const createInjectable = makeMetadataFactory('Injectable');
export const createSelf = makeMetadataFactory('Self'); export const createSelf = makeMetadataFactory('Self');
export const createSkipSelf = makeMetadataFactory('SkipSelf'); export const createSkipSelf = makeMetadataFactory('SkipSelf');
export const createHost = makeMetadataFactory('Host'); export const createHost = makeMetadataFactory('Host');
@ -205,7 +213,18 @@ export const enum DepFlags {
None = 0, None = 0,
SkipSelf = 1 << 0, SkipSelf = 1 << 0,
Optional = 1 << 1, 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} export const enum ArgumentType {Inline = 0, Dynamic = 1}

View File

@ -61,7 +61,9 @@ export class Identifiers {
moduleName: CORE, moduleName: CORE,
}; };
static inject: o.ExternalReference = {name: 'inject', moduleName: CORE};
static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE}; static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE};
static defineInjectable: o.ExternalReference = {name: 'defineInjectable', moduleName: CORE};
static ViewEncapsulation: o.ExternalReference = { static ViewEncapsulation: o.ExternalReference = {
name: 'ViewEncapsulation', name: 'ViewEncapsulation',
moduleName: CORE, 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 * as cpl from './compile_metadata';
import {CompileReflector} from './compile_reflector'; import {CompileReflector} from './compile_reflector';
import {CompilerConfig} from './config'; 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 {DirectiveNormalizer} from './directive_normalizer';
import {DirectiveResolver} from './directive_resolver'; import {DirectiveResolver} from './directive_resolver';
import {Identifiers} from './identifiers'; import {Identifiers} from './identifiers';
@ -771,7 +771,7 @@ export class CompileMetadataResolver {
} }
isInjectable(type: any): boolean { isInjectable(type: any): boolean {
const annotations = this._reflector.annotations(type); const annotations = this._reflector.tryAnnotations(type);
return annotations.some(ann => createInjectable.isTypeOf(ann)); return annotations.some(ann => createInjectable.isTypeOf(ann));
} }
@ -782,13 +782,32 @@ export class CompileMetadataResolver {
}; };
} }
private _getInjectableMetadata(type: Type, dependencies: any[]|null = null): getInjectableMetadata(
cpl.CompileTypeMetadata { type: any, dependencies: any[]|null = null,
throwOnUnknownDeps: boolean = true): cpl.CompileInjectableMetadata|null {
const typeSummary = this._loadSummary(type, cpl.CompileSummaryKind.Injectable); const typeSummary = this._loadSummary(type, cpl.CompileSummaryKind.Injectable);
if (typeSummary) { const typeMetadata = typeSummary ?
return typeSummary.type; 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): private _getTypeMetadata(type: Type, dependencies: any[]|null = null, throwOnUnknownDeps = true):
@ -1042,6 +1061,15 @@ export class CompileMetadataResolver {
return null; 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 { getProviderMetadata(provider: cpl.ProviderMeta): cpl.CompileProviderMetadata {
let compileDeps: cpl.CompileDiDependencyMetadata[] = undefined !; let compileDeps: cpl.CompileDiDependencyMetadata[] = undefined !;
let compileTypeMetadata: cpl.CompileTypeMetadata = null !; let compileTypeMetadata: cpl.CompileTypeMetadata = null !;
@ -1049,7 +1077,8 @@ export class CompileMetadataResolver {
let token: cpl.CompileTokenMetadata = this._getTokenMetadata(provider.token); let token: cpl.CompileTokenMetadata = this._getTokenMetadata(provider.token);
if (provider.useClass) { if (provider.useClass) {
compileTypeMetadata = this._getInjectableMetadata(provider.useClass, provider.dependencies); compileTypeMetadata =
this._getInjectableTypeMetadata(provider.useClass, provider.dependencies);
compileDeps = compileTypeMetadata.diDeps; compileDeps = compileTypeMetadata.diDeps;
if (provider.token === provider.useClass) { if (provider.token === provider.useClass) {
// use the compileTypeMetadata as it contains information about lifecycleHooks... // 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) { this.viewContext.viewProviders.get(tokenReference(dep.token !)) != null) {
result = dep; result = dep;
} else { } 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}; const ngModuleProvider = {token: {identifier: ngModuleType}, useClass: ngModuleType};
_resolveProviders( _resolveProviders(
[ngModuleProvider], ProviderAstType.PublicService, true, sourceSpan, this._errors, [ngModuleProvider], ProviderAstType.PublicService, true, sourceSpan, this._errors,
this._allProviders, true); this._allProviders, /* isModule */ true);
}); });
_resolveProviders( _resolveProviders(
ngModule.transitiveModule.providers.map(entry => entry.provider).concat(extraProviders), 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[] { parse(): ProviderAst[] {
@ -415,16 +416,7 @@ export class NgModuleProviderAnalyzer {
foundLocal = true; foundLocal = true;
} }
} }
let result: CompileDiDependencyMetadata = dep; return 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;
} }
} }
@ -461,7 +453,7 @@ function _resolveProvidersFromDirectives(
_resolveProviders( _resolveProviders(
[dirProvider], [dirProvider],
directive.isComponent ? ProviderAstType.Component : ProviderAstType.Directive, true, 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! // Note: directives need to be able to overwrite providers of a component!
@ -470,10 +462,10 @@ function _resolveProvidersFromDirectives(
directivesWithComponentFirst.forEach((directive) => { directivesWithComponentFirst.forEach((directive) => {
_resolveProviders( _resolveProviders(
directive.providers, ProviderAstType.PublicService, false, sourceSpan, targetErrors, directive.providers, ProviderAstType.PublicService, false, sourceSpan, targetErrors,
providersByToken, false); providersByToken, /* isModule */ false);
_resolveProviders( _resolveProviders(
directive.viewProviders, ProviderAstType.PrivateService, false, sourceSpan, targetErrors, directive.viewProviders, ProviderAstType.PrivateService, false, sourceSpan, targetErrors,
providersByToken, false); providersByToken, /* isModule */ false);
}); });
return providersByToken; 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 { export function depDef(ctx: OutputContext, dep: CompileDiDependencyMetadata): o.Expression {
// Note: the following fields have already been normalized out by provider_analyzer: // 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 !); const expr = dep.isValue ? convertValueToOutputAst(ctx, dep.value) : tokenExpr(ctx, dep.token !);
let flags = DepFlags.None; let flags = DepFlags.None;
if (dep.isSkipSelf) { if (dep.isSkipSelf) {
@ -138,6 +138,9 @@ export function depDef(ctx: OutputContext, dep: CompileDiDependencyMetadata): o.
if (dep.isOptional) { if (dep.isOptional) {
flags |= DepFlags.Optional; flags |= DepFlags.Optional;
} }
if (dep.isSelf) {
flags |= DepFlags.Self;
}
if (dep.isValue) { if (dep.isValue) {
flags |= DepFlags.Value; flags |= DepFlags.Value;
} }

View File

@ -531,6 +531,7 @@ const minCoreIndex = `
export * from './src/change_detection'; export * from './src/change_detection';
export * from './src/metadata'; export * from './src/metadata';
export * from './src/di/metadata'; export * from './src/di/metadata';
export * from './src/di/injectable';
export * from './src/di/injector'; export * from './src/di/injector';
export * from './src/di/injection_token'; export * from './src/di/injection_token';
export * from './src/linker'; 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.Optional).toBe(core.ɵDepFlags.Optional);
expect(compilerCore.DepFlags.Value).toBe(core.ɵDepFlags.Value); 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.Inline).toBe(core.ɵArgumentType.Inline);
expect(compilerCore.ArgumentType.Dynamic).toBe(core.ɵArgumentType.Dynamic); expect(compilerCore.ArgumentType.Dynamic).toBe(core.ɵArgumentType.Dynamic);

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license * 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 * from './di/metadata';
export {defineInjectable, Injectable, InjectableDecorator, InjectableProvider, InjectableType} from './di/injectable';
export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref'; 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 {ReflectiveInjector} from './di/reflective_injector';
export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider';
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_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 * 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. * Creates a token that can be used in a DI Provider.
* *
@ -32,7 +40,18 @@ export class InjectionToken<T> {
/** @internal */ /** @internal */
readonly ngMetadataName = 'InjectionToken'; 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}`; } toString(): string { return `InjectionToken ${this._desc}`; }
} }

View File

@ -57,7 +57,7 @@ export abstract class Injector {
* Injector.THROW_IF_NOT_FOUND is given * Injector.THROW_IF_NOT_FOUND is given
* - Returns the `notFoundValue` otherwise * - 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> * @deprecated from v4.0.0 use Type<T> or InjectionToken<T>
* @suppress {duplicate} * @suppress {duplicate}
@ -130,12 +130,12 @@ export class StaticInjector implements Injector {
recursivelyProcessProviders(records, providers); 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): any { get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any {
const record = this._records.get(token); const record = this._records.get(token);
try { try {
return tryResolveToken(token, record, this._records, this.parent, notFoundValue); return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags);
} catch (e) { } catch (e) {
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
if (token[SOURCE]) { if (token[SOURCE]) {
@ -253,9 +253,9 @@ function recursivelyProcessProviders(records: Map<any, Record>, provider: Static
function tryResolveToken( function tryResolveToken(
token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector, token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector,
notFoundValue: any): any { notFoundValue: any, flags: InjectFlags): any {
try { try {
return resolveToken(token, record, records, parent, notFoundValue); return resolveToken(token, record, records, parent, notFoundValue, flags);
} catch (e) { } catch (e) {
// ensure that 'e' is of type Error. // ensure that 'e' is of type Error.
if (!(e instanceof Error)) { if (!(e instanceof Error)) {
@ -273,9 +273,9 @@ function tryResolveToken(
function resolveToken( function resolveToken(
token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector, token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector,
notFoundValue: any): any { notFoundValue: any, flags: InjectFlags): any {
let value; 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 // If we don't have a record, this implies that we don't own the provider hence don't know how
// to resolve it. // to resolve it.
value = record.value; 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, // If we don't know how to resolve dependency and we should not check parent for it,
// than pass in Null injector. // than pass in Null injector.
!childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent, !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); record.value = value = useNew ? new (fn as any)(...deps) : fn.apply(obj, deps);
} }
} else { } else if (!(flags & InjectFlags.Self)) {
value = parent.get(token, notFoundValue); value = parent.get(token, notFoundValue, InjectFlags.Default);
} }
return value; return value;
} }
@ -386,3 +387,73 @@ function getClosureSafeProperty<T>(objWithPropertyToExtract: T): string {
} }
throw Error('!prop'); 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 * 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 {makeDecorator, makeParamDecorator} from '../util/decorators';
import {EMPTY_ARRAY} from '../view/util';
/** /**
@ -106,53 +110,6 @@ export interface Optional {}
*/ */
export const Optional: OptionalDecorator = makeParamDecorator('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. * Type of the Self decorator / constructor function.
* *

View File

@ -8,6 +8,30 @@
import {Type} from '../type'; 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. * @whatItDoes Configures the {@link Injector} to return a value for a token.
* @howToUse * @howToUse
@ -24,17 +48,12 @@ import {Type} from '../type';
* *
* @stable * @stable
*/ */
export interface ValueProvider { export interface ValueProvider extends ValueSansProvider {
/** /**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/ */
provide: any; provide: any;
/**
* The value to inject.
*/
useValue: any;
/** /**
* If true, then injector returns an array of instances. This is useful to allow multiple * 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. * providers spread across many files to provide configuration information to a common token.
@ -46,6 +65,37 @@ export interface ValueProvider {
multi?: boolean; 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. * @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token.
* @howToUse * @howToUse
@ -68,25 +118,12 @@ export interface ValueProvider {
* *
* @stable * @stable
*/ */
export interface StaticClassProvider { export interface StaticClassProvider extends StaticClassSansProvider {
/** /**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/ */
provide: 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 * 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. * providers spread across many files to provide configuration information to a common token.
@ -98,6 +135,31 @@ export interface StaticClassProvider {
multi?: boolean; 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. * @whatItDoes Configures the {@link Injector} to return an instance of a token.
* @howToUse * @howToUse
@ -117,18 +179,12 @@ export interface StaticClassProvider {
* *
* @stable * @stable
*/ */
export interface ConstructorProvider { export interface ConstructorProvider extends ConstructorSansProvider {
/** /**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/ */
provide: Type<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 * 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. * providers spread across many files to provide configuration information to a common token.
@ -140,6 +196,30 @@ export interface ConstructorProvider {
multi?: boolean; 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. * @whatItDoes Configures the {@link Injector} to return a value of another `useExisting` token.
* @howToUse * @howToUse
@ -156,17 +236,12 @@ export interface ConstructorProvider {
* *
* @stable * @stable
*/ */
export interface ExistingProvider { export interface ExistingProvider extends ExistingSansProvider {
/** /**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/ */
provide: 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 * 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. * providers spread across many files to provide configuration information to a common token.
@ -178,6 +253,40 @@ export interface ExistingProvider {
multi?: boolean; 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` * @whatItDoes Configures the {@link Injector} to return a value by invoking a `useFactory`
* function. * function.
@ -200,24 +309,12 @@ export interface ExistingProvider {
* *
* @stable * @stable
*/ */
export interface FactoryProvider { export interface FactoryProvider extends FactorySansProvider {
/** /**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/ */
provide: 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 * 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. * 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> {} 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. * @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token.
* @howToUse * @howToUse
@ -292,17 +417,12 @@ export interface TypeProvider extends Type<any> {}
* *
* @stable * @stable
*/ */
export interface ClassProvider { export interface ClassProvider extends ClassSansProvider {
/** /**
* An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`).
*/ */
provide: 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 * 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. * 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( export function makeDecorator(
name: string, props?: (...args: any[]) => any, parentClass?: any, 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;} { {new (...args: any[]): any; (...args: any[]): any; (...args: any[]): (cls: any) => any;} {
const metaCtor = makeMetadataCtor(props); const metaCtor = makeMetadataCtor(props);
function DecoratorFactory(objOrType: any): (cls: any) => any { function DecoratorFactory(...args: any[]): (cls: any) => any {
if (this instanceof DecoratorFactory) { if (this instanceof DecoratorFactory) {
metaCtor.call(this, objOrType); metaCtor.call(this, ...args);
return this; return this;
} }
const annotationInstance = new (<any>DecoratorFactory)(objOrType); const annotationInstance = new (<any>DecoratorFactory)(...args);
const TypeDecorator: TypeDecorator = <TypeDecorator>function TypeDecorator(cls: Type<any>) { 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 // Use of Object.defineProperty is important since it creates non-enumerable property which
// prevents the property is copied during subclassing. // prevents the property is copied during subclassing.
const annotations = cls.hasOwnProperty(ANNOTATIONS) ? 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 {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 {NgModuleRef} from '../linker/ng_module_factory';
import {stringify} from '../util'; 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'; import {splitDepsDsl, tokenKey} from './util';
const UNDEFINED_VALUE = new Object(); const UNDEFINED_VALUE = new Object();
@ -19,6 +19,12 @@ const UNDEFINED_VALUE = new Object();
const InjectorRefTokenKey = tokenKey(Injector); const InjectorRefTokenKey = tokenKey(Injector);
const NgModuleRefTokenKey = tokenKey(NgModuleRef); const NgModuleRefTokenKey = tokenKey(NgModuleRef);
export function injectableDef(scope: any, factory: () => any): InjectableDef {
return {
scope, factory,
};
}
export function moduleProvideDef( export function moduleProvideDef(
flags: NodeFlags, token: any, value: any, flags: NodeFlags, token: any, value: any,
deps: ([DepFlags, any] | any)[]): NgModuleProviderDef { deps: ([DepFlags, any] | any)[]): NgModuleProviderDef {
@ -90,10 +96,32 @@ export function resolveNgModuleDep(
_createProviderInstance(data, providerDef); _createProviderInstance(data, providerDef);
} }
return providerInstance === UNDEFINED_VALUE ? undefined : providerInstance; 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); 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 { function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any {
let injectable: any; let injectable: any;

View File

@ -346,50 +346,56 @@ export function resolveDep(
elDef = elDef.parent !; elDef = elDef.parent !;
} }
while (view) { let searchView: ViewData|null = view;
while (searchView) {
if (elDef) { if (elDef) {
switch (tokenKey) { switch (tokenKey) {
case RendererV1TokenKey: { case RendererV1TokenKey: {
const compView = findCompView(view, elDef, allowPrivateServices); const compView = findCompView(searchView, elDef, allowPrivateServices);
return createRendererV1(compView); return createRendererV1(compView);
} }
case Renderer2TokenKey: { case Renderer2TokenKey: {
const compView = findCompView(view, elDef, allowPrivateServices); const compView = findCompView(searchView, elDef, allowPrivateServices);
return compView.renderer; return compView.renderer;
} }
case ElementRefTokenKey: case ElementRefTokenKey:
return new ElementRef(asElementData(view, elDef.nodeIndex).renderElement); return new ElementRef(asElementData(searchView, elDef.nodeIndex).renderElement);
case ViewContainerRefTokenKey: case ViewContainerRefTokenKey:
return asElementData(view, elDef.nodeIndex).viewContainer; return asElementData(searchView, elDef.nodeIndex).viewContainer;
case TemplateRefTokenKey: { case TemplateRefTokenKey: {
if (elDef.element !.template) { if (elDef.element !.template) {
return asElementData(view, elDef.nodeIndex).template; return asElementData(searchView, elDef.nodeIndex).template;
} }
break; break;
} }
case ChangeDetectorRefTokenKey: { case ChangeDetectorRefTokenKey: {
let cdView = findCompView(view, elDef, allowPrivateServices); let cdView = findCompView(searchView, elDef, allowPrivateServices);
return createChangeDetectorRef(cdView); return createChangeDetectorRef(cdView);
} }
case InjectorRefTokenKey: case InjectorRefTokenKey:
return createInjector(view, elDef); return createInjector(searchView, elDef);
default: default:
const providerDef = const providerDef =
(allowPrivateServices ? elDef.element !.allProviders : (allowPrivateServices ? elDef.element !.allProviders :
elDef.element !.publicProviders) ![tokenKey]; elDef.element !.publicProviders) ![tokenKey];
if (providerDef) { if (providerDef) {
let providerData = asProviderData(view, providerDef.nodeIndex); let providerData = asProviderData(searchView, providerDef.nodeIndex);
if (!providerData) { if (!providerData) {
providerData = {instance: _createProviderInstance(view, providerDef)}; providerData = {instance: _createProviderInstance(searchView, providerDef)};
view.nodes[providerDef.nodeIndex] = providerData as any; searchView.nodes[providerDef.nodeIndex] = providerData as any;
} }
return providerData.instance; return providerData.instance;
} }
} }
} }
allowPrivateServices = isComponentView(view);
elDef = viewParentEl(view) !; allowPrivateServices = isComponentView(searchView);
view = view.parent !; 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); 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 {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef} from '../change_detection/change_detection'; 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 {ComponentFactory, ComponentRef} from '../linker/component_factory';
import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver';
import {ElementRef} from '../linker/element_ref'; import {ElementRef} from '../linker/element_ref';
@ -480,6 +480,7 @@ class NgModuleRef_ implements NgModuleData, InternalNgModuleRef<any> {
private _destroyed: boolean = false; private _destroyed: boolean = false;
/** @internal */ /** @internal */
_providers: any[]; _providers: any[];
/** @internal */
_modules: any[]; _modules: any[];
readonly injector: Injector = this; readonly injector: Injector = this;
@ -490,9 +491,16 @@ class NgModuleRef_ implements NgModuleData, InternalNgModuleRef<any> {
initNgModule(this); 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( 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); } get instance() { return this.get(this._moduleType); }

View File

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

View File

@ -945,15 +945,6 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(inj.get(Car)).toBeAnInstanceOf(Car); 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', () => { 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[][] { parameters(typeOrFunc: /*Type*/ any): any[][] {
return this.reflectionCapabilities.parameters(typeOrFunc); return this.reflectionCapabilities.parameters(typeOrFunc);
} }
tryAnnotations(typeOrFunc: /*Type*/ any): any[] { return this.annotations(typeOrFunc); }
annotations(typeOrFunc: /*Type*/ any): any[] { annotations(typeOrFunc: /*Type*/ any): any[] {
return this.reflectionCapabilities.annotations(typeOrFunc); return this.reflectionCapabilities.annotations(typeOrFunc);
} }

View File

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