diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 2caa8b7270a423..bd0bcf9c447fae 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "inline": 1447, - "main": 155112, + "main": 157654, "polyfills": 59179 } } diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index bcc9533c878186..7d8039f3dff769 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -79,7 +79,13 @@ def _expected_outs(ctx): i18n_messages = i18n_messages_files, ) +def _ivy_tsconfig(ctx, files, srcs, **kwargs): + return _ngc_tsconfig_helper(ctx, files, srcs, True, **kwargs) + def _ngc_tsconfig(ctx, files, srcs, **kwargs): + return _ngc_tsconfig_helper(ctx, files, srcs, False, **kwargs) + +def _ngc_tsconfig_helper(ctx, files, srcs, enable_ivy, **kwargs): outs = _expected_outs(ctx) if "devmode_manifest" in kwargs: expected_outs = outs.devmode_js + outs.declarations + outs.summaries @@ -91,6 +97,7 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs): "generateCodeForLibraries": False, "allowEmptyCodegenFiles": True, "enableSummariesForJit": True, + "enableIvy": enable_ivy, "fullTemplateTypeCheck": ctx.attr.type_check, # FIXME: wrong place to de-dupe "expectedOut": depset([o.path for o in expected_outs]).to_list() @@ -281,7 +288,7 @@ def _write_bundle_index(ctx): ) return outputs -def ng_module_impl(ctx, ts_compile_actions): +def ng_module_impl(ctx, ts_compile_actions, ivy = False): """Implementation function for the ng_module rule. This is exposed so that google3 can have its own entry point that re-uses this @@ -290,16 +297,19 @@ def ng_module_impl(ctx, ts_compile_actions): Args: ctx: the skylark rule context ts_compile_actions: generates all the actions to run an ngc compilation + ivy: if True, run the compiler in Ivy mode (internal only) Returns: the result of the ng_module rule as a dict, suitable for conversion by ts_providers_dict_to_struct """ + tsconfig = _ngc_tsconfig if not ivy else _ivy_tsconfig + providers = ts_compile_actions( ctx, is_library=True, compile_action=_prodmode_compile_action, devmode_compile_action=_devmode_compile_action, - tsc_wrapped_tsconfig=_ngc_tsconfig, + tsc_wrapped_tsconfig=tsconfig, outputs = _ts_expected_outs) outs = _expected_outs(ctx) @@ -320,6 +330,9 @@ def ng_module_impl(ctx, ts_compile_actions): def _ng_module_impl(ctx): return ts_providers_dict_to_struct(ng_module_impl(ctx, compile_ts)) +def _ivy_module_impl(ctx): + return ts_providers_dict_to_struct(ng_module_impl(ctx, compile_ts, True)) + NG_MODULE_ATTRIBUTES = { "srcs": attr.label_list(allow_files = [".ts"]), @@ -356,24 +369,35 @@ NG_MODULE_ATTRIBUTES = { "_supports_workers": attr.bool(default = True), } +NG_MODULE_RULE_ATTRS = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **{ + "tsconfig": attr.label(allow_files = True, single_file = True), + + # @// is special syntax for the "main" repository + # The default assumes the user specified a target "node_modules" in their + # root BUILD file. + "node_modules": attr.label( + default = Label("@//:node_modules") + ), + + "entry_point": attr.string(), + + "_index_bundler": attr.label( + executable = True, + cfg = "host", + default = Label("//packages/bazel/src:index_bundler")), +}) + ng_module = rule( implementation = _ng_module_impl, - attrs = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **{ - "tsconfig": attr.label(allow_files = True, single_file = True), - - # @// is special syntax for the "main" repository - # The default assumes the user specified a target "node_modules" in their - # root BUILD file. - "node_modules": attr.label( - default = Label("@//:node_modules") - ), - - "entry_point": attr.string(), - - "_index_bundler": attr.label( - executable = True, - cfg = "host", - default = Label("//packages/bazel/src:index_bundler")), - }), + attrs = NG_MODULE_RULE_ATTRS, + outputs = COMMON_OUTPUTS, +) + +# TODO(alxhub): this rule exists to allow early testing of the Ivy compiler within angular/angular, +# and should not be made public. When ng_module() supports Ivy-mode outputs, this rule should be +# removed and its usages refactored to use ng_module() directly. +ivy_ng_module = rule( + implementation = _ivy_module_impl, + attrs = NG_MODULE_RULE_ATTRS, outputs = COMMON_OUTPUTS, ) diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/BUILD.bazel new file mode 100644 index 00000000000000..8cbf1b54a4e47a --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/BUILD.bazel @@ -0,0 +1,18 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ivy_ng_module", "ts_library") +load("//packages/bazel/src:ng_rollup_bundle.bzl", "ng_rollup_bundle") + +ivy_ng_module( + name = "app", + srcs = glob( + [ + "src/**/*.ts", + ], + ), + module_name = "app_built", + deps = [ + "//packages/core", + "@rxjs", + ], +) diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/src/module.ts b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/src/module.ts new file mode 100644 index 00000000000000..e1eb0c205eb2bd --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/src/module.ts @@ -0,0 +1,40 @@ +/** + * @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, InjectionToken, NgModule} from '@angular/core'; + +export const AOT_TOKEN = new InjectionToken('TOKEN'); + +@Injectable() +export class AotService { +} + +@NgModule({ + providers: [AotService], +}) +export class AotServiceModule { +} + +@NgModule({ + providers: [{provide: AOT_TOKEN, useValue: 'imports'}], +}) +export class AotImportedModule { +} + +@NgModule({ + providers: [{provide: AOT_TOKEN, useValue: 'exports'}], +}) +export class AotExportedModule { +} + +@NgModule({ + imports: [AotServiceModule, AotImportedModule], + exports: [AotExportedModule], +}) +export class AotModule { +} diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/BUILD.bazel new file mode 100644 index 00000000000000..1d89f707656102 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/BUILD.bazel @@ -0,0 +1,27 @@ +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/injector_def/ivy_build/app", + "//packages/core", + ], +) + +jasmine_node_test( + name = "test", + bootstrap = ["angular/tools/testing/init_node_spec.js"], + deps = [ + ":test_lib", + "//tools/testing:node", + ], +) diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts new file mode 100644 index 00000000000000..ae41aaf4c1068f --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts @@ -0,0 +1,87 @@ +/** + * @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, InjectionToken, Injector, NgModule, forwardRef} from '@angular/core'; +import {AOT_TOKEN, AotModule, AotService} from 'app_built/src/module'; + +describe('Ivy NgModule', () => { + describe('AOT', () => { + let injector: Injector; + + beforeEach(() => { injector = Injector.create({definitions: [AotModule]}); }); + it('works', () => { expect(injector.get(AotService) instanceof AotService).toBeTruthy(); }); + + it('merges imports and exports', () => { expect(injector.get(AOT_TOKEN)).toEqual('exports'); }); + }); + + + + describe('JIT', () => { + @Injectable() + class Service { + } + + @NgModule({ + providers: [Service], + }) + class JitModule { + } + + @NgModule({ + imports: [JitModule], + }) + class JitAppModule { + } + + it('works', () => { Injector.create({definitions: [JitAppModule]}); }); + + it('throws an error on circular module dependencies', () => { + @NgModule({ + imports: [forwardRef(() => BModule)], + }) + class AModule { + } + + @NgModule({ + imports: [AModule], + }) + class BModule { + } + + expect(() => Injector.create({ + definitions: [AModule] + })).toThrowError('Circular dependency: module AModule ends up importing itself.'); + }); + + it('merges imports and exports', () => { + const TOKEN = new InjectionToken('TOKEN'); + @NgModule({ + providers: [{provide: TOKEN, useValue: 'provided from A'}], + }) + class AModule { + } + @NgModule({ + providers: [{provide: TOKEN, useValue: 'provided from B'}], + }) + class BModule { + } + + @NgModule({ + imports: [AModule], + exports: [BModule], + }) + class CModule { + } + + const injector = Injector.create({ + definitions: [CModule], + }); + expect(injector.get(TOKEN)).toEqual('provided from B'); + }); + }); +}); \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/lower_expressions.ts b/packages/compiler-cli/src/transformers/lower_expressions.ts index 0793782a504656..3acc61a3c774e6 100644 --- a/packages/compiler-cli/src/transformers/lower_expressions.ts +++ b/packages/compiler-cli/src/transformers/lower_expressions.ts @@ -248,11 +248,14 @@ function isLiteralFieldNamed(node: ts.Node, names: Set): boolean { return false; } -const LOWERABLE_FIELD_NAMES = new Set(['useValue', 'useFactory', 'data']); - export class LowerMetadataTransform implements RequestsMap, MetadataTransformer { private cache: MetadataCache; private requests = new Map(); + private lowerableFieldNames: Set; + + constructor(lowerableFieldNames: string[]) { + this.lowerableFieldNames = new Set(lowerableFieldNames); + } // RequestMap getRequests(sourceFile: ts.SourceFile): RequestLocationMap { @@ -309,17 +312,41 @@ export class LowerMetadataTransform implements RequestsMap, MetadataTransformer return false; }; + const hasLowerableParentCache = new Map(); + let hasLowerableParent: (node: ts.Node | undefined) => boolean; + + const isLowerable = (node: ts.Node | undefined): boolean => { + if (node === undefined) { + return false; + } + let lowerable: boolean = false; + if ((node.kind === ts.SyntaxKind.ArrowFunction || + node.kind === ts.SyntaxKind.FunctionExpression) && + shouldLower(node)) { + lowerable = true; + } + if (isLiteralFieldNamed(node, this.lowerableFieldNames) && shouldLower(node) && + !isExportedSymbol(node) && !isExportedPropertyAccess(node)) { + lowerable = true; + } + lowerable = lowerable && !hasLowerableParent(node); + return lowerable; + }; + + hasLowerableParent = (node: ts.Node | undefined): boolean => { + if (node === undefined) { + return false; + } + if (!hasLowerableParentCache.has(node)) { + hasLowerableParentCache.set( + node, isLowerable(node.parent) || hasLowerableParent(node.parent)); + } + return hasLowerableParentCache.get(node) !; + }; + return (value: MetadataValue, node: ts.Node): MetadataValue => { - if (!isPrimitive(value) && !isRewritten(value)) { - if ((node.kind === ts.SyntaxKind.ArrowFunction || - node.kind === ts.SyntaxKind.FunctionExpression) && - shouldLower(node)) { - return replaceNode(node); - } - if (isLiteralFieldNamed(node, LOWERABLE_FIELD_NAMES) && shouldLower(node) && - !isExportedSymbol(node) && !isExportedPropertyAccess(node)) { - return replaceNode(node); - } + if (!isPrimitive(value) && !isRewritten(value) && isLowerable(node)) { + return replaceNode(node); } return value; }; diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 9970fb692eff3a..dbb533287e26a8 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -21,6 +21,7 @@ import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './l import {MetadataCache, MetadataTransformer} from './metadata_cache'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; +import {getDecoratorStripTransformerFactory} from './r3_strip_decorators'; import {getAngularClassTransformerFactory} from './r3_transform'; import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util'; @@ -32,6 +33,14 @@ import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, is */ const MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT = 20; +// TODO(alxhub): develop a way to track the origin of metadata annotations and make this robust. +const R3_REIFIED_DECORATORS = [ + 'Component', + 'Directive', + 'Injectable', + 'NgModule', +]; + const emptyModules: NgAnalyzedModules = { ngModules: [], ngModuleByPipeOrDirective: new Map(), @@ -99,7 +108,12 @@ class AngularCompilerProgram implements Program { this.host = bundleHost; } } - this.loweringMetadataTransform = new LowerMetadataTransform(); + + const lowerFields = ['useValue', 'useFactory', 'data']; + if (options.enableIvy) { + lowerFields.push('providers', 'imports', 'exports'); + } + this.loweringMetadataTransform = new LowerMetadataTransform(lowerFields); this.metadataCache = this.createMetadataCache([this.loweringMetadataTransform]); } @@ -235,7 +249,8 @@ class AngularCompilerProgram implements Program { 0) { return {emitSkipped: true, diagnostics: [], emittedFiles: []}; } - const modules = this.compiler.emitAllPartialModules(this.analyzedModules); + const modules = + this.compiler.emitAllPartialModules(this.analyzedModules, this._analyzedInjectables !); const writeTsFile: ts.WriteFileCallback = (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { @@ -247,7 +262,8 @@ class AngularCompilerProgram implements Program { const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS; const tsCustomTransformers = this.calculateTransforms( - /* genFiles */ undefined, /* partialModules */ modules, customTransformers); + /* genFiles */ undefined, /* partialModules */ modules, + /* stripDecorators */ R3_REIFIED_DECORATORS, customTransformers); const emitResult = emitCallback({ program: this.tsProgram, @@ -312,8 +328,8 @@ class AngularCompilerProgram implements Program { const modules = this._analyzedInjectables && this.compiler.emitAllPartialModules2(this._analyzedInjectables); - const tsCustomTransformers = - this.calculateTransforms(genFileByFileName, modules, customTransformers); + const tsCustomTransformers = this.calculateTransforms( + genFileByFileName, modules, /* stripDecorators */ undefined, 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. @@ -470,6 +486,7 @@ class AngularCompilerProgram implements Program { private calculateTransforms( genFiles: Map|undefined, partialModules: PartialModule[]|undefined, + stripDecorators: string[]|undefined, customTransformers?: CustomTransformers): ts.CustomTransformers { const beforeTs: ts.TransformerFactory[] = []; if (!this.options.disableExpressionLowering) { @@ -487,6 +504,11 @@ class AngularCompilerProgram implements Program { this.metadataCache = this.createMetadataCache( [this.loweringMetadataTransform, new PartialModuleMetadataTransformer(partialModules)]); } + + if (stripDecorators) { + beforeTs.push(getDecoratorStripTransformerFactory(stripDecorators)); + } + if (customTransformers && customTransformers.beforeTs) { beforeTs.push(...customTransformers.beforeTs); } @@ -781,6 +803,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions { preserveWhitespaces: options.preserveWhitespaces, fullTemplateTypeCheck: options.fullTemplateTypeCheck, allowEmptyCodegenFiles: options.allowEmptyCodegenFiles, + enableIvy: options.enableIvy, }; } diff --git a/packages/compiler-cli/src/transformers/r3_strip_decorators.ts b/packages/compiler-cli/src/transformers/r3_strip_decorators.ts new file mode 100644 index 00000000000000..94a4ab0536d07a --- /dev/null +++ b/packages/compiler-cli/src/transformers/r3_strip_decorators.ts @@ -0,0 +1,55 @@ +/** + * @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 * as ts from 'typescript'; + +export type Transformer = (sourceFile: ts.SourceFile) => ts.SourceFile; +export type TransformerFactory = (context: ts.TransformationContext) => Transformer; + +export function getDecoratorStripTransformerFactory(names: string[]): TransformerFactory { + let stripSet = new Set(names); + return function(context: ts.TransformationContext) { + return function(sourceFile: ts.SourceFile): ts.SourceFile { + let modified: boolean = false; + let newStatements = sourceFile.statements.map(node => { + if (isClassDeclarationStatement(node) && node.decorators) { + let decorators = node.decorators.filter(decorator => { + const callExpr = decorator.expression; + if (isCallExpression(callExpr)) { + const id = callExpr.expression; + if (isIdentifier(id) && stripSet.has(id.getText())) { + return false; + } + } + return true; + }); + if (decorators.length !== node.decorators.length) { + modified = true; + return ts.updateClassDeclaration( + node, decorators, node.modifiers, node.name, node.typeParameters, + node.heritageClauses || [], node.members, ); + } + } + return node; + }); + return modified ? ts.updateSourceFileNode(sourceFile, newStatements) : sourceFile; + }; + }; +} + +function isClassDeclarationStatement(node: ts.Node): node is ts.ClassDeclaration { + return node.kind == ts.SyntaxKind.ClassDeclaration; +} + +function isCallExpression(node: ts.Node): node is ts.CallExpression { + return node.kind == ts.SyntaxKind.CallExpression; +} + +function isIdentifier(node: ts.Node): node is ts.Identifier { + return node.kind == ts.SyntaxKind.Identifier; +} diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 6c53bb441778c9..bb1741bb4428b8 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -2114,4 +2114,63 @@ describe('ngc transformer command-line', () => { expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/); }); }); + + describe('ngInjectorDef', () => { + it('is applied with lowered metadata ', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["module.ts"], + "angularCompilerOptions": { + "enableIvy": true, + "skipTemplateCodegen": true + } + }`); + write('module.ts', ` + import {Injectable, NgModule} from '@angular/core'; + + @Injectable() + export class ServiceA {} + + @Injectable() + export class ServiceB {} + + @NgModule() + export class Exported {} + + @NgModule({ + providers: [ServiceA] + }) + export class Imported { + static forRoot() { + console.log('not statically analyzable'); + return { + ngModule: Imported, + providers: [] as any, + }; + } + } + + @NgModule({ + providers: [ServiceA, ServiceB], + imports: [Imported.forRoot()], + exports: [Exported], + }) + export class Module {} + `); + + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + expect(exitCode).toEqual(0); + + const modulePath = path.resolve(outDir, 'module.js'); + const moduleSource = fs.readFileSync(modulePath, 'utf8'); + expect(moduleSource) + .toContain('var ɵ1 = [ServiceA, ServiceB], ɵ2 = [Imported.forRoot()], ɵ3 = [Exported];'); + expect(moduleSource) + .toContain( + 'Imported.ngInjectorDef = i0.ɵdefineInjector({ factory: function Imported_Factory() { return new Imported(); }, providers: ɵ0, imports: [] });'); + expect(moduleSource) + .toContain( + 'Module.ngInjectorDef = i0.ɵdefineInjector({ factory: function Module_Factory() { return new Module(); }, providers: ɵ1, imports: [ɵ2, ɵ3] });'); + }); + }); }); diff --git a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts index 88669935196bd7..0e03f6e5b6dbd5 100644 --- a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts +++ b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts @@ -13,6 +13,8 @@ import {LowerMetadataTransform, LoweringRequest, RequestLocationMap, getExpressi import {MetadataCache} from '../../src/transformers/metadata_cache'; import {Directory, MockAotContext, MockCompilerHost} from '../mocks'; +const DEFAULT_FIELDS_TO_LOWER = ['useFactory', 'useValue', 'data']; + describe('Expression lowering', () => { describe('transform', () => { it('should be able to lower a simple expression', () => { @@ -112,7 +114,8 @@ describe('Expression lowering', () => { it('should throw a validation exception for invalid files', () => { const cache = new MetadataCache( - new MetadataCollector({}), /* strict */ true, [new LowerMetadataTransform()]); + new MetadataCollector({}), /* strict */ true, + [new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER)]); const sourceFile = ts.createSourceFile( 'foo.ts', ` import {Injectable} from '@angular/core'; @@ -129,7 +132,8 @@ describe('Expression lowering', () => { it('should not report validation errors on a .d.ts file', () => { const cache = new MetadataCache( - new MetadataCollector({}), /* strict */ true, [new LowerMetadataTransform()]); + new MetadataCollector({}), /* strict */ true, + [new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER)]); const dtsFile = ts.createSourceFile( 'foo.d.ts', ` import {Injectable} from '@angular/core'; @@ -244,7 +248,7 @@ function normalizeResult(result: string): string { function collect(annotatedSource: string) { const {annotations, unannotatedSource} = getAnnotations(annotatedSource); - const transformer = new LowerMetadataTransform(); + const transformer = new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER); const cache = new MetadataCache(new MetadataCollector({}), false, [transformer]); const sourceFile = ts.createSourceFile( 'someName.ts', unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true); diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index ad4dd14e2cc28b..cb2d12c110a922 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileInjectableMetadata, 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, CompileShallowModuleMetadata, 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'; @@ -20,6 +20,7 @@ import {NgModuleCompiler} from '../ng_module_compiler'; import {OutputEmitter} from '../output/abstract_emitter'; import * as o from '../output/output_ast'; import {ParseError} from '../parse_util'; +import {compileInjectorDefFromModule as compileIvyModule} from '../render3/r3_module_compiler'; import {compilePipe as compileIvyPipe} from '../render3/r3_pipe_compiler'; import {compileComponent as compileIvyComponent, compileDirective as compileIvyDirective} from '../render3/r3_view_compiler'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; @@ -330,24 +331,45 @@ export class AotCompiler { return messageBundle; } - emitAllPartialModules({ngModuleByPipeOrDirective, files}: NgAnalyzedModules): PartialModule[] { - // Using reduce like this is a select many pattern (where map is a select pattern) - return files.reduce((r, file) => { - r.push(...this._emitPartialModule( - file.fileName, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules, - file.injectables)); - return r; - }, []); + emitAllPartialModules( + {ngModuleByPipeOrDirective, files}: NgAnalyzedModules, + r3Files: NgAnalyzedFileWithInjectables[]): PartialModule[] { + const contextMap = new Map(); + + const getContext = (name: string): OutputContext => { + if (!contextMap.has(name)) { + contextMap.set(name, this._createOutputContext(name)); + } + return contextMap.get(name) !; + }; + + files.forEach( + file => this._compilePartialModule( + file.fileName, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules, + file.injectables, getContext(file.fileName))); + r3Files.forEach( + file => this._compileShallowModules( + file.fileName, file.shallowModules, getContext(file.fileName))); + + return Array.from(contextMap.values()) + .map(context => ({ + fileName: context.genFilePath, + statements: [...context.constantPool.statements, ...context.statements], + })); } - private _emitPartialModule( + private _compileShallowModules( + fileName: string, shallowModules: CompileShallowModuleMetadata[], + context: OutputContext): void { + shallowModules.forEach(module => compileIvyModule(context, module, this._injectableCompiler)); + } + + private _compilePartialModule( fileName: string, ngModuleByPipeOrDirective: Map, directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], - injectables: CompileInjectableMetadata[]): PartialModule[] { + injectables: CompileInjectableMetadata[], context: OutputContext): void { const classes: o.ClassStmt[] = []; - const context = this._createOutputContext(fileName); - // Process all components and directives directives.forEach(directiveType => { const directiveMetadata = this._metadataResolver.getDirectiveMetadata(directiveType); @@ -374,11 +396,6 @@ export class AotCompiler { }); 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[] { @@ -739,6 +756,7 @@ export interface NgAnalyzedModules { export interface NgAnalyzedFileWithInjectables { fileName: string; injectables: CompileInjectableMetadata[]; + shallowModules: CompileShallowModuleMetadata[]; } export interface NgAnalyzedFile { @@ -859,6 +877,7 @@ export function analyzeFileForInjectables( host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver, metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFileWithInjectables { const injectables: CompileInjectableMetadata[] = []; + const shallowModules: CompileShallowModuleMetadata[] = []; if (staticSymbolResolver.hasDecorators(fileName)) { staticSymbolResolver.getSymbolsOf(fileName).forEach((symbol) => { const resolvedSymbol = staticSymbolResolver.resolveSymbol(symbol); @@ -874,11 +893,17 @@ export function analyzeFileForInjectables( if (injectable) { injectables.push(injectable); } + } else if (metadataResolver.isNgModule(symbol)) { + isNgSymbol = true; + const module = metadataResolver.getShallowModuleMetadata(symbol); + if (module) { + shallowModules.push(module); + } } } }); } - return {fileName, injectables}; + return {fileName, injectables, shallowModules}; } function isValueExportingNonSourceFile(host: NgAnalyzeModulesHost, metadata: any): boolean { diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index 708cef7aa23d35..b2143d5d5df99e 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -91,7 +91,8 @@ export function createAotCompiler( const compiler = new AotCompiler( config, options, compilerHost, staticReflector, resolver, tmplParser, new StyleCompiler(urlResolver), viewCompiler, typeCheckCompiler, - new NgModuleCompiler(staticReflector), new InjectableCompiler(staticReflector), - new TypeScriptEmitter(), summaryResolver, symbolResolver); + new NgModuleCompiler(staticReflector), + new InjectableCompiler(staticReflector, !!options.enableIvy), new TypeScriptEmitter(), + summaryResolver, symbolResolver); return {compiler, reflector: staticReflector}; } diff --git a/packages/compiler/src/aot/compiler_options.ts b/packages/compiler/src/aot/compiler_options.ts index b7bd69d32009f5..b87d4cded1f35d 100644 --- a/packages/compiler/src/aot/compiler_options.ts +++ b/packages/compiler/src/aot/compiler_options.ts @@ -19,4 +19,5 @@ export interface AotCompilerOptions { fullTemplateTypeCheck?: boolean; allowEmptyCodegenFiles?: boolean; strictInjectionParameters?: boolean; + enableIvy?: boolean; } diff --git a/packages/compiler/src/aot/static_reflector.ts b/packages/compiler/src/aot/static_reflector.ts index bc4380d6f88b70..8cacaa6523dcc7 100644 --- a/packages/compiler/src/aot/static_reflector.ts +++ b/packages/compiler/src/aot/static_reflector.ts @@ -42,6 +42,7 @@ function shouldIgnore(value: any): boolean { */ export class StaticReflector implements CompileReflector { private annotationCache = new Map(); + private shallowAnnotationCache = new Map(); private propertyCache = new Map(); private parameterCache = new Map(); private methodCache = new Map(); @@ -135,7 +136,21 @@ export class StaticReflector implements CompileReflector { } public annotations(type: StaticSymbol): any[] { - let annotations = this.annotationCache.get(type); + return this._annotations( + type, (type: StaticSymbol, decorators: any) => this.simplify(type, decorators), + this.annotationCache); + } + + public shallowAnnotations(type: StaticSymbol): any[] { + return this._annotations( + type, (type: StaticSymbol, decorators: any) => this.simplify(type, decorators, true), + this.shallowAnnotationCache); + } + + private _annotations( + type: StaticSymbol, simplify: (type: StaticSymbol, decorators: any) => any, + annotationCache: Map): any[] { + let annotations = annotationCache.get(type); if (!annotations) { annotations = []; const classMetadata = this.getTypeMetadata(type); @@ -146,7 +161,7 @@ export class StaticReflector implements CompileReflector { } let ownAnnotations: any[] = []; if (classMetadata['decorators']) { - ownAnnotations = this.simplify(type, classMetadata['decorators']); + ownAnnotations = simplify(type, classMetadata['decorators']); annotations.push(...ownAnnotations); } if (parentType && !this.summaryResolver.isLibraryFile(type.filePath) && @@ -169,7 +184,7 @@ export class StaticReflector implements CompileReflector { } } } - this.annotationCache.set(type, annotations.filter(ann => !!ann)); + annotationCache.set(type, annotations.filter(ann => !!ann)); } return annotations; } @@ -414,7 +429,7 @@ export class StaticReflector implements CompileReflector { } /** @internal */ - public simplify(context: StaticSymbol, value: any): any { + public simplify(context: StaticSymbol, value: any, lazy: boolean = false): any { const self = this; let scope = BindingScope.empty; const calling = new Map(); @@ -775,7 +790,7 @@ export class StaticReflector implements CompileReflector { let result: any; try { - result = simplifyInContext(context, value, 0, 0); + result = simplifyInContext(context, value, 0, lazy ? 1 : 0); } catch (e) { if (this.errorRecorder) { this.reportError(e, context); diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 816494c8c78cbd..fe8d3568f63d8e 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -527,6 +527,14 @@ export interface CompileNgModuleSummary extends CompileTypeSummary { modules: CompileTypeMetadata[]; } +export class CompileShallowModuleMetadata { + type: CompileTypeMetadata; + + rawExports: any; + rawImports: any; + rawProviders: any; +} + /** * Metadata regarding compilation of a module. */ diff --git a/packages/compiler/src/compile_reflector.ts b/packages/compiler/src/compile_reflector.ts index 9700634fc53ccd..c6ffc9fed4b365 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 shallowAnnotations(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; diff --git a/packages/compiler/src/injectable_compiler.ts b/packages/compiler/src/injectable_compiler.ts index 2ae1c7af7b9e78..c8909cd58a8a83 100644 --- a/packages/compiler/src/injectable_compiler.ts +++ b/packages/compiler/src/injectable_compiler.ts @@ -29,7 +29,7 @@ function mapEntry(key: string, value: o.Expression): MapEntry { } export class InjectableCompiler { - constructor(private reflector: CompileReflector) {} + constructor(private reflector: CompileReflector, private alwaysGenerateDef: boolean) {} private depsArray(deps: any[], ctx: OutputContext): o.Expression[] { return deps.map(dep => { @@ -64,7 +64,7 @@ export class InjectableCompiler { }); } - private factoryFor(injectable: CompileInjectableMetadata, ctx: OutputContext): o.Expression { + factoryFor(injectable: CompileInjectableMetadata, ctx: OutputContext): o.Expression { let retValue: o.Expression; if (injectable.useExisting) { retValue = o.importExpr(Identifiers.inject).callFn([ctx.importExpr(injectable.useExisting)]); @@ -91,13 +91,15 @@ export class InjectableCompiler { const def: MapLiteral = [ mapEntry('factory', this.factoryFor(injectable, ctx)), mapEntry('token', ctx.importExpr(injectable.type.reference)), - mapEntry('scope', ctx.importExpr(injectable.module !)), + mapEntry( + 'scope', + injectable.module != null ? ctx.importExpr(injectable.module) : o.literal(undefined)), ]; return o.importExpr(Identifiers.defineInjectable).callFn([o.literalMap(def)]); } compile(injectable: CompileInjectableMetadata, ctx: OutputContext): void { - if (injectable.module) { + if (this.alwaysGenerateDef || injectable.module) { const className = identifierName(injectable.type) !; const clazz = new o.ClassStmt( className, null, diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index b4d50aa321eab1..1e5953f9403fb4 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -12,9 +12,9 @@ import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions'; import * as cpl from './compile_metadata'; import {CompileReflector} from './compile_reflector'; import {CompilerConfig} from './config'; -import {ChangeDetectionStrategy, Component, Directive, Injectable, 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, createNgModule, createOptional, createSelf, createSkipSelf} from './core'; import {DirectiveNormalizer} from './directive_normalizer'; -import {DirectiveResolver} from './directive_resolver'; +import {DirectiveResolver, findLast} from './directive_resolver'; import {Identifiers} from './identifiers'; import {getAllLifecycleHooks} from './lifecycle_reflector'; import {HtmlParser} from './ml_parser/html_parser'; @@ -44,6 +44,7 @@ export class CompileMetadataResolver { private _pipeCache = new Map(); private _ngModuleCache = new Map(); private _ngModuleOfTypes = new Map(); + private _shallowModuleCache = new Map(); constructor( private _config: CompilerConfig, private _htmlParser: HtmlParser, @@ -477,6 +478,25 @@ export class CompileMetadataResolver { return Promise.all(loading); } + getShallowModuleMetadata(moduleType: any): cpl.CompileShallowModuleMetadata|null { + let compileMeta = this._shallowModuleCache.get(moduleType); + if (compileMeta) { + return compileMeta; + } + + const ngModuleMeta = + findLast(this._reflector.shallowAnnotations(moduleType), createNgModule.isTypeOf); + + compileMeta = { + type: this._getTypeMetadata(moduleType), + rawExports: ngModuleMeta.exports, + rawImports: ngModuleMeta.imports, + rawProviders: ngModuleMeta.providers, + }; + + return compileMeta || null; + } + getNgModuleMetadata( moduleType: any, throwIfNotFound = true, alreadyCollecting: Set|null = null): cpl.CompileNgModuleMetadata|null { diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 4fbe0e5954fae5..2195c31157821e 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -97,6 +97,11 @@ export class Identifiers { moduleName: CORE, }; + static defineInjector: o.ExternalReference = { + name: 'ɵdefineInjector', + moduleName: CORE, + }; + static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE}; static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_module_compiler.ts b/packages/compiler/src/render3/r3_module_compiler.ts new file mode 100644 index 00000000000000..e9cff08bd548cf --- /dev/null +++ b/packages/compiler/src/render3/r3_module_compiler.ts @@ -0,0 +1,78 @@ +/** + * @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 {StaticSymbol} from '../aot/static_symbol'; +import {CompileShallowModuleMetadata, identifierName} from '../compile_metadata'; +import {InjectableCompiler} from '../injectable_compiler'; +import * as o from '../output/output_ast'; +import {OutputContext} from '../util'; + +import {Identifiers as R3} from './r3_identifiers'; + +const EMPTY_ARRAY = o.literalArr([]); + +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}; +} + +function toMapLiteral(obj: {[key: string]: o.Expression}): o.Expression { + return o.literalMap(Object.keys(obj).map(key => ({ + key, + quoted: false, + value: obj[key], + }))); +} + +function convertMetaToOutput(meta: any, ctx: OutputContext): o.Expression { + if (Array.isArray(meta)) { + return o.literalArr(meta.map(entry => convertMetaToOutput(entry, ctx))); + } else if (meta instanceof StaticSymbol) { + return ctx.importExpr(meta); + } else if (meta == null) { + return o.literal(meta); + } else { + throw new Error(`Unsupported or unknown metadata: ${meta}`); + } +} + +export function compileInjectorDefFromModule( + ctx: OutputContext, ngModule: CompileShallowModuleMetadata, + injectableCompiler: InjectableCompiler): void { + const className = identifierName(ngModule.type) !; + + const rawImports = ngModule.rawImports ? [ngModule.rawImports] : []; + const rawExports = ngModule.rawExports ? [ngModule.rawExports] : []; + + const injectorDefArg = toMapLiteral({ + 'factory': + injectableCompiler.factoryFor({type: ngModule.type, symbol: ngModule.type.reference}, ctx), + 'providers': convertMetaToOutput(ngModule.rawProviders, ctx), + 'imports': convertMetaToOutput([...rawImports, ...rawExports], ctx), + }); + + const injectorDef = o.importExpr(R3.defineInjector).callFn([injectorDefArg]); + + ctx.statements.push(new o.ClassStmt( + /* name */ className, + /* parent */ null, + /* fields */[new o.ClassField( + /* name */ 'ngInjectorDef', + /* type */ o.INFERRED_TYPE, + /* modifiers */[o.StmtModifier.Static], + /* initializer */ injectorDef, )], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[])); +} \ No newline at end of file diff --git a/packages/core/src/codegen_private_exports.ts b/packages/core/src/codegen_private_exports.ts index bddc41e14dd626..fc299a049a1ed8 100644 --- a/packages/core/src/codegen_private_exports.ts +++ b/packages/core/src/codegen_private_exports.ts @@ -8,4 +8,5 @@ export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver'; export {registerModuleFactory as ɵregisterModuleFactory} from './linker/ng_module_factory_loader'; +export {defineInjector as ɵdefineInjector} from './metadata/ng_module'; export {ArgumentType as ɵArgumentType, BindingFlags as ɵBindingFlags, DepFlags as ɵDepFlags, EMPTY_ARRAY as ɵEMPTY_ARRAY, EMPTY_MAP as ɵEMPTY_MAP, NodeFlags as ɵNodeFlags, QueryBindingType as ɵQueryBindingType, QueryValueType as ɵQueryValueType, ViewDefinition as ɵViewDefinition, ViewFlags as ɵViewFlags, anchorDef as ɵand, createComponentFactory as ɵccf, createNgModuleFactory as ɵcmf, createRendererType2 as ɵcrt, directiveDef as ɵdid, elementDef as ɵeld, elementEventFullName as ɵelementEventFullName, getComponentViewDefinitionFactory as ɵgetComponentViewDefinitionFactory, inlineInterpolate as ɵinlineInterpolate, interpolate as ɵinterpolate, moduleDef as ɵmod, moduleProvideDef as ɵmpd, ngContentDef as ɵncd, nodeValue as ɵnov, pipeDef as ɵpid, providerDef as ɵprd, pureArrayDef as ɵpad, pureObjectDef as ɵpod, purePipeDef as ɵppd, queryDef as ɵqud, textDef as ɵted, unwrapValue as ɵunv, viewDef as ɵvid} from './view/index'; diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index 756904237a0b35..da449a30f187db 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -13,10 +13,9 @@ */ export * from './di/metadata'; -export {defineInjectable, Injectable, InjectableDecorator, InjectableProvider, InjectableType} from './di/injectable'; - +export * from './di/defs'; export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref'; - +export {defineInjectable, Injectable, InjectableDecorator, InjectableProvider} from './di/injectable'; export {inject, InjectFlags, Injector} from './di/injector'; export {ReflectiveInjector} from './di/reflective_injector'; export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; diff --git a/packages/core/src/di/defs.ts b/packages/core/src/di/defs.ts new file mode 100644 index 00000000000000..5f6fee49c0c0d5 --- /dev/null +++ b/packages/core/src/di/defs.ts @@ -0,0 +1,82 @@ +/** + * @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 {Type} from '../type'; +import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, ValueProvider} from './provider'; + +/** + * Information about how a type or `InjectionToken` interfaces with the DI system. + * + * At a minimum, this includes a `factory` which defines how to create the given type `T`, possibly + * requesting injection of other types if necessary. + * + * Optionally, a `scope` parameter specifies that the given type belongs to a particular + * `InjectorDef`, `NgModule`, or a special scope (e.g. `APP_ROOT_SCOPE`). + * + * This type is typically generated by the Angular compiler, but can be hand-written if needed. + * + * @experimental + */ +export interface InjectableDef { + scope?: any; + factory: () => T; +} + +/** + * Information about the providers to be included in an `Injector` as well as how the given type + * which carries the information should be created by the DI system. + * + * An `InjectorDef` can import other types which have `InjectorDefs`, forming a deep nested + * structure of providers with a defined priority (identically to how `NgModule`s also have + * an import/dependency structure). + * + * @experimental + */ +export interface InjectorDef { + factory: () => T; + + // TODO(alxhub): Narrow down the type here once decorators properly change the return type of the + // class they are decorating (to add the ngInjectableDef property for example). + providers?: (Type|ValueProvider|ExistingProvider|FactoryProvider|ConstructorProvider| + StaticClassProvider|ClassProvider|any[])[]; + + imports?: (InjectorDefType|InjectorDefTypeWithProviders)[]; +} + +/** + * A `Type` which has an `InjectableDef` static field. + * + * `InjectableDefType`s contain their own Dependency Injection metadata and are usable in an + * `InjectorDef`-based `StaticInjector. + * + * @experimental + */ +export interface InjectableDefType extends Type { ngInjectableDef: InjectableDef; } + +/** + * A type which has an `InjectorDef` static field. + * + * `InjectorDefTypes` can be used to configure a `StaticInjector`. + * + * @experimental + */ +export interface InjectorDefType extends Type { ngInjectorDef: InjectorDef; } + +/** + * Describes the `InjectorDef` equivalent of a `ModuleWithProviders`, an `InjectorDefType` with an + * associated array of providers. + * + * Objects of this type can be listed in the imports section of an `InjectorDef`. + * + * @experimental + */ +export interface InjectorDefTypeWithProviders { + ngModule: InjectorDefType; + providers?: (Type|ValueProvider|ExistingProvider|FactoryProvider|ConstructorProvider| + StaticClassProvider|ClassProvider|any[])[]; +} diff --git a/packages/core/src/di/injectable.ts b/packages/core/src/di/injectable.ts index b9f7ec22c703ad..cd9ac305fe40fd 100644 --- a/packages/core/src/di/injectable.ts +++ b/packages/core/src/di/injectable.ts @@ -11,6 +11,7 @@ import {Type} from '../type'; import {makeDecorator, makeParamDecorator} from '../util/decorators'; import {getClosureSafeProperty} from '../util/property'; +import {InjectableDef, InjectableDefType} from './defs'; import {inject, injectArgs} from './injector'; import {ClassSansProvider, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, StaticClassProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} from './provider'; @@ -113,7 +114,7 @@ export function convertInjectableProviderToFactory( * * @experimental */ -export function defineInjectable(opts: Injectable): Injectable { +export function defineInjectable(opts: Injectable): InjectableDef { return opts; } @@ -125,19 +126,10 @@ export function defineInjectable(opts: Injectable): Injectable { */ 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) - }); - } + (injectableType: InjectableDefType, options: {scope: Type} & InjectableProvider) => { + injectableType.ngInjectableDef = defineInjectable({ + scope: options && options.scope, + factory: convertInjectableProviderToFactory( + injectableType, options || {useClass: injectableType}), + }); }); - - -/** - * 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 933051c340c4af..bc9947aa79e0fa 100644 --- a/packages/core/src/di/injection_token.ts +++ b/packages/core/src/di/injection_token.ts @@ -8,7 +8,7 @@ import {Type} from '../type'; -import {Injectable, defineInjectable} from './injectable'; +import {InjectableDef} from './defs'; /** * Creates a token that can be used in a DI Provider. @@ -36,14 +36,14 @@ export class InjectionToken { /** @internal */ readonly ngMetadataName = 'InjectionToken'; - readonly ngInjectableDef: Injectable|undefined; + readonly ngInjectableDef: InjectableDef|undefined; constructor(protected _desc: string, options?: {scope: Type, factory: () => T}) { if (options !== undefined) { - this.ngInjectableDef = defineInjectable({ + this.ngInjectableDef = { scope: options.scope, factory: options.factory, - }); + }; } else { this.ngInjectableDef = undefined; } @@ -51,3 +51,7 @@ export class InjectionToken { toString(): string { return `InjectionToken ${this._desc}`; } } + +export interface InjectableDefToken extends InjectionToken { + ngInjectableDef: InjectableDef; +} diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index e428aa9a964c2c..dbc66f6ea673f5 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -6,17 +6,28 @@ * found in the LICENSE file at https://angular.io/license */ +import {OnDestroy} from '../metadata/lifecycle_hooks'; import {Type} from '../type'; import {stringify} from '../util'; + +import {InjectableDef, InjectableDefType, InjectorDef, InjectorDefType, InjectorDefTypeWithProviders} from './defs'; import {resolveForwardRef} from './forward_ref'; -import {InjectionToken} from './injection_token'; +import {InjectableDefToken, InjectionToken} from './injection_token'; import {Inject, Optional, Self, SkipSelf} from './metadata'; -import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './provider'; +import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, TypeProvider, ValueProvider} from './provider'; +import {APP_ROOT_SCOPE} from './scope'; export const SOURCE = '__source'; const _THROW_IF_NOT_FOUND = new Object(); export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; +// These are internal types for InjectorDef providers. Ideally these would be part of the public +// type for ngInjectorDef.providers, but Typescript cannot know that @Injectable() changes a +// Type to an `InjectableDefType, so for now providers are cast. +type InjectorDefProvider = InjectableDefType| InjectableDefToken| InjectorDefDeepProvider; +type InjectorDefDeepProvider = ValueProvider | ExistingProvider | FactoryProvider | + ConstructorProvider | StaticClassProvider | ClassProvider; + class _NullInjector implements Injector { get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any { if (notFoundValue === _THROW_IF_NOT_FOUND) { @@ -69,28 +80,37 @@ export abstract class Injector { */ static create(providers: StaticProvider[], parent?: Injector): Injector; - static create(options: {providers: StaticProvider[], parent?: Injector, name?: string}): Injector; + /** + * Create an `Injector` from a list of `InjectorDefType`s. + */ + static create(options: {definitions: any[], parent?: Injector, name?: string}): Injector; /** - * Create a new Injector which is configure using `StaticProvider`s. + * Create an `Injector` from a list of `StaticProviders`s. + */ + static create(options: {providers?: StaticProvider[], parent?: Injector, name?: string}): + Injector; + + /** + * Create a new Injector which is configure using `StaticProvider`s or `InjectorDefType`s. * * ### Example * * {@example core/di/ts/provider_spec.ts region='ConstructorProvider'} */ static create( - options: StaticProvider[]|{providers: StaticProvider[], parent?: Injector, name?: string}, + options: StaticProvider[]| + {providers?: StaticProvider[], definitions?: any[], parent?: Injector, name?: string}, parent?: Injector): Injector { if (Array.isArray(options)) { - return new StaticInjector(options, parent); + return new StaticInjector(options, [], parent); } else { - return new StaticInjector(options.providers, options.parent, options.name || null); + return new StaticInjector( + options.providers || [], options.definitions || [], options.parent, options.name || null); } } } - - const IDENT = function(value: T): T { return value; }; @@ -120,22 +140,79 @@ export class StaticInjector implements Injector { private _records: Map; + /** + * All of the `InjectorDefType`s from which this `Injector` was built. + */ + private _injectorDefTypes = new Set>(); + + /** + * Collected instances which will be destroyed when this injector is destroyed. + */ + private _onDestroy = new Set(); + + /** + * Internal field which marks whether this injector has been destroyed. + */ + private _destroyed = false; + + private readonly _isRootScope: boolean; + constructor( - providers: StaticProvider[], parent: Injector = NULL_INJECTOR, source: string|null = null) { + providers: StaticProvider[], definitions: InjectorDefType[], + parent: Injector = NULL_INJECTOR, source: string|null = null) { this.parent = parent; this.source = source; const records = this._records = new Map(); records.set( Injector, {token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); - recursivelyProcessProviders(records, providers); + + // Process all non-InjectorDef providers. + recursivelyProcessProviders(records, providers, false); + + // Process all of the InjectorDefs. + deepForEach( + definitions, defType => processInjectorDef(defType, records, this._injectorDefTypes)); + + this._isRootScope = records.has(APP_ROOT_SCOPE); + + this._injectorDefTypes.forEach(moduleType => { + records.set(moduleType, { + token: moduleType, + fn: moduleType.ngInjectorDef.factory, + deps: EMPTY, + value: EMPTY, + useNew: false + }); + }); + // Eagerly instantiate all of the `InjectorDefType`s. + this._injectorDefTypes.forEach(injectorDefType => this.get(injectorDefType)); } get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; get(token: any, notFoundValue?: any): any; get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any { - const record = this._records.get(token); + this._assertNotDestroyed(); + let record = this._records.get(token); + + // If no record of this type in the Injector exists, it could be a type with an + // `ngInjectableDef` field that happens to be scoped to this injector. + if (record === undefined && (isInjectableDefType(token) || isInjectableDefToken(token))) { + const scope = token.ngInjectableDef.scope; + + // For the type to be considered a part of this Injector, one of two conditions must be met: + // 1) the scope is an InjectorDefType that's part of this Injector, or + // 2) the scope is APP_ROOT_SCOPE and APP_ROOT_SCOPE is provided in this Injector + // (_isRootScope is true). + if (this._injectorDefTypes.has(scope) || (scope === APP_ROOT_SCOPE && this._isRootScope)) { + // The type belongs to this Injector, so create a Record for it. + record = injectableDefRecord(token); + this._records.set(token, record); + } + } + const previousInjector = setCurrentInjector(this); try { - return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags); + return tryResolveToken( + token, record, this._records, this.parent, notFoundValue, flags, this._onDestroy); } catch (e) { const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; if (token[SOURCE]) { @@ -145,16 +222,31 @@ export class StaticInjector implements Injector { e[NG_TOKEN_PATH] = tokenPath; e[NG_TEMP_TOKEN_PATH] = null; throw e; + } finally { + setCurrentInjector(previousInjector); } } + destroy(): void { + this._assertNotDestroyed(); + this._destroyed = true; + this._onDestroy.forEach(onDestroy => onDestroy.ngOnDestroy()); + } + toString() { const tokens = [], records = this._records; records.forEach((v, token) => tokens.push(stringify(token))); return `StaticInjector[${tokens.join(', ')}]`; } + + private _assertNotDestroyed(): void { + if (this._destroyed) { + throw new Error('StaticInjector: injector has already been destroyed.'); + } + } } +// StaticProvider without the recursive array. type SupportedProvider = ValueProvider | ExistingProvider | StaticClassProvider | ConstructorProvider | FactoryProvider; @@ -178,17 +270,17 @@ function resolveProvider(provider: SupportedProvider): Record { let value: any = EMPTY; let useNew: boolean = false; let provide = resolveForwardRef(provider.provide); - if (USE_VALUE in provider) { + if (isValueProvider(provider)) { // We need to use USE_VALUE in provider since provider.useValue could be defined as undefined. - value = (provider as ValueProvider).useValue; - } else if ((provider as FactoryProvider).useFactory) { - fn = (provider as FactoryProvider).useFactory; - } else if ((provider as ExistingProvider).useExisting) { + value = provider.useValue; + } else if (isFactoryProvider(provider)) { + fn = provider.useFactory; + } else if (isExistingProvider(provider)) { // Just use IDENT - } else if ((provider as StaticClassProvider).useClass) { + } else if (isClassProvider(provider)) { useNew = true; - fn = resolveForwardRef((provider as StaticClassProvider).useClass); - } else if (typeof provide == 'function') { + fn = resolveForwardRef(provider.useClass); + } else if (typeof provide === 'function') { useNew = true; fn = provide; } else { @@ -203,22 +295,42 @@ function multiProviderMixError(token: any) { return staticError('Cannot mix multi providers and regular providers', token); } -function recursivelyProcessProviders(records: Map, provider: StaticProvider) { +function recursivelyProcessProviders( + records: Map, provider: StaticProvider | any[], fromInjectorDef: false): void; +function recursivelyProcessProviders( + records: Map, provider: InjectorDefProvider | any[], fromInjectorDef: true): void; +function recursivelyProcessProviders( + records: Map, provider: StaticProvider | InjectorDefProvider | any[], + fromInjectorDef: boolean): void; +function recursivelyProcessProviders( + records: Map, provider: StaticProvider | InjectorDefProvider | any[], + fromInjectorDef: boolean): void { if (provider) { provider = resolveForwardRef(provider); if (provider instanceof Array) { // if we have an array recurse into the array for (let i = 0; i < provider.length; i++) { - recursivelyProcessProviders(records, provider[i]); + recursivelyProcessProviders(records, provider[i], fromInjectorDef); + } + } else if ( + isTypeProvider(provider) || isInjectableDefType(provider) || + isInjectableDefToken(provider)) { + // For StaticInjectors created from an array of providers, ngInjectableDef-bearing types are + // not supported (there is no way to interpret the scope). + if (!fromInjectorDef) { + // Functions were supported in ReflectiveInjector, but are not here. For safety give useful + // error messages + throw staticError('Function/Class not supported', provider); + } else if (!isInjectableDefType(provider) && !isInjectableDefToken(provider)) { + throw staticError('No ngInjectableDef on token', provider); + } else { + records.set(provider, injectableDefRecord(provider)); } - } else if (typeof provider === 'function') { - // Functions were supported in ReflectiveInjector, but are not here. For safety give useful - // error messages - throw staticError('Function/Class not supported', provider); } else if (provider && typeof provider === 'object' && provider.provide) { // At this point we have what looks like a provider: {provide: ?, ....} let token = resolveForwardRef(provider.provide); - const resolvedProvider = resolveProvider(provider); + const resolvedProvider = + fromInjectorDef ? providerToRecord(provider) : resolveProvider(provider); if (provider.multi === true) { // This is a multi provider. let multiProvider: Record|undefined = records.get(token); @@ -253,9 +365,9 @@ function recursivelyProcessProviders(records: Map, provider: Static function tryResolveToken( token: any, record: Record | undefined, records: Map, parent: Injector, - notFoundValue: any, flags: InjectFlags): any { + notFoundValue: any, flags: InjectFlags, onDestroy: Set): any { try { - return resolveToken(token, record, records, parent, notFoundValue, flags); + return resolveToken(token, record, records, parent, notFoundValue, flags, onDestroy); } catch (e) { // ensure that 'e' is of type Error. if (!(e instanceof Error)) { @@ -273,7 +385,7 @@ function tryResolveToken( function resolveToken( token: any, record: Record | undefined, records: Map, parent: Injector, - notFoundValue: any, flags: InjectFlags): any { + notFoundValue: any, flags: InjectFlags, onDestroy: Set): any { let value; 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 @@ -307,10 +419,13 @@ function resolveToken( // than pass in Null injector. !childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent, options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND, - InjectFlags.Default)); + InjectFlags.Default, onDestroy)); } } record.value = value = useNew ? new (fn as any)(...deps) : fn.apply(obj, deps); + if (hasOnDestroy(record.value)) { + onDestroy.add(record.value); + } } } else if (!(flags & InjectFlags.Self)) { value = parent.get(token, notFoundValue, InjectFlags.Default); @@ -471,3 +586,167 @@ export function injectArgs(types: (Type| InjectionToken| any[])[]): an } return args; } + +/** + * Process an InjectorDefType (or InjectorDefTypeWithProviders) and accumulate any providers it + * declares in the `records` Map. + * + * There may also be circular dependencies among `InjectorDefType`s, so a Set of `InjectorDefType`s + * in the chain of imports going back to this root is maintained, and if a type is seen twice a + * circular import is present. + * + * A helper method does all the actual work of interpreting the `InjectorDefType`. + */ +function processInjectorDef( + injectorDef: InjectorDefType| InjectorDefTypeWithProviders, records: Map, + injectorDefSet: Set>): void { + processInjectorDefHelper(injectorDef, records, injectorDefSet, new Set>()); +} + +/** + * Helper method for the above `processInjectorDef`. + */ +function processInjectorDefHelper( + injectorDef: InjectorDefType| InjectorDefTypeWithProviders, records: Map, + injectorDefSet: Set>, circular: Set>): void { + injectorDef = resolveForwardRef(injectorDef); + // Either the injectorDef is actually an `InjectorDefType` or an object with both such a type + // and an array of providers. + if (isInjectorDefType(injectorDef)) { + // Check for circular imports. + if (circular.has(injectorDef)) { + const moduleName = (injectorDef instanceof Function) ? injectorDef.name : `${injectorDef}`; + throw new Error(`Circular dependency: module ${moduleName} ends up importing itself.`); + } + + injectorDefSet.add(injectorDef); + const def = injectorDef.ngInjectorDef; + + if (def.imports) { + // Add this type to the circular import tracking Set before processing its imports. If any of + // its imports end up importing this type again, an error will be raised. + circular.add(injectorDef); + + // Process and add providers from all of the imported types. + deepForEach( + def.imports, + imported => processInjectorDefHelper(imported, records, injectorDefSet, circular)); + + // Remove the type from the circular import tracking Set after processing its imports. + circular.delete(injectorDef); + } + if (def.providers) { + recursivelyProcessProviders(records, def.providers, true); + } + } else if (isInjectorDefTypeWithProviders(injectorDef)) { + assertIsInjectorDefType(injectorDef.ngModule); + // First process the InjectableDefType, and then the providers. + processInjectorDefHelper(injectorDef.ngModule, records, injectorDefSet, circular); + if (injectorDef.providers) { + recursivelyProcessProviders(records, injectorDef.providers, true); + } + } else { + // Fail on purpose. + assertIsInjectorDefType(injectorDef); + } +} + +function injectableDefRecord(token: InjectableDefType| InjectableDefToken): Record { + if (!isInjectableDefType(token)) { + throw new Error(`No ngInjectableDef found on ${token}.`); + } + return { + deps: [], + fn: token.ngInjectableDef.factory, + useNew: false, + value: EMPTY, + }; +} + +function providerToRecord(provider: InjectorDefProvider): Record { + const token = resolveForwardRef(provider); + let value: any = EMPTY; + let factory = IDENT; + if (isInjectableDefType(provider) || isInjectableDefToken(provider)) { + return injectableDefRecord(provider); + } else if (isTypeProvider(provider)) { + throw staticError(`Missing ngInjectableDef on type`, provider); + } else if (isValueProvider(provider)) { + value = provider.useValue; + } else if (isExistingProvider(provider)) { + factory = () => inject(provider.useExisting); + } else if (isFactoryProvider(provider)) { + factory = () => provider.useFactory(...injectArgs(provider.deps || [])); + } else { + const useClass = isClassProvider(provider) ? provider.useClass : provider.provide; + if (isStaticClassProvider(provider)) { + factory = () => new (useClass)(...injectArgs(provider.deps)); + } else if (!isInjectableDefType(useClass)) { + throw staticError(`Missing ngInjectableDef on type`, useClass); + } else { + return injectableDefRecord(useClass); + } + } + return { + value, + deps: [], + useNew: false, + fn: factory, + }; +} + +function deepForEach(input: (T | any[])[], fn: (value: T) => void): void { + input.forEach(value => Array.isArray(value) ? deepForEach(value, fn) : fn(value)); +} + +function isValueProvider(value: any): value is ValueProvider { + return USE_VALUE in value; +} + +function isExistingProvider(value: any): value is ExistingProvider { + return (value as ExistingProvider).useExisting != null; +} + +function isFactoryProvider(value: any): value is FactoryProvider { + return (value as FactoryProvider).useFactory != null; +} + +function isStaticClassProvider(value: any): value is StaticClassProvider { + return (value as StaticClassProvider).useClass != null && + (value as StaticClassProvider).deps != null; +} + +function isClassProvider(value: any): value is ClassProvider|StaticClassProvider { + return (value as ClassProvider | StaticClassProvider).useClass != null; +} + +function isTypeProvider(value: any): value is TypeProvider { + return typeof value === 'function'; +} + +function isInjectableDefType(value: any): value is InjectableDefType { + return typeof value === 'function' && (value as InjectableDefType).ngInjectableDef != null; +} + +function isInjectableDefToken(value: any): value is InjectableDefToken { + return value instanceof InjectionToken && + (value as InjectableDefToken).ngInjectableDef != null; +} + +function isInjectorDefType(value: any): value is InjectorDefType { + return (value as InjectorDefType).ngInjectorDef != null; +} + +function isInjectorDefTypeWithProviders(value: any): value is InjectorDefTypeWithProviders { + return (value as InjectorDefTypeWithProviders).ngModule != null; +} + +function assertIsInjectorDefType(value: any): void { + if (!isInjectorDefType(value)) { + throw new Error(`Expected an ngInjectorDef property on ${value}`); + } +} + +function hasOnDestroy(value: any): value is OnDestroy { + return typeof value === 'object' && value != null && typeof value.ngOnDestroy === 'function'; +} diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index c48376605e7699..78ec9f8b1097c4 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider} from '../di'; +import {InjectorDef, InjectorDefType} from '../di/defs'; +import {convertInjectableProviderToFactory} from '../di/injectable'; +import {Provider} from '../di/provider'; import {Type} from '../type'; import {TypeDecorator, makeDecorator} from '../util/decorators'; @@ -190,5 +192,23 @@ export interface NgModule { * @stable * @Annotation */ -export const NgModule: NgModuleDecorator = - makeDecorator('NgModule', (ngModule: NgModule) => ngModule); +export const NgModule: NgModuleDecorator = makeDecorator( + 'NgModule', (ngModule: NgModule) => ngModule, undefined, undefined, + (moduleType: InjectorDefType, metadata: NgModule) => { + let imports = (metadata && metadata.imports) || []; + if (metadata && metadata.exports) { + imports = [...imports, metadata.exports]; + } + + moduleType.ngInjectorDef = defineInjector({ + factory: () => convertInjectableProviderToFactory(moduleType, {useClass: moduleType}), + providers: metadata && metadata.providers, + imports: imports, + }); + }); + +export function defineInjector( + options: {factory: () => any, providers: any[] | undefined, imports: any[] | undefined}): + InjectorDef { + return options; +} diff --git a/packages/core/test/di/definition_injector_spec.ts b/packages/core/test/di/definition_injector_spec.ts new file mode 100644 index 00000000000000..ee3991bb7c99db --- /dev/null +++ b/packages/core/test/di/definition_injector_spec.ts @@ -0,0 +1,205 @@ +/** + * @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 {InjectionToken} from '../../src/di/injection_token'; +import {Injector, StaticInjector, inject} from '../../src/di/injector'; + +describe('InjectorDef-based StaticInjector', () => { + class CircularA { + static ngInjectableDef = { + scope: undefined, + factory: () => inject(CircularB), + }; + } + + class CircularB { + static ngInjectableDef = { + scope: undefined, + factory: () => inject(CircularA), + }; + } + + class Service { + static ngInjectableDef = { + scope: undefined, + factory: () => new Service(), + }; + } + + class StaticService { + constructor(readonly dep: Service) {} + } + + const SERVICE_TOKEN = new InjectionToken('SERVICE_TOKEN'); + + const STATIC_TOKEN = new InjectionToken('STATIC_TOKEN'); + + class ServiceWithDep { + constructor(readonly service: Service) {} + + static ngInjectableDef = { + scope: undefined, + factory: () => new ServiceWithDep(inject(Service)), + }; + } + + class ServiceTwo { + static ngInjectableDef = { + scope: undefined, + factory: () => new ServiceTwo(), + }; + } + + let deepServiceDestroyed = false; + class DeepService { + static ngInjectableDef = { + scope: undefined, + factory: () => new DeepService(), + }; + + ngOnDestroy(): void { deepServiceDestroyed = true; } + } + + let eagerServiceCreated: boolean = false; + class EagerService { + static ngInjectableDef = { + scope: undefined, + factory: () => new EagerService(), + }; + + constructor() { eagerServiceCreated = true; } + } + + let deepModuleCreated: boolean = false; + class DeepModule { + constructor(eagerService: EagerService) { deepModuleCreated = true; } + + static ngInjectorDef = { + factory: () => new DeepModule(inject(EagerService)), + imports: undefined, + providers: [ + EagerService, + {provide: DeepService, useFactory: () => { throw new Error('Not overridden!'); }}, + ], + }; + + static safe() { + return { + ngModule: DeepModule, + providers: [{provide: DeepService}], + }; + } + } + + class IntermediateModule { + static ngInjectorDef = { + factory: () => new IntermediateModule(), + imports: [DeepModule.safe()], + providers: [], + }; + } + + class Module { + static ngInjectorDef = { + factory: () => new Module(), + imports: [IntermediateModule], + providers: [ + ServiceWithDep, + Service, + {provide: SERVICE_TOKEN, useExisting: Service}, + CircularA, + CircularB, + {provide: STATIC_TOKEN, useClass: StaticService, deps: [Service]}, + ], + }; + } + + class OtherModule { + static ngInjectorDef = { + factory: () => new OtherModule(), + imports: undefined, + providers: [], + }; + } + + class ScopedService { + static ngInjectableDef = { + scope: Module, + factory: () => new ScopedService(), + }; + } + + class WrongScopeService { + static ngInjectableDef = { + scope: OtherModule, + factory: () => new WrongScopeService(), + }; + } + + let injector: Injector; + + beforeEach(() => { + deepModuleCreated = eagerServiceCreated = deepServiceDestroyed = false; + injector = Injector.create({definitions: [Module]}); + }); + + it('injects a simple class', () => { + const instance = injector.get(Service); + expect(instance instanceof Service).toBeTruthy(); + expect(injector.get(Service)).toBe(instance); + }); + it('throws an error when a token is not found', + () => { expect(() => injector.get(ServiceTwo)).toThrow(); }); + it('returns the default value if a provider isn\'t present', + () => { expect(injector.get(ServiceTwo, null)).toBeNull(); }); + it('injects a service with dependencies', () => { + const instance = injector.get(ServiceWithDep); + expect(instance instanceof ServiceWithDep); + expect(instance.service).toBe(injector.get(Service)); + }); + it('injects a token with useExisting', () => { + const instance = injector.get(SERVICE_TOKEN); + expect(instance).toBe(injector.get(Service)); + }); + it('instantiates a class with useClass and deps', () => { + const instance = injector.get(STATIC_TOKEN); + expect(instance instanceof StaticService).toBeTruthy(); + expect(instance.dep).toBe(injector.get(Service)); + }); + it('throws an error on circular deps', + () => { expect(() => injector.get(CircularA)).toThrow(); }); + it('allows injecting itself', () => { expect(injector.get(Injector)).toBe(injector); }); + it('allows injecting a deeply imported service', + () => { expect(injector.get(DeepService) instanceof DeepService).toBeTruthy(); }); + it('allows injecting a scoped service', () => { + const instance = injector.get(ScopedService); + expect(instance instanceof ScopedService).toBeTruthy(); + expect(instance).toBe(injector.get(ScopedService)); + }); + it('does not create instances of a service not in scope', + () => { expect(injector.get(WrongScopeService, null)).toBeNull(); }); + it('eagerly instantiates the injectordef types', () => { + expect(deepModuleCreated).toBe(true, 'DeepModule not instantiated'); + expect(eagerServiceCreated).toBe(true, 'EagerSerivce not instantiated'); + }); + it('calls ngOnDestroy on services when destroyed', () => { + injector.get(DeepService); + (injector as StaticInjector).destroy(); + expect(deepServiceDestroyed).toBe(true, 'DeepService not destroyed'); + }); + it('does not allow injection after destroy', () => { + (injector as StaticInjector).destroy(); + expect(() => injector.get(DeepService)) + .toThrowError('StaticInjector: injector has already been destroyed.'); + }); + it('does not allow double destroy', () => { + (injector as StaticInjector).destroy(); + expect(() => (injector as StaticInjector).destroy()) + .toThrowError('StaticInjector: injector has already been destroyed.'); + }); +}); \ No newline at end of file diff --git a/packages/platform-browser-dynamic/src/compiler_reflector.ts b/packages/platform-browser-dynamic/src/compiler_reflector.ts index 14652fc056f7bc..a7744a2a6867a4 100644 --- a/packages/platform-browser-dynamic/src/compiler_reflector.ts +++ b/packages/platform-browser-dynamic/src/compiler_reflector.ts @@ -37,6 +37,9 @@ export class JitReflector implements CompileReflector { annotations(typeOrFunc: /*Type*/ any): any[] { return this.reflectionCapabilities.annotations(typeOrFunc); } + shallowAnnotations(typeOrFunc: /*Type*/ any): any[] { + throw new Error('Not supported in JIT mode'); + } propMetadata(typeOrFunc: /*Type*/ any): {[key: string]: any[]} { return this.reflectionCapabilities.propMetadata(typeOrFunc); } diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 9a4d24b8702710..4f76457171c574 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -1,6 +1,7 @@ """Re-export of some bazel rules with repository-wide defaults.""" load("@build_bazel_rules_typescript//:defs.bzl", _ts_library = "ts_library") load("//packages/bazel:index.bzl", _ng_module = "ng_module", _ng_package = "ng_package") +load("//packages/bazel/src:ng_module.bzl", _ivy_ng_module = "ivy_ng_module") DEFAULT_TSCONFIG = "//packages:tsconfig-build.json" @@ -30,3 +31,8 @@ def ng_package(name, readme_md = None, license_banner = None, stamp_data = None, license_banner = license_banner, stamp_data = stamp_data, **kwargs) + +def ivy_ng_module(name, tsconfig = None, **kwargs): + if not tsconfig: + tsconfig = DEFAULT_TSCONFIG + _ivy_ng_module(name = name, tsconfig = tsconfig, **kwargs) diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 835512649c3620..ad9a933e72bd87 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -347,7 +347,7 @@ export declare class DefaultIterableDiffer implements IterableDiffer, Iter } /** @experimental */ -export declare function defineInjectable(opts: Injectable): Injectable; +export declare function defineInjectable(opts: Injectable): InjectableDef; /** @experimental */ export declare function destroyPlatform(): void; @@ -469,13 +469,19 @@ export interface InjectableDecorator { } /** @experimental */ -export declare type InjectableProvider = ValueSansProvider | ExistingSansProvider | StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider; +export interface InjectableDef { + factory: () => T; + scope?: any; +} /** @experimental */ -export interface InjectableType extends Type { - ngInjectableDef?: Injectable; +export interface InjectableDefType extends Type { + ngInjectableDef: InjectableDef; } +/** @experimental */ +export declare type InjectableProvider = ValueSansProvider | ExistingSansProvider | StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider; + /** @stable */ export interface InjectDecorator { /** @stable */ (token: any): any; @@ -492,7 +498,7 @@ export declare const enum InjectFlags { /** @stable */ export declare class InjectionToken { protected _desc: string; - readonly ngInjectableDef: Injectable | undefined; + readonly ngInjectableDef: InjectableDef | undefined; constructor(_desc: string, options?: { scope: Type; factory: () => T; @@ -508,12 +514,35 @@ export declare abstract class Injector { static THROW_IF_NOT_FOUND: Object; /** @deprecated */ static create(providers: StaticProvider[], parent?: Injector): Injector; static create(options: { - providers: StaticProvider[]; + definitions: any[]; + parent?: Injector; + name?: string; + }): Injector; + static create(options: { + providers?: StaticProvider[]; parent?: Injector; name?: string; }): Injector; } +/** @experimental */ +export interface InjectorDef { + factory: () => T; + imports?: (InjectorDefType | InjectorDefTypeWithProviders)[]; + providers?: (Type | ValueProvider | ExistingProvider | FactoryProvider | ConstructorProvider | StaticClassProvider | ClassProvider | any[])[]; +} + +/** @experimental */ +export interface InjectorDefType extends Type { + ngInjectorDef: InjectorDef; +} + +/** @experimental */ +export interface InjectorDefTypeWithProviders { + ngModule: InjectorDefType; + providers?: (Type | ValueProvider | ExistingProvider | FactoryProvider | ConstructorProvider | StaticClassProvider | ClassProvider | any[])[]; +} + /** @stable */ export declare const Input: InputDecorator;