diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 2caa8b7270..9b651c3449 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 } } @@ -11,7 +11,7 @@ "hello_world__closure": { "master": { "uncompressed": { - "bundle": 105779 + "bundle": 106550 } } }, diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index ac1b1de544..6540307f59 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 @@ -92,6 +98,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() @@ -283,7 +290,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 @@ -292,16 +299,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) @@ -325,6 +335,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"]), @@ -363,24 +376,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. +internal_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 0000000000..8cbf1b54a4 --- /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 0000000000..e1eb0c205e --- /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 0000000000..1d89f70765 --- /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 0000000000..89c0c09c2b --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts @@ -0,0 +1,84 @@ +/** + * @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, createInjector, forwardRef} from '@angular/core'; +import {AOT_TOKEN, AotModule, AotService} from 'app_built/src/module'; + +describe('Ivy NgModule', () => { + describe('AOT', () => { + let injector: Injector; + + beforeEach(() => { injector = createInjector(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({providedIn: null}) + class Service { + } + + @NgModule({ + providers: [Service], + }) + class JitModule { + } + + @NgModule({ + imports: [JitModule], + }) + class JitAppModule { + } + + it('works', () => { createInjector(JitAppModule); }); + + it('throws an error on circular module dependencies', () => { + @NgModule({ + imports: [forwardRef(() => BModule)], + }) + class AModule { + } + + @NgModule({ + imports: [AModule], + }) + class BModule { + } + + expect(() => createInjector(AModule)) + .toThrowError('Circular dependency: type 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 = createInjector(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 6134d62c84..dbdf027d51 100644 --- a/packages/compiler-cli/src/transformers/lower_expressions.ts +++ b/packages/compiler-cli/src/transformers/lower_expressions.ts @@ -208,7 +208,7 @@ interface MetadataAndLoweringRequests { requests: RequestLocationMap; } -function shouldLower(node: ts.Node | undefined): boolean { +function isEligibleForLowering(node: ts.Node | undefined): boolean { if (node) { switch (node.kind) { case ts.SyntaxKind.SourceFile: @@ -226,7 +226,7 @@ function shouldLower(node: ts.Node | undefined): boolean { // Avoid lowering expressions already in an exported variable declaration return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) == 0; } - return shouldLower(node.parent); + return isEligibleForLowering(node.parent); } return true; } @@ -251,11 +251,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 { @@ -312,17 +315,46 @@ export class LowerMetadataTransform implements RequestsMap, MetadataTransformer return false; }; + const hasLowerableParentCache = new Map(); + + const shouldBeLowered = (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) && + isEligibleForLowering(node)) { + lowerable = true; + } else if ( + isLiteralFieldNamed(node, this.lowerableFieldNames) && isEligibleForLowering(node) && + !isExportedSymbol(node) && !isExportedPropertyAccess(node)) { + lowerable = true; + } + return lowerable; + }; + + const hasLowerableParent = (node: ts.Node | undefined): boolean => { + if (node === undefined) { + return false; + } + if (!hasLowerableParentCache.has(node)) { + hasLowerableParentCache.set( + node, shouldBeLowered(node.parent) || hasLowerableParent(node.parent)); + } + return hasLowerableParentCache.get(node) !; + }; + + const isLowerable = (node: ts.Node | undefined): boolean => { + if (node === undefined) { + return false; + } + return shouldBeLowered(node) && !hasLowerableParent(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 54a52b9f47..686526a1bb 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, NgAnalyzedFileWithInjectables, 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, StaticSymbol, 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,9 +22,11 @@ import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './l import {MetadataCache, MetadataTransformer} from './metadata_cache'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; +import {StripDecoratorsMetadataTransformer, getDecoratorStripTransformerFactory} from './r3_strip_decorators'; import {getAngularClassTransformerFactory} from './r3_transform'; import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util'; + // Closure compiler transforms the form `Service.ngInjectableDef = X` into // `Service$ngInjectableDef = X`. To prevent this transformation, such assignments need to be // annotated with @nocollapse. Unfortunately, a bug in Typescript where comments aren't propagated @@ -62,6 +64,25 @@ const R3_NOCOLLAPSE_DEFS = '$1\/** @nocollapse *\/ $2'; */ const MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT = 20; + +/** + * Fields to lower within metadata in render2 mode. + */ +const LOWER_FIELDS = ['useValue', 'useFactory', 'data']; + +/** + * Fields to lower within metadata in render3 mode. + */ +const R3_LOWER_FIELDS = [...LOWER_FIELDS, 'providers', 'imports', 'exports']; + +const R3_REIFIED_DECORATORS = [ + 'Component', + 'Directive', + 'Injectable', + 'NgModule', + 'Pipe', +]; + const emptyModules: NgAnalyzedModules = { ngModules: [], ngModuleByPipeOrDirective: new Map(), @@ -96,6 +117,7 @@ class AngularCompilerProgram implements Program { private _structuralDiagnostics: Diagnostic[]|undefined; private _programWithStubs: ts.Program|undefined; private _optionsDiagnostics: Diagnostic[] = []; + private _reifiedDecorators: Set; constructor( rootNames: ReadonlyArray, private options: CompilerOptions, @@ -129,7 +151,9 @@ class AngularCompilerProgram implements Program { this.host = bundleHost; } } - this.loweringMetadataTransform = new LowerMetadataTransform(); + + this.loweringMetadataTransform = + new LowerMetadataTransform(options.enableIvy ? R3_LOWER_FIELDS : LOWER_FIELDS); this.metadataCache = this.createMetadataCache([this.loweringMetadataTransform]); } @@ -269,7 +293,11 @@ class AngularCompilerProgram implements Program { 0) { return {emitSkipped: true, diagnostics: [], emittedFiles: []}; } - const modules = this.compiler.emitAllPartialModules(this.analyzedModules); + + // analyzedModules and analyzedInjectables are created together. If one exists, so does the + // other. + const modules = + this.compiler.emitAllPartialModules(this.analyzedModules, this._analyzedInjectables !); const writeTsFile: ts.WriteFileCallback = (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { @@ -285,7 +313,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 */ this.reifiedDecorators, customTransformers); const emitResult = emitCallback({ program: this.tsProgram, @@ -356,8 +385,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. @@ -512,8 +541,18 @@ class AngularCompilerProgram implements Program { return this._tsProgram !; } + private get reifiedDecorators(): Set { + if (!this._reifiedDecorators) { + const reflector = this.compiler.reflector; + this._reifiedDecorators = new Set( + R3_REIFIED_DECORATORS.map(name => reflector.findDeclaration('@angular/core', name))); + } + return this._reifiedDecorators; + } + private calculateTransforms( genFiles: Map|undefined, partialModules: PartialModule[]|undefined, + stripDecorators: Set|undefined, customTransformers?: CustomTransformers): ts.CustomTransformers { const beforeTs: Array> = []; const metadataTransforms: MetadataTransformer[] = []; @@ -521,6 +560,7 @@ class AngularCompilerProgram implements Program { beforeTs.push(getInlineResourcesTransformFactory(this.tsProgram, this.hostAdapter)); metadataTransforms.push(new InlineResourcesMetadataTransformer(this.hostAdapter)); } + if (!this.options.disableExpressionLowering) { beforeTs.push( getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram)); @@ -536,6 +576,14 @@ class AngularCompilerProgram implements Program { // the partial module transforms. metadataTransforms.push(new PartialModuleMetadataTransformer(partialModules)); } + + if (stripDecorators) { + beforeTs.push(getDecoratorStripTransformerFactory( + stripDecorators, this.compiler.reflector, this.getTsProgram().getTypeChecker())); + metadataTransforms.push( + new StripDecoratorsMetadataTransformer(stripDecorators, this.compiler.reflector)); + } + if (customTransformers && customTransformers.beforeTs) { beforeTs.push(...customTransformers.beforeTs); } @@ -832,6 +880,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 0000000000..c5feabee50 --- /dev/null +++ b/packages/compiler-cli/src/transformers/r3_strip_decorators.ts @@ -0,0 +1,161 @@ +/** + * @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 {StaticReflector, StaticSymbol} from '@angular/compiler'; +import * as ts from 'typescript'; + +import {MetadataValue, isClassMetadata, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicCallExpression} from '../metadata'; + +import {MetadataTransformer, ValueTransform} from './metadata_cache'; + +export type Transformer = (sourceFile: ts.SourceFile) => ts.SourceFile; +export type TransformerFactory = (context: ts.TransformationContext) => Transformer; + +export function getDecoratorStripTransformerFactory( + coreDecorators: Set, reflector: StaticReflector, + checker: ts.TypeChecker): TransformerFactory { + return function(context: ts.TransformationContext) { + return function(sourceFile: ts.SourceFile): ts.SourceFile { + const stripDecoratorsFromClassDeclaration = + (node: ts.ClassDeclaration): ts.ClassDeclaration => { + if (node.decorators === undefined) { + return node; + } + const decorators = node.decorators.filter(decorator => { + const callExpr = decorator.expression; + if (ts.isCallExpression(callExpr)) { + const id = callExpr.expression; + if (ts.isIdentifier(id)) { + const symbol = resolveToStaticSymbol(id, sourceFile.fileName, reflector, checker); + return symbol && coreDecorators.has(symbol); + } + } + return true; + }); + if (decorators.length !== node.decorators.length) { + return ts.updateClassDeclaration( + node, decorators, node.modifiers, node.name, node.typeParameters, + node.heritageClauses || [], node.members, ); + } + return node; + }; + + const stripDecoratorPropertyAssignment = (node: ts.ClassDeclaration): ts.ClassDeclaration => { + return ts.visitEachChild(node, member => { + if (!ts.isPropertyDeclaration(member) || !isDecoratorAssignment(member) || + !member.initializer || !ts.isArrayLiteralExpression(member.initializer)) { + return member; + } + + const newInitializer = ts.visitEachChild(member.initializer, decorator => { + if (!ts.isObjectLiteralExpression(decorator)) { + return decorator; + } + const type = lookupProperty(decorator, 'type'); + if (!type || !ts.isIdentifier(type)) { + return decorator; + } + const symbol = resolveToStaticSymbol(type, sourceFile.fileName, reflector, checker); + if (!symbol || !coreDecorators.has(symbol)) { + return decorator; + } + return undefined; + }, context); + + if (newInitializer === member.initializer) { + return member; + } else if (newInitializer.elements.length === 0) { + return undefined; + } else { + return ts.updateProperty( + member, member.decorators, member.modifiers, member.name, member.questionToken, + member.type, newInitializer); + } + }, context); + }; + + return ts.visitEachChild(sourceFile, stmt => { + if (ts.isClassDeclaration(stmt)) { + let decl = stmt; + if (stmt.decorators) { + decl = stripDecoratorsFromClassDeclaration(stmt); + } + return stripDecoratorPropertyAssignment(decl); + } + return stmt; + }, context); + }; + }; +} + +function isDecoratorAssignment(member: ts.ClassElement): boolean { + if (!ts.isPropertyDeclaration(member)) { + return false; + } + if (!member.modifiers || + !member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) { + return false; + } + if (!ts.isIdentifier(member.name) || member.name.text !== 'decorators') { + return false; + } + if (!member.initializer || !ts.isArrayLiteralExpression(member.initializer)) { + return false; + } + return true; +} + +function lookupProperty(expr: ts.ObjectLiteralExpression, prop: string): ts.Expression|undefined { + const decl = expr.properties.find( + elem => !!elem.name && ts.isIdentifier(elem.name) && elem.name.text === prop); + if (decl === undefined || !ts.isPropertyAssignment(decl)) { + return undefined; + } + return decl.initializer; +} + +function resolveToStaticSymbol( + id: ts.Identifier, containingFile: string, reflector: StaticReflector, + checker: ts.TypeChecker): StaticSymbol|null { + const res = checker.getSymbolAtLocation(id); + if (!res || !res.declarations || res.declarations.length === 0) { + return null; + } + const decl = res.declarations[0]; + if (!ts.isImportSpecifier(decl)) { + return null; + } + const moduleSpecifier = decl.parent !.parent !.parent !.moduleSpecifier; + if (!ts.isStringLiteral(moduleSpecifier)) { + return null; + } + return reflector.tryFindDeclaration(moduleSpecifier.text, id.text, containingFile); +} + +export class StripDecoratorsMetadataTransformer implements MetadataTransformer { + constructor(private coreDecorators: Set, private reflector: StaticReflector) {} + + start(sourceFile: ts.SourceFile): ValueTransform|undefined { + return (value: MetadataValue, node: ts.Node): MetadataValue => { + if (isClassMetadata(value) && ts.isClassDeclaration(node) && value.decorators) { + value.decorators = value.decorators.filter(d => { + if (isMetadataSymbolicCallExpression(d) && + isMetadataImportedSymbolReferenceExpression(d.expression)) { + const declaration = this.reflector.tryFindDeclaration( + d.expression.module, d.expression.name, sourceFile.fileName); + if (declaration && this.coreDecorators.has(declaration)) { + return false; + } + } + return true; + }); + } + return value; + }; + } +} diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 260439207f..fe8be7d934 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -2205,4 +2205,120 @@ describe('ngc transformer command-line', () => { expect(source).toMatch(/new Service\(i0\.inject\(exports\.TOKEN\)\);/); }); }); + + 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] });'); + }); + + it('strips decorator in ivy mode', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["service.ts"], + "angularCompilerOptions": { + "enableIvy": true + } + }`); + write('service.ts', ` + import {Injectable, Self} from '@angular/core'; + + @Injectable() + export class ServiceA {} + + @Injectable() + @Self() + export class ServiceB {} + `); + + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + expect(exitCode).toEqual(0); + + const modulePath = path.resolve(outDir, 'service.js'); + const moduleSource = fs.readFileSync(modulePath, 'utf8'); + expect(moduleSource).not.toMatch(/ServiceA\.decorators =/); + expect(moduleSource).toMatch(/ServiceB\.decorators =/); + expect(moduleSource).toMatch(/type: Self/); + expect(moduleSource).not.toMatch(/type: Injectable/); + }); + + it('rewrites Injector to INJECTOR in Ivy factory functions ', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["service.ts"], + "angularCompilerOptions": { + "enableIvy": true + } + }`); + + write('service.ts', ` + import {Injectable, Injector} from '@angular/core'; + + @Injectable() + export class Service { + constructor(private injector: Injector) {} + } + `); + + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + expect(exitCode).toEqual(0); + + const modulePath = path.resolve(outDir, 'service.js'); + const moduleSource = fs.readFileSync(modulePath, 'utf8'); + expect(moduleSource).not.toMatch(/inject\(i0\.Injector/); + expect(moduleSource).toMatch(/inject\(i0\.INJECTOR/); + }); + }); }); diff --git a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts index 4c429c2008..da873f8f0b 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 54491bf563..8c11916130 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 {compileNgModule as compileIvyModule} from '../render3/r3_module_compiler'; import {compilePipe as compileIvyPipe} from '../render3/r3_pipe_compiler'; import {OutputMode} from '../render3/r3_types'; import {compileComponent as compileIvyComponent, compileDirective as compileIvyDirective} from '../render3/r3_view_compiler'; @@ -57,7 +58,7 @@ export class AotCompiler { constructor( private _config: CompilerConfig, private _options: AotCompilerOptions, - private _host: AotCompilerHost, private _reflector: StaticReflector, + private _host: AotCompilerHost, readonly reflector: StaticReflector, private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler, @@ -283,7 +284,7 @@ export class AotCompiler { private _externalIdentifierReferences(references: o.ExternalReference[]): StaticSymbol[] { const result: StaticSymbol[] = []; for (let reference of references) { - const token = createTokenForExternalReference(this._reflector, reference); + const token = createTokenForExternalReference(this.reflector, reference); if (token.identifier) { result.push(token.identifier.reference); } @@ -332,28 +333,49 @@ 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 = (fileName: string): OutputContext => { + if (!contextMap.has(fileName)) { + contextMap.set(fileName, this._createOutputContext(fileName)); + } + return contextMap.get(fileName) !; + }; + + 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 errors: ParseError[] = []; - const context = this._createOutputContext(fileName); const hostBindingParser = new BindingParser( this._templateParser.expressionParser, DEFAULT_INTERPOLATION_CONFIG, null !, [], errors); - // Process all components and directives directives.forEach(directiveType => { const directiveMetadata = this._metadataResolver.getDirectiveMetadata(directiveType); @@ -366,28 +388,22 @@ export class AotCompiler { const {template: parsedTemplate, pipes: parsedPipes} = this._parseTemplate(directiveMetadata, module, module.transitiveModule.directives); compileIvyComponent( - context, directiveMetadata, parsedPipes, parsedTemplate, this._reflector, + context, directiveMetadata, parsedPipes, parsedTemplate, this.reflector, hostBindingParser, OutputMode.PartialClass); } else { compileIvyDirective( - context, directiveMetadata, this._reflector, hostBindingParser, - OutputMode.PartialClass); + context, directiveMetadata, this.reflector, hostBindingParser, OutputMode.PartialClass); } }); pipes.forEach(pipeType => { const pipeMetadata = this._metadataResolver.getPipeMetadata(pipeType); if (pipeMetadata) { - compileIvyPipe(context, pipeMetadata, this._reflector, OutputMode.PartialClass); + compileIvyPipe(context, pipeMetadata, this.reflector, OutputMode.PartialClass); } }); 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[] { @@ -531,14 +547,14 @@ export class AotCompiler { if (this._options.locale) { const normalizedLocale = this._options.locale.replace(/_/g, '-'); providers.push({ - token: createTokenForExternalReference(this._reflector, Identifiers.LOCALE_ID), + token: createTokenForExternalReference(this.reflector, Identifiers.LOCALE_ID), useValue: normalizedLocale, }); } if (this._options.i18nFormat) { providers.push({ - token: createTokenForExternalReference(this._reflector, Identifiers.TRANSLATIONS_FORMAT), + token: createTokenForExternalReference(this.reflector, Identifiers.TRANSLATIONS_FORMAT), useValue: this._options.i18nFormat }); } @@ -682,12 +698,12 @@ export class AotCompiler { listLazyRoutes(entryRoute?: string, analyzedModules?: NgAnalyzedModules): LazyRoute[] { const self = this; if (entryRoute) { - const symbol = parseLazyRoute(entryRoute, this._reflector).referencedModule; + const symbol = parseLazyRoute(entryRoute, this.reflector).referencedModule; return visitLazyRoute(symbol); } else if (analyzedModules) { const allLazyRoutes: LazyRoute[] = []; for (const ngModule of analyzedModules.ngModules) { - const lazyRoutes = listLazyRoutes(ngModule, this._reflector); + const lazyRoutes = listLazyRoutes(ngModule, this.reflector); for (const lazyRoute of lazyRoutes) { allLazyRoutes.push(lazyRoute); } @@ -707,7 +723,7 @@ export class AotCompiler { } seenRoutes.add(symbol); const lazyRoutes = listLazyRoutes( - self._metadataResolver.getNgModuleMetadata(symbol, true) !, self._reflector); + self._metadataResolver.getNgModuleMetadata(symbol, true) !, self.reflector); for (const lazyRoute of lazyRoutes) { allLazyRoutes.push(lazyRoute); visitLazyRoute(lazyRoute.referencedModule, seenRoutes, allLazyRoutes); @@ -748,6 +764,7 @@ export interface NgAnalyzedModules { export interface NgAnalyzedFileWithInjectables { fileName: string; injectables: CompileInjectableMetadata[]; + shallowModules: CompileShallowModuleMetadata[]; } export interface NgAnalyzedFile { @@ -868,6 +885,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); @@ -883,11 +901,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 a1106d6cbb..acdb12021a 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -90,7 +90,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 f2a9fa75ab..b8d98cee40 100644 --- a/packages/compiler/src/aot/compiler_options.ts +++ b/packages/compiler/src/aot/compiler_options.ts @@ -18,4 +18,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 bc4380d6f8..2254e0f0d0 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(); @@ -106,8 +107,9 @@ export class StaticReflector implements CompileReflector { this.symbolResolver.getSymbolByModule(moduleUrl, name, containingFile)); } - tryFindDeclaration(moduleUrl: string, name: string): StaticSymbol { - return this.symbolResolver.ignoreErrorsFor(() => this.findDeclaration(moduleUrl, name)); + tryFindDeclaration(moduleUrl: string, name: string, containingFile?: string): StaticSymbol { + return this.symbolResolver.ignoreErrorsFor( + () => this.findDeclaration(moduleUrl, name, containingFile)); } findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol { @@ -135,7 +137,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 +162,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 +185,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 +430,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 +791,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 d915bc8405..6571596514 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 9700634fc5..c6ffc9fed4 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/identifiers.ts b/packages/compiler/src/identifiers.ts index 7e9c28076d..9c85cae1e9 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -62,6 +62,7 @@ export class Identifiers { }; static inject: o.ExternalReference = {name: 'inject', moduleName: CORE}; + static INJECTOR: o.ExternalReference = {name: 'INJECTOR', moduleName: CORE}; static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE}; static defineInjectable: o.ExternalReference = {name: 'defineInjectable', moduleName: CORE}; static ViewEncapsulation: o.ExternalReference = { @@ -88,7 +89,6 @@ export class Identifiers { static inlineInterpolate: o.ExternalReference = { name: 'ɵinlineInterpolate', moduleName: CORE, - }; static interpolate: o.ExternalReference = {name: 'ɵinterpolate', moduleName: CORE}; static EMPTY_ARRAY: o.ExternalReference = {name: 'ɵEMPTY_ARRAY', moduleName: CORE}; diff --git a/packages/compiler/src/injectable_compiler.ts b/packages/compiler/src/injectable_compiler.ts index 900b566bbc..263a48ef9d 100644 --- a/packages/compiler/src/injectable_compiler.ts +++ b/packages/compiler/src/injectable_compiler.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {StaticSymbol} from './aot/static_symbol'; import {CompileInjectableMetadata, CompileNgModuleMetadata, CompileProviderMetadata, identifierName} from './compile_metadata'; import {CompileReflector} from './compile_reflector'; import {InjectFlags, NodeFlags} from './core'; @@ -29,7 +30,10 @@ function mapEntry(key: string, value: o.Expression): MapEntry { } export class InjectableCompiler { - constructor(private reflector: CompileReflector) {} + private tokenInjector: StaticSymbol; + constructor(private reflector: CompileReflector, private alwaysGenerateDef: boolean) { + this.tokenInjector = reflector.resolveExternalReference(Identifiers.Injector); + } private depsArray(deps: any[], ctx: OutputContext): o.Expression[] { return deps.map(dep => { @@ -55,7 +59,16 @@ export class InjectableCompiler { } } } - const tokenExpr = typeof token === 'string' ? o.literal(token) : ctx.importExpr(token); + + let tokenExpr: o.Expression; + if (typeof token === 'string') { + tokenExpr = o.literal(token); + } else if (token === this.tokenInjector && this.alwaysGenerateDef) { + tokenExpr = o.importExpr(Identifiers.INJECTOR); + } else { + tokenExpr = ctx.importExpr(token); + } + if (flags !== InjectFlags.Default || defaultValue !== undefined) { args = [tokenExpr, o.literal(defaultValue), o.literal(flags)]; } else { @@ -65,7 +78,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)]); @@ -90,8 +103,10 @@ export class InjectableCompiler { injectableDef(injectable: CompileInjectableMetadata, ctx: OutputContext): o.Expression { let providedIn: o.Expression = o.NULL_EXPR; - if (injectable.providedIn) { - if (typeof injectable.providedIn === 'string') { + if (injectable.providedIn !== undefined) { + if (injectable.providedIn === null) { + providedIn = o.NULL_EXPR; + } else if (typeof injectable.providedIn === 'string') { providedIn = o.literal(injectable.providedIn); } else { providedIn = ctx.importExpr(injectable.providedIn); @@ -106,7 +121,7 @@ export class InjectableCompiler { } compile(injectable: CompileInjectableMetadata, ctx: OutputContext): void { - if (injectable.providedIn) { + if (this.alwaysGenerateDef || injectable.providedIn !== undefined) { 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 c8746e01f4..db4c7decf0 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,26 @@ 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, + }; + + this._shallowModuleCache.set(moduleType, compileMeta); + return compileMeta; + } + getNgModuleMetadata( moduleType: any, throwIfNotFound = true, alreadyCollecting: Set|null = null): cpl.CompileNgModuleMetadata|null { diff --git a/packages/compiler/src/output/map_util.ts b/packages/compiler/src/output/map_util.ts new file mode 100644 index 0000000000..359702bfc6 --- /dev/null +++ b/packages/compiler/src/output/map_util.ts @@ -0,0 +1,29 @@ +/** + * @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 o from './output_ast'; + +export type MapEntry = { + key: string, + quoted: boolean, + value: o.Expression +}; + +export type MapLiteral = MapEntry[]; + +export function mapEntry(key: string, value: o.Expression): MapEntry { + return {key, value, quoted: false}; +} + +export function mapLiteral(obj: {[key: string]: o.Expression}): o.Expression { + return o.literalMap(Object.keys(obj).map(key => ({ + key, + quoted: false, + value: obj[key], + }))); +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index fdff4adff3..bcb95ba672 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -102,6 +102,11 @@ export class Identifiers { moduleName: CORE, }; + static defineInjector: o.ExternalReference = { + name: 'defineInjector', + moduleName: CORE, + }; + static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE}; static query: o.ExternalReference = {name: 'ɵQ', 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 0000000000..547ed95fcc --- /dev/null +++ b/packages/compiler/src/render3/r3_module_compiler.ts @@ -0,0 +1,60 @@ +/** + * @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 {mapLiteral} from '../output/map_util'; +import * as o from '../output/output_ast'; +import {OutputContext} from '../util'; + +import {Identifiers as R3} from './r3_identifiers'; + +const EMPTY_ARRAY = o.literalArr([]); + +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(`Internal error: Unsupported or unknown metadata: ${meta}`); + } +} + +export function compileNgModule( + 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 = mapLiteral({ + '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/di.ts b/packages/core/src/di.ts index 2ce39f9ae4..e866ec2c02 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -13,13 +13,13 @@ */ 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 {inject, InjectFlags, Injector} from './di/injector'; +export {Injectable, InjectableDecorator, InjectableProvider} from './di/injectable'; +export {inject, InjectFlags, INJECTOR, Injector} from './di/injector'; export {ReflectiveInjector} from './di/reflective_injector'; export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; +export {createInjector} from './di/r3_injector'; export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider'; export {ReflectiveKey} from './di/reflective_key'; export {InjectionToken} from './di/injection_token'; diff --git a/packages/core/src/di/defs.ts b/packages/core/src/di/defs.ts new file mode 100644 index 0000000000..97dd574f4f --- /dev/null +++ b/packages/core/src/di/defs.ts @@ -0,0 +1,140 @@ +/** + * @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, ClassSansProvider, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, StaticClassProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} 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 `providedIn` parameter specifies that the given type belongs to a particular + * `InjectorDef`, `NgModule`, or a special scope (e.g. `'root'`). A value of `null` indicates + * that the injectable does not belong to any scope. + * + * This type is typically generated by the Angular compiler, but can be hand-written if needed. + * + * @experimental + */ +export interface InjectableDef { + providedIn: InjectorType|'root'|'any'|null; + 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: (InjectorType|InjectorTypeWithProviders)[]; +} + +/** + * 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 InjectableType extends Type { ngInjectableDef: InjectableDef; } + +/** + * A type which has an `InjectorDef` static field. + * + * `InjectorDefTypes` can be used to configure a `StaticInjector`. + * + * @experimental + */ +export interface InjectorType 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 InjectorTypeWithProviders { + ngModule: InjectorType; + providers?: (Type|ValueProvider|ExistingProvider|FactoryProvider|ConstructorProvider| + StaticClassProvider|ClassProvider|any[])[]; +} + + +/** + * Construct an `InjectableDef` which defines how a token will be constructed by the DI system, and + * in which injectors (if any) it will be available. + * + * This should be assigned to a static `ngInjectableDef` field on a type, which will then be an + * `InjectableType`. + * + * Options: + * * `providedIn` determines which injectors will include the injectable, by either associating it + * with an `@NgModule` or other `InjectorType`, or by specifying that this injectable should be + * provided in the `'root'` injector, which will be the application-level injector in most apps. + * * `factory` gives the zero argument function which will create an instance of the injectable. + * The factory can call `inject` to access the `Injector` and request injection of dependencies. + * + * @experimental + */ +export function defineInjectable(opts: { + providedIn?: Type| 'root' | null, + factory: () => T, +}): InjectableDef { + return { + providedIn: (opts.providedIn as InjectorType| 'root' | null | undefined) || null, + factory: opts.factory, + }; +} + +/** + * Construct an `InjectorDef` which configures an injector. + * + * This should be assigned to a static `ngInjectorDef` field on a type, which will then be an + * `InjectorType`. + * + * Options: + * + * * `factory`: an `InjectorType` is an instantiable type, so a zero argument `factory` function to + * create the type must be provided. If that factory function needs to inject arguments, it can + * use the `inject` function. + * * `providers`: an optional array of providers to add to the injector. Each provider must + * either have a factory or point to a type which has an `ngInjectableDef` static property (the + * type must be an `InjectableType`). + * * `imports`: an optional array of imports of other `InjectorType`s or `InjectorTypeWithModule`s + * whose providers will also be added to the injector. Locally provided types will override + * providers from imports. + * + * @experimental + */ +export function defineInjector(options: {factory: () => any, providers?: any[], imports?: any[]}): + InjectorDef { + return { + factory: options.factory, + providers: options.providers || [], + imports: options.imports || [], + }; +} diff --git a/packages/core/src/di/injectable.ts b/packages/core/src/di/injectable.ts index 8b6c667554..d58884699b 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, InjectableType, defineInjectable} from './defs'; import {inject, injectArgs} from './injector'; import {ClassSansProvider, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, StaticClassProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} from './provider'; @@ -108,22 +109,6 @@ export function convertInjectableProviderToFactory( } } -/** -* Construct an `InjectableDef` which defines how a token will be constructed by the DI system, and -* in which injectors (if any) it will be available. -* -* @experimental -*/ -export function defineInjectable(opts: { - providedIn?: Type| 'root' | null, - factory: () => T, -}): InjectableDef { - return { - providedIn: opts.providedIn || null, - factory: opts.factory, - }; -} - /** * Injectable decorator and metadata. * @@ -132,21 +117,16 @@ export function defineInjectable(opts: { */ export const Injectable: InjectableDecorator = makeDecorator( 'Injectable', undefined, undefined, undefined, - (injectableType: Type, + (injectableType: InjectableType, options: {providedIn?: Type| 'root' | null} & InjectableProvider) => { - if (options && options.providedIn) { - (injectableType as InjectableType).ngInjectableDef = defineInjectable({ + if (options && options.providedIn !== undefined) { + injectableType.ngInjectableDef = defineInjectable({ providedIn: options.providedIn, factory: convertInjectableProviderToFactory(injectableType, options) }); } }); -export interface InjectableDef { - providedIn: Type|'root'|null; - factory: () => T; -} - /** * Type representing injectable service. * diff --git a/packages/core/src/di/injection_token.ts b/packages/core/src/di/injection_token.ts index 430fb9b26a..adea877791 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 {InjectableDef, defineInjectable} from './injectable'; +import {InjectableDef, defineInjectable} from './defs'; /** * Creates a token that can be used in a DI Provider. @@ -26,8 +26,24 @@ import {InjectableDef, defineInjectable} from './injectable'; * // myInterface is inferred to be MyInterface. * ``` * + * When creating an `InjectionToken`, you can optionally specify a factory function which returns + * (possibly by creating) a default value of the parameterized type `T`. This sets up the + * `InjectionToken` using this factory as a provider as if it was defined explicitly in the + * application's root injector. If the factory function, which takes zero arguments, needs to inject + * dependencies, it can do so using the `inject` function. See below for an example. + * + * Additionally, if a `factory` is specified you can also specify the `providedIn` option, which + * overrides the above behavior and marks the token as belonging to a particular `@NgModule`. As + * mentioned above, `'root'` is the default value for `providedIn`. + * * ### Example * + * #### Tree-shakeable InjectionToken + * + * {@example core/di/ts/injector_spec.ts region='ShakeableInjectionToken'} + * + * #### Plain InjectionToken + * * {@example core/di/ts/injector_spec.ts region='InjectionToken'} * * @stable @@ -54,3 +70,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 e428aa9a96..25331a179c 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -8,6 +8,8 @@ import {Type} from '../type'; import {stringify} from '../util'; + +import {InjectableDef, defineInjectable} from './defs'; import {resolveForwardRef} from './forward_ref'; import {InjectionToken} from './injection_token'; import {Inject, Optional, Self, SkipSelf} from './metadata'; @@ -17,7 +19,17 @@ export const SOURCE = '__source'; const _THROW_IF_NOT_FOUND = new Object(); export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; -class _NullInjector implements Injector { +/** + * An InjectionToken that gets the current `Injector` for `createInjector()`-style injectors. + * + * Requesting this token instead of `Injector` allows `StaticInjector` to be tree-shaken from a + * project. + * + * @experimental + */ +export const INJECTOR = new InjectionToken('INJECTOR'); + +export class NullInjector implements Injector { get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any { if (notFoundValue === _THROW_IF_NOT_FOUND) { throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`); @@ -48,7 +60,7 @@ class _NullInjector implements Injector { */ export abstract class Injector { static THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; - static NULL: Injector = new _NullInjector(); + static NULL: Injector = new NullInjector(); /** * Retrieves an instance from the injector based on the provided token. @@ -87,6 +99,11 @@ export abstract class Injector { return new StaticInjector(options.providers, options.parent, options.name || null); } } + + static ngInjectableDef = defineInjectable({ + providedIn: 'any' as any, + factory: () => inject(INJECTOR), + }); } @@ -100,7 +117,7 @@ const MULTI_PROVIDER_FN = function(): any[] { return Array.prototype.slice.call(arguments); }; const GET_PROPERTY_NAME = {} as any; -const USE_VALUE = +export const USE_VALUE = getClosureSafeProperty({provide: String, useValue: GET_PROPERTY_NAME}); const NG_TOKEN_PATH = 'ngTokenPath'; const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath'; @@ -127,6 +144,8 @@ export class StaticInjector implements Injector { const records = this._records = new Map(); records.set( Injector, {token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); + records.set( + INJECTOR, {token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); recursivelyProcessProviders(records, providers); } diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts new file mode 100644 index 0000000000..8601d1af7e --- /dev/null +++ b/packages/core/src/di/r3_injector.ts @@ -0,0 +1,408 @@ +/** + * @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 {OnDestroy} from '../metadata/lifecycle_hooks'; +import {Type} from '../type'; +import {stringify} from '../util'; + +import {InjectableDef, InjectableType, InjectorDef, InjectorType, InjectorTypeWithProviders} from './defs'; +import {resolveForwardRef} from './forward_ref'; +import {InjectableDefToken, InjectionToken} from './injection_token'; +import {INJECTOR, InjectFlags, Injector, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE, inject, injectArgs, setCurrentInjector} from './injector'; +import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, Provider, StaticClassProvider, TypeProvider, ValueProvider} from './provider'; +import {APP_ROOT} from './scope'; + + + +/** + * Internal type for a single provider in a deep provider array. + */ +type SingleProvider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | + ExistingProvider | FactoryProvider | StaticClassProvider; + +/** + * Marker which indicates that a value has not yet been created from the factory function. + */ +const NOT_YET = {}; + +/** + * Marker which indicates that the factory function for a token is in the process of being called. + * + * If the injector is asked to inject a token with its value set to CIRCULAR, that indicates + * injection of a dependency has recursively attempted to inject the original token, and there is + * a circular dependency among the providers. + */ +const CIRCULAR = {}; + +const EMPTY_ARRAY = [] as any[]; + +/** + * A lazily initialized NullInjector. + */ +let NULL_INJECTOR: Injector|undefined = undefined; + +function getNullInjector(): Injector { + if (NULL_INJECTOR === undefined) { + NULL_INJECTOR = new NullInjector(); + } + return NULL_INJECTOR; +} + +/** + * An entry in the injector which tracks information about the given token, including a possible + * current value. + */ +interface Record { + factory: (() => T)|undefined; + value: T|{}; + multi: any[]|undefined; +} + +/** + * Create a new `Injector` which is configured using `InjectorDefType`s. + * + * @experimental + */ +export function createInjector( + defType: /* InjectorDefType */ any, parent: Injector | null = null): Injector { + parent = parent || getNullInjector(); + return new R3Injector(defType, parent); +} + +export class R3Injector { + /** + * Map of tokens to records which contain the instances of those tokens. + */ + private records = new Map|InjectionToken, Record>(); + + /** + * The transitive set of `InjectorDefType`s which define this injector. + */ + private injectorDefTypes = new Set>(); + + /** + * Set of values instantiated by this injector which contain `ngOnDestroy` lifecycle hooks. + */ + private onDestroy = new Set(); + + /** + * Flag indicating this injector provides the APP_ROOT_SCOPE token, and thus counts as the + * root scope. + */ + private readonly isRootInjector: boolean; + + /** + * Flag indicating that this injector was previously destroyed. + */ + private destroyed = false; + + constructor(def: InjectorType, readonly parent: Injector) { + // Start off by creating Records for every provider declared in every InjectorDefType + // included transitively in `def`. + deepForEach( + [def], injectorDef => this.processInjectorType(injectorDef, new Set>())); + + // Make sure the INJECTOR token provides this injector. + this.records.set(INJECTOR, makeRecord(undefined, this)); + + // Detect whether this injector has the APP_ROOT_SCOPE token and thus should provide + // any injectable scoped to APP_ROOT_SCOPE. + this.isRootInjector = this.records.has(APP_ROOT); + + // Eagerly instantiate the InjectorDefType classes themselves. + this.injectorDefTypes.forEach(defType => this.get(defType)); + } + + /** + * Destroy the injector and release references to every instance or provider associated with it. + * + * Also calls the `OnDestroy` lifecycle hooks of every instance that was created for which a + * hook was found. + */ + destroy(): void { + this.assertNotDestroyed(); + + // Set destroyed = true first, in case lifecycle hooks re-enter destroy(). + this.destroyed = true; + try { + // Call all the lifecycle hooks. + this.onDestroy.forEach(service => service.ngOnDestroy()); + } finally { + // Release all references. + this.records.clear(); + this.onDestroy.clear(); + this.injectorDefTypes.clear(); + } + } + + get( + token: Type|InjectionToken, notFoundValue: any = THROW_IF_NOT_FOUND, + flags = InjectFlags.Default): T { + this.assertNotDestroyed(); + // Set the injection context. + const previousInjector = setCurrentInjector(this); + try { + // Check for the SkipSelf flag. + if (!(flags & InjectFlags.SkipSelf)) { + // SkipSelf isn't set, check if the record belongs to this injector. + let record: Record|undefined = this.records.get(token); + if (record === undefined) { + // No record, but maybe the token is scoped to this injector. Look for an ngInjectableDef + // with a scope matching this injector. + const def = couldBeInjectableType(token) && + (token as InjectableType| InjectableDefToken).ngInjectableDef || + undefined; + if (def !== undefined && this.injectableDefInScope(def)) { + // Found an ngInjectableDef and it's scoped to this injector. Pretend as if it was here + // all along. + record = injectableDefRecord(token); + this.records.set(token, record); + } + } + // If a record was found, get the instance for it and return it. + if (record !== undefined) { + return this.hydrate(token, record); + } + } + + // Select the next injector based on the Self flag - if self is set, the next injector is + // the NullInjector, otherwise it's the parent. + let next = !(flags & InjectFlags.Self) ? this.parent : getNullInjector(); + return this.parent.get(token, notFoundValue); + } finally { + // Lastly, clean up the state by restoring the previous injector. + setCurrentInjector(previousInjector); + } + } + + private assertNotDestroyed(): void { + if (this.destroyed) { + throw new Error('Injector has already been destroyed.'); + } + } + + /** + * Add an `InjectorDefType` or `InjectorDefTypeWithProviders` and all of its transitive providers + * to this injector. + */ + private processInjectorType( + defOrWrappedDef: InjectorType|InjectorTypeWithProviders, + parents: Set>) { + defOrWrappedDef = resolveForwardRef(defOrWrappedDef); + + // Either the defOrWrappedDef is an InjectorDefType (with ngInjectorDef) or an + // InjectorDefTypeWithProviders (aka ModuleWithProviders). Detecting either is a megamorphic + // read, so care is taken to only do the read once. + + // First attempt to read the ngInjectorDef. + let def = (defOrWrappedDef as InjectorType).ngInjectorDef as(InjectorDef| undefined); + + // If that's not present, then attempt to read ngModule from the InjectorDefTypeWithProviders. + const ngModule = + (def == null) && (defOrWrappedDef as InjectorTypeWithProviders).ngModule || undefined; + + // Determine the InjectorDefType. In the case where `defOrWrappedDef` is an `InjectorDefType`, + // then this is easy. In the case of an InjectorDefTypeWithProviders, then the definition type + // is the `ngModule`. + const defType: InjectorType = + (ngModule === undefined) ? (defOrWrappedDef as InjectorType) : ngModule; + + // If defOrWrappedType was an InjectorDefTypeWithProviders, then .providers may hold some + // extra providers. + const providers = + (ngModule !== undefined) && (defOrWrappedDef as InjectorTypeWithProviders).providers || + EMPTY_ARRAY; + + // Finally, if defOrWrappedType was an `InjectorDefTypeWithProviders`, then the actual + // `InjectorDef` is on its `ngModule`. + if (ngModule !== undefined) { + def = ngModule.ngInjectorDef; + } + + // If no definition was found, throw. + if (def == null) { + throw new Error(`Type ${stringify(defType)} is missing an ngInjectorDef definition.`); + } + + // Check for circular dependencies. + if (parents.has(defType)) { + throw new Error(`Circular dependency: type ${stringify(defType)} ends up importing itself.`); + } + + // Track the InjectorDefType and add a provider for it. + this.injectorDefTypes.add(defType); + this.records.set(defType, makeRecord(def.factory)); + + // Add providers in the same way that @NgModule resolution did: + + // First, include providers from any imports. + if (def.imports != null) { + // Before processing defType's imports, add it to the set of parents. This way, if it ends + // up deeply importing itself, this can be detected. + parents.add(defType); + try { + deepForEach(def.imports, imported => this.processInjectorType(imported, parents)); + } finally { + // Remove it from the parents set when finished. + parents.delete(defType); + } + } + + // Next, include providers listed on the definition itself. + if (def.providers != null) { + deepForEach(def.providers, provider => this.processProvider(provider)); + } + + // Finally, include providers from an InjectorDefTypeWithProviders if there was one. + deepForEach(providers, provider => this.processProvider(provider)); + } + + /** + * Process a `SingleProvider` and add it. + */ + private processProvider(provider: SingleProvider): void { + // Determine the token from the provider. Either it's its own token, or has a {provide: ...} + // property. + provider = resolveForwardRef(provider); + let token: any = isTypeProvider(provider) ? provider : resolveForwardRef(provider.provide); + + // Construct a `Record` for the provider. + const record = providerToRecord(provider); + + if (!isTypeProvider(provider) && provider.multi === true) { + // If the provider indicates that it's a multi-provider, process it specially. + // First check whether it's been defined already. + let multiRecord = this.records.get(token); + if (multiRecord) { + // It has. Throw a nice error if + if (multiRecord.multi === undefined) { + throw new Error(`Mixed multi-provider for ${token}.`); + } + } else { + token = provider; + multiRecord = makeRecord(undefined, NOT_YET, true); + multiRecord.factory = () => injectArgs(multiRecord !.multi !); + this.records.set(token, multiRecord); + } + token = provider; + multiRecord.multi !.push(provider); + } + + const existing = this.records.get(token); + if (existing && existing.multi !== undefined) { + throw new Error(`Mixed multi-provider for ${token}`); + } + + this.records.set(token, record); + } + + private hydrate(token: Type|InjectionToken, record: Record): T { + if (record.value === CIRCULAR) { + throw new Error(`Circular dep for ${stringify(token)}`); + } else if (record.value === NOT_YET) { + record.value = CIRCULAR; + record.value = record.factory !(); + } + if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) { + this.onDestroy.add(record.value); + } + return record.value as T; + } + + private injectableDefInScope(def: InjectableDef): boolean { + if (!def.providedIn) { + return false; + } else if (typeof def.providedIn === 'string') { + return def.providedIn === 'any' || (def.providedIn === 'root' && this.isRootInjector); + } else { + return this.injectorDefTypes.has(def.providedIn); + } + } +} + +function injectableDefRecord(token: Type| InjectionToken): Record { + const def = (token as InjectableType).ngInjectableDef; + if (def === undefined) { + throw new Error(`Type ${stringify(token)} is missing an ngInjectableDef definition.`); + } + return makeRecord(def.factory); +} + +function providerToRecord(provider: SingleProvider): Record { + let token = resolveForwardRef(provider); + let value: any = NOT_YET; + let factory: (() => any)|undefined = undefined; + if (isTypeProvider(provider)) { + return injectableDefRecord(provider); + } else { + token = resolveForwardRef(provider.provide); + 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 classRef = (provider as StaticClassProvider | ClassProvider).useClass || token; + if (hasDeps(provider)) { + factory = () => new (classRef)(...injectArgs(provider.deps)); + } else { + return injectableDefRecord(classRef); + } + } + } + return makeRecord(factory, value); +} + +function makeRecord( + factory: (() => T) | undefined, value: T | {} = NOT_YET, multi: boolean = false): Record { + return { + factory: factory, + value: value, + multi: multi ? [] : undefined, + }; +} + +function deepForEach(input: (T | any[])[], fn: (value: T) => void): void { + input.forEach(value => Array.isArray(value) ? deepForEach(value, fn) : fn(value)); +} + +function isValueProvider(value: SingleProvider): value is ValueProvider { + return USE_VALUE in value; +} + +function isExistingProvider(value: SingleProvider): value is ExistingProvider { + return !!(value as ExistingProvider).useExisting; +} + +function isFactoryProvider(value: SingleProvider): value is FactoryProvider { + return !!(value as FactoryProvider).useFactory; +} + +function isClassProvider(value: SingleProvider): value is ClassProvider { + return !!(value as ClassProvider).useClass; +} + +function isTypeProvider(value: SingleProvider): value is TypeProvider { + return typeof value === 'function'; +} + +function hasDeps(value: ClassProvider | ConstructorProvider | StaticClassProvider): + value is ClassProvider&{deps: any[]} { + return !!(value as any).deps; +} + +function hasOnDestroy(value: any): value is OnDestroy { + return typeof value === 'object' && value != null && (value as OnDestroy).ngOnDestroy && + typeof(value as OnDestroy).ngOnDestroy === 'function'; +} + +function couldBeInjectableType(value: any): value is Type|InjectionToken { + return (typeof value === 'function') || + (typeof value === 'object' && value instanceof InjectionToken); +} diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index c48376605e..363ada2612 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -6,10 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider} from '../di'; +import {InjectorDef, InjectorType, defineInjector} from '../di/defs'; +import {convertInjectableProviderToFactory} from '../di/injectable'; +import {Provider} from '../di/provider'; import {Type} from '../type'; import {TypeDecorator, makeDecorator} from '../util/decorators'; + /** * A wrapper around a module that also includes the providers. * @@ -190,5 +193,17 @@ 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: InjectorType, 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, + }); + }); diff --git a/packages/core/src/view/ng_module.ts b/packages/core/src/view/ng_module.ts index ecb9b02830..4fa7615a8b 100644 --- a/packages/core/src/view/ng_module.ts +++ b/packages/core/src/view/ng_module.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {InjectableDef} from '../di/defs'; import {resolveForwardRef} from '../di/forward_ref'; -import {InjectableDef} from '../di/injectable'; -import {InjectFlags, Injector, setCurrentInjector} from '../di/injector'; +import {INJECTOR, InjectFlags, Injector, setCurrentInjector} from '../di/injector'; import {APP_ROOT} from '../di/scope'; import {NgModuleRef} from '../linker/ng_module_factory'; import {stringify} from '../util'; diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index bc3fd20019..6a58b75925 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -8,7 +8,8 @@ import {isDevMode} from '../application_ref'; import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node'; -import {InjectableType, Injector} from '../di'; +import {Injector} from '../di'; +import {InjectableType} from '../di/injectable'; import {ErrorHandler} from '../error_handler'; import {ComponentFactory} from '../linker/component_factory'; import {NgModuleRef} from '../linker/ng_module_factory'; diff --git a/packages/core/test/bundling/injection/BUILD.bazel b/packages/core/test/bundling/injection/BUILD.bazel new file mode 100644 index 0000000000..740816265c --- /dev/null +++ b/packages/core/test/bundling/injection/BUILD.bazel @@ -0,0 +1,54 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools/symbol-extractor:index.bzl", "js_expected_symbol_test") +load("//packages/bazel/src:ng_rollup_bundle.bzl", "ng_rollup_bundle") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +ts_library( + name = "injection", + srcs = [ + "index.ts", + "usage.ts", + ], + deps = [ + "//packages/core", + ], +) + +ng_rollup_bundle( + name = "bundle", + # TODO(alexeagle): This is inconsistent. + # We try to teach users to always have their workspace at the start of a + # path, to disambiguate from other workspaces. + # Here, the rule implementation is looking in an execroot where the layout + # has an "external" directory for external dependencies. + # This should probably start with "angular/" and let the rule deal with it. + entry_point = "packages/core/test/bundling/injection/index.js", + deps = [ + ":injection", + "//packages/core", + ], +) + +ts_library( + name = "test_lib", + testonly = 1, + srcs = glob(["*_spec.ts"]), + deps = [ + ":injection", + "//packages:types", + "//packages/core/testing", + ], +) + +jasmine_node_test( + name = "test", + deps = [":test_lib"], +) + +js_expected_symbol_test( + name = "symbol_test", + src = ":bundle.min_debug.js", + golden = ":bundle.golden_symbols.json", +) diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json new file mode 100644 index 0000000000..7536843059 --- /dev/null +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -0,0 +1,161 @@ +[ + { + "name": "APP_ROOT" + }, + { + "name": "CIRCULAR$1" + }, + { + "name": "EMPTY_ARRAY$1" + }, + { + "name": "GET_PROPERTY_NAME$1" + }, + { + "name": "INJECTOR$1" + }, + { + "name": "Inject" + }, + { + "name": "InjectionToken" + }, + { + "name": "NOT_YET" + }, + { + "name": "NULL_INJECTOR$1" + }, + { + "name": "NullInjector" + }, + { + "name": "Optional" + }, + { + "name": "PARAMETERS" + }, + { + "name": "R3Injector" + }, + { + "name": "ScopedService" + }, + { + "name": "Self" + }, + { + "name": "SkipSelf" + }, + { + "name": "Symbol$1" + }, + { + "name": "THROW_IF_NOT_FOUND" + }, + { + "name": "USE_VALUE$1" + }, + { + "name": "_THROW_IF_NOT_FOUND" + }, + { + "name": "__global$1" + }, + { + "name": "__read" + }, + { + "name": "__self$1" + }, + { + "name": "__spread" + }, + { + "name": "__window$1" + }, + { + "name": "_currentInjector" + }, + { + "name": "_root" + }, + { + "name": "couldBeInjectableType" + }, + { + "name": "createInjector" + }, + { + "name": "deepForEach" + }, + { + "name": "defineInjectable" + }, + { + "name": "defineInjector" + }, + { + "name": "forwardRef" + }, + { + "name": "getClosureSafeProperty$1" + }, + { + "name": "getNullInjector" + }, + { + "name": "getSymbolObservable" + }, + { + "name": "hasDeps" + }, + { + "name": "hasOnDestroy" + }, + { + "name": "inject" + }, + { + "name": "injectArgs" + }, + { + "name": "injectableDefRecord" + }, + { + "name": "isExistingProvider" + }, + { + "name": "isFactoryProvider" + }, + { + "name": "isTypeProvider" + }, + { + "name": "isValueProvider" + }, + { + "name": "makeMetadataCtor" + }, + { + "name": "makeParamDecorator" + }, + { + "name": "makeRecord" + }, + { + "name": "providerToRecord" + }, + { + "name": "resolveForwardRef" + }, + { + "name": "setCurrentInjector" + }, + { + "name": "stringify" + }, + { + "name": "symbolIteratorPonyfill" + } +] \ No newline at end of file diff --git a/packages/core/test/bundling/injection/index.ts b/packages/core/test/bundling/injection/index.ts new file mode 100644 index 0000000000..7599c81978 --- /dev/null +++ b/packages/core/test/bundling/injection/index.ts @@ -0,0 +1,11 @@ +/** + * @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 {INJECTOR, ScopedService} from './usage'; + +INJECTOR.get(ScopedService).doSomething(); diff --git a/packages/core/test/bundling/injection/treeshaking_spec.ts b/packages/core/test/bundling/injection/treeshaking_spec.ts new file mode 100644 index 0000000000..8ff300a520 --- /dev/null +++ b/packages/core/test/bundling/injection/treeshaking_spec.ts @@ -0,0 +1,22 @@ +/** + * @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 fs from 'fs'; +import * as path from 'path'; + +import {INJECTOR, ScopedService} from './usage'; + +const UTF8 = { + encoding: 'utf-8' +}; +const PACKAGE = 'angular/packages/core/test/bundling/hello_world'; + +describe('functional test for injection system bundling', () => { + it('should be able to inject the scoped service', + () => { expect(INJECTOR.get(ScopedService) instanceof ScopedService).toBe(true); }); +}); diff --git a/packages/core/test/bundling/injection/usage.ts b/packages/core/test/bundling/injection/usage.ts new file mode 100644 index 0000000000..669aae806b --- /dev/null +++ b/packages/core/test/bundling/injection/usage.ts @@ -0,0 +1,37 @@ +/** + * @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 {InjectableDef, Injector, InjectorDef, createInjector, defineInjectable, defineInjector} from '@angular/core'; + +export class RootService { + static ngInjectableDef = defineInjectable({ + providedIn: 'root', + factory: () => new RootService(), + }); +} + +export class ScopedService { + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => new ScopedService(), + }); + + doSomething(): void { + // tslint:disable-next-line:no-console + console.log('Ensure this isn\'t tree-shaken.'); + } +} + +export class DefinedInjector { + static ngInjectorDef = defineInjector({ + factory: () => new DefinedInjector(), + providers: [ScopedService], + }); +} + +export const INJECTOR = createInjector(DefinedInjector); diff --git a/packages/core/test/di/r3_injector_spec.ts b/packages/core/test/di/r3_injector_spec.ts new file mode 100644 index 0000000000..7d08f65de5 --- /dev/null +++ b/packages/core/test/di/r3_injector_spec.ts @@ -0,0 +1,224 @@ +/** + * @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 {defineInjectable, defineInjector} from '../../src/di/defs'; +import {InjectionToken} from '../../src/di/injection_token'; +import {INJECTOR, Injector, inject} from '../../src/di/injector'; +import {R3Injector, createInjector} from '../../src/di/r3_injector'; + +describe('InjectorDef-based createInjector()', () => { + class CircularA { + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => inject(CircularB), + }); + } + + class CircularB { + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => inject(CircularA), + }); + } + + class Service { + static ngInjectableDef = defineInjectable({ + providedIn: null, + 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 = defineInjectable({ + providedIn: null, + factory: () => new ServiceWithDep(inject(Service)), + }); + } + + class ServiceTwo { + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => new ServiceTwo(), + }); + } + + let deepServiceDestroyed = false; + class DeepService { + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => new DeepService(), + }); + + ngOnDestroy(): void { deepServiceDestroyed = true; } + } + + let eagerServiceCreated: boolean = false; + class EagerService { + static ngInjectableDef = defineInjectable({ + providedIn: undefined, + factory: () => new EagerService(), + }); + + constructor() { eagerServiceCreated = true; } + } + + let deepModuleCreated: boolean = false; + class DeepModule { + constructor(eagerService: EagerService) { deepModuleCreated = true; } + + static ngInjectorDef = defineInjector({ + 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 = defineInjector({ + factory: () => new IntermediateModule(), + imports: [DeepModule.safe()], + providers: [], + }); + } + + class Module { + static ngInjectorDef = defineInjector({ + 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 = defineInjector({ + factory: () => new OtherModule(), + imports: undefined, + providers: [], + }); + } + + class ScopedService { + static ngInjectableDef = defineInjectable({ + providedIn: Module, + factory: () => new ScopedService(), + }); + } + + class WrongScopeService { + static ngInjectableDef = defineInjectable({ + providedIn: OtherModule, + factory: () => new WrongScopeService(), + }); + } + + let injector: Injector; + + beforeEach(() => { + deepModuleCreated = eagerServiceCreated = deepServiceDestroyed = false; + injector = createInjector(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 via INJECTOR', + () => { expect(injector.get(INJECTOR)).toBe(injector); }); + + it('allows injecting itself via Injector', + () => { 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 R3Injector).destroy(); + expect(deepServiceDestroyed).toBe(true, 'DeepService not destroyed'); + }); + + it('does not allow injection after destroy', () => { + (injector as R3Injector).destroy(); + expect(() => injector.get(DeepService)).toThrowError('Injector has already been destroyed.'); + }); + + it('does not allow double destroy', () => { + (injector as R3Injector).destroy(); + expect(() => (injector as R3Injector).destroy()) + .toThrowError('Injector has already been destroyed.'); + }); +}); diff --git a/packages/core/test/di/static_injector_spec.ts b/packages/core/test/di/static_injector_spec.ts index e9842c8517..37e63876a5 100644 --- a/packages/core/test/di/static_injector_spec.ts +++ b/packages/core/test/di/static_injector_spec.ts @@ -473,7 +473,7 @@ function factoryFn(a: any){} describe('displayName', () => { it('should work', () => { expect(Injector.create([Engine.PROVIDER, {provide: BrokenEngine, useValue: null}]).toString()) - .toEqual('StaticInjector[Injector, Engine, BrokenEngine]'); + .toEqual('StaticInjector[Injector, InjectionToken INJECTOR, Engine, BrokenEngine]'); }); }); } diff --git a/packages/core/test/render3/compiler_canonical/back_patch_types_specs.ts b/packages/core/test/render3/compiler_canonical/back_patch_types_specs.ts index a97e726a1f..180d555bb9 100644 --- a/packages/core/test/render3/compiler_canonical/back_patch_types_specs.ts +++ b/packages/core/test/render3/compiler_canonical/back_patch_types_specs.ts @@ -6,11 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ContentChild, Directive, Injectable, Injector, Input, NgModule, NgModuleFactory, NgModuleRef, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef} from '../../../src/core'; +import {Component, ContentChild, Directive, Injectable, Injector, InjectorDef, Input, NgModule, NgModuleFactory, NgModuleRef, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef, defineInjector} from '../../../src/core'; import * as r3 from '../../../src/render3/index'; -import {$pending_pr_22458$} from './pending_api_spec'; - const details_elided = { type: Object, } as any; @@ -60,7 +58,7 @@ export class LibBComponent { @NgModule({declarations: [LibAComponent], imports: []}) export class LibBModule { // COMPILER GENERATED - static ngInjectorDef = $pending_pr_22458$.defineInjector(details_elided); + static ngInjectorDef = defineInjector(details_elided); } // END FILE: node_modules/libB/module.ts // BEGIN FILE: node_modules/libB/module.metadata.json @@ -92,7 +90,7 @@ export class AppComponent { @NgModule({declarations: [LibAComponent], imports: []}) export class AppModule { // COMPILER GENERATED - static ngInjectorDef = $pending_pr_22458$.defineInjector(details_elided); + static ngInjectorDef = defineInjector(details_elided); } // END FILE: src/app.ts @@ -113,7 +111,7 @@ function ngBackPatch_node_modules_libB_module_LibAComponent() { } function ngBackPatch_node_modules_libB_module_LibAModule() { - (LibAModule as any).ngInjectorDef = $pending_pr_22458$.defineInjector(details_elided); + (LibAModule as any).ngInjectorDef = defineInjector(details_elided); } export const AppModuleFactory: NgModuleFactory&{patchedDeps: boolean} = { diff --git a/packages/core/test/render3/compiler_canonical/injection_spec.ts b/packages/core/test/render3/compiler_canonical/injection_spec.ts index a709ab9bfa..c478394bfa 100644 --- a/packages/core/test/render3/compiler_canonical/injection_spec.ts +++ b/packages/core/test/render3/compiler_canonical/injection_spec.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Inject, InjectFlags, Injectable, Injector, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, SkipSelf, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; +import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, INJECTOR, Inject, InjectFlags, Injectable, InjectableDef, Injector, InjectorDef, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, SkipSelf, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, defineInjectable, defineInjector} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; import {renderComponent, toHtml} from '../render_util'; -import {$pending_pr_22458$} from './pending_api_spec'; - /// See: `normative.md` @@ -118,7 +116,7 @@ describe('injection', () => { @Injectable() class ServiceA { // NORMATIVE - static ngInjectableDef = $pending_pr_22458$.defineInjectable({ + static ngInjectableDef = defineInjectable({ factory: function ServiceA_Factory() { return new ServiceA(); }, }); // /NORMATIVE @@ -127,7 +125,7 @@ describe('injection', () => { @Injectable() class ServiceB { // NORMATIVE - static ngInjectableDef = $pending_pr_22458$.defineInjectable({ + static ngInjectableDef = defineInjectable({ factory: function ServiceA_Factory() { return new ServiceB(); }, }); // /NORMATIVE @@ -146,8 +144,7 @@ describe('injection', () => { tag: 'my-app', factory: function MyApp_Factory() { return new MyApp( - $r3$.ɵdirectiveInject(ServiceA), $r3$.ɵdirectiveInject(ServiceB), - $pending_pr_22458$.injectInjector()); + $r3$.ɵdirectiveInject(ServiceA), $r3$.ɵdirectiveInject(ServiceB), inject(INJECTOR)); }, /** */ template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) {}, @@ -169,10 +166,9 @@ describe('injection', () => { constructor(@Inject(String) name: String, injector: Injector) {} // NORMATIVE - static ngInjectableDef = $pending_pr_22458$.defineInjectable({ + static ngInjectableDef = defineInjectable({ factory: function ServiceA_Factory() { - return new ServiceA( - $pending_pr_22458$.inject(String), $pending_pr_22458$.injectInjector()); + return new ServiceA(inject(String), inject(INJECTOR)); }, }); // /NORMATIVE @@ -182,11 +178,9 @@ describe('injection', () => { class ServiceB { constructor(serviceA: ServiceA, @SkipSelf() injector: Injector) {} // NORMATIVE - static ngInjectableDef = $pending_pr_22458$.defineInjectable({ + static ngInjectableDef = defineInjectable({ factory: function ServiceA_Factory() { - return new ServiceB( - $pending_pr_22458$.inject(ServiceA), - $pending_pr_22458$.injectInjector(InjectFlags.SkipSelf)); + return new ServiceB(inject(ServiceA), inject(INJECTOR, undefined, InjectFlags.SkipSelf)); }, }); // /NORMATIVE diff --git a/packages/core/test/render3/compiler_canonical/patch_types_spec.ts b/packages/core/test/render3/compiler_canonical/patch_types_spec.ts index bb63645229..188de69b32 100644 --- a/packages/core/test/render3/compiler_canonical/patch_types_spec.ts +++ b/packages/core/test/render3/compiler_canonical/patch_types_spec.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ContentChild, Directive, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef} from '../../../src/core'; +import {Component, ContentChild, Directive, Injectable, InjectableDef, InjectorDef, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef, defineInjectable, defineInjector} from '../../../src/core'; import * as r3 from '../../../src/render3/index'; -import {$pending_pr_22458$} from './pending_api_spec'; + /** * GOALS: @@ -29,7 +29,7 @@ class ThirdPartyClass { @Injectable() class CompiledWithIvy { // NORMATIVE - static ngInjectableDef = $pending_pr_22458$.defineInjectable( + static ngInjectableDef = defineInjectable( {factory: function CompileWithIvy_Factory() { return new CompiledWithIvy(); }}); // /NORMATIVE } @@ -38,7 +38,7 @@ class CompiledWithIvy { @NgModule({providers: [ThirdPartyClass, CompiledWithIvy]}) class CompiledWithIvyModule { // NORMATIVE - static ngInjectorDef = $pending_pr_22458$.defineInjector({ + static ngInjectorDef = defineInjector({ providers: [ThirdPartyClass, CompiledWithIvy], factory: function CompiledWithIvyModule_Factory() { return new CompiledWithIvyModule(); } }); @@ -72,7 +72,7 @@ function ngPatch_depsOf_CompiledWithIvyModule() { } function ngPatch_node_modules_some_library_path_public_CompileWithIvy() { /** @__BUILD_OPTIMIZER_COLOCATE__ */ - (ThirdPartyClass as any).ngInjectableDef = $pending_pr_22458$.defineInjectable( + (ThirdPartyClass as any).ngInjectableDef = defineInjectable( {factory: function CompileWithIvy_Factory() { return new ThirdPartyClass(); }}); } // /NORMATIVE diff --git a/packages/core/test/render3/compiler_canonical/pending_api_spec.ts b/packages/core/test/render3/compiler_canonical/pending_api_spec.ts deleted file mode 100644 index 9e69b29b76..0000000000 --- a/packages/core/test/render3/compiler_canonical/pending_api_spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @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 {InjectFlags, InjectionToken, Injector, Type} from '@angular/core'; - - - -// TODO: remove once https://github.com/angular/angular/pull/22458 lands -export class $pending_pr_22458$ { - static defineInjectable({providerFor, factory}: {providerFor?: Type, factory: () => T}): - {providerFor: Type| null, factory: () => T} { - return {providerFor: providerFor || null, factory: factory}; - } - - static defineInjector({factory, providers}: {factory: () => T, providers: any[]}): - {factory: () => T, providers: any[]} { - return {factory: factory, providers: providers}; - } - - static injectInjector(flags?: InjectFlags): Injector { return null !; } - - static inject(token: Type|InjectionToken, flags?: InjectFlags): T { return null as any; } -} diff --git a/packages/core/test/render3/compiler_canonical/small_app_spec.ts b/packages/core/test/render3/compiler_canonical/small_app_spec.ts index ec70c726d8..ac4f61e9ed 100644 --- a/packages/core/test/render3/compiler_canonical/small_app_spec.ts +++ b/packages/core/test/render3/compiler_canonical/small_app_spec.ts @@ -7,11 +7,10 @@ */ import {NgForOf, NgForOfContext} from '@angular/common'; -import {Component, ContentChild, Directive, EventEmitter, Injectable, Input, NgModule, OnDestroy, Optional, Output, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {Component, ContentChild, Directive, EventEmitter, Injectable, InjectableDef, InjectorDef, Input, NgModule, OnDestroy, Optional, Output, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef, defineInjectable, defineInjector} from '@angular/core'; import {withBody} from '@angular/core/testing'; import * as r3 from '../../../src/render3/index'; -import {$pending_pr_22458$} from './pending_api_spec'; /// See: `normative.md` @@ -33,7 +32,7 @@ class AppState { ]; // NORMATIVE - static ngInjectableDef = $pending_pr_22458$.defineInjectable({factory: () => new AppState()}); + static ngInjectableDef = defineInjectable({factory: () => new AppState()}); // /NORMATIVE } @@ -158,7 +157,7 @@ const e1_attrs = ['type', 'checkbox']; }) class ToDoAppModule { // NORMATIVE - static ngInjectorDef = $pending_pr_22458$.defineInjector({ + static ngInjectorDef = defineInjector({ factory: () => new ToDoAppModule(), providers: [AppState], }); diff --git a/packages/core/test/view/ng_module_spec.ts b/packages/core/test/view/ng_module_spec.ts index 373433f47d..f1433b351b 100644 --- a/packages/core/test/view/ng_module_spec.ts +++ b/packages/core/test/view/ng_module_spec.ts @@ -7,7 +7,7 @@ */ import {NgModuleRef} from '@angular/core'; -import {InjectableDef} from '@angular/core/src/di/injectable'; +import {InjectableDef, defineInjectable} from '@angular/core/src/di/defs'; import {InjectFlags, Injector, inject} from '@angular/core/src/di/injector'; import {makePropDecorator} from '@angular/core/src/util/decorators'; import {NgModuleDefinition, NgModuleProviderDef, NodeFlags} from '@angular/core/src/view'; @@ -24,68 +24,68 @@ class MyChildModule {} class NotMyModule {} class Bar { - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new Bar(), providedIn: MyModule, - }; + }); } class Baz { - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new Baz(), providedIn: NotMyModule, - }; + }); } class HasNormalDep { constructor(public foo: Foo) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new HasNormalDep(inject(Foo)), providedIn: MyModule, - }; + }); } class HasDefinedDep { constructor(public bar: Bar) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new HasDefinedDep(inject(Bar)), providedIn: MyModule, - }; + }); } class HasOptionalDep { constructor(public baz: Baz|null) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new HasOptionalDep(inject(Baz, null)), providedIn: MyModule, - }; + }); } class ChildDep { - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new ChildDep(), providedIn: MyChildModule, - }; + }); } class FromChildWithOptionalDep { constructor(public baz: Baz|null) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new FromChildWithOptionalDep(inject(Baz, null, InjectFlags.Default)), providedIn: MyChildModule, - }; + }); } class FromChildWithSkipSelfDep { constructor(public depFromParent: ChildDep|null, public depFromChild: Bar|null) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = defineInjectable({ factory: () => new FromChildWithSkipSelfDep( inject(ChildDep, null, InjectFlags.SkipSelf), inject(Bar, null, InjectFlags.Self)), providedIn: MyChildModule, - }; + }); } function makeProviders(classes: any[], modules: any[]): NgModuleDefinition { diff --git a/packages/platform-browser-dynamic/src/compiler_reflector.ts b/packages/platform-browser-dynamic/src/compiler_reflector.ts index 14652fc056..a7744a2a68 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 c4041ddaa0..221d0953eb 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -2,6 +2,7 @@ load("@build_bazel_rules_nodejs//:defs.bzl", _npm_package = "npm_package") load("@build_bazel_rules_typescript//:defs.bzl", _ts_library = "ts_library", _ts_web_test = "ts_web_test") load("//packages/bazel:index.bzl", _ng_module = "ng_module", _ng_package = "ng_package") +load("//packages/bazel/src:ng_module.bzl", _ivy_ng_module = "internal_ivy_ng_module") DEFAULT_TSCONFIG = "//packages:tsconfig-build.json" @@ -78,4 +79,9 @@ def ts_web_test(bootstrap = [], deps = [], **kwargs): _ts_web_test( bootstrap = bootstrap, deps = local_deps, - **kwargs) \ No newline at end of file + **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 d892d44392..eff1662adf 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -186,6 +186,9 @@ export interface ContentChildrenDecorator { }): Query; } +/** @experimental */ +export declare function createInjector(defType: any, parent?: Injector | null): Injector; + /** @experimental */ export declare function createPlatform(injector: Injector): PlatformRef; @@ -263,6 +266,13 @@ export declare function defineInjectable(opts: { factory: () => T; }): InjectableDef; +/** @experimental */ +export declare function defineInjector(options: { + factory: () => any; + providers?: any[]; + imports?: any[]; +}): InjectorDef; + /** @experimental */ export declare function destroyPlatform(): void; @@ -380,6 +390,12 @@ export interface InjectableDecorator { } & InjectableProvider): Injectable; } +/** @experimental */ +export interface InjectableDef { + factory: () => T; + providedIn: InjectorType | 'root' | 'any' | null; +} + /** @experimental */ export declare type InjectableProvider = ValueSansProvider | ExistingSansProvider | StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider; @@ -418,6 +434,7 @@ export declare abstract class Injector { /** @deprecated */ abstract get(token: any, notFoundValue?: any): any; static NULL: Injector; static THROW_IF_NOT_FOUND: Object; + static ngInjectableDef: InjectableDef; /** @deprecated */ static create(providers: StaticProvider[], parent?: Injector): Injector; static create(options: { providers: StaticProvider[]; @@ -426,6 +443,27 @@ export declare abstract class Injector { }): Injector; } +/** @experimental */ +export declare const INJECTOR: InjectionToken; + +/** @experimental */ +export interface InjectorDef { + factory: () => T; + imports: (InjectorType | InjectorTypeWithProviders)[]; + providers: (Type | ValueProvider | ExistingProvider | FactoryProvider | ConstructorProvider | StaticClassProvider | ClassProvider | any[])[]; +} + +/** @experimental */ +export interface InjectorType extends Type { + ngInjectorDef: InjectorDef; +} + +/** @experimental */ +export interface InjectorTypeWithProviders { + ngModule: InjectorType; + providers?: (Type | ValueProvider | ExistingProvider | FactoryProvider | ConstructorProvider | StaticClassProvider | ClassProvider | any[])[]; +} + /** @stable */ export declare const Input: InputDecorator;