From 235a235fab45b2c82f8758fc9c0779f62a5b6c04 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 2 Feb 2018 10:33:48 -0800 Subject: [PATCH] feat: change @Injectable() to support tree-shakeable tokens (#22005) This commit bundles 3 important changes, with the goal of enabling tree-shaking of services which are never injected. Ordinarily, this tree-shaking is prevented by the existence of a hard dependency on the service by the module in which it is declared. Firstly, @Injectable() is modified to accept a 'scope' parameter, which points to an @NgModule(). This reverses the dependency edge, permitting the module to not depend on the service which it "provides". Secondly, the runtime is modified to understand the new relationship created above. When a module receives a request to inject a token, and cannot find that token in its list of providers, it will then look at the token for a special ngInjectableDef field which indicates which module the token is scoped to. If that module happens to be in the injector, it will behave as if the token itself was in the injector to begin with. Thirdly, the compiler is modified to read the @Injectable() metadata and to generate the special ngInjectableDef field as part of TS compilation, using the PartialModules system. Additionally, this commit adds several unit and integration tests of various flavors to test this change. PR Close #22005 --- integration/_payload-limits.json | 4 +- integration/injectable-def/package.json | 29 +++ integration/injectable-def/src/app.ts | 21 ++ integration/injectable-def/src/lib1.ts | 10 + integration/injectable-def/src/lib2.ts | 23 ++ integration/injectable-def/src/main.ts | 21 ++ .../injectable-def/src/package-lib1.json | 7 + .../injectable-def/src/package-lib2.json | 7 + integration/injectable-def/test.sh | 17 ++ integration/injectable-def/tsconfig-app.json | 17 ++ integration/injectable-def/tsconfig-lib1.json | 22 ++ integration/injectable-def/tsconfig-lib2.json | 22 ++ .../bazel/injectable_def/app/BUILD.bazel | 21 ++ .../bazel/injectable_def/app/src/basic.ts | 31 +++ .../bazel/injectable_def/app/src/hierarchy.ts | 44 ++++ .../bazel/injectable_def/app/src/self.ts | 41 ++++ .../bazel/injectable_def/app/src/token.ts | 42 ++++ .../bazel/injectable_def/app/test/BUILD.bazel | 30 +++ .../bazel/injectable_def/app/test/app_spec.ts | 58 +++++ .../bazel/injectable_def/lib1/BUILD.bazel | 17 ++ .../bazel/injectable_def/lib1/module.ts | 21 ++ .../bazel/injectable_def/lib2/BUILD.bazel | 18 ++ .../bazel/injectable_def/lib2/module.ts | 32 +++ .../compiler-cli/src/transformers/program.ts | 48 ++-- .../src/transformers/r3_transform.ts | 2 +- .../compiler-cli/src/transformers/util.ts | 1 + packages/compiler-cli/test/ngc_spec.ts | 153 ++++++++++++ packages/compiler/src/aot/compiler.ts | 113 +++++++-- packages/compiler/src/aot/compiler_factory.ts | 5 +- packages/compiler/src/aot/static_reflector.ts | 14 +- .../src/aot/static_symbol_resolver.ts | 6 +- packages/compiler/src/compile_metadata.ts | 13 + packages/compiler/src/compile_reflector.ts | 1 + packages/compiler/src/core.ts | 29 ++- packages/compiler/src/identifiers.ts | 2 + packages/compiler/src/injectable_compiler.ts | 111 +++++++++ packages/compiler/src/metadata_resolver.ts | 45 +++- packages/compiler/src/provider_analyzer.ts | 24 +- .../src/view_compiler/provider_compiler.ts | 5 +- packages/compiler/test/aot/test_util.ts | 1 + packages/compiler/test/core_spec.ts | 4 + .../differs/iterable_differs.ts | 3 +- packages/core/src/di.ts | 3 +- packages/core/src/di/injectable.ts | 143 +++++++++++ packages/core/src/di/injection_token.ts | 21 +- packages/core/src/di/injector.ts | 93 +++++++- packages/core/src/di/metadata.ts | 51 +--- packages/core/src/di/provider.ts | 224 ++++++++++++++---- packages/core/src/util/decorators.ts | 9 +- packages/core/src/util/property.ts | 16 ++ packages/core/src/view/ng_module.ts | 32 ++- packages/core/src/view/provider.ts | 34 +-- packages/core/src/view/refs.ts | 14 +- packages/core/src/view/types.ts | 3 +- .../test/linker/ng_module_integration_spec.ts | 9 - packages/core/test/view/ng_module_spec.ts | 149 ++++++++++++ .../src/compiler_reflector.ts | 1 + tools/public_api_guard/core/core.d.ts | 44 +++- 58 files changed, 1753 insertions(+), 228 deletions(-) create mode 100644 integration/injectable-def/package.json create mode 100644 integration/injectable-def/src/app.ts create mode 100644 integration/injectable-def/src/lib1.ts create mode 100644 integration/injectable-def/src/lib2.ts create mode 100644 integration/injectable-def/src/main.ts create mode 100644 integration/injectable-def/src/package-lib1.json create mode 100644 integration/injectable-def/src/package-lib2.json create mode 100755 integration/injectable-def/test.sh create mode 100644 integration/injectable-def/tsconfig-app.json create mode 100644 integration/injectable-def/tsconfig-lib1.json create mode 100644 integration/injectable-def/tsconfig-lib2.json create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/BUILD.bazel create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/BUILD.bazel create mode 100644 packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts create mode 100644 packages/compiler/src/injectable_compiler.ts create mode 100644 packages/core/src/di/injectable.ts create mode 100644 packages/core/src/util/property.ts create mode 100644 packages/core/test/view/ng_module_spec.ts 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 */