diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 6866a62790..0a7c71295a 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "inline": 1447, - "main": 154185, + "main": 159944, "polyfills": 59179 } } @@ -11,7 +11,7 @@ "hello_world__closure": { "master": { "uncompressed": { - "bundle": 101744 + "bundle": 105779 } } }, diff --git a/integration/injectable-def/package.json b/integration/injectable-def/package.json new file mode 100644 index 0000000000..6b48359758 --- /dev/null +++ b/integration/injectable-def/package.json @@ -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" + } +} diff --git a/integration/injectable-def/src/app.ts b/integration/injectable-def/src/app.ts new file mode 100644 index 0000000000..bc93d3432e --- /dev/null +++ b/integration/injectable-def/src/app.ts @@ -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: '', +}) +export class TestApp {} + +@NgModule({ + declarations: [TestApp], + bootstrap: [TestApp], + imports: [ + Lib2Module, + BrowserModule.withServerTransition({appId: 'appId'}), + ServerModule, + ], +}) +export class AppModule {} diff --git a/integration/injectable-def/src/lib1.ts b/integration/injectable-def/src/lib1.ts new file mode 100644 index 0000000000..093339129f --- /dev/null +++ b/integration/injectable-def/src/lib1.ts @@ -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++; +} diff --git a/integration/injectable-def/src/lib2.ts b/integration/injectable-def/src/lib2.ts new file mode 100644 index 0000000000..ebf7ce9bdd --- /dev/null +++ b/integration/injectable-def/src/lib2.ts @@ -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 {} diff --git a/integration/injectable-def/src/main.ts b/integration/injectable-def/src/main.ts new file mode 100644 index 0000000000..85cb135d51 --- /dev/null +++ b/integration/injectable-def/src/main.ts @@ -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: '', + url: '/', +}).then(html => { + if (/>0:0 { + console.error(err); + process.exit(2); +}) \ No newline at end of file diff --git a/integration/injectable-def/src/package-lib1.json b/integration/injectable-def/src/package-lib1.json new file mode 100644 index 0000000000..bbed601241 --- /dev/null +++ b/integration/injectable-def/src/package-lib1.json @@ -0,0 +1,7 @@ +{ + "name": "lib1_built", + "version": "0.0.0", + "license": "MIT", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} \ No newline at end of file diff --git a/integration/injectable-def/src/package-lib2.json b/integration/injectable-def/src/package-lib2.json new file mode 100644 index 0000000000..e6d2dbaa09 --- /dev/null +++ b/integration/injectable-def/src/package-lib2.json @@ -0,0 +1,7 @@ +{ + "name": "lib2_built", + "version": "0.0.0", + "license": "MIT", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} \ No newline at end of file diff --git a/integration/injectable-def/test.sh b/integration/injectable-def/test.sh new file mode 100755 index 0000000000..e0bceaeb07 --- /dev/null +++ b/integration/injectable-def/test.sh @@ -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 diff --git a/integration/injectable-def/tsconfig-app.json b/integration/injectable-def/tsconfig-app.json new file mode 100644 index 0000000000..bb53b78664 --- /dev/null +++ b/integration/injectable-def/tsconfig-app.json @@ -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" + ] +} \ No newline at end of file diff --git a/integration/injectable-def/tsconfig-lib1.json b/integration/injectable-def/tsconfig-lib1.json new file mode 100644 index 0000000000..f51660e935 --- /dev/null +++ b/integration/injectable-def/tsconfig-lib1.json @@ -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" + } +} \ No newline at end of file diff --git a/integration/injectable-def/tsconfig-lib2.json b/integration/injectable-def/tsconfig-lib2.json new file mode 100644 index 0000000000..c0bf416395 --- /dev/null +++ b/integration/injectable-def/tsconfig-lib2.json @@ -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" + } +} \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel new file mode 100644 index 0000000000..daf7755a35 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts new file mode 100644 index 0000000000..899eccc89d --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts @@ -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: '', +}) +export class AppComponent { +} + +@NgModule({ + imports: [ + Lib2Module, + BrowserModule.withServerTransition({appId: 'id-app'}), + ServerModule, + ], + declarations: [AppComponent], + bootstrap: [AppComponent], +}) +export class BasicAppModule { +} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts new file mode 100644 index 0000000000..86daf88798 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts @@ -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: '', + 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 { +} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts new file mode 100644 index 0000000000..9a211bc030 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts @@ -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 { +} \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts new file mode 100644 index 0000000000..ae07315cba --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts @@ -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: [], +}); diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel new file mode 100644 index 0000000000..570c6fa0e9 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts new file mode 100644 index 0000000000..3cf74df8f9 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts @@ -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: '', + url: '/', + }).then(html => { + expect(html).toMatch(/>0:0<\//); + done(); + }); + }); + + it('@Self() works in component hierarchies', done => { + renderModuleFactory(HierarchyAppModuleNgFactory, { + document: '', + url: '/', + }).then(html => { + expect(html).toMatch(/>false<\//); + done(); + }); + }); + + it('@Optional() Self() resolves to @Injectable() scoped service', done => { + renderModuleFactory(SelfAppModuleNgFactory, { + document: '', + url: '/', + }).then(html => { + expect(html).toMatch(/>true<\//); + done(); + }); + }); + + it('InjectionToken ngInjectableDef works', done => { + renderModuleFactory(TokenAppModuleNgFactory, { + document: '', + url: '/', + }).then(html => { + expect(html).toMatch(/>fromToken<\//); + done(); + }); + }); +}); diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/BUILD.bazel new file mode 100644 index 0000000000..4caedba714 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts new file mode 100644 index 0000000000..297dc07804 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts @@ -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++; +} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/BUILD.bazel new file mode 100644 index 0000000000..1c08147d30 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts new file mode 100644 index 0000000000..ebeccb0a81 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts @@ -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 { +} diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index ebe789dbd4..1e1864773c 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -7,7 +7,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler'; +import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; @@ -22,7 +22,7 @@ import {MetadataCache, MetadataTransformer} from './metadata_cache'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; import {getAngularClassTransformerFactory} from './r3_transform'; -import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util'; +import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util'; @@ -62,6 +62,7 @@ class AngularCompilerProgram implements Program { private _hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter; private _tsProgram: ts.Program; private _analyzedModules: NgAnalyzedModules|undefined; + private _analyzedInjectables: NgAnalyzedFileWithInjectables[]|undefined; private _structuralDiagnostics: Diagnostic[]|undefined; private _programWithStubs: ts.Program|undefined; private _optionsDiagnostics: Diagnostic[] = []; @@ -191,13 +192,15 @@ class AngularCompilerProgram implements Program { } return Promise.resolve() .then(() => { - const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs(); - return this.compiler.loadFilesAsync(sourceFiles).then(analyzedModules => { - if (this._analyzedModules) { - throw new Error('Angular structure loaded both synchronously and asynchronously'); - } - this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames); - }); + const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs(); + return this.compiler.loadFilesAsync(sourceFiles, tsFiles) + .then(({analyzedModules, analyzedInjectables}) => { + if (this._analyzedModules) { + throw new Error('Angular structure loaded both synchronously and asynchronously'); + } + this._updateProgramWithTypeCheckStubs( + tmpProgram, analyzedModules, analyzedInjectables, rootNames); + }); }) .catch(e => this._createProgramOnError(e)); } @@ -304,8 +307,12 @@ class AngularCompilerProgram implements Program { } this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); }; - const tsCustomTransformers = this.calculateTransforms( - genFileByFileName, /* partialModules */ undefined, customTransformers); + + const modules = this._analyzedInjectables && + this.compiler.emitAllPartialModules2(this._analyzedInjectables); + + const tsCustomTransformers = + this.calculateTransforms(genFileByFileName, modules, customTransformers); const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS; // Restore the original references before we emit so TypeScript doesn't emit // a reference to the .d.ts file. @@ -491,9 +498,11 @@ class AngularCompilerProgram implements Program { return; } try { - const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs(); - const analyzedModules = this.compiler.loadFilesSync(sourceFiles); - this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames); + const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs(); + const {analyzedModules, analyzedInjectables} = + this.compiler.loadFilesSync(sourceFiles, tsFiles); + this._updateProgramWithTypeCheckStubs( + tmpProgram, analyzedModules, analyzedInjectables, rootNames); } catch (e) { this._createProgramOnError(e); } @@ -520,6 +529,7 @@ class AngularCompilerProgram implements Program { tmpProgram: ts.Program, rootNames: string[], sourceFiles: string[], + tsFiles: string[], } { if (this._analyzedModules) { throw new Error(`Internal Error: already initialized!`); @@ -553,17 +563,23 @@ class AngularCompilerProgram implements Program { const tmpProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, oldTsProgram); const sourceFiles: string[] = []; + const tsFiles: string[] = []; tmpProgram.getSourceFiles().forEach(sf => { if (this.hostAdapter.isSourceFile(sf.fileName)) { sourceFiles.push(sf.fileName); } + if (TS.test(sf.fileName) && !DTS.test(sf.fileName)) { + tsFiles.push(sf.fileName); + } }); - return {tmpProgram, sourceFiles, rootNames}; + return {tmpProgram, sourceFiles, tsFiles, rootNames}; } private _updateProgramWithTypeCheckStubs( - tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules, rootNames: string[]) { + tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules, + analyzedInjectables: NgAnalyzedFileWithInjectables[], rootNames: string[]) { this._analyzedModules = analyzedModules; + this._analyzedInjectables = analyzedInjectables; tmpProgram.getSourceFiles().forEach(sf => { if (sf.fileName.endsWith('.ngfactory.ts')) { const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName); diff --git a/packages/compiler-cli/src/transformers/r3_transform.ts b/packages/compiler-cli/src/transformers/r3_transform.ts index c162c62c7c..5c21b8bc82 100644 --- a/packages/compiler-cli/src/transformers/r3_transform.ts +++ b/packages/compiler-cli/src/transformers/r3_transform.ts @@ -26,7 +26,7 @@ export function getAngularClassTransformerFactory(modules: PartialModule[]): Tra return function(context: ts.TransformationContext) { return function(sourceFile: ts.SourceFile): ts.SourceFile { const module = moduleMap.get(sourceFile.fileName); - if (module) { + if (module && module.statements.length > 0) { const [newSourceFile] = updateSourceFile(sourceFile, module, context); return newSourceFile; } diff --git a/packages/compiler-cli/src/transformers/util.ts b/packages/compiler-cli/src/transformers/util.ts index 858be64594..8d35326e27 100644 --- a/packages/compiler-cli/src/transformers/util.ts +++ b/packages/compiler-cli/src/transformers/util.ts @@ -14,6 +14,7 @@ import {CompilerOptions, DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from './api'; export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/; export const DTS = /\.d\.ts$/; +export const TS = /^(?!.*\.d\.ts$).*\.ts$/; export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2} diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 834e87d6d3..de60f5630a 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -1942,4 +1942,157 @@ describe('ngc transformer command-line', () => { expect(emittedFile('hello-world.js')).toContain('ngComponentDef'); }); }); + + describe('tree shakeable services', () => { + + function compileService(source: string): string { + write('service.ts', source); + + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + expect(exitCode).toEqual(0); + + const servicePath = path.resolve(outDir, 'service.js'); + return fs.readFileSync(servicePath, 'utf8'); + } + + beforeEach(() => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["service.ts"] + }`); + write('module.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Module {} + `); + }); + + describe(`doesn't break existing injectables`, () => { + it('on simple services', () => { + const source = compileService(` + import {Injectable, NgModule} from '@angular/core'; + + @Injectable() + export class Service { + constructor(public param: string) {} + } + + @NgModule({ + providers: [{provide: Service, useValue: new Service('test')}], + }) + export class ServiceModule {} + `); + expect(source).not.toMatch(/ngInjectableDef/); + }); + it('on a service with a base class service', () => { + const source = compileService(` + import {Injectable, NgModule} from '@angular/core'; + + @Injectable() + export class Dep {} + + export class Base { + constructor(private dep: Dep) {} + } + @Injectable() + export class Service extends Base {} + + @NgModule({ + providers: [Service], + }) + export class ServiceModule {} + `); + expect(source).not.toMatch(/ngInjectableDef/); + }); + }); + + it('compiles a basic InjectableDef', () => { + const source = compileService(` + import {Injectable} from '@angular/core'; + import {Module} from './module'; + + @Injectable({ + scope: Module, + }) + export class Service {} + `); + expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/); + expect(source).toMatch(/ngInjectableDef.*token: Service/); + expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/); + }); + + it('compiles a useValue InjectableDef', () => { + const source = compileService(` + import {Injectable} from '@angular/core'; + import {Module} from './module'; + + export const CONST_SERVICE: Service = null; + + @Injectable({ + scope: Module, + useValue: CONST_SERVICE + }) + export class Service {} + `); + expect(source).toMatch(/ngInjectableDef.*return CONST_SERVICE/); + }); + + it('compiles a useExisting InjectableDef', () => { + const source = compileService(` + import {Injectable} from '@angular/core'; + import {Module} from './module'; + + @Injectable() + export class Existing {} + + @Injectable({ + scope: Module, + useExisting: Existing, + }) + export class Service {} + `); + expect(source).toMatch(/ngInjectableDef.*return ..\.inject\(Existing\)/); + }); + + it('compiles a useFactory InjectableDef with optional dep', () => { + const source = compileService(` + import {Injectable, Optional} from '@angular/core'; + import {Module} from './module'; + + @Injectable() + export class Existing {} + + @Injectable({ + scope: Module, + useFactory: (existing: Existing|null) => new Service(existing), + deps: [[new Optional(), Existing]], + }) + export class Service { + constructor(e: Existing|null) {} + } + `); + expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, null, 0\)/); + }); + + it('compiles a useFactory InjectableDef with skip-self dep', () => { + const source = compileService(` + import {Injectable, SkipSelf} from '@angular/core'; + import {Module} from './module'; + + @Injectable() + export class Existing {} + + @Injectable({ + scope: Module, + useFactory: (existing: Existing) => new Service(existing), + deps: [[new SkipSelf(), Existing]], + }) + export class Service { + constructor(e: Existing) {} + } + `); + expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, undefined, 1\)/); + }); + }); }); diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index c8f1787c99..98814d35eb 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, flatten, identifierName, templateSourceUrl, tokenReference} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileInjectableMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, flatten, identifierName, templateSourceUrl, tokenReference} from '../compile_metadata'; import {CompilerConfig} from '../config'; import {ConstantPool} from '../constant_pool'; import {ViewEncapsulation} from '../core'; import {MessageBundle} from '../i18n/message_bundle'; import {Identifiers, createTokenForExternalReference} from '../identifiers'; +import {InjectableCompiler} from '../injectable_compiler'; import {CompileMetadataResolver} from '../metadata_resolver'; import {HtmlParser} from '../ml_parser/html_parser'; import {InterpolationConfig} from '../ml_parser/interpolation_config'; @@ -49,6 +50,7 @@ export class AotCompiler { private _templateAstCache = new Map(); private _analyzedFiles = new Map(); + private _analyzedFilesForInjectables = new Map(); constructor( private _config: CompilerConfig, private _options: AotCompilerOptions, @@ -56,7 +58,7 @@ export class AotCompiler { private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler, - private _outputEmitter: OutputEmitter, + private _injectableCompiler: InjectableCompiler, private _outputEmitter: OutputEmitter, private _summaryResolver: SummaryResolver, private _symbolResolver: StaticSymbolResolver) {} @@ -91,6 +93,16 @@ export class AotCompiler { return analyzedFile; } + private _analyzeFileForInjectables(fileName: string): NgAnalyzedFileWithInjectables { + let analyzedFile = this._analyzedFilesForInjectables.get(fileName); + if (!analyzedFile) { + analyzedFile = analyzeFileForInjectables( + this._host, this._symbolResolver, this._metadataResolver, fileName); + this._analyzedFilesForInjectables.set(fileName, analyzedFile); + } + return analyzedFile; + } + findGeneratedFileNames(fileName: string): string[] { const genFileNames: string[] = []; const file = this._analyzeFile(fileName); @@ -174,7 +186,8 @@ export class AotCompiler { null; } - loadFilesAsync(fileNames: string[]): Promise { + loadFilesAsync(fileNames: string[], tsFiles: string[]): Promise< + {analyzedModules: NgAnalyzedModules, analyzedInjectables: NgAnalyzedFileWithInjectables[]}> { const files = fileNames.map(fileName => this._analyzeFile(fileName)); const loadingPromises: Promise[] = []; files.forEach( @@ -182,16 +195,25 @@ export class AotCompiler { ngModule => loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( ngModule.type.reference, false)))); - return Promise.all(loadingPromises).then(_ => mergeAndValidateNgFiles(files)); + const analyzedInjectables = tsFiles.map(tsFile => this._analyzeFileForInjectables(tsFile)); + return Promise.all(loadingPromises).then(_ => ({ + analyzedModules: mergeAndValidateNgFiles(files), + analyzedInjectables: analyzedInjectables, + })); } - loadFilesSync(fileNames: string[]): NgAnalyzedModules { + loadFilesSync(fileNames: string[], tsFiles: string[]): + {analyzedModules: NgAnalyzedModules, analyzedInjectables: NgAnalyzedFileWithInjectables[]} { const files = fileNames.map(fileName => this._analyzeFile(fileName)); files.forEach( file => file.ngModules.forEach( ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( ngModule.type.reference, true))); - return mergeAndValidateNgFiles(files); + const analyzedInjectables = tsFiles.map(tsFile => this._analyzeFileForInjectables(tsFile)); + return { + analyzedModules: mergeAndValidateNgFiles(files), + analyzedInjectables: analyzedInjectables, + }; } private _createNgFactoryStub( @@ -320,7 +342,7 @@ export class AotCompiler { private _emitPartialModule( fileName: string, ngModuleByPipeOrDirective: Map, directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], - injectables: StaticSymbol[]): PartialModule[] { + injectables: CompileInjectableMetadata[]): PartialModule[] { const classes: o.ClassStmt[] = []; const context = this._createOutputContext(fileName); @@ -342,7 +364,29 @@ export class AotCompiler { } }); - if (context.statements) { + injectables.forEach(injectable => this._injectableCompiler.compile(injectable, context)); + + if (context.statements && context.statements.length > 0) { + return [{fileName, statements: [...context.constantPool.statements, ...context.statements]}]; + } + return []; + } + + emitAllPartialModules2(files: NgAnalyzedFileWithInjectables[]): PartialModule[] { + // Using reduce like this is a select many pattern (where map is a select pattern) + return files.reduce((r, file) => { + r.push(...this._emitPartialModule2(file.fileName, file.injectables)); + return r; + }, []); + } + + private _emitPartialModule2(fileName: string, injectables: CompileInjectableMetadata[]): + PartialModule[] { + const context = this._createOutputContext(fileName); + + injectables.forEach(injectable => this._injectableCompiler.compile(injectable, context)); + + if (context.statements && context.statements.length > 0) { return [{fileName, statements: [...context.constantPool.statements, ...context.statements]}]; } return []; @@ -360,7 +404,7 @@ export class AotCompiler { private _compileImplFile( srcFileUrl: string, ngModuleByPipeOrDirective: Map, directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], - injectables: StaticSymbol[]): GeneratedFile[] { + injectables: CompileInjectableMetadata[]): GeneratedFile[] { const fileSuffix = normalizeGenFileSuffix(splitTypescriptSuffix(srcFileUrl, true)[1]); const generatedFiles: GeneratedFile[] = []; @@ -414,7 +458,7 @@ export class AotCompiler { private _createSummary( srcFileName: string, directives: StaticSymbol[], pipes: StaticSymbol[], - ngModules: CompileNgModuleMetadata[], injectables: StaticSymbol[], + ngModules: CompileNgModuleMetadata[], injectables: CompileInjectableMetadata[], ngFactoryCtx: OutputContext): GeneratedFile[] { const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileName) .map(symbol => this._symbolResolver.resolveSymbol(symbol)); @@ -437,10 +481,11 @@ export class AotCompiler { summary: this._metadataResolver.getPipeSummary(ref) !, metadata: this._metadataResolver.getPipeMetadata(ref) ! })), - ...injectables.map(ref => ({ - summary: this._metadataResolver.getInjectableSummary(ref) !, - metadata: this._metadataResolver.getInjectableSummary(ref) !.type - })) + ...injectables.map( + ref => ({ + summary: this._metadataResolver.getInjectableSummary(ref.symbol) !, + metadata: this._metadataResolver.getInjectableSummary(ref.symbol) !.type + })) ]; const forJitOutputCtx = this._options.enableSummariesForJit ? this._createOutputContext(summaryForJitFileName(srcFileName, true)) : @@ -682,12 +727,17 @@ export interface NgAnalyzedModules { symbolsMissingModule?: StaticSymbol[]; } +export interface NgAnalyzedFileWithInjectables { + fileName: string; + injectables: CompileInjectableMetadata[]; +} + export interface NgAnalyzedFile { fileName: string; directives: StaticSymbol[]; pipes: StaticSymbol[]; ngModules: CompileNgModuleMetadata[]; - injectables: StaticSymbol[]; + injectables: CompileInjectableMetadata[]; exportsNonSourceFiles: boolean; } @@ -747,7 +797,7 @@ export function analyzeFile( metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFile { const directives: StaticSymbol[] = []; const pipes: StaticSymbol[] = []; - const injectables: StaticSymbol[] = []; + const injectables: CompileInjectableMetadata[] = []; const ngModules: CompileNgModuleMetadata[] = []; const hasDecorators = staticSymbolResolver.hasDecorators(fileName); let exportsNonSourceFiles = false; @@ -779,7 +829,10 @@ export function analyzeFile( } } else if (metadataResolver.isInjectable(symbol)) { isNgSymbol = true; - injectables.push(symbol); + const injectable = metadataResolver.getInjectableMetadata(symbol, null, false); + if (injectable) { + injectables.push(injectable); + } } } if (!isNgSymbol) { @@ -793,6 +846,32 @@ export function analyzeFile( }; } +export function analyzeFileForInjectables( + host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver, + metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFileWithInjectables { + const injectables: CompileInjectableMetadata[] = []; + if (staticSymbolResolver.hasDecorators(fileName)) { + staticSymbolResolver.getSymbolsOf(fileName).forEach((symbol) => { + const resolvedSymbol = staticSymbolResolver.resolveSymbol(symbol); + const symbolMeta = resolvedSymbol.metadata; + if (!symbolMeta || symbolMeta.__symbolic === 'error') { + return; + } + let isNgSymbol = false; + if (symbolMeta.__symbolic === 'class') { + if (metadataResolver.isInjectable(symbol)) { + isNgSymbol = true; + const injectable = metadataResolver.getInjectableMetadata(symbol, null, false); + if (injectable) { + injectables.push(injectable); + } + } + } + }); + } + return {fileName, injectables}; +} + function isValueExportingNonSourceFile(host: NgAnalyzeModulesHost, metadata: any): boolean { let exportsNonSourceFiles = false; diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index 20dd4a941b..708cef7aa2 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -13,6 +13,7 @@ import {DirectiveResolver} from '../directive_resolver'; import {Lexer} from '../expression_parser/lexer'; import {Parser} from '../expression_parser/parser'; import {I18NHtmlParser} from '../i18n/i18n_html_parser'; +import {InjectableCompiler} from '../injectable_compiler'; import {CompileMetadataResolver} from '../metadata_resolver'; import {HtmlParser} from '../ml_parser/html_parser'; import {NgModuleCompiler} from '../ng_module_compiler'; @@ -90,7 +91,7 @@ export function createAotCompiler( const compiler = new AotCompiler( config, options, compilerHost, staticReflector, resolver, tmplParser, new StyleCompiler(urlResolver), viewCompiler, typeCheckCompiler, - new NgModuleCompiler(staticReflector), new TypeScriptEmitter(), summaryResolver, - symbolResolver); + new NgModuleCompiler(staticReflector), new InjectableCompiler(staticReflector), + new TypeScriptEmitter(), summaryResolver, symbolResolver); return {compiler, reflector: staticReflector}; } diff --git a/packages/compiler/src/aot/static_reflector.ts b/packages/compiler/src/aot/static_reflector.ts index c8349e517d..bc4380d6f8 100644 --- a/packages/compiler/src/aot/static_reflector.ts +++ b/packages/compiler/src/aot/static_reflector.ts @@ -124,6 +124,16 @@ export class StaticReflector implements CompileReflector { return symbol; } + public tryAnnotations(type: StaticSymbol): any[] { + const originalRecorder = this.errorRecorder; + this.errorRecorder = (error: any, fileName: string) => {}; + try { + return this.annotations(type); + } finally { + this.errorRecorder = originalRecorder; + } + } + public annotations(type: StaticSymbol): any[] { let annotations = this.annotationCache.get(type); if (!annotations) { @@ -331,6 +341,8 @@ export class StaticReflector implements CompileReflector { } private initializeConversionMap(): void { + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Injectable'), createInjectable); this.injectionToken = this.findDeclaration(ANGULAR_CORE, 'InjectionToken'); this.opaqueToken = this.findDeclaration(ANGULAR_CORE, 'OpaqueToken'); this.ROUTES = this.tryFindDeclaration(ANGULAR_ROUTER, 'ROUTES'); @@ -338,8 +350,6 @@ export class StaticReflector implements CompileReflector { this.findDeclaration(ANGULAR_CORE, 'ANALYZE_FOR_ENTRY_COMPONENTS'); this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), createHost); - this._registerDecoratorOrConstructor( - this.findDeclaration(ANGULAR_CORE, 'Injectable'), createInjectable); this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), createSelf); this._registerDecoratorOrConstructor( this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf); diff --git a/packages/compiler/src/aot/static_symbol_resolver.ts b/packages/compiler/src/aot/static_symbol_resolver.ts index 0cafa222cd..641a96e4c2 100644 --- a/packages/compiler/src/aot/static_symbol_resolver.ts +++ b/packages/compiler/src/aot/static_symbol_resolver.ts @@ -12,6 +12,9 @@ import {ValueTransformer, visitValue} from '../util'; import {StaticSymbol, StaticSymbolCache} from './static_symbol'; import {isGeneratedFile, stripSummaryForJitFileSuffix, stripSummaryForJitNameSuffix, summaryForJitFileName, summaryForJitName} from './util'; +const DTS = /\.d\.ts$/; +const TS = /^(?!.*\.d\.ts$).*\.ts$/; + export class ResolvedStaticSymbol { constructor(public symbol: StaticSymbol, public metadata: any) {} } @@ -374,7 +377,8 @@ export class StaticSymbolResolver { // (e.g. their constructor parameters). // We do this to prevent introducing deep imports // as we didn't generate .ngfactory.ts files with proper reexports. - if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath) && metadata && + const isTsFile = TS.test(sourceSymbol.filePath); + if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath) && !isTsFile && metadata && metadata['__symbolic'] === 'class') { const transformedMeta = {__symbolic: 'class', arity: metadata.arity}; return new ResolvedStaticSymbol(sourceSymbol, transformedMeta); diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 33c930ea01..816494c8c7 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -136,6 +136,19 @@ export interface CompileTokenMetadata { identifier?: CompileIdentifierMetadata|CompileTypeMetadata; } +export interface CompileInjectableMetadata { + symbol: StaticSymbol; + type: CompileTypeMetadata; + + module?: StaticSymbol; + + useValue?: any; + useClass?: StaticSymbol; + useExisting?: StaticSymbol; + useFactory?: StaticSymbol; + deps?: any[]; +} + /** * Metadata regarding compilation of a type. */ diff --git a/packages/compiler/src/compile_reflector.ts b/packages/compiler/src/compile_reflector.ts index 44970091a2..9700634fc5 100644 --- a/packages/compiler/src/compile_reflector.ts +++ b/packages/compiler/src/compile_reflector.ts @@ -15,6 +15,7 @@ import * as o from './output/output_ast'; export abstract class CompileReflector { abstract parameters(typeOrFunc: /*Type*/ any): any[][]; abstract annotations(typeOrFunc: /*Type*/ any): any[]; + abstract tryAnnotations(typeOrFunc: /*Type*/ any): any[]; abstract propMetadata(typeOrFunc: /*Type*/ any): {[key: string]: any[]}; abstract hasLifecycleHook(type: any, lcProperty: string): boolean; abstract guards(typeOrFunc: /* Type */ any): {[key: string]: any}; diff --git a/packages/compiler/src/core.ts b/packages/compiler/src/core.ts index 4f0161dcaf..264043cfeb 100644 --- a/packages/compiler/src/core.ts +++ b/packages/compiler/src/core.ts @@ -14,8 +14,8 @@ export interface Inject { token: any; } export const createInject = makeMetadataFactory('Inject', (token: any) => ({token})); -export const createInjectionToken = - makeMetadataFactory('InjectionToken', (desc: string) => ({_desc: desc})); +export const createInjectionToken = makeMetadataFactory( + 'InjectionToken', (desc: string) => ({_desc: desc, ngInjectableDef: undefined})); export interface Attribute { attributeName?: string; } export const createAttribute = @@ -126,7 +126,16 @@ export interface ModuleWithProviders { ngModule: Type; providers?: Provider[]; } - +export interface Injectable { + scope?: Type|any; + useClass?: Type|any; + useExisting?: Type|any; + useValue?: any; + useFactory?: Type|any; + deps?: Array; +} +export const createInjectable = + makeMetadataFactory('Injectable', (injectable: Injectable = {}) => injectable); export interface SchemaMetadata { name: string; } export const CUSTOM_ELEMENTS_SCHEMA: SchemaMetadata = { @@ -138,7 +147,6 @@ export const NO_ERRORS_SCHEMA: SchemaMetadata = { }; export const createOptional = makeMetadataFactory('Optional'); -export const createInjectable = makeMetadataFactory('Injectable'); export const createSelf = makeMetadataFactory('Self'); export const createSkipSelf = makeMetadataFactory('SkipSelf'); export const createHost = makeMetadataFactory('Host'); @@ -205,7 +213,18 @@ export const enum DepFlags { None = 0, SkipSelf = 1 << 0, Optional = 1 << 1, - Value = 2 << 2, + Self = 1 << 2, + Value = 1 << 3, +} + +/** Injection flags for DI. */ +export const enum InjectFlags { + Default = 0, + + /** Skip the node that is requesting injection. */ + SkipSelf = 1 << 0, + /** Don't descend into ancestors of the node requesting injection. */ + Self = 1 << 1, } export const enum ArgumentType {Inline = 0, Dynamic = 1} diff --git a/packages/compiler/src/identifiers.ts b/packages/compiler/src/identifiers.ts index 87c7b09c87..7e9c28076d 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -61,7 +61,9 @@ export class Identifiers { moduleName: CORE, }; + static inject: o.ExternalReference = {name: 'inject', moduleName: CORE}; static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE}; + static defineInjectable: o.ExternalReference = {name: 'defineInjectable', moduleName: CORE}; static ViewEncapsulation: o.ExternalReference = { name: 'ViewEncapsulation', moduleName: CORE, diff --git a/packages/compiler/src/injectable_compiler.ts b/packages/compiler/src/injectable_compiler.ts new file mode 100644 index 0000000000..a991dc6b8f --- /dev/null +++ b/packages/compiler/src/injectable_compiler.ts @@ -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); + } + } +} diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index 8f8fd4e887..b4d50aa321 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -12,7 +12,7 @@ import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions'; import * as cpl from './compile_metadata'; import {CompileReflector} from './compile_reflector'; import {CompilerConfig} from './config'; -import {ChangeDetectionStrategy, Component, Directive, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, ViewEncapsulation, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core'; +import {ChangeDetectionStrategy, Component, Directive, Injectable, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, ViewEncapsulation, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core'; import {DirectiveNormalizer} from './directive_normalizer'; import {DirectiveResolver} from './directive_resolver'; import {Identifiers} from './identifiers'; @@ -771,7 +771,7 @@ export class CompileMetadataResolver { } isInjectable(type: any): boolean { - const annotations = this._reflector.annotations(type); + const annotations = this._reflector.tryAnnotations(type); return annotations.some(ann => createInjectable.isTypeOf(ann)); } @@ -782,13 +782,32 @@ export class CompileMetadataResolver { }; } - private _getInjectableMetadata(type: Type, dependencies: any[]|null = null): - cpl.CompileTypeMetadata { + getInjectableMetadata( + type: any, dependencies: any[]|null = null, + throwOnUnknownDeps: boolean = true): cpl.CompileInjectableMetadata|null { const typeSummary = this._loadSummary(type, cpl.CompileSummaryKind.Injectable); - if (typeSummary) { - return typeSummary.type; + const typeMetadata = typeSummary ? + typeSummary.type : + this._getTypeMetadata(type, dependencies, throwOnUnknownDeps); + + const annotations: Injectable[] = + this._reflector.annotations(type).filter(ann => createInjectable.isTypeOf(ann)); + + if (annotations.length === 0) { + return null; } - return this._getTypeMetadata(type, dependencies); + + const meta = annotations[annotations.length - 1]; + return { + symbol: type, + type: typeMetadata, + module: meta.scope || undefined, + useValue: meta.useValue, + useClass: meta.useClass, + useExisting: meta.useExisting, + useFactory: meta.useFactory, + deps: meta.deps, + }; } private _getTypeMetadata(type: Type, dependencies: any[]|null = null, throwOnUnknownDeps = true): @@ -1042,6 +1061,15 @@ export class CompileMetadataResolver { return null; } + private _getInjectableTypeMetadata(type: Type, dependencies: any[]|null = null): + cpl.CompileTypeMetadata { + const typeSummary = this._loadSummary(type, cpl.CompileSummaryKind.Injectable); + if (typeSummary) { + return typeSummary.type; + } + return this._getTypeMetadata(type, dependencies); + } + getProviderMetadata(provider: cpl.ProviderMeta): cpl.CompileProviderMetadata { let compileDeps: cpl.CompileDiDependencyMetadata[] = undefined !; let compileTypeMetadata: cpl.CompileTypeMetadata = null !; @@ -1049,7 +1077,8 @@ export class CompileMetadataResolver { let token: cpl.CompileTokenMetadata = this._getTokenMetadata(provider.token); if (provider.useClass) { - compileTypeMetadata = this._getInjectableMetadata(provider.useClass, provider.dependencies); + compileTypeMetadata = + this._getInjectableTypeMetadata(provider.useClass, provider.dependencies); compileDeps = compileTypeMetadata.diDeps; if (provider.token === provider.useClass) { // use the compileTypeMetadata as it contains information about lifecycleHooks... diff --git a/packages/compiler/src/provider_analyzer.ts b/packages/compiler/src/provider_analyzer.ts index 8bd133a7f9..f4b335957a 100644 --- a/packages/compiler/src/provider_analyzer.ts +++ b/packages/compiler/src/provider_analyzer.ts @@ -294,7 +294,7 @@ export class ProviderElementContext { this.viewContext.viewProviders.get(tokenReference(dep.token !)) != null) { result = dep; } else { - result = dep.isOptional ? result = {isValue: true, value: null} : null; + result = dep.isOptional ? {isValue: true, value: null} : null; } } } @@ -321,11 +321,12 @@ export class NgModuleProviderAnalyzer { const ngModuleProvider = {token: {identifier: ngModuleType}, useClass: ngModuleType}; _resolveProviders( [ngModuleProvider], ProviderAstType.PublicService, true, sourceSpan, this._errors, - this._allProviders, true); + this._allProviders, /* isModule */ true); }); _resolveProviders( ngModule.transitiveModule.providers.map(entry => entry.provider).concat(extraProviders), - ProviderAstType.PublicService, false, sourceSpan, this._errors, this._allProviders, false); + ProviderAstType.PublicService, false, sourceSpan, this._errors, this._allProviders, + /* isModule */ false); } parse(): ProviderAst[] { @@ -415,16 +416,7 @@ export class NgModuleProviderAnalyzer { foundLocal = true; } } - let result: CompileDiDependencyMetadata = dep; - if (dep.isSelf && !foundLocal) { - if (dep.isOptional) { - result = {isValue: true, value: null}; - } else { - this._errors.push( - new ProviderError(`No provider for ${tokenName(dep.token!)}`, requestorSourceSpan)); - } - } - return result; + return dep; } } @@ -461,7 +453,7 @@ function _resolveProvidersFromDirectives( _resolveProviders( [dirProvider], directive.isComponent ? ProviderAstType.Component : ProviderAstType.Directive, true, - sourceSpan, targetErrors, providersByToken, false); + sourceSpan, targetErrors, providersByToken, /* isModule */ false); }); // Note: directives need to be able to overwrite providers of a component! @@ -470,10 +462,10 @@ function _resolveProvidersFromDirectives( directivesWithComponentFirst.forEach((directive) => { _resolveProviders( directive.providers, ProviderAstType.PublicService, false, sourceSpan, targetErrors, - providersByToken, false); + providersByToken, /* isModule */ false); _resolveProviders( directive.viewProviders, ProviderAstType.PrivateService, false, sourceSpan, targetErrors, - providersByToken, false); + providersByToken, /* isModule */ false); }); return providersByToken; } diff --git a/packages/compiler/src/view_compiler/provider_compiler.ts b/packages/compiler/src/view_compiler/provider_compiler.ts index 09b53e42ae..baa833cd53 100644 --- a/packages/compiler/src/view_compiler/provider_compiler.ts +++ b/packages/compiler/src/view_compiler/provider_compiler.ts @@ -129,7 +129,7 @@ function tokenExpr(ctx: OutputContext, tokenMeta: CompileTokenMetadata): o.Expre export function depDef(ctx: OutputContext, dep: CompileDiDependencyMetadata): o.Expression { // Note: the following fields have already been normalized out by provider_analyzer: - // - isAttribute, isSelf, isHost + // - isAttribute, isHost const expr = dep.isValue ? convertValueToOutputAst(ctx, dep.value) : tokenExpr(ctx, dep.token !); let flags = DepFlags.None; if (dep.isSkipSelf) { @@ -138,6 +138,9 @@ export function depDef(ctx: OutputContext, dep: CompileDiDependencyMetadata): o. if (dep.isOptional) { flags |= DepFlags.Optional; } + if (dep.isSelf) { + flags |= DepFlags.Self; + } if (dep.isValue) { flags |= DepFlags.Value; } diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index c279b9cec3..57d5019047 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -531,6 +531,7 @@ const minCoreIndex = ` export * from './src/change_detection'; export * from './src/metadata'; export * from './src/di/metadata'; + export * from './src/di/injectable'; export * from './src/di/injector'; export * from './src/di/injection_token'; export * from './src/linker'; diff --git a/packages/compiler/test/core_spec.ts b/packages/compiler/test/core_spec.ts index fb022ead68..c28abf31d9 100644 --- a/packages/compiler/test/core_spec.ts +++ b/packages/compiler/test/core_spec.ts @@ -148,6 +148,10 @@ import * as core from '@angular/core'; expect(compilerCore.DepFlags.Optional).toBe(core.ɵDepFlags.Optional); expect(compilerCore.DepFlags.Value).toBe(core.ɵDepFlags.Value); + expect(compilerCore.InjectFlags.Default).toBe(core.InjectFlags.Default); + expect(compilerCore.InjectFlags.SkipSelf).toBe(core.InjectFlags.SkipSelf); + expect(compilerCore.InjectFlags.Self).toBe(core.InjectFlags.Self); + expect(compilerCore.ArgumentType.Inline).toBe(core.ɵArgumentType.Inline); expect(compilerCore.ArgumentType.Dynamic).toBe(core.ɵArgumentType.Dynamic); diff --git a/packages/core/src/change_detection/differs/iterable_differs.ts b/packages/core/src/change_detection/differs/iterable_differs.ts index 3af661d154..991a77465f 100644 --- a/packages/core/src/change_detection/differs/iterable_differs.ts +++ b/packages/core/src/change_detection/differs/iterable_differs.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Optional, SkipSelf, StaticProvider} from '../../di'; +import {Optional, SkipSelf} from '../../di/metadata'; +import {StaticProvider} from '../../di/provider'; /** diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index d9150eac52..bb49c55376 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -13,10 +13,11 @@ */ export * from './di/metadata'; +export {defineInjectable, Injectable, InjectableDecorator, InjectableProvider, InjectableType} from './di/injectable'; export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref'; -export {Injector} from './di/injector'; +export {InjectFlags, Injector} from './di/injector'; export {ReflectiveInjector} from './di/reflective_injector'; export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider'; diff --git a/packages/core/src/di/injectable.ts b/packages/core/src/di/injectable.ts new file mode 100644 index 0000000000..b9f7ec22c7 --- /dev/null +++ b/packages/core/src/di/injectable.ts @@ -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( + {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}&InjectableProvider): any; + new (): Injectable; + new (options?: {scope: Type}&InjectableProvider): Injectable; +} + +/** + * Type of the Injectable metadata. + * + * @experimental + */ +export interface Injectable { + scope?: Type; + factory: () => any; +} + +const EMPTY_ARRAY: any[] = []; + +export function convertInjectableProviderToFactory( + type: Type, 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, options: {scope: Type} & InjectableProvider) => { + if (options && options.scope) { + (injectableType as InjectableType).ngInjectableDef = defineInjectable({ + scope: options.scope, + factory: convertInjectableProviderToFactory(injectableType, options) + }); + } + }); + + +/** + * Type representing injectable service. + * + * @experimental + */ +export interface InjectableType extends Type { ngInjectableDef?: Injectable; } diff --git a/packages/core/src/di/injection_token.ts b/packages/core/src/di/injection_token.ts index 9fd360fa62..81329e7eef 100644 --- a/packages/core/src/di/injection_token.ts +++ b/packages/core/src/di/injection_token.ts @@ -6,6 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ +import {Type} from '../type'; + +import {Injectable, convertInjectableProviderToFactory, defineInjectable} from './injectable'; +import {ClassSansProvider, ExistingSansProvider, FactorySansProvider, StaticClassSansProvider, ValueSansProvider} from './provider'; + +export type InjectionTokenProvider = ValueSansProvider | ExistingSansProvider | + FactorySansProvider | ClassSansProvider | StaticClassSansProvider; + /** * Creates a token that can be used in a DI Provider. * @@ -32,7 +40,18 @@ export class InjectionToken { /** @internal */ readonly ngMetadataName = 'InjectionToken'; - constructor(protected _desc: string) {} + readonly ngInjectableDef: Injectable|undefined; + + constructor(protected _desc: string, options?: {scope: Type}&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}`; } } diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index e6df410642..0758464957 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -57,7 +57,7 @@ export abstract class Injector { * Injector.THROW_IF_NOT_FOUND is given * - Returns the `notFoundValue` otherwise */ - abstract get(token: Type|InjectionToken, notFoundValue?: T): T; + abstract get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; /** * @deprecated from v4.0.0 use Type or InjectionToken * @suppress {duplicate} @@ -130,12 +130,12 @@ export class StaticInjector implements Injector { recursivelyProcessProviders(records, providers); } - get(token: Type|InjectionToken, notFoundValue?: T): T; + get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; get(token: any, notFoundValue?: any): any; - get(token: any, notFoundValue?: any): any { + get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any { const record = this._records.get(token); try { - return tryResolveToken(token, record, this._records, this.parent, notFoundValue); + return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags); } catch (e) { const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; if (token[SOURCE]) { @@ -253,9 +253,9 @@ function recursivelyProcessProviders(records: Map, provider: Static function tryResolveToken( token: any, record: Record | undefined, records: Map, parent: Injector, - notFoundValue: any): any { + notFoundValue: any, flags: InjectFlags): any { try { - return resolveToken(token, record, records, parent, notFoundValue); + return resolveToken(token, record, records, parent, notFoundValue, flags); } catch (e) { // ensure that 'e' is of type Error. if (!(e instanceof Error)) { @@ -273,9 +273,9 @@ function tryResolveToken( function resolveToken( token: any, record: Record | undefined, records: Map, parent: Injector, - notFoundValue: any): any { + notFoundValue: any, flags: InjectFlags): any { let value; - if (record) { + if (record && !(flags & InjectFlags.SkipSelf)) { // If we don't have a record, this implies that we don't own the provider hence don't know how // to resolve it. value = record.value; @@ -306,13 +306,14 @@ function resolveToken( // If we don't know how to resolve dependency and we should not check parent for it, // than pass in Null injector. !childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent, - options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND)); + options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND, + InjectFlags.Default)); } } record.value = value = useNew ? new (fn as any)(...deps) : fn.apply(obj, deps); } - } else { - value = parent.get(token, notFoundValue); + } else if (!(flags & InjectFlags.Self)) { + value = parent.get(token, notFoundValue, InjectFlags.Default); } return value; } @@ -386,3 +387,73 @@ function getClosureSafeProperty(objWithPropertyToExtract: T): string { } throw Error('!prop'); } + +/** + * Injection flags for DI. + * + * @stable + */ +export const enum InjectFlags { + Default = 0, + + /** Skip the node that is requesting injection. */ + SkipSelf = 1 << 0, + /** Don't descend into ancestors of the node requesting injection. */ + Self = 1 << 1, +} + +let _currentInjector: Injector|null = null; + +export function setCurrentInjector(injector: Injector | null): Injector|null { + const former = _currentInjector; + _currentInjector = injector; + return former; +} + +export function inject( + token: Type| InjectionToken, notFoundValue?: undefined, flags?: InjectFlags): T; +export function inject( + token: Type| InjectionToken, notFoundValue: T | null, flags?: InjectFlags): T|null; +export function inject( + token: Type| InjectionToken, 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| InjectionToken| 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|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; +} diff --git a/packages/core/src/di/metadata.ts b/packages/core/src/di/metadata.ts index d367785252..b3f155d6c7 100644 --- a/packages/core/src/di/metadata.ts +++ b/packages/core/src/di/metadata.ts @@ -6,7 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {ClassSansProvider, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, StaticClassProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} from '../di/provider'; +import {ReflectionCapabilities} from '../reflection/reflection_capabilities'; +import {Type} from '../type'; import {makeDecorator, makeParamDecorator} from '../util/decorators'; +import {EMPTY_ARRAY} from '../view/util'; /** @@ -106,53 +110,6 @@ export interface Optional {} */ export const Optional: OptionalDecorator = makeParamDecorator('Optional'); -/** - * Type of the Injectable decorator / constructor function. - * - * @stable - */ -export interface InjectableDecorator { - /** - * @whatItDoes A marker metadata that marks a class as available to {@link Injector} for creation. - * @howToUse - * ``` - * @Injectable() - * class Car {} - * ``` - * - * @description - * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. - * - * ### Example - * - * {@example core/di/ts/metadata_spec.ts region='Injectable'} - * - * {@link Injector} will throw an error when trying to instantiate a class that - * does not have `@Injectable` marker, as shown in the example below. - * - * {@example core/di/ts/metadata_spec.ts region='InjectableThrows'} - * - * @stable - */ - (): any; - new (): Injectable; -} - -/** - * Type of the Injectable metadata. - * - * @stable - */ -export interface Injectable {} - -/** - * Injectable decorator and metadata. - * - * @stable - * @Annotation - */ -export const Injectable: InjectableDecorator = makeDecorator('Injectable'); - /** * Type of the Self decorator / constructor function. * diff --git a/packages/core/src/di/provider.ts b/packages/core/src/di/provider.ts index 9ad449e3ff..5326ad7355 100644 --- a/packages/core/src/di/provider.ts +++ b/packages/core/src/di/provider.ts @@ -8,6 +8,30 @@ import {Type} from '../type'; +/** + * @whatItDoes Configures the {@link Injector} to return a value for a token. + * @howToUse + * ``` + * @Injectable(SomeModule, {useValue: 'someValue'}) + * class SomeClass {} + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ValueSansProvider'} + * + * @experimental + */ +export interface ValueSansProvider { + /** + * The value to inject. + */ + useValue: any; +} + /** * @whatItDoes Configures the {@link Injector} to return a value for a token. * @howToUse @@ -24,17 +48,12 @@ import {Type} from '../type'; * * @stable */ -export interface ValueProvider { +export interface ValueProvider extends ValueSansProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: any; - /** - * The value to inject. - */ - useValue: any; - /** * If true, then injector returns an array of instances. This is useful to allow multiple * providers spread across many files to provide configuration information to a common token. @@ -46,6 +65,37 @@ export interface ValueProvider { multi?: boolean; } +/** + * @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token. + * @howToUse + * ``` + * @Injectable(SomeModule, {useClass: MyService, deps: []}) + * class MyService {} + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='StaticClassSansProvider'} + * + * @experimental + */ +export interface StaticClassSansProvider { + /** + * An optional class to instantiate for the `token`. (If not provided `provide` is assumed to be a + * class to instantiate) + */ + useClass: Type; + + /** + * A list of `token`s which need to be resolved by the injector. The list of values is then + * used as arguments to the `useClass` constructor. + */ + deps: any[]; +} + /** * @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token. * @howToUse @@ -68,25 +118,12 @@ export interface ValueProvider { * * @stable */ -export interface StaticClassProvider { +export interface StaticClassProvider extends StaticClassSansProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: any; - /** - * An optional class to instantiate for the `token`. (If not provided `provide` is assumed to be a - * class to - * instantiate) - */ - useClass: Type; - - /** - * A list of `token`s which need to be resolved by the injector. The list of values is then - * used as arguments to the `useClass` constructor. - */ - deps: any[]; - /** * If true, then injector returns an array of instances. This is useful to allow multiple * providers spread across many files to provide configuration information to a common token. @@ -98,6 +135,31 @@ export interface StaticClassProvider { multi?: boolean; } +/** + * @whatItDoes Configures the {@link Injector} to return an instance of a token. + * @howToUse + * ``` + * @Injectable(SomeModule, {deps: []}) + * class MyService {} + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ConstructorSansProvider'} + * + * @experimental + */ +export interface ConstructorSansProvider { + /** + * A list of `token`s which need to be resolved by the injector. The list of values is then + * used as arguments to the `useClass` constructor. + */ + deps?: any[]; +} + /** * @whatItDoes Configures the {@link Injector} to return an instance of a token. * @howToUse @@ -117,18 +179,12 @@ export interface StaticClassProvider { * * @stable */ -export interface ConstructorProvider { +export interface ConstructorProvider extends ConstructorSansProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: Type; - /** - * A list of `token`s which need to be resolved by the injector. The list of values is then - * used as arguments to the `useClass` constructor. - */ - deps: any[]; - /** * If true, then injector returns an array of instances. This is useful to allow multiple * providers spread across many files to provide configuration information to a common token. @@ -140,6 +196,30 @@ export interface ConstructorProvider { multi?: boolean; } +/** + * @whatItDoes Configures the {@link Injector} to return a value of another `useExisting` token. + * @howToUse + * ``` + * @Injectable(SomeModule, {useExisting: 'someOtherToken'}) + * class SomeClass {} + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ExistingSansProvider'} + * + * @stable + */ +export interface ExistingSansProvider { + /** + * Existing `token` to return. (equivalent to `injector.get(useExisting)`) + */ + useExisting: any; +} + /** * @whatItDoes Configures the {@link Injector} to return a value of another `useExisting` token. * @howToUse @@ -156,17 +236,12 @@ export interface ConstructorProvider { * * @stable */ -export interface ExistingProvider { +export interface ExistingProvider extends ExistingSansProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: any; - /** - * Existing `token` to return. (equivalent to `injector.get(useExisting)`) - */ - useExisting: any; - /** * If true, then injector returns an array of instances. This is useful to allow multiple * providers spread across many files to provide configuration information to a common token. @@ -178,6 +253,40 @@ export interface ExistingProvider { multi?: boolean; } +/** + * @whatItDoes Configures the {@link Injector} to return a value by invoking a `useFactory` + * function. + * @howToUse + * ``` + * function serviceFactory() { ... } + * + * @Injectable(SomeModule, {useFactory: serviceFactory, deps: []}) + * class SomeClass {} + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='FactorySansProvider'} + * + * @experimental + */ +export interface FactorySansProvider { + /** + * A function to invoke to create a value for this `token`. The function is invoked with + * resolved values of `token`s in the `deps` field. + */ + useFactory: Function; + + /** + * A list of `token`s which need to be resolved by the injector. The list of values is then + * used as arguments to the `useFactory` function. + */ + deps?: any[]; +} + /** * @whatItDoes Configures the {@link Injector} to return a value by invoking a `useFactory` * function. @@ -200,24 +309,12 @@ export interface ExistingProvider { * * @stable */ -export interface FactoryProvider { +export interface FactoryProvider extends FactorySansProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: any; - /** - * A function to invoke to create a value for this `token`. The function is invoked with - * resolved values of `token`s in the `deps` field. - */ - useFactory: Function; - - /** - * A list of `token`s which need to be resolved by the injector. The list of values is then - * used as arguments to the `useFactory` function. - */ - deps?: any[]; - /** * If true, then injector returns an array of instances. This is useful to allow multiple * providers spread across many files to provide configuration information to a common token. @@ -270,6 +367,34 @@ export type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvi */ export interface TypeProvider extends Type {} +/** + * @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; +} + /** * @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token. * @howToUse @@ -292,17 +417,12 @@ export interface TypeProvider extends Type {} * * @stable */ -export interface ClassProvider { +export interface ClassProvider extends ClassSansProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: any; - /** - * Class to instantiate for the `token`. - */ - useClass: Type; - /** * 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. diff --git a/packages/core/src/util/decorators.ts b/packages/core/src/util/decorators.ts index 868106b69f..d5de421942 100644 --- a/packages/core/src/util/decorators.ts +++ b/packages/core/src/util/decorators.ts @@ -43,18 +43,19 @@ export const PROP_METADATA = '__prop__metadata__'; */ export function makeDecorator( name: string, props?: (...args: any[]) => any, parentClass?: any, - chainFn?: (fn: Function) => void): + chainFn?: (fn: Function) => void, typeFn?: (type: Type, ...args: any[]) => void): {new (...args: any[]): any; (...args: any[]): any; (...args: any[]): (cls: any) => any;} { const metaCtor = makeMetadataCtor(props); - function DecoratorFactory(objOrType: any): (cls: any) => any { + function DecoratorFactory(...args: any[]): (cls: any) => any { if (this instanceof DecoratorFactory) { - metaCtor.call(this, objOrType); + metaCtor.call(this, ...args); return this; } - const annotationInstance = new (DecoratorFactory)(objOrType); + const annotationInstance = new (DecoratorFactory)(...args); const TypeDecorator: TypeDecorator = function TypeDecorator(cls: Type) { + typeFn && typeFn(cls, ...args); // Use of Object.defineProperty is important since it creates non-enumerable property which // prevents the property is copied during subclassing. const annotations = cls.hasOwnProperty(ANNOTATIONS) ? diff --git a/packages/core/src/util/property.ts b/packages/core/src/util/property.ts new file mode 100644 index 0000000000..b473aeb2ca --- /dev/null +++ b/packages/core/src/util/property.ts @@ -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(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.'); +} diff --git a/packages/core/src/view/ng_module.ts b/packages/core/src/view/ng_module.ts index b438555a83..252edef56b 100644 --- a/packages/core/src/view/ng_module.ts +++ b/packages/core/src/view/ng_module.ts @@ -7,11 +7,11 @@ */ import {resolveForwardRef} from '../di/forward_ref'; -import {Injector} from '../di/injector'; +import {InjectFlags, Injector, setCurrentInjector} from '../di/injector'; import {NgModuleRef} from '../linker/ng_module_factory'; import {stringify} from '../util'; -import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types'; +import {DepDef, DepFlags, InjectableDef, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types'; import {splitDepsDsl, tokenKey} from './util'; const UNDEFINED_VALUE = new Object(); @@ -19,6 +19,12 @@ const UNDEFINED_VALUE = new Object(); const InjectorRefTokenKey = tokenKey(Injector); const NgModuleRefTokenKey = tokenKey(NgModuleRef); +export function injectableDef(scope: any, factory: () => any): InjectableDef { + return { + scope, factory, + }; +} + export function moduleProvideDef( flags: NodeFlags, token: any, value: any, deps: ([DepFlags, any] | any)[]): NgModuleProviderDef { @@ -90,10 +96,32 @@ export function resolveNgModuleDep( _createProviderInstance(data, providerDef); } return providerInstance === UNDEFINED_VALUE ? undefined : providerInstance; + } else if (depDef.token.ngInjectableDef && targetsModule(data, depDef.token.ngInjectableDef)) { + const injectableDef = depDef.token.ngInjectableDef as InjectableDef; + const key = tokenKey; + const index = data._providers.length; + data._def.providersByKey[depDef.tokenKey] = { + flags: NodeFlags.TypeFactoryProvider | NodeFlags.LazyProvider, + value: injectableDef.factory, + deps: [], index, + token: depDef.token, + }; + const former = setCurrentInjector(data); + try { + data._providers[index] = UNDEFINED_VALUE; + return ( + data._providers[index] = + _createProviderInstance(data, data._def.providersByKey[depDef.tokenKey])); + } finally { + setCurrentInjector(former); + } } return data._parent.get(depDef.token, notFoundValue); } +function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean { + return def.scope != null && ngModule._def.modules.indexOf(def.scope) > -1; +} function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any { let injectable: any; diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index 41a3605c03..b8a9b7df53 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -346,50 +346,56 @@ export function resolveDep( elDef = elDef.parent !; } - while (view) { + let searchView: ViewData|null = view; + while (searchView) { if (elDef) { switch (tokenKey) { case RendererV1TokenKey: { - const compView = findCompView(view, elDef, allowPrivateServices); + const compView = findCompView(searchView, elDef, allowPrivateServices); return createRendererV1(compView); } case Renderer2TokenKey: { - const compView = findCompView(view, elDef, allowPrivateServices); + const compView = findCompView(searchView, elDef, allowPrivateServices); return compView.renderer; } case ElementRefTokenKey: - return new ElementRef(asElementData(view, elDef.nodeIndex).renderElement); + return new ElementRef(asElementData(searchView, elDef.nodeIndex).renderElement); case ViewContainerRefTokenKey: - return asElementData(view, elDef.nodeIndex).viewContainer; + return asElementData(searchView, elDef.nodeIndex).viewContainer; case TemplateRefTokenKey: { if (elDef.element !.template) { - return asElementData(view, elDef.nodeIndex).template; + return asElementData(searchView, elDef.nodeIndex).template; } break; } case ChangeDetectorRefTokenKey: { - let cdView = findCompView(view, elDef, allowPrivateServices); + let cdView = findCompView(searchView, elDef, allowPrivateServices); return createChangeDetectorRef(cdView); } case InjectorRefTokenKey: - return createInjector(view, elDef); + return createInjector(searchView, elDef); default: const providerDef = (allowPrivateServices ? elDef.element !.allProviders : elDef.element !.publicProviders) ![tokenKey]; if (providerDef) { - let providerData = asProviderData(view, providerDef.nodeIndex); + let providerData = asProviderData(searchView, providerDef.nodeIndex); if (!providerData) { - providerData = {instance: _createProviderInstance(view, providerDef)}; - view.nodes[providerDef.nodeIndex] = providerData as any; + providerData = {instance: _createProviderInstance(searchView, providerDef)}; + searchView.nodes[providerDef.nodeIndex] = providerData as any; } return providerData.instance; } } } - allowPrivateServices = isComponentView(view); - elDef = viewParentEl(view) !; - view = view.parent !; + + allowPrivateServices = isComponentView(searchView); + elDef = viewParentEl(searchView) !; + searchView = searchView.parent !; + + if (depDef.flags & DepFlags.Self) { + searchView = null; + } } const value = startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR); diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 010d6d8a6d..0daada3109 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -8,7 +8,7 @@ import {ApplicationRef} from '../application_ref'; import {ChangeDetectorRef} from '../change_detection/change_detection'; -import {Injector} from '../di/injector'; +import {InjectFlags, Injector} from '../di/injector'; import {ComponentFactory, ComponentRef} from '../linker/component_factory'; import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {ElementRef} from '../linker/element_ref'; @@ -480,6 +480,7 @@ class NgModuleRef_ implements NgModuleData, InternalNgModuleRef { private _destroyed: boolean = false; /** @internal */ _providers: any[]; + /** @internal */ _modules: any[]; readonly injector: Injector = this; @@ -490,9 +491,16 @@ class NgModuleRef_ implements NgModuleData, InternalNgModuleRef { initNgModule(this); } - get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { + get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND, + injectFlags: InjectFlags = InjectFlags.Default): any { + let flags = DepFlags.None; + if (injectFlags & InjectFlags.SkipSelf) { + flags |= DepFlags.SkipSelf; + } else if (injectFlags & InjectFlags.Self) { + flags |= DepFlags.Self; + } return resolveNgModuleDep( - this, {token: token, tokenKey: tokenKey(token), flags: DepFlags.None}, notFoundValue); + this, {token: token, tokenKey: tokenKey(token), flags: flags}, notFoundValue); } get instance() { return this.get(this._moduleType); } diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index d6dab11ee6..44c190eb10 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -292,7 +292,8 @@ export const enum DepFlags { None = 0, SkipSelf = 1 << 0, Optional = 1 << 1, - Value = 2 << 2, + Self = 1 << 2, + Value = 1 << 3, } export interface InjectableDef { diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index b47384d400..70194daa79 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -945,15 +945,6 @@ function declareTests({useJit}: {useJit: boolean}) { expect(inj.get(Car)).toBeAnInstanceOf(Car); }); - - it('should throw when not requested provider on self', () => { - expect(() => createInjector([{ - provide: Car, - useFactory: (e: Engine) => new Car(e), - deps: [[Engine, new Self()]] - }])) - .toThrowError(/No provider for Engine/g); - }); }); describe('default', () => { diff --git a/packages/core/test/view/ng_module_spec.ts b/packages/core/test/view/ng_module_spec.ts new file mode 100644 index 0000000000..5592356384 --- /dev/null +++ b/packages/core/test/view/ng_module_spec.ts @@ -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; + let childRef: NgModuleRef; + 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(); }); +}); diff --git a/packages/platform-browser-dynamic/src/compiler_reflector.ts b/packages/platform-browser-dynamic/src/compiler_reflector.ts index b959b4870d..14652fc056 100644 --- a/packages/platform-browser-dynamic/src/compiler_reflector.ts +++ b/packages/platform-browser-dynamic/src/compiler_reflector.ts @@ -33,6 +33,7 @@ export class JitReflector implements CompileReflector { parameters(typeOrFunc: /*Type*/ any): any[][] { return this.reflectionCapabilities.parameters(typeOrFunc); } + tryAnnotations(typeOrFunc: /*Type*/ any): any[] { return this.annotations(typeOrFunc); } annotations(typeOrFunc: /*Type*/ any): any[] { return this.reflectionCapabilities.annotations(typeOrFunc); } diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 33ecbd89c5..3774caf383 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -164,10 +164,9 @@ export declare abstract class ChangeDetectorRef { } /** @stable */ -export interface ClassProvider { +export interface ClassProvider extends ClassSansProvider { multi?: boolean; provide: any; - useClass: Type; } /** @deprecated */ @@ -344,6 +343,9 @@ export declare class DefaultIterableDiffer implements IterableDiffer, Iter onDestroy(): void; } +/** @experimental */ +export declare function defineInjectable(opts: Injectable): Injectable; + /** @experimental */ export declare function destroyPlatform(): void; @@ -390,18 +392,15 @@ export declare class EventEmitter extends Subject { } /** @stable */ -export interface ExistingProvider { +export interface ExistingProvider extends ExistingSansProvider { multi?: boolean; provide: any; - useExisting: any; } /** @stable */ -export interface FactoryProvider { - deps?: any[]; +export interface FactoryProvider extends FactorySansProvider { multi?: boolean; provide: any; - useFactory: Function; } /** @experimental */ @@ -454,7 +453,21 @@ export declare const Injectable: InjectableDecorator; /** @stable */ export interface InjectableDecorator { /** @stable */ (): any; + (options?: { + scope: Type; + } & InjectableProvider): any; new (): Injectable; + new (options?: { + scope: Type; + } & InjectableProvider): Injectable; +} + +/** @experimental */ +export declare type InjectableProvider = ValueSansProvider | ExistingSansProvider | StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider; + +/** @experimental */ +export interface InjectableType extends Type { + ngInjectableDef?: Injectable; } /** @stable */ @@ -463,16 +476,26 @@ export interface InjectDecorator { new (token: any): Inject; } +/** @stable */ +export declare const enum InjectFlags { + Default = 0, + SkipSelf = 1, + Self = 2, +} + /** @stable */ export declare class InjectionToken { protected _desc: string; - constructor(_desc: string); + readonly ngInjectableDef: Injectable | undefined; + constructor(_desc: string, options?: { + scope: Type; + } & InjectionTokenProvider); toString(): string; } /** @stable */ export declare abstract class Injector { - abstract get(token: Type | InjectionToken, notFoundValue?: T): T; + abstract get(token: Type | InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; /** @deprecated */ abstract get(token: any, notFoundValue?: any): any; static NULL: Injector; static THROW_IF_NOT_FOUND: Object; @@ -1016,10 +1039,9 @@ export interface TypeProvider extends Type { } /** @stable */ -export interface ValueProvider { +export interface ValueProvider extends ValueSansProvider { multi?: boolean; provide: any; - useValue: any; } /** @stable */