From 85a16137eaa8bc3e914263ff0141e7ce44a807b0 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:33 +0100 Subject: [PATCH 01/15] test(ivy): ngcc - check the actual file that is passed to `renderImports` Previously we were just checking that the object was "any" object but now we check that it is the file object that we expected. --- .../ngcc/test/rendering/renderer_spec.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 9a28d8b01adf..3a9d7570740a 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -80,8 +80,12 @@ function createTestRenderer( spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough(); - return {renderer, decorationAnalyses, switchMarkerAnalyses, moduleWithProvidersAnalyses, - privateDeclarationsAnalyses}; + return {renderer, + decorationAnalyses, + switchMarkerAnalyses, + moduleWithProvidersAnalyses, + privateDeclarationsAnalyses, + bundle}; } @@ -488,8 +492,12 @@ describe('Renderer', () => { contents: 'export declare class LibraryModule {}' }, ]; - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = + const {renderer, + decorationAnalyses, + switchMarkerAnalyses, + privateDeclarationsAnalyses, + moduleWithProvidersAnalyses, + bundle} = createTestRenderer( 'test-package', MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); @@ -526,7 +534,7 @@ describe('Renderer', () => { {specifier: '@angular/core', qualifier: 'ɵngcc1'}, {specifier: 'some-library', qualifier: 'ɵngcc2'}, ], - jasmine.anything()); + bundle.dts !.file); // The following expectation checks that we do not mistake `ModuleWithProviders` types From fcb9cd7c6dd667e78df285434bd588f5a7a4a725 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:33 +0100 Subject: [PATCH 02/15] refactor(ivy): use a named type for ImportManager import structures Previously we were using an anonymous type `{specifier: string; qualifier: string;}` throughout the code base. This commit gives this type a name and ensures it is only defined in one place. --- .../ngcc/src/rendering/esm_renderer.ts | 5 ++-- .../ngcc/src/rendering/renderer.ts | 7 ++--- packages/compiler-cli/ngcc/test/BUILD.bazel | 1 + .../ngcc/test/rendering/renderer_spec.ts | 4 +-- .../src/ngtsc/translator/index.ts | 2 +- .../src/ngtsc/translator/src/translator.ts | 26 ++++++++++++++++--- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts index 15fe6f61fbee..607595449fe1 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts @@ -9,6 +9,7 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; +import {Import} from '../../../src/ngtsc/translator'; import {CompiledClass} from '../analysis/decoration_analyzer'; import {ExportInfo} from '../analysis/private_declarations_analyzer'; import {FileSystem} from '../file_system/file_system'; @@ -27,9 +28,7 @@ export class EsmRenderer extends Renderer { /** * Add the imports at the top of the file */ - addImports( - output: MagicString, imports: {specifier: string; qualifier: string;}[], - sf: ts.SourceFile): void { + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void { const insertionPoint = findEndOfImports(sf); const renderedImports = imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join(''); diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index b1410eed1a0a..eb6958408613 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -14,8 +14,7 @@ import * as ts from 'typescript'; import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports'; import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {CompileResult} from '../../../src/ngtsc/transform'; -import {translateStatement, translateType, ImportManager} from '../../../src/ngtsc/translator'; - +import {translateStatement, translateType, Import, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer'; import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer'; import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer'; @@ -250,9 +249,7 @@ export abstract class Renderer { protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile): void; - protected abstract addImports( - output: MagicString, imports: {specifier: string, qualifier: string}[], - sf: ts.SourceFile): void; + protected abstract addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void; protected abstract addExports( output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void; protected abstract addDefinitions( diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 7aabc69b433e..4ed0921b8de1 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -18,6 +18,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/transform", + "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/test:test_utils", "@npm//@types/convert-source-map", "@npm//@types/mock-fs", diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 3a9d7570740a..cac283fccdfd 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -9,6 +9,7 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {fromObject, generateMapFileComment} from 'convert-source-map'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {Import} from '../../../src/ngtsc/translator'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; @@ -31,8 +32,7 @@ class TestRenderer extends Renderer { bundle: EntryPointBundle) { super(fs, logger, host, isCore, bundle); } - addImports( - output: MagicString, imports: {specifier: string, qualifier: string}[], sf: ts.SourceFile) { + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { output.prepend('\n// ADD IMPORTS\n'); } addExports(output: MagicString, baseEntryPointPath: string, exports: { diff --git a/packages/compiler-cli/src/ngtsc/translator/index.ts b/packages/compiler-cli/src/ngtsc/translator/index.ts index b71e3e79001b..44120dae41a8 100644 --- a/packages/compiler-cli/src/ngtsc/translator/index.ts +++ b/packages/compiler-cli/src/ngtsc/translator/index.ts @@ -6,4 +6,4 @@ * found in the LICENSE file at https://angular.io/license */ -export {ImportManager, translateExpression, translateStatement, translateType} from './src/translator'; +export {Import, ImportManager, NamedImport, translateExpression, translateStatement, translateType} from './src/translator'; diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 94f630ae6be4..f1b6e478ffb0 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -38,6 +38,27 @@ const BINARY_OPERATORS = new Map([ [BinaryOperator.Plus, ts.SyntaxKind.PlusToken], ]); +/** + * Information about an import that has been added to a module. + */ +export interface Import { + /** The name of the module that has been imported. */ + specifier: string; + /** The alias of the imported module. */ + qualifier: string; +} + +/** + * The symbol name and import namespace of an imported symbol, + * which has been registered through the ImportManager. + */ +export interface NamedImport { + /** The import namespace containing this imported symbol. */ + moduleImport: string|null; + /** The (possibly rewritten) name of the imported symbol. */ + symbol: string; +} + export class ImportManager { private specifierToIdentifier = new Map(); private nextIndex = 0; @@ -45,8 +66,7 @@ export class ImportManager { constructor(protected rewriter: ImportRewriter = new NoopImportRewriter(), private prefix = 'i') { } - generateNamedImport(moduleName: string, originalSymbol: string): - {moduleImport: string | null, symbol: string} { + generateNamedImport(moduleName: string, originalSymbol: string): NamedImport { // First, rewrite the symbol name. const symbol = this.rewriter.rewriteSymbol(originalSymbol, moduleName); @@ -67,7 +87,7 @@ export class ImportManager { return {moduleImport, symbol}; } - getAllImports(contextPath: string): {specifier: string, qualifier: string}[] { + getAllImports(contextPath: string): Import[] { const imports: {specifier: string, qualifier: string}[] = []; this.specifierToIdentifier.forEach((qualifier, specifier) => { specifier = this.rewriter.rewriteSpecifier(specifier, contextPath); From 26bd454d4adb3f367dab566ab61cfa75f27a53e0 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 03/15] test(ivy): fix ESM5 test code to use `var` rather than `const` --- packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 657b299b316b..9c38c7f4fb8e 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -402,7 +402,7 @@ const IMPORTS_FILES = [ { name: '/a.js', contents: ` - export const a = 'a'; + export var a = 'a'; `, }, { @@ -422,7 +422,7 @@ const EXPORTS_FILES = [ { name: '/a.js', contents: ` - export const a = 'a'; + export var a = 'a'; `, }, { From d356146bbf45c0699efd6aa19308b2e3ec4269a6 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 04/15] test(ivy): enhance the in-memory-typescript helper The `getDeclaration()` function now searches down into the AST for matching nodes, which is needed for UMD testing. --- .../src/ngtsc/testing/in_memory_typescript.ts | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts index 51bb2be60df9..5ceffe41189e 100644 --- a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts +++ b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts @@ -117,30 +117,7 @@ export function getDeclaration( if (!sf) { throw new Error(`No such file: ${fileName}`); } - - let chosenDecl: ts.Declaration|null = null; - - sf.statements.forEach(stmt => { - if (chosenDecl !== null) { - return; - } else if (ts.isVariableStatement(stmt)) { - stmt.declarationList.declarations.forEach(decl => { - if (bindingNameEquals(decl.name, name)) { - chosenDecl = decl; - } - }); - } else if (ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) { - if (stmt.name !== undefined && stmt.name.text === name) { - chosenDecl = stmt; - } - } else if ( - ts.isImportDeclaration(stmt) && stmt.importClause !== undefined && - stmt.importClause.name !== undefined && stmt.importClause.name.text === name) { - chosenDecl = stmt.importClause; - } - }); - - chosenDecl = chosenDecl as ts.Declaration | null; + const chosenDecl = walkForDeclaration(sf); if (chosenDecl === null) { throw new Error(`No such symbol: ${name} in ${fileName}`); @@ -148,6 +125,33 @@ export function getDeclaration( if (!assert(chosenDecl)) { throw new Error(`Symbol ${name} from ${fileName} is a ${ts.SyntaxKind[chosenDecl.kind]}`); } - return chosenDecl; + + // We walk the AST tree looking for a declaration that matches + function walkForDeclaration(rootNode: ts.Node): ts.Declaration|null { + let chosenDecl: ts.Declaration|null = null; + rootNode.forEachChild(node => { + if (chosenDecl !== null) { + return; + } + if (ts.isVariableStatement(node)) { + node.declarationList.declarations.forEach(decl => { + if (bindingNameEquals(decl.name, name)) { + chosenDecl = decl; + } + }); + } else if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) { + if (node.name !== undefined && node.name.text === name) { + chosenDecl = node; + } + } else if ( + ts.isImportDeclaration(node) && node.importClause !== undefined && + node.importClause.name !== undefined && node.importClause.name.text === name) { + chosenDecl = node.importClause; + } else { + chosenDecl = walkForDeclaration(node); + } + }); + return chosenDecl; + } } From 84a92324f8907cc140fb9cc10d2fd664f97c53e1 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 05/15] fix(ivy): ngcc - support namespaced identifiers In UMD formats, imports are always namespaced. This commit makes ngcc more tolerant of such structures. --- .../ngcc/src/host/esm2015_host.ts | 84 ++++++++++++------- .../ngcc/test/host/esm2015_host_spec.ts | 39 +++++++++ .../ngcc/test/host/esm5_host_spec.ts | 45 ++++++++++ 3 files changed, 137 insertions(+), 31 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 3758af6851e1..e2d666773edb 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -305,19 +305,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N return null; } - /** - * Determine if an identifier was imported from another module and return `Import` metadata - * describing its origin. - * - * @param id a TypeScript `ts.Identifer` to reflect. - * - * @returns metadata about the `Import` if the identifier was imported from another module, or - * `null` if the identifier doesn't resolve to an import but instead is locally defined. - */ - getImportOfIdentifier(id: ts.Identifier): Import|null { - return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id); - } - /** * Find all the classes that contain decorations in a given file. * @param sourceFile The source file to search for decorated classes. @@ -877,14 +864,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N const decorator = reflectObjectLiteral(node); // Is the value of the `type` property an identifier? - const typeIdentifier = decorator.get('type'); - if (typeIdentifier && ts.isIdentifier(typeIdentifier)) { - decorators.push({ - name: typeIdentifier.text, - identifier: typeIdentifier, - import: this.getImportOfIdentifier(typeIdentifier), node, - args: getDecoratorArgs(node), - }); + let typeIdentifier = decorator.get('type'); + if (typeIdentifier) { + if (ts.isPropertyAccessExpression(typeIdentifier)) { + // the type is in a namespace, e.g. `core.Directive` + typeIdentifier = typeIdentifier.name; + } + if (ts.isIdentifier(typeIdentifier)) { + decorators.push({ + name: typeIdentifier.text, + identifier: typeIdentifier, + import: this.getImportOfIdentifier(typeIdentifier), node, + args: getDecoratorArgs(node), + }); + } } } }); @@ -1327,27 +1320,56 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N prop => !!prop.name && ts.isIdentifier(prop.name) && prop.name.text === 'ngModule') || null; - const ngModuleIdentifier = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) && - ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer || - null; - // If no `ngModule` property was found in an object literal return value, return `null` to - // indicate that the provided node does not appear to be a `ModuleWithProviders` function. - if (ngModuleIdentifier === null) { + if (!ngModuleProperty || !ts.isPropertyAssignment(ngModuleProperty)) { return null; } - const ngModuleDeclaration = this.getDeclarationOfIdentifier(ngModuleIdentifier); + // The ngModuleValue could be of the form `SomeModule` or `namespace_1.SomeModule` + const ngModuleValue = ngModuleProperty.initializer; + if (!ts.isIdentifier(ngModuleValue) && !ts.isPropertyAccessExpression(ngModuleValue)) { + return null; + } + + const ngModuleDeclaration = this.getDeclarationOfExpression(ngModuleValue); if (!ngModuleDeclaration) { throw new Error( - `Cannot find a declaration for NgModule ${ngModuleIdentifier.text} referenced in "${declaration!.getText()}"`); + `Cannot find a declaration for NgModule ${ngModuleValue.getText()} referenced in "${declaration!.getText()}"`); } if (!hasNameIdentifier(ngModuleDeclaration.node)) { return null; } - const ngModule = ngModuleDeclaration as Declaration; + return { + name, + ngModule: ngModuleDeclaration as Declaration, declaration, container + }; + } + + protected getDeclarationOfExpression(expression: ts.Expression): Declaration|null { + if (ts.isIdentifier(expression)) { + return this.getDeclarationOfIdentifier(expression); + } + + if (!ts.isPropertyAccessExpression(expression) || !ts.isIdentifier(expression.expression)) { + return null; + } + + const namespaceDecl = this.getDeclarationOfIdentifier(expression.expression); + if (!namespaceDecl || !ts.isSourceFile(namespaceDecl.node)) { + return null; + } + + const namespaceExports = this.getExportsOfModule(namespaceDecl.node); + if (namespaceExports === null) { + return null; + } + + if (!namespaceExports.has(expression.name.text)) { + return null; + } - return {name, ngModule, declaration, container}; + const exportDecl = namespaceExports.get(expression.name.text) !; + return {...exportDecl, viaModule: namespaceDecl.viaModule}; } } diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index 98e20267ffae..1da2f5b0eb4f 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -521,6 +521,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ name: '/src/functions.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; export class SomeService {} export class InternalModule {} export function aNumber() { return 42; } @@ -534,12 +535,14 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ export function ngModuleString() { return { ngModule: 'foo' }; } export function ngModuleObject() { return { ngModule: { foo: 42 } }; } export function externalNgModule() { return { ngModule: ExternalModule }; } + export function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } ` }, { name: '/src/methods.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; export class SomeService {} export class InternalModule { static aNumber() { return 42; } @@ -553,11 +556,13 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ static ngModuleString() { return { ngModule: 'foo' }; } static ngModuleObject() { return { ngModule: { foo: 42 } }; } static externalNgModule() { return { ngModule: ExternalModule }; } + static namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } instanceNgModuleIdentifier() { return { ngModule: InternalModule }; } instanceNgModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; } instanceNgModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; } instanceExternalNgModule() { return { ngModule: ExternalModule }; } + instanceNamespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } } ` }, @@ -574,6 +579,19 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ {name: '/src/module.js', contents: 'export class ExternalModule {}'}, ]; +const NAMESPACED_IMPORT_FILE = { + name: '/some_directive.js', + contents: ` + import * as core from '@angular/core'; + + class SomeDirective { + } + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + ` +}; + describe('Esm2015ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1348,6 +1366,25 @@ describe('Esm2015ReflectionHost', () => { expect(actualDeclaration !.viaModule).toBe('@angular/core'); }); + it('should return the source-file of an import namespace', () => { + const {program} = makeTestBundleProgram([NAMESPACED_IMPORT_FILE]); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, NAMESPACED_IMPORT_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifier = (((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.PropertyAccessExpression) + .expression as ts.Identifier; + + const expectedDeclarationNode = + program.getSourceFile('node_modules/@angular/core/index.d.ts') !; + const actualDeclaration = host.getDeclarationOfIdentifier(identifier); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + it('should return the original declaration of an aliased class', () => { const program = makeTestProgram(CLASS_EXPRESSION_FILE); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); @@ -1649,6 +1686,7 @@ describe('Esm2015ReflectionHost', () => { ['ngModuleWithEmptyProviders', 'InternalModule'], ['ngModuleWithProviders', 'InternalModule'], ['externalNgModule', 'ExternalModule'], + ['namespacedExternalNgModule', 'ExternalModule'], ]); }); @@ -1665,6 +1703,7 @@ describe('Esm2015ReflectionHost', () => { ['ngModuleWithEmptyProviders', 'InternalModule'], ['ngModuleWithProviders', 'InternalModule'], ['externalNgModule', 'ExternalModule'], + ['namespacedExternalNgModule', 'ExternalModule'], ]); }); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 9c38c7f4fb8e..c4966b90232e 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -643,6 +643,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ name: '/src/functions.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; var SomeService = (function() { function SomeService() {} @@ -664,6 +665,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ export function ngModuleString() { return { ngModule: 'foo' }; } export function ngModuleObject() { return { ngModule: { foo: 42 } }; } export function externalNgModule() { return { ngModule: ExternalModule }; } + export function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } export {SomeService, InternalModule}; ` }, @@ -671,6 +673,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ name: '/src/methods.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; var SomeService = (function() { function SomeService() {} return SomeService; @@ -683,6 +686,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ instanceNgModuleWithEmptyProviders: function() { return { ngModule: InternalModule, providers: [] }; }, instanceNgModuleWithProviders: function() { return { ngModule: InternalModule, providers: [SomeService] }; }, instanceExternalNgModule: function() { return { ngModule: ExternalModule }; }, + namespacedExternalNgModule = function() { return { ngModule: mod.ExternalModule }; }, }; InternalModule.aNumber = function() { return 42; }; InternalModule.aString = function() { return 'foo'; }; @@ -695,6 +699,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ InternalModule.ngModuleString = function() { return { ngModule: 'foo' }; }; InternalModule.ngModuleObject = function() { return { ngModule: { foo: 42 } }; }; InternalModule.externalNgModule = function() { return { ngModule: ExternalModule }; }; + InternalModule.namespacedExternalNgModule = function() { return { ngModule: mod.ExternalModule }; }; return InternalModule; }()); export {SomeService, InternalModule}; @@ -716,6 +721,22 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ {name: '/src/module.js', contents: 'export class ExternalModule {}'}, ]; +const NAMESPACED_IMPORT_FILE = { + name: '/some_directive.js', + contents: ` + import * as core from '@angular/core'; + + var SomeDirective = (function() { + function SomeDirective() { + } + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + return SomeDirective; + }()); + ` +}; + describe('Esm5ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1500,6 +1521,25 @@ describe('Esm5ReflectionHost', () => { expect(actualDeclaration !.viaModule).toBe('@angular/core'); }); + it('should return the source-file of an import namespace', () => { + const program = makeTestProgram(NAMESPACED_IMPORT_FILE); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, NAMESPACED_IMPORT_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifier = (((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.PropertyAccessExpression) + .expression as ts.Identifier; + + const expectedDeclarationNode = + program.getSourceFile('node_modules/@angular/core/index.d.ts') !; + const actualDeclaration = host.getDeclarationOfIdentifier(identifier); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + it('should return the correct declaration for an inner function identifier inside an ES5 IIFE', () => { const superGetDeclarationOfIdentifierSpy = @@ -1851,6 +1891,7 @@ describe('Esm5ReflectionHost', () => { ['ngModuleWithEmptyProviders', 'InternalModule'], ['ngModuleWithProviders', 'InternalModule'], ['externalNgModule', 'ExternalModule'], + ['namespacedExternalNgModule', 'ExternalModule'], ]); }); @@ -1877,6 +1918,10 @@ describe('Esm5ReflectionHost', () => { 'function() { return { ngModule: ExternalModule }; }', 'ExternalModule', ], + [ + 'function() { return { ngModule: mod.ExternalModule }; }', + 'ExternalModule', + ], ]); }); From 74dae75456d9650da31c1ccba7ac21bc3b658a55 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 06/15] refactor(ivy): ngcc - abstract how module statements are found This will be important for UMD support. --- packages/compiler-cli/ngcc/src/host/esm2015_host.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index e2d666773edb..d65a6f32d65a 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -312,7 +312,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[] { const classes: DecoratedClass[] = []; - sourceFile.statements.map(statement => { + this.getModuleStatements(sourceFile).forEach(statement => { if (ts.isVariableStatement(statement)) { statement.declarationList.declarations.forEach(declaration => { const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(declaration)); @@ -474,6 +474,16 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N this.aliasedClassDeclarations.set(aliasedDeclaration.node, declaration.name); } + /** Get the top level statements for a module. + * + * In ES5 and ES2015 this is just the top level statements of the file. + * @param sourceFile The module whose statements we want. + * @returns An array of top level statements for the given module. + */ + protected getModuleStatements(sourceFile: ts.SourceFile): ts.Statement[] { + return Array.from(sourceFile.statements); + } + protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null { const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS); if (decoratorsProperty) { From 0462a4e35af0787a59e1e4f36f46f0c7753fedd6 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 07/15] refactor(ivy): ngcc - fake core and tslib should be typings files Previously these fake files were full TypeScript source files (`.ts`) but this is not necessary as we only need the typings not the implementation. --- .../compiler-cli/ngcc/test/helpers/utils.ts | 45 +++++++------------ .../host/esm2015_host_import_helper_spec.ts | 2 +- .../ngcc/test/host/esm2015_host_spec.ts | 7 ++- .../test/host/esm5_host_import_helper_spec.ts | 2 +- .../ngcc/test/host/esm5_host_spec.ts | 7 ++- 5 files changed, 25 insertions(+), 38 deletions(-) diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index f4eae77e54e0..5463251b4696 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -64,49 +64,38 @@ export function makeTestProgram( // TODO: unify this with the //packages/compiler-cli/test/ngtsc/fake_core package export function getFakeCore() { return { - name: 'node_modules/@angular/core/index.ts', + name: 'node_modules/@angular/core/index.d.ts', contents: ` type FnWithArg = (arg?: any) => T; - function callableClassDecorator(): FnWithArg<(clazz: any) => any> { - return null !; - } - - function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> { - return null !; - } - - function makePropDecorator(): any { - } - - export const Component = callableClassDecorator(); - export const Directive = callableClassDecorator(); - export const Injectable = callableClassDecorator(); - export const NgModule = callableClassDecorator(); + export declare const Component: FnWithArg<(clazz: any) => any>; + export declare const Directive: FnWithArg<(clazz: any) => any>; + export declare const Injectable: FnWithArg<(clazz: any) => any>; + export declare const NgModule: FnWithArg<(clazz: any) => any>; - export const Input = makePropDecorator(); + export declare const Input: any; - export const Inject = callableParamDecorator(); - export const Self = callableParamDecorator(); - export const SkipSelf = callableParamDecorator(); - export const Optional = callableParamDecorator(); + export declare const Inject: FnWithArg<(a: any, b: any, c: any) => void>; + export declare const Self: FnWithArg<(a: any, b: any, c: any) => void>; + export declare const SkipSelf: FnWithArg<(a: any, b: any, c: any) => void>; + export declare const Optional: FnWithArg<(a: any, b: any, c: any) => void>; - export class InjectionToken { - constructor(name: string) {} + export declare class InjectionToken { + constructor(name: string); } - export interface ModuleWithProviders {} + export declare interface ModuleWithProviders {} ` }; } export function getFakeTslib() { return { - name: 'node_modules/tslib/index.ts', + name: 'node_modules/tslib/index.d.ts', contents: ` - export function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any) {} - export function __param(paramIndex: number, decorator: any) {} - export function __metadata(metadataKey: any, metadataValue: any) {} + export declare function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any); + export declare function __param(paramIndex: number, decorator: any); + export declare function __metadata(metadataKey: any, metadataValue: any); ` }; } diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts index da5834541ac5..44d6315a52ab 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts @@ -346,7 +346,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { null; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', + program, 'node_modules/@angular/core/index.d.ts', 'Directive', isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !); expect(actualDeclaration).not.toBe(null); diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index 1da2f5b0eb4f..75defb06006c 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -1359,7 +1359,8 @@ describe('Esm2015ReflectionHost', () => { .initializer as ts.Identifier; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', isNamedVariableDeclaration); + program, 'node_modules/@angular/core/index.d.ts', 'Directive', + isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); expect(actualDeclaration).not.toBe(null); expect(actualDeclaration !.node).toBe(expectedDeclarationNode); @@ -1419,9 +1420,7 @@ describe('Esm2015ReflectionHost', () => { const values = Array.from(exportDeclarations !.values()) .map(declaration => [declaration.node.getText(), declaration.viaModule]); expect(values).toEqual([ - // TODO clarify what is expected here... - // [`Directive = callableClassDecorator()`, '@angular/core'], - [`Directive = callableClassDecorator()`, null], + [`Directive: FnWithArg<(clazz: any) => any>`, null], [`a = 'a'`, null], [`b = a`, null], [`c = foo`, null], diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts index bcaadcc8a9f4..65b02de6ffca 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts @@ -365,7 +365,7 @@ describe('Esm5ReflectionHost [import helper style]', () => { null; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', + program, 'node_modules/@angular/core/index.d.ts', 'Directive', isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !); expect(actualDeclaration).not.toBe(null); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index c4966b90232e..76cc48b1565f 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -1514,7 +1514,8 @@ describe('Esm5ReflectionHost', () => { .initializer as ts.Identifier; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', isNamedVariableDeclaration); + program, 'node_modules/@angular/core/index.d.ts', 'Directive', + isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); expect(actualDeclaration).not.toBe(null); expect(actualDeclaration !.node).toBe(expectedDeclarationNode); @@ -1590,9 +1591,7 @@ describe('Esm5ReflectionHost', () => { const values = Array.from(exportDeclarations !.values()) .map(declaration => [declaration.node.getText(), declaration.viaModule]); expect(values).toEqual([ - // TODO: clarify what is expected here... - //[`Directive = callableClassDecorator()`, '@angular/core'], - [`Directive = callableClassDecorator()`, null], + [`Directive: FnWithArg<(clazz: any) => any>`, null], [`a = 'a'`, null], [`b = a`, null], [`c = foo`, null], From c36cc88a2e545ec5ccac686c473ce04f3b4689ab Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 08/15] test(ivy): ngcc - remove unnecessary code --- .../compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index 83c8b253cf0c..21e015357548 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -281,7 +281,7 @@ SOME DEFINITION TEXT }); it('should error if the compiledClass is not valid', () => { - const {renderer, host, sourceFile, program} = setup(PROGRAM); + const {renderer, sourceFile, program} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); const noIifeDeclaration = @@ -355,9 +355,7 @@ SOME DEFINITION TEXT expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); - expect(output.toString()).not.toContain(`C.decorators = [ - { type: Directive, args: [{ selector: '[c]' }] }, -];`); + expect(output.toString()).not.toContain(`C.decorators`); }); }); From bc2ddff4010d1ab0f5fbc600389e70a66c3ea2ea Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 09/15] feat(ivy): ngtsc - support namespaced `forwardRef` calls In some cases the `forwardRef` helper has been imported via a namespace, e.g. `core.forwardRef(...)`. This commit adds support for unwrapping such namespaced imports when ngtsc is statically evaluating code. --- .../compiler-cli/src/ngtsc/annotations/src/util.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index b8f9bff34024..a9866b33e82f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -230,15 +230,21 @@ function expandForwardRef(arg: ts.Expression): ts.Expression|null { */ export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost): ts.Expression { node = unwrapExpression(node); - if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || - node.arguments.length !== 1) { + if (!ts.isCallExpression(node) || node.arguments.length !== 1) { return node; } + + const fn = + ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression; + if (!ts.isIdentifier(fn)) { + return node; + } + const expr = expandForwardRef(node.arguments[0]); if (expr === null) { return node; } - const imp = reflector.getImportOfIdentifier(node.expression); + const imp = reflector.getImportOfIdentifier(fn); if (imp === null || imp.from !== '@angular/core' || imp.name !== 'forwardRef') { return node; } else { From 7e1eb3ab443ab1598efd72f4277d6cf2d671a4a1 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:34 +0100 Subject: [PATCH 10/15] feat(ivy): ngcc - implement `UmdReflectionHost` --- .../compiler-cli/ngcc/src/host/umd_host.ts | 278 +++ .../ngcc/test/host/umd_host_spec.ts | 1857 +++++++++++++++++ 2 files changed, 2135 insertions(+) create mode 100644 packages/compiler-cli/ngcc/src/host/umd_host.ts create mode 100644 packages/compiler-cli/ngcc/test/host/umd_host_spec.ts diff --git a/packages/compiler-cli/ngcc/src/host/umd_host.ts b/packages/compiler-cli/ngcc/src/host/umd_host.ts new file mode 100644 index 000000000000..33283a638013 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/host/umd_host.ts @@ -0,0 +1,278 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {Declaration, Import} from '../../../src/ngtsc/reflection'; +import {Logger} from '../logging/logger'; +import {BundleProgram} from '../packages/bundle_program'; +import {Esm5ReflectionHost} from './esm5_host'; + +export class UmdReflectionHost extends Esm5ReflectionHost { + protected umdModules = new Map(); + protected umdExports = new Map|null>(); + protected umdImportPaths = new Map(); + constructor( + logger: Logger, isCore: boolean, protected program: ts.Program, + protected compilerHost: ts.CompilerHost, dts?: BundleProgram|null) { + super(logger, isCore, program.getTypeChecker(), dts); + } + + getImportOfIdentifier(id: ts.Identifier): Import|null { + const importParameter = this.findUmdImportParameter(id); + const from = importParameter && this.getUmdImportPath(importParameter); + return from !== null ? {from, name: id.text} : null; + } + + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + return this.getUmdImportedDeclaration(id) || super.getDeclarationOfIdentifier(id); + } + + getExportsOfModule(module: ts.Node): Map|null { + return super.getExportsOfModule(module) || this.getUmdExports(module.getSourceFile()); + } + + getUmdModule(sourceFile: ts.SourceFile): UmdModule|null { + if (sourceFile.isDeclarationFile) { + return null; + } + if (!this.umdModules.has(sourceFile)) { + if (sourceFile.statements.length !== 1) { + throw new Error( + `Expected UMD module file (${sourceFile.fileName}) to contain exactly one statement, but found ${sourceFile.statements}.`); + } + this.umdModules.set(sourceFile, parseStatementForUmdModule(sourceFile.statements[0])); + } + return this.umdModules.get(sourceFile) !; + } + + getUmdImportPath(importParameter: ts.ParameterDeclaration): string|null { + if (this.umdImportPaths.has(importParameter)) { + return this.umdImportPaths.get(importParameter) !; + } + + const umdModule = this.getUmdModule(importParameter.getSourceFile()); + if (umdModule === null) { + return null; + } + + const imports = getImportsOfUmdModule(umdModule); + if (imports === null) { + return null; + } + + for (const i of imports) { + this.umdImportPaths.set(i.parameter, i.path); + if (i.parameter === importParameter) { + return i.path; + } + } + + return null; + } + + getUmdExports(sourceFile: ts.SourceFile): Map|null { + if (!this.umdExports.has(sourceFile)) { + const moduleExports = this.computeExportsOfUmdModule(sourceFile); + this.umdExports.set(sourceFile, moduleExports); + } + return this.umdExports.get(sourceFile) !; + } + + /** Get the top level statements for a module. + * + * In UMD modules these are the body of the UMD factory function. + * + * @param sourceFile The module whose statements we want. + * @returns An array of top level statements for the given module. + */ + protected getModuleStatements(sourceFile: ts.SourceFile): ts.Statement[] { + const umdModule = this.getUmdModule(sourceFile); + return umdModule !== null ? Array.from(umdModule.factoryFn.body.statements) : []; + } + + private computeExportsOfUmdModule(sourceFile: ts.SourceFile): Map|null { + const moduleMap = new Map(); + const exportStatements = this.getModuleStatements(sourceFile).filter(isUmdExportStatement); + const exportDeclarations = + exportStatements.map(statement => this.extractUmdExportDeclaration(statement)); + exportDeclarations.forEach(decl => { + if (decl) { + moduleMap.set(decl.name, decl.declaration); + } + }); + return moduleMap; + } + + private extractUmdExportDeclaration(statement: UmdExportStatement): UmdExportDeclaration|null { + const exportExpression = statement.expression.right; + const name = statement.expression.left.name.text; + + const declaration = this.getDeclarationOfExpression(exportExpression); + if (declaration === null) { + return null; + } + + return {name, declaration}; + } + + private findUmdImportParameter(id: ts.Identifier): ts.ParameterDeclaration|null { + // Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`? + // If so capture the symbol of the namespace, e.g. `core`. + const nsIdentifier = findNamespaceOfIdentifier(id); + const nsSymbol = nsIdentifier && this.checker.getSymbolAtLocation(nsIdentifier) || null; + + // Is the namespace a parameter on a UMD factory function, e.g. `function factory(this, core)`? + // If so then return its declaration. + const nsDeclaration = nsSymbol && nsSymbol.valueDeclaration; + return nsDeclaration && ts.isParameter(nsDeclaration) ? nsDeclaration : null; + } + + private getUmdImportedDeclaration(id: ts.Identifier): Declaration|null { + const importInfo = this.getImportOfIdentifier(id); + if (importInfo === null) { + return null; + } + + const importedFile = this.resolveModuleName(importInfo.from, id.getSourceFile()); + if (importedFile === undefined) { + return null; + } + + // We need to add the `viaModule` because the `getExportsOfModule()` call + // did not know that we were importing the declaration. + return {node: importedFile, viaModule: importInfo.from}; + } + + private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile + |undefined { + if (this.compilerHost.resolveModuleNames) { + const moduleInfo = + this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0]; + return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName); + } else { + const moduleInfo = ts.resolveModuleName( + moduleName, containingFile.fileName, this.program.getCompilerOptions(), + this.compilerHost); + return moduleInfo.resolvedModule && + this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName); + } + } +} + +export function parseStatementForUmdModule(statement: ts.Statement): UmdModule|null { + const wrapperCall = getUmdWrapperCall(statement); + if (!wrapperCall) return null; + + const wrapperFn = wrapperCall.expression; + if (!ts.isFunctionExpression(wrapperFn)) return null; + + const factoryFnParamIndex = wrapperFn.parameters.findIndex( + parameter => ts.isIdentifier(parameter.name) && parameter.name.text === 'factory'); + if (factoryFnParamIndex === -1) return null; + + const factoryFn = stripParentheses(wrapperCall.arguments[factoryFnParamIndex]); + if (!factoryFn || !ts.isFunctionExpression(factoryFn)) return null; + + return {wrapperFn, factoryFn}; +} + +function getUmdWrapperCall(statement: ts.Statement): ts.CallExpression& + {expression: ts.FunctionExpression}|null { + if (!ts.isExpressionStatement(statement) || !ts.isParenthesizedExpression(statement.expression) || + !ts.isCallExpression(statement.expression.expression) || + !ts.isFunctionExpression(statement.expression.expression.expression)) { + return null; + } + return statement.expression.expression as ts.CallExpression & {expression: ts.FunctionExpression}; +} + + +export function getImportsOfUmdModule(umdModule: UmdModule): + {parameter: ts.ParameterDeclaration, path: string}[] { + const imports: {parameter: ts.ParameterDeclaration, path: string}[] = []; + for (let i = 1; i < umdModule.factoryFn.parameters.length; i++) { + imports.push({ + parameter: umdModule.factoryFn.parameters[i], + path: getRequiredModulePath(umdModule.wrapperFn, i) + }); + } + return imports; +} + +interface UmdModule { + wrapperFn: ts.FunctionExpression; + factoryFn: ts.FunctionExpression; +} + +type UmdExportStatement = ts.ExpressionStatement & { + expression: + ts.BinaryExpression & {left: ts.PropertyAccessExpression & {expression: ts.Identifier}} +}; + +function isUmdExportStatement(s: ts.Statement): s is UmdExportStatement { + return ts.isExpressionStatement(s) && ts.isBinaryExpression(s.expression) && + ts.isPropertyAccessExpression(s.expression.left) && + ts.isIdentifier(s.expression.left.expression) && + s.expression.left.expression.text === 'exports'; +} + +interface UmdExportDeclaration { + name: string; + declaration: Declaration; +} + +function getRequiredModulePath(wrapperFn: ts.FunctionExpression, paramIndex: number): string { + const statement = wrapperFn.body.statements[0]; + if (!ts.isExpressionStatement(statement)) { + throw new Error( + 'UMD wrapper body is not an expression statement:\n' + wrapperFn.body.getText()); + } + const modulePaths: string[] = []; + findModulePaths(statement.expression); + + // Since we were only interested in the `require()` calls, we miss the `exports` argument, so we + // need to subtract 1. + // E.g. `function(exports, dep1, dep2)` maps to `function(exports, require('path/to/dep1'), + // require('path/to/dep2'))` + return modulePaths[paramIndex - 1]; + + // Search the statement for calls to `require('...')` and extract the string value of the first + // argument + function findModulePaths(node: ts.Node) { + if (isRequireCall(node)) { + const argument = node.arguments[0]; + if (ts.isStringLiteral(argument)) { + modulePaths.push(argument.text); + } + } else { + node.forEachChild(findModulePaths); + } + } +} + +function isRequireCall(node: ts.Node): node is ts.CallExpression { + return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && + node.expression.text === 'require' && node.arguments.length === 1; +} + +/** + * If the identifier `id` is the RHS of a property access of the form `namespace.id` + * and `namespace` is an identifer then return `namespace`, otherwise `null`. + * @param id The identifier whose namespace we want to find. + */ +function findNamespaceOfIdentifier(id: ts.Identifier): ts.Identifier|null { + return id.parent && ts.isPropertyAccessExpression(id.parent) && + ts.isIdentifier(id.parent.expression) ? + id.parent.expression : + null; +} + +export function stripParentheses(node: ts.Node): ts.Node { + return ts.isParenthesizedExpression(node) ? node.expression : node; +} \ No newline at end of file diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts new file mode 100644 index 000000000000..bf76f5a5d96e --- /dev/null +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -0,0 +1,1857 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {getIifeBody} from '../../src/host/esm5_host'; +import {UmdReflectionHost} from '../../src/host/umd_host'; +import {MockLogger} from '../helpers/mock_logger'; +import {getDeclaration, makeTestBundleProgram} from '../helpers/utils'; + +import {expectTypeValueReferencesForParameters} from './util'; + +const SOME_DIRECTIVE_FILE = { + name: '/some_directive.umd.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('some_directive', ['exports', '@angular/core'], factory) : + (factory(global.some_directive,global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + + var INJECTED_TOKEN = new InjectionToken('injected'); + var ViewContainerRef = {}; + var TemplateRef = {}; + + var SomeDirective = (function() { + function SomeDirective(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + } + SomeDirective.prototype = { + instanceMethod: function() {}, + }; + SomeDirective.staticMethod = function() {}; + SomeDirective.staticProperty = 'static'; + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = function() { return [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: core.Inject, args: [INJECTED_TOKEN,] },] }, + ]; }; + SomeDirective.propDecorators = { + "input1": [{ type: core.Input },], + "input2": [{ type: core.Input },], + }; + return SomeDirective; + }()); + exports.SomeDirective = SomeDirective; +})));`, +}; + +const SIMPLE_ES2015_CLASS_FILE = { + name: '/simple_es2015_class.d.ts', + contents: ` + export class EmptyClass {} + `, +}; + +const SIMPLE_CLASS_FILE = { + name: '/simple_class.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('simple_class', ['exports'], factory) : + (factory(global.simple_class)); +}(this, (function (exports) { 'use strict'; + var EmptyClass = (function() { + function EmptyClass() { + } + return EmptyClass; + }()); + var NoDecoratorConstructorClass = (function() { + function NoDecoratorConstructorClass(foo) { + } + return NoDecoratorConstructorClass; + }()); + exports.EmptyClass = EmptyClass; + exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass; +})));`, +}; + +const FOO_FUNCTION_FILE = { + name: '/foo_function.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('foo_function', ['exports', '@angular/core'], factory) : + (factory(global.foo_function,global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + function foo() {} + foo.decorators = [ + { type: core.Directive, args: [{ selector: '[ignored]' },] } + ]; + exports.foo = foo; +})));`, +}; + +const INVALID_DECORATORS_FILE = { + name: '/invalid_decorators.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_decorators', ['exports', '@angular/core'], factory) : + (factory(global.invalid_decorators, global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = () => [ + { type: core.Directive, args: [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; + }()); + + var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.decorators = [ + "This is not an object literal", + { type: core.Directive }, + ]; + return NotObjectLiteral; + }()); + + var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.decorators = [ + { notType: core.Directive }, + { type: core.Directive }, + ]; + return NoTypeProperty; + }()); + + var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.decorators = [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Directive }, + ]; + return NotIdentifier; + }()); +})));`, +}; + +const INVALID_DECORATOR_ARGS_FILE = { + name: '/invalid_decorator_args.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_decorator_args', ['exports', '@angular/core'], factory) : + (factory(global.invalid_decorator_args, global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.decorators = [ + { type: core.Directive }, + ]; + return NoArgsProperty; + }()); + + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.decorators = [ + { type: core.Directive, args }, + ]; + return NoPropertyAssignment; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = [ + { type: core.Directive, args: () => [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; + }()); +})));`, +}; + +const INVALID_PROP_DECORATORS_FILE = { + name: '/invalid_prop_decorators.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_prop_decorators', ['exports', '@angular/core'], factory) : + (factory(global.invalid_prop_decorators, global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.propDecorators = () => ({ + "prop": [{ type: core.Directive },] + }); + return NotObjectLiteral; + }()); + + var NotObjectLiteralProp = (function() { + function NotObjectLiteralProp() { + } + NotObjectLiteralProp.propDecorators = { + "prop": [ + "This is not an object literal", + { type: core.Directive }, + ] + }; + return NotObjectLiteralProp; + }()); + + var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.propDecorators = { + "prop": [ + { notType: core.Directive }, + { type: core.Directive }, + ] + }; + return NoTypeProperty; + }()); + + var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.propDecorators = { + "prop": [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Directive }, + ] + }; + return NotIdentifier; + }()); +})));`, +}; + +const INVALID_PROP_DECORATOR_ARGS_FILE = { + name: '/invalid_prop_decorator_args.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_prop_decorator_args', ['exports', '@angular/core'], factory) : + (factory(global.invalid_prop_decorator_args, global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.propDecorators = { + "prop": [{ type: core.Input },] + }; + return NoArgsProperty; + }()); + + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.propDecorators = { + "prop": [{ type: core.Input, args },] + }; + return NoPropertyAssignment; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.propDecorators = { + "prop": [{ type: core.Input, args: () => [{ selector: '[ignored]' },] },], + }; + return NotArrayLiteral; + }()); +})));`, +}; + +const INVALID_CTOR_DECORATORS_FILE = { + name: '/invalid_ctor_decorators.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_ctor_decorators', ['exports', '@angular/core'], factory) : + (factory(global.invalid_ctor_decorators,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var NoParameters = (function() { + function NoParameters() {} + return NoParameters; + }()); + + var ArrowFunction = (function() { + function ArrowFunction(arg1) { + } + ArrowFunction.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: core.Inject },] } + ]; + return ArrowFunction; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return 'StringsAreNotArrayLiterals'; }; + return NotArrayLiteral; + }()); + + var NotObjectLiteral = (function() { + function NotObjectLiteral(arg1, arg2) { + } + NotObjectLiteral.ctorParameters = function() { return [ + "This is not an object literal", + { type: 'ParamType', decorators: [{ type: core.Inject },] }, + ]; }; + return NotObjectLiteral; + }()); + + var NoTypeProperty = (function() { + function NoTypeProperty(arg1, arg2) { + } + NoTypeProperty.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { notType: core.Inject }, + { type: core.Inject }, + ] + }, + ]; }; + return NoTypeProperty; + }()); + + var NotIdentifier = (function() { + function NotIdentifier(arg1, arg2) { + } + NotIdentifier.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Inject }, + ] + }, + ]; }; + return NotIdentifier; + }()); +})));`, +}; + +const INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: '/invalid_ctor_decorator_args.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_ctor_decorator_args', ['exports', '@angular/core'], factory) : + (factory(global.invalid_ctor_decorator_args,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var NoArgsProperty = (function() { + function NoArgsProperty(arg1) { + } + NoArgsProperty.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject },] }, + ]; }; + return NoArgsProperty; + }()); + + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment(arg1) { + } + NoPropertyAssignment.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject, args },] }, + ]; }; + return NoPropertyAssignment; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject, args: () => [{ selector: '[ignored]' },] },] }, + ]; }; + return NotArrayLiteral; + }()); +})));`, +}; + +const IMPORTS_FILES = [ + { + name: '/file_a.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('file_a', ['exports'], factory) : + (factory(global.file_a)); +}(this, (function (exports) { 'use strict'; + var a = 'a'; + exports.a = a; +})));`, + }, + { + name: '/file_b.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./file_a')) : + typeof define === 'function' && define.amd ? define('file_b', ['exports', './file_a'], factory) : + (factory(global.file_b,global.file_a)); +}(this, (function (exports, file_a) { 'use strict'; + var b = file_a.a; + var c = 'c'; + var d = c; +})));`, + }, + { + name: '/file_c.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./file_a')) : + typeof define === 'function' && define.amd ? define('file_c', ['exports', 'file_a'], factory) : + (factory(global.file_c,global.file_a)); +}(this, function (exports, file_a) { 'use strict'; + var c = file_a.a; +}));`, + }, +]; + +const EXPORTS_FILES = [ + { + name: '/a_module.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('a_module', ['exports'], factory) : + (factory(global.a_module)); +}(this, (function (exports) { 'use strict'; + var a = 'a'; + exports.a = a; +})));`, + }, + { + name: '/b_module.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('/a_module')) : + typeof define === 'function' && define.amd ? define('b_module', ['exports', '@angular/core', 'a_module'], factory) : + (factory(global.b_module)); +}(this, (function (exports, core, a_module) { 'use strict'; + var b = a_module.a; + var e = 'e'; + var SomeClass = (function() { + function SomeClass() {} + return SomeClass; + }()); + + exports.Directive = core.Directive; + exports.a = a_module.a; + exports.b = b; + exports.c = a_module.a; + exports.d = b; + exports.e = e; + exports.DirectiveX = core.Directive; + exports.SomeClass = SomeClass; +})));`, + }, +]; + +const FUNCTION_BODY_FILE = { + name: '/function_body.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('function_body', ['exports'], factory) : + (factory(global.function_body)); +}(this, (function (exports) { 'use strict'; + function foo(x) { + return x; + } + function bar(x, y) { + if (y === void 0) { y = 42; } + return x + y; + } + function complex() { + var x = 42; + return 42; + } + function baz(x) { + var y; + if (x === void 0) { y = 42; } + return y; + } + var y; + function qux(x) { + if (x === void 0) { y = 42; } + return y; + } + function moo() { + var x; + if (x === void 0) { x = 42; } + return x; + } + var x; + function juu() { + if (x === void 0) { x = 42; } + return x; + } +})));` +}; + +const DECORATED_FILES = [ + { + name: '/primary.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('./secondary')) : + typeof define === 'function' && define.amd ? define('primary', ['exports', '@angular/core', './secondary'], factory) : + (factory(global.primary,global.ng.core, global.secondary)); + }(this, (function (exports,core,secondary) { 'use strict'; + var A = (function() { + function A() {} + A.decorators = [ + { type: core.Directive, args: [{ selector: '[a]' }] } + ]; + return A; + }()); + var B = (function() { + function B() {} + B.decorators = [ + { type: core.Directive, args: [{ selector: '[b]' }] } + ]; + return B; + }()); + function x() {} + function y() {} + var C = (function() { + function C() {} + return C; + }); + exports.A = A; + exports.x = x; + exports.C = C; + })));` + }, + { + name: '/secondary.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('primary', ['exports', '@angular/core'], factory) : + (factory(global.primary,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var D = (function() { + function D() {} + D.decorators = [ + { type: core.Directive, args: [{ selector: '[d]' }] } + ]; + return D; + }()); + exports.D = D; + }))); + ` + } +]; + +const TYPINGS_SRC_FILES = [ + { + name: '/src/index.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./internal'), require('./class1'), require('./class2')) : + typeof define === 'function' && define.amd ? define('index', ['exports', './internal', './class1', './class2'], factory) : + (factory(global.index,global.internal,global.class1,global.class2)); + }(this, (function (exports,internal,class1,class2) { 'use strict'; + function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; + } + var InternalClass = internal.InternalClass; + __export(class1); + __export(class2); + }))); + ` + }, + { + name: '/src/class1.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('class1', ['exports'], factory) : + (factory(global.class1)); + }(this, (function (exports) { 'use strict'; + var Class1 = (function() { + function Class1() {} + return Class1; + }()); + var MissingClass1 = (function() { + function MissingClass1() {} + return MissingClass1; + }()); + exports.Class1 = Class1; + exports.MissingClass1 = MissingClass1; + }))); + ` + }, + { + name: '/src/class2.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('class2', ['exports'], factory) : + (factory(global.class2)); + }(this, (function (exports) { 'use strict'; + var Class2 = (function() { + function Class2() {} + return Class2; + }()); + exports.Class2 = Class2; + }))); + ` + }, + {name: '/src/func1.js', contents: 'function mooFn() {} export {mooFn}'}, { + name: '/src/internal.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('internal', ['exports'], factory) : + (factory(global.internal)); + }(this, (function (exports) { 'use strict'; + var InternalClass = (function() { + function InternalClass() {} + return InternalClass; + }()); + var Class2 = (function() { + function Class2() {} + return Class2; + }()); + exports.InternalClass =InternalClass; + exports.Class2 = Class2; + }))); + ` + }, + { + name: '/src/missing-class.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('missingClass', ['exports'], factory) : + (factory(global.missingClass)); + }(this, (function (exports) { 'use strict'; + var MissingClass2 = (function() { + function MissingClass2() {} + return MissingClass2; + }()); + exports. MissingClass2 = MissingClass2; + }))); + ` + }, + { + name: '/src/flat-file.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('missingClass', ['exports'], factory) : + (factory(global.missingClass)); + }(this, (function (exports) { 'use strict'; + var Class1 = (function() { + function Class1() {} + return Class1; + }()); + var MissingClass1 = (function() { + function MissingClass1() {} + return MissingClass1; + }()); + var MissingClass2 = (function() { + function MissingClass2() {} + return MissingClass2; + }()); + var Class3 = (function() { + function Class3() {} + return Class3; + }()); + exports.Class1 = Class1; + exports.xClass3 = Class3; + exports.MissingClass1 = MissingClass1; + exports.MissingClass2 = MissingClass2; + }))); + ` + } +]; + +const TYPINGS_DTS_FILES = [ + { + name: '/typings/index.d.ts', + contents: + `import {InternalClass} from './internal'; export * from './class1'; export * from './class2';` + }, + { + name: '/typings/class1.d.ts', + contents: `export declare class Class1 {}\nexport declare class OtherClass {}` + }, + { + name: '/typings/class2.d.ts', + contents: + `export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';` + }, + {name: '/typings/func1.d.ts', contents: 'export declare function mooFn(): void;'}, + { + name: '/typings/internal.d.ts', + contents: `export declare class InternalClass {}\nexport declare class Class2 {}` + }, + {name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`}, +]; + +const MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: '/src/functions.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./module')) : + typeof define === 'function' && define.amd ? define('functions', ['exports', './module'], factory) : + (factory(global.functions,global.module)); + }(this, (function (exports,module) { 'use strict'; + var SomeService = (function() { + function SomeService() {} + return SomeService; + }()); + + var InternalModule = (function() { + function InternalModule() {} + return InternalModule; + }()); + + function aNumber() { return 42; } + function aString() { return 'foo'; } + function emptyObject() { return {}; } + function ngModuleIdentifier() { return { ngModule: InternalModule }; } + function ngModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; } + function ngModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; } + function onlyProviders() { return { providers: [SomeService] }; } + function ngModuleNumber() { return { ngModule: 42 }; } + function ngModuleString() { return { ngModule: 'foo' }; } + function ngModuleObject() { return { ngModule: { foo: 42 } }; } + function externalNgModule() { return { ngModule: module.ExternalModule }; } + // NOTE: We do not include the "namespaced" export tests in UMD as all UMD exports are already namespaced. + // function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } + + exports.aNumber = aNumber; + exports.aString = aString; + exports.emptyObject = emptyObject; + exports.ngModuleIdentifier = ngModuleIdentifier; + exports.ngModuleWithEmptyProviders = ngModuleWithEmptyProviders; + exports.ngModuleWithProviders = ngModuleWithProviders; + exports.onlyProviders = onlyProviders; + exports.ngModuleNumber = ngModuleNumber; + exports.ngModuleString = ngModuleString; + exports.ngModuleObject = ngModuleObject; + exports.externalNgModule = externalNgModule; + exports.SomeService = SomeService; + exports.InternalModule = InternalModule; + }))); + ` + }, + { + name: '/src/methods.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./module')) : + typeof define === 'function' && define.amd ? define('methods', ['exports', './module'], factory) : + (factory(global.methods,global.module)); + }(this, (function (exports,module) { 'use strict'; + var SomeService = (function() { + function SomeService() {} + return SomeService; + }()); + + var InternalModule = (function() { + function InternalModule() {} + InternalModule.prototype = { + instanceNgModuleIdentifier: function() { return { ngModule: InternalModule }; }, + instanceNgModuleWithEmptyProviders: function() { return { ngModule: InternalModule, providers: [] }; }, + instanceNgModuleWithProviders: function() { return { ngModule: InternalModule, providers: [SomeService] }; }, + instanceExternalNgModule: function() { return { ngModule: module.ExternalModule }; }, + }; + InternalModule.aNumber = function() { return 42; }; + InternalModule.aString = function() { return 'foo'; }; + InternalModule.emptyObject = function() { return {}; }; + InternalModule.ngModuleIdentifier = function() { return { ngModule: InternalModule }; }; + InternalModule.ngModuleWithEmptyProviders = function() { return { ngModule: InternalModule, providers: [] }; }; + InternalModule.ngModuleWithProviders = function() { return { ngModule: InternalModule, providers: [SomeService] }; }; + InternalModule.onlyProviders = function() { return { providers: [SomeService] }; }; + InternalModule.ngModuleNumber = function() { return { ngModule: 42 }; }; + InternalModule.ngModuleString = function() { return { ngModule: 'foo' }; }; + InternalModule.ngModuleObject = function() { return { ngModule: { foo: 42 } }; }; + InternalModule.externalNgModule = function() { return { ngModule: module.ExternalModule }; }; + return InternalModule; + }()); + + exports.SomeService = SomeService; + exports.InternalModule = InternalModule; + }))); + ` + }, + { + name: '/src/aliased_class.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('aliased_class', ['exports'], factory) : + (factory(global.aliased_class)); + }(this, (function (exports,module) { 'use strict'; + var AliasedModule = (function() { + function AliasedModule() {} + AliasedModule_1 = AliasedModule; + AliasedModule.forRoot = function() { return { ngModule: AliasedModule_1 }; }; + var AliasedModule_1; + return AliasedModule; + }()); + exports.AliasedModule = AliasedModule; + }))); + ` + }, + { + name: '/src/module.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('module', ['exports'], factory) : + (factory(global.module)); + }(this, (function (exports,module) { 'use strict'; + var ExternalModule = (function() { + function ExternalModule() {} + return ExternalModule; + }()); + exports.ExternalModule = ExternalModule; + }))); + ` + }, +]; + + +describe('UmdReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + + it('should return null if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const decorators = host.getDecoratorsOfDeclaration(functionNode); + expect(decorators).toBe(null); + }); + + it('should return null if there are no decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toBe(null); + }); + + it('should ignore `decorators` if it is not an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); + + it('should ignore decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'}; + const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo); + + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find non decorated properties on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticMethod = members.find(member => member.name === 'staticMethod') !; + expect(staticMethod.kind).toEqual(ClassMemberKind.Method); + expect(staticMethod.isStatic).toEqual(true); + expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should throw if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(() => { + host.getMembersOfClass(functionNode); + }).toThrowError(`Attempted to get members of a non-class: "function foo() {}"`); + }); + + it('should return an empty array if there are no prop decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members).toEqual([]); + }); + + it('should not process decorated properties in `propDecorators` if it is not an object literal', + () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members.map(member => member.name)).not.toContain('prop'); + }); + + it('should ignore prop decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const mockImportInfo = { name: 'mock', from: '@angular/core' } as Import; + const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo); + + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + null, + ]); + }); + + it('should throw if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(() => { host.getConstructorParameters(functionNode); }) + .toThrowError( + 'Attempted to get constructor parameters of a non-class: "function foo() {}"'); + }); + + // In ES5 there is no such thing as a constructor-less class + // it('should return `null` if there is no constructor', () => { }); + + it('should return an array even if there are no decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual(jasmine.any(Array)); + expect(parameters !.length).toEqual(1); + expect(parameters ![0].name).toEqual('foo'); + expect(parameters ![0].decorators).toBe(null); + }); + + it('should return an empty array if there are no constructor parameters', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual([]); + }); + + // In ES5 there are no arrow functions + // it('should ignore `ctorParameters` if it is an arrow function', () => { }); + + it('should ignore `ctorParameters` if it does not return an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(2); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + expect(parameters ![1]).toEqual(jasmine.objectContaining({ + name: 'arg2', + decorators: jasmine.any(Array) as any + })); + }); + + it('should ignore param decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); + }); + + it('should ignore param decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'}; + const spy = spyOn(UmdReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters !.length).toBe(1); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if param decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FUNCTION_BODY_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + + const fooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', isNamedFunctionDeclaration) !; + const fooDef = host.getDefinitionOfFunction(fooNode); + expect(fooDef.node).toBe(fooNode); + expect(fooDef.body !.length).toEqual(1); + expect(fooDef.body ![0].getText()).toEqual(`return x;`); + expect(fooDef.parameters.length).toEqual(1); + expect(fooDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + + const barNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', isNamedFunctionDeclaration) !; + const barDef = host.getDefinitionOfFunction(barNode); + expect(barDef.node).toBe(barNode); + expect(barDef.body !.length).toEqual(1); + expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy(); + expect(barDef.body ![0].getText()).toEqual(`return x + y;`); + expect(barDef.parameters.length).toEqual(2); + expect(barDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + expect(barDef.parameters[1].name).toEqual('y'); + expect(barDef.parameters[1].initializer !.getText()).toEqual('42'); + + const bazNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', isNamedFunctionDeclaration) !; + const bazDef = host.getDefinitionOfFunction(bazNode); + expect(bazDef.node).toBe(bazNode); + expect(bazDef.body !.length).toEqual(3); + expect(bazDef.parameters.length).toEqual(1); + expect(bazDef.parameters[0].name).toEqual('x'); + expect(bazDef.parameters[0].initializer).toBe(null); + + const quxNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', isNamedFunctionDeclaration) !; + const quxDef = host.getDefinitionOfFunction(quxNode); + expect(quxDef.node).toBe(quxNode); + expect(quxDef.body !.length).toEqual(2); + expect(quxDef.parameters.length).toEqual(1); + expect(quxDef.parameters[0].name).toEqual('x'); + expect(quxDef.parameters[0].initializer).toBe(null); + }); + }); + + describe('getImportOfIdentifier', () => { + it('should find the import of an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_b.js', 'b', isNamedVariableDeclaration); + const identifier = + (variableNode.initializer && ts.isPropertyAccessExpression(variableNode.initializer)) ? + variableNode.initializer.name : + null; + + expect(identifier).not.toBe(null); + const importOfIdent = host.getImportOfIdentifier(identifier !); + expect(importOfIdent).toEqual({name: 'a', from: './file_a'}); + }); + + it('should return null if the identifier was not imported', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_b.js', 'd', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); + + it('should handle factory functions not wrapped in parentheses', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_c.js', 'c', isNamedVariableDeclaration); + const identifier = + (variableNode.initializer && ts.isPropertyAccessExpression(variableNode.initializer)) ? + variableNode.initializer.name : + null; + + expect(identifier).not.toBe(null); + const importOfIdent = host.getImportOfIdentifier(identifier !); + expect(importOfIdent).toEqual({name: 'a', from: './file_a'}); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier, + defaultImportStatement: null, + }).expression; + + const expectedDeclarationNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', isNamedVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + + it('should return the source-file of an import namespace', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifierOfDirective = (((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.PropertyAccessExpression) + .expression as ts.Identifier; + + const expectedDeclarationNode = + program.getSourceFile('node_modules/@angular/core/index.d.ts') !; + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + }); + + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + const {program, host: compilerHost} = makeTestBundleProgram(EXPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile(EXPORTS_FILES[1].name) !; + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.entries()) + .map(entry => [entry[0], entry[1].node.getText(), entry[1].viaModule])) + .toEqual([ + ['Directive', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'], + ['a', `a = 'a'`, '/a_module'], + ['b', `b = a_module.a`, null], + ['c', `a = 'a'`, '/a_module'], + ['d', `b = a_module.a`, null], + ['e', `e = 'e'`, null], + ['DirectiveX', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'], + [ + 'SomeClass', `SomeClass = (function() { + function SomeClass() {} + return SomeClass; + }())`, + null + ], + ]); + }); + + // Currently we do not support UMD versions of `export * from 'x';` + // because it gets compiled to something like: + // + // __export(m) { + // for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; + // } + // __export(x); + // + // So far all UMD formatted entry-points are flat so this should not occur. + // If it does later then we should implement parsing. + }); + + describe('getClassSymbol()', () => { + it('should return the class symbol for an ES2015 class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_ES2015_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for an ES5 class (outer variable declaration)', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for an ES5 class (inner function declaration)', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(innerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(outerNode); + }); + + it('should return the same class symbol (of the outer declaration) for outer and inner declarations', + () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + + expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + }); + + it('should return undefined if node is not an ES5 class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); + }); + + describe('isClass()', () => { + let host: UmdReflectionHost; + let mockNode: ts.Node; + let getClassDeclarationSpy: jasmine.Spy; + let superGetClassDeclarationSpy: jasmine.Spy; + + beforeEach(() => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + mockNode = {} as any; + + getClassDeclarationSpy = spyOn(UmdReflectionHost.prototype, 'getClassDeclaration'); + superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); + }); + + it('should return true if superclass returns true', () => { + superGetClassDeclarationSpy.and.returnValue(true); + getClassDeclarationSpy.and.callThrough(); + + expect(host.isClass(mockNode)).toBe(true); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return true if it can find a declaration for the class', () => { + getClassDeclarationSpy.and.returnValue(true); + + expect(host.isClass(mockNode)).toBe(true); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return false if it cannot find a declaration for the class', () => { + getClassDeclarationSpy.and.returnValue(false); + + expect(host.isClass(mockNode)).toBe(false); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + }); + + describe('hasBaseClass()', () => { + function hasBaseClass(source: string) { + const file = { + name: '/synthesized_constructors.js', + contents: source, + }; + + const {program, host: compilerHost} = makeTestBundleProgram([file]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + return host.hasBaseClass(classNode); + } + + it('should consider an IIFE with _super parameter as having a base class', () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(true); + }); + + it('should consider an IIFE with a unique name generated for the _super parameter as having a base class', + () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function (_super_1) { + __extends(TestClass, _super_1); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(true); + }); + + it('should not consider an IIFE without parameter as having a base class', () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function () { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(false); + }); + }); + + describe('findDecoratedClasses()', () => { + it('should return an array of all decorated classes in the given source file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const primary = program.getSourceFile(DECORATED_FILES[0].name) !; + + const primaryDecoratedClasses = host.findDecoratedClasses(primary); + expect(primaryDecoratedClasses.length).toEqual(2); + const classA = primaryDecoratedClasses.find(c => c.name === 'A') !; + expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + // Note that `B` is not exported from `primary.js` + const classB = primaryDecoratedClasses.find(c => c.name === 'B') !; + expect(classB.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + + const secondary = program.getSourceFile(DECORATED_FILES[1].name) !; + const secondaryDecoratedClasses = host.findDecoratedClasses(secondary); + expect(secondaryDecoratedClasses.length).toEqual(1); + // Note that `D` is exported from `secondary.js` but not exported from `primary.js` + const classD = secondaryDecoratedClasses.find(c => c.name === 'D') !; + expect(classD.name).toEqual('D'); + expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + }); + }); + + describe('getDtsDeclarationsOfClass()', () => { + it('should find the dts declaration that has the same relative path to the source file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class1 = getDeclaration(program, '/src/class1.js', 'Class1', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + }); + + it('should find the dts declaration for exported functions', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dtsProgram = makeTestBundleProgram(TYPINGS_DTS_FILES); + const mooFn = getDeclaration(program, '/src/func1.js', 'mooFn', ts.isFunctionDeclaration); + const host = + new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dtsProgram); + + const dtsDeclaration = host.getDtsDeclaration(mooFn); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/func1.d.ts'); + }); + + it('should return null if there is no matching class in the matching dts file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const missingClass = + getDeclaration(program, '/src/class1.js', 'MissingClass1', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); + + it('should return null if there is no matching dts file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const missingClass = getDeclaration( + program, '/src/missing-class.js', 'MissingClass2', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); + + it('should find the dts file that contains a matching class declaration, even if the source files do not match', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class1 = + getDeclaration(program, '/src/flat-file.js', 'Class1', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + }); + + it('should find aliased exports', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class3 = + getDeclaration(program, '/src/flat-file.js', 'Class3', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class3); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts'); + }); + + it('should find the dts file that contains a matching class declaration, even if the class is not publicly exported', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const internalClass = + getDeclaration(program, '/src/internal.js', 'InternalClass', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(internalClass); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts'); + }); + + it('should prefer the publicly exported class if there are multiple classes with the same name', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class2 = + getDeclaration(program, '/src/class2.js', 'Class2', ts.isVariableDeclaration); + const internalClass2 = + getDeclaration(program, '/src/internal.js', 'Class2', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const class2DtsDeclaration = host.getDtsDeclaration(class2); + expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts'); + + const internalClass2DtsDeclaration = host.getDtsDeclaration(internalClass2); + expect(internalClass2DtsDeclaration !.getSourceFile().fileName) + .toEqual('/typings/class2.d.ts'); + }); + }); + + describe('getModuleWithProvidersFunctions', () => { + it('should find every exported function that returns an object that looks like a ModuleWithProviders object', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/functions.js') !; + const fns = host.getModuleWithProvidersFunctions(file); + expect(fns.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])) + .toEqual([ + ['ngModuleIdentifier', 'InternalModule'], + ['ngModuleWithEmptyProviders', 'InternalModule'], + ['ngModuleWithProviders', 'InternalModule'], + ['externalNgModule', 'ExternalModule'], + ]); + }); + + it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/methods.js') !; + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([ + [ + 'function() { return { ngModule: InternalModule }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [SomeService] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: module.ExternalModule }; }', + 'ExternalModule', + ], + ]); + }); + + // https://github.com/angular/angular/issues/29078 + it('should resolve aliased module references to their original declaration', () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/aliased_class.js') !; + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([ + ['function() { return { ngModule: AliasedModule_1 }; }', 'AliasedModule'], + ]); + }); + }); +}); From 27f91ae170394b732d954d9e6d12955b5f6a09a9 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:35 +0100 Subject: [PATCH 11/15] feat(ivy): ngcc - implement `UmdRenderer` --- .../ngcc/src/rendering/esm_renderer.ts | 6 +- .../ngcc/src/rendering/renderer.ts | 35 +- .../ngcc/src/rendering/umd_renderer.ts | 207 ++++++++ .../test/rendering/esm2015_renderer_spec.ts | 40 +- .../ngcc/test/rendering/esm5_renderer_spec.ts | 52 +- .../ngcc/test/rendering/renderer_spec.ts | 44 +- .../ngcc/test/rendering/umd_renderer_spec.ts | 490 ++++++++++++++++++ 7 files changed, 812 insertions(+), 62 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts create mode 100644 packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts index 607595449fe1..3b2be815d3be 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts @@ -9,7 +9,7 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; -import {Import} from '../../../src/ngtsc/translator'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass} from '../analysis/decoration_analyzer'; import {ExportInfo} from '../analysis/private_declarations_analyzer'; import {FileSystem} from '../file_system/file_system'; @@ -35,7 +35,9 @@ export class EsmRenderer extends Renderer { output.appendLeft(insertionPoint, renderedImports); } - addExports(output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void { + addExports( + output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { exports.forEach(e => { let exportFrom = ''; const isDtsFile = isDtsPath(entryPointBasePath); diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index eb6958408613..f2dbf5b276b8 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -127,6 +127,7 @@ export abstract class Renderer { sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined, switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] { + const isEntryPoint = sourceFile === this.bundle.src.file; const input = this.extractSourceMap(sourceFile); const outputText = new MagicString(input.source); @@ -135,11 +136,11 @@ export abstract class Renderer { outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations); } - if (compiledFile) { - const importManager = new ImportManager( - this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlatCore), - IMPORT_PREFIX); + const importManager = new ImportManager( + this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlatCore), + IMPORT_PREFIX); + if (compiledFile) { // TODO: remove constructor param metadata and property decorators (we need info from the // handlers to do this) const decoratorsToRemove = this.computeDecoratorsToRemove(compiledFile.compiledClasses); @@ -154,19 +155,24 @@ export abstract class Renderer { outputText, renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager), compiledFile.sourceFile); - - this.addImports( - outputText, importManager.getAllImports(compiledFile.sourceFile.fileName), - compiledFile.sourceFile); } // Add exports to the entry-point file - if (sourceFile === this.bundle.src.file) { + if (isEntryPoint) { const entryPointBasePath = stripExtension(this.bundle.src.path); - this.addExports(outputText, entryPointBasePath, privateDeclarationsAnalyses); + this.addExports( + outputText, entryPointBasePath, privateDeclarationsAnalyses, importManager, sourceFile); } - return this.renderSourceAndMap(sourceFile, input, outputText); + if (isEntryPoint || compiledFile) { + this.addImports(outputText, importManager.getAllImports(sourceFile.fileName), sourceFile); + } + + if (compiledFile || switchMarkerAnalysis || isEntryPoint) { + return this.renderSourceAndMap(sourceFile, input, outputText); + } else { + return []; + } } renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] { @@ -189,7 +195,9 @@ export abstract class Renderer { this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager); this.addImports(outputText, importManager.getAllImports(dtsFile.fileName), dtsFile); - this.addExports(outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports); + this.addExports( + outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports, + importManager, dtsFile); return this.renderSourceAndMap(dtsFile, input, outputText); @@ -251,7 +259,8 @@ export abstract class Renderer { void; protected abstract addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void; protected abstract addExports( - output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void; + output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void; protected abstract addDefinitions( output: MagicString, compiledClass: CompiledClass, definitions: string): void; protected abstract removeDecorators( diff --git a/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts new file mode 100644 index 000000000000..e3ccfff46fc8 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts @@ -0,0 +1,207 @@ +/** + * @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 {dirname, relative} from 'canonical-path'; +import * as ts from 'typescript'; +import MagicString from 'magic-string'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {FileSystem} from '../file_system/file_system'; +import {UmdReflectionHost} from '../host/umd_host'; +import {Logger} from '../logging/logger'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {Esm5Renderer} from './esm5_renderer'; +import {stripExtension} from './renderer'; + +type CommonJsConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; +type AmdConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; + +export class UmdRenderer extends Esm5Renderer { + constructor( + fs: FileSystem, logger: Logger, protected umdHost: UmdReflectionHost, isCore: boolean, + bundle: EntryPointBundle) { + super(fs, logger, umdHost, isCore, bundle); + } + + /** + * Add the imports at the top of the file + */ + addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void { + // Assume there is only one UMD module in the file + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + + const wrapperFunction = umdModule.wrapperFn; + + // We need to add new `require()` calls for each import in the CommonJS initializer + renderCommonJsDependencies(output, wrapperFunction, imports); + renderAmdDependencies(output, wrapperFunction, imports); + renderGlobalDependencies(output, wrapperFunction, imports); + renderFactoryParameters(output, wrapperFunction, imports); + } + + addExports( + output: MagicString, entryPointBasePath: string, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + const factoryFunction = umdModule.factoryFn; + const lastStatement = + factoryFunction.body.statements[factoryFunction.body.statements.length - 1]; + const insertionPoint = + lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1; + exports.forEach(e => { + const basePath = stripExtension(e.from); + const relativePath = './' + relative(dirname(entryPointBasePath), basePath); + const namedImport = entryPointBasePath !== basePath ? + importManager.generateNamedImport(relativePath, e.identifier) : + {symbol: e.identifier, moduleImport: null}; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`; + output.appendRight(insertionPoint, exportStr); + }); + } + + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + if (constants === '') { + return; + } + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + const factoryFunction = umdModule.factoryFn; + const firstStatement = factoryFunction.body.statements[0]; + const insertionPoint = + firstStatement ? firstStatement.getStart() : factoryFunction.body.getStart() + 1; + output.appendLeft(insertionPoint, '\n' + constants + '\n'); + } +} + +function renderCommonJsDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const conditional = find(wrapperFunction.body.statements[0], isCommonJSConditional); + if (!conditional) { + return; + } + const factoryCall = conditional.whenTrue; + const injectionPoint = factoryCall.getEnd() - + 1; // Backup one char to account for the closing parenthesis on the call + imports.forEach(i => output.appendLeft(injectionPoint, `,require('${i.specifier}')`)); +} + +function renderAmdDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const conditional = find(wrapperFunction.body.statements[0], isAmdConditional); + if (!conditional) { + return; + } + const dependencyArray = conditional.whenTrue.arguments[1]; + if (!dependencyArray || !ts.isArrayLiteralExpression(dependencyArray)) { + return; + } + const injectionPoint = dependencyArray.getEnd() - + 1; // Backup one char to account for the closing square bracket on the array + imports.forEach(i => output.appendLeft(injectionPoint, `,'${i.specifier}'`)); +} + +function renderGlobalDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const globalFactoryCall = find(wrapperFunction.body.statements[0], isGlobalFactoryCall); + if (!globalFactoryCall) { + return; + } + const injectionPoint = globalFactoryCall.getEnd() - + 1; // Backup one char to account for the closing parenthesis on the call + imports.forEach(i => output.appendLeft(injectionPoint, `,global.${getGlobalIdentifier(i)}`)); +} + +function renderFactoryParameters( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const wrapperCall = wrapperFunction.parent as ts.CallExpression; + const secondArgument = wrapperCall.arguments[1]; + if (!secondArgument) { + return; + } + + // Be resilient to the factory being inside parentheses + const factoryFunction = + ts.isParenthesizedExpression(secondArgument) ? secondArgument.expression : secondArgument; + if (!ts.isFunctionExpression(factoryFunction)) { + return; + } + const parameters = factoryFunction.parameters; + const injectionPoint = parameters[parameters.length - 1].getEnd(); + imports.forEach(i => output.appendLeft(injectionPoint, `,${i.qualifier}`)); +} + +function isCommonJSConditional(value: ts.Node): value is CommonJsConditional { + if (!ts.isConditionalExpression(value)) { + return false; + } + if (!ts.isBinaryExpression(value.condition) || + value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return false; + } + if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'exports', 'module'))) { + return false; + } + if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { + return false; + } + return value.whenTrue.expression.text === 'factory'; +} + +function isAmdConditional(value: ts.Node): value is AmdConditional { + if (!ts.isConditionalExpression(value)) { + return false; + } + if (!ts.isBinaryExpression(value.condition) || + value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return false; + } + if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'define'))) { + return false; + } + if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { + return false; + } + return value.whenTrue.expression.text === 'define'; +} + +function isGlobalFactoryCall(value: ts.Node): value is ts.CallExpression { + if (ts.isCallExpression(value) && !!value.parent) { + // Be resilient to the value being inside parentheses + const expression = ts.isParenthesizedExpression(value.parent) ? value.parent : value; + return !!expression.parent && ts.isConditionalExpression(expression.parent) && + expression.parent.whenFalse === expression; + } else { + return false; + } +} + +function getGlobalIdentifier(i: Import) { + return i.specifier.replace('@angular/', 'ng.').replace(/^\//, ''); +} + +function find(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T|undefined { + return test(node) ? node : node.forEachChild(child => find(child, test)); +} + +function oneOfBinaryConditions( + node: ts.BinaryExpression, test: (expression: ts.Expression) => boolean) { + return test(node.left) || test(node.right); +} + +function isTypeOf(node: ts.Expression, ...types: string[]): boolean { + return ts.isBinaryExpression(node) && ts.isTypeOfExpression(node.left) && + ts.isIdentifier(node.left.expression) && types.indexOf(node.left.expression.text) !== -1; +} diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts index 34ba2583bfe5..df75d709b176 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -7,10 +7,13 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../../src/constants'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {EsmRenderer} from '../../src/rendering/esm_renderer'; import {makeTestEntryPointBundle} from '../helpers/utils'; @@ -32,10 +35,11 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new EsmRenderer(fs, logger, host, false, bundle); + const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, program: bundle.src.program, - sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses + sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager, }; } @@ -136,14 +140,17 @@ import * as i1 from '@angular/common';`); describe('addExports', () => { it('should insert the given exports at the end of the source file', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); expect(output.toString()).toContain(` // Some other content export {ComponentA1} from './a'; @@ -153,14 +160,17 @@ export {TopLevelComponent};`); }); it('should not insert alias exports in js output', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); expect(outputString).not.toContain(`{eComponentB as ComponentB}`); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index 21e015357548..fc57b1e65334 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -7,10 +7,13 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../../src/constants'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; @@ -33,10 +36,11 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new Esm5Renderer(fs, logger, host, false, bundle); + const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, program: bundle.src.program, - sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses + sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager }; } @@ -87,8 +91,8 @@ var BadIife = (function() { var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { - const compilerFactory = injector.get(CompilerFactory); - const compiler = compilerFactory.createCompiler([options]); + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); return compiler.compileModuleAsync(moduleType); } @@ -174,14 +178,17 @@ import * as i1 from '@angular/common';`); describe('addExports', () => { it('should insert the given exports at the end of the source file', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); expect(output.toString()).toContain(` export {A, B, C, NoIife, BadIife}; export {ComponentA1} from './a'; @@ -191,14 +198,17 @@ export {TopLevelComponent};`); }); it('should not insert alias exports in js output', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); expect(outputString).not.toContain(`{eComponentB as ComponentB}`); @@ -214,11 +224,11 @@ export {TopLevelComponent};`); throw new Error(`Could not find source file`); } const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); + renderer.addConstants(output, 'var x = 3;', file); expect(output.toString()).toContain(` import {Directive} from '@angular/core'; -const x = 3; +var x = 3; var A = (function() {`); }); @@ -229,13 +239,13 @@ var A = (function() {`); throw new Error(`Could not find source file`); } const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); + renderer.addConstants(output, 'var x = 3;', file); renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); expect(output.toString()).toContain(` import {Directive} from '@angular/core'; import * as i0 from '@angular/core'; -const x = 3; +var x = 3; var A = (function() {`); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index cac283fccdfd..81b5f97575c3 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -13,7 +13,7 @@ import {Import} from '../../../src/ngtsc/translator'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; -import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; +import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer'; @@ -35,10 +35,7 @@ class TestRenderer extends Renderer { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { output.prepend('\n// ADD IMPORTS\n'); } - addExports(output: MagicString, baseEntryPointPath: string, exports: { - identifier: string, - from: string - }[]) { + addExports(output: MagicString, baseEntryPointPath: string, exports: ExportInfo[]) { output.prepend('\n// ADD EXPORTS\n'); } addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { @@ -76,8 +73,10 @@ function createTestRenderer( const privateDeclarationsAnalyses = new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); const renderer = new TestRenderer(fs, logger, host, isCore, bundle); + spyOn(renderer, 'addExports').and.callThrough(); spyOn(renderer, 'addImports').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough(); + spyOn(renderer, 'addConstants').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough(); return {renderer, @@ -117,9 +116,17 @@ describe('Renderer', () => { 'sourcesContent': [INPUT_PROGRAM.contents] }); - const RENDERED_CONTENTS = - `\n// ADD EXPORTS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n\n// REMOVE DECORATORS\n` + - INPUT_PROGRAM.contents; + const RENDERED_CONTENTS = ` +// ADD IMPORTS + +// ADD EXPORTS + +// ADD CONSTANTS + +// ADD DEFINITIONS + +// REMOVE DECORATORS +` + INPUT_PROGRAM.contents; const OUTPUT_PROGRAM_MAP = fromObject({ 'version': 3, @@ -240,6 +247,22 @@ describe('Renderer', () => { expect(values[0][0].getText()) .toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`); }); + + it('should call renderImports after other abstract methods', () => { + // This allows the other methods to add additional imports if necessary + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const addExportsSpy = renderer.addExports as jasmine.Spy; + const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + const addConstantsSpy = renderer.addConstants as jasmine.Spy; + const addImportsSpy = renderer.addImports as jasmine.Spy; + renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); + expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); + }); }); describe('source map merging', () => { @@ -355,7 +378,7 @@ describe('Renderer', () => { moduleWithProvidersAnalyses); const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`); + expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); }); it('should render exports into typings files', () => { @@ -372,8 +395,7 @@ describe('Renderer', () => { moduleWithProvidersAnalyses); const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents) - .toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`); + expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`); }); it('should fixup functions/methods that return ModuleWithProviders structures', () => { diff --git a/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts new file mode 100644 index 000000000000..e3dd4df9a6ee --- /dev/null +++ b/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts @@ -0,0 +1,490 @@ +/** + * @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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {UmdReflectionHost} from '../../src/host/umd_host'; +import {ImportManager} from '../../../src/ngtsc/translator'; +import {UmdRenderer} from '../../src/rendering/umd_renderer'; +import {MockFileSystem} from '../helpers/mock_file_system'; +import {MockLogger} from '../helpers/mock_logger'; +import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; + +const _ = AbsoluteFsPath.fromUnchecked; + +function setup(file: {name: string, contents: string}) { + const fs = new MockFileSystem(createFileSystemFromProgramFiles([file])); + const logger = new MockLogger(); + const bundle = makeTestEntryPointBundle('esm5', 'esm5', false, [file]); + const src = bundle.src; + const typeChecker = src.program.getTypeChecker(); + const host = new UmdReflectionHost(logger, false, src.program, src.host); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = new DecorationAnalyzer( + fs, src.program, src.options, src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); + const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); + const renderer = new UmdRenderer(fs, logger, host, false, bundle); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + return { + decorationAnalyses, + host, + importManager, + program: src.program, renderer, + sourceFile: src.file, switchMarkerAnalyses + }; +} + +const PROGRAM = { + name: _('/some/file.js'), + contents: ` +/* A copyright notice */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('some-side-effect'),require('/local-dep'),require('@angular/core')) : +typeof define === 'function' && define.amd ? define('file', ['exports','some-side-effect','/local-dep','@angular/core'], factory) : +(factory(global.file,global.someSideEffect,global.localDep,global.ng.core)); +}(this, (function (exports,someSideEffect,localDep,core) {'use strict'; +var A = (function() { + function A() {} + A.decorators = [ + { type: core.Directive, args: [{ selector: '[a]' }] }, + { type: OtherA } + ]; + A.prototype.ngDoCheck = function() { + // + }; + return A; +}()); + +var B = (function() { + function B() {} + B.decorators = [ + { type: OtherB }, + { type: core.Directive, args: [{ selector: '[b]' }] } + ]; + return B; +}()); + +var C = (function() { + function C() {} + C.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; + return C; +}()); + +function NoIife() {} + +var BadIife = (function() { + function BadIife() {} + BadIife.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; +}()); + +var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; +var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; +function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); +} + +function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); +} +// Some other content +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife; +})));`, +}; + + +const PROGRAM_DECORATE_HELPER = { + name: '/some/file.js', + contents: ` +/* A copyright notice */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('tslib'),require('@angular/core')) : +typeof define === 'function' && define.amd ? define('file', ['exports','/tslib','@angular/core'], factory) : +(factory(global.file,global.tslib,global.ng.core)); +}(this, (function (exports,tslib,core) {'use strict'; + var OtherA = function () { return function (node) { }; }; + var OtherB = function () { return function (node) { }; }; + var A = /** @class */ (function () { + function A() { + } + A = tslib.__decorate([ + core.Directive({ selector: '[a]' }), + OtherA() + ], A); + return A; + }()); + export { A }; + var B = /** @class */ (function () { + function B() { + } + B = tslib.__decorate([ + OtherB(), + core.Directive({ selector: '[b]' }) + ], B); + return B; + }()); + export { B }; + var C = /** @class */ (function () { + function C() { + } + C = tslib.__decorate([ + core.Directive({ selector: '[c]' }) + ], C); + return C; + }()); + export { C }; + var D = /** @class */ (function () { + function D() { + } + D_1 = D; + var D_1; + D = D_1 = tslib.__decorate([ + core.Directive({ selector: '[d]', providers: [D_1] }) + ], D); + return D; + }()); + exports.D = D; + // Some other content +})));` +}; + +describe('UmdRenderer', () => { + + describe('addImports', () => { + it('should append the given imports into the CommonJS factory call', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('some-side-effect'),require('/local-dep'),require('@angular/core'),require('@angular/core'),require('@angular/common')) :`); + }); + + it('should append the given imports into the AMD initialization', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `typeof define === 'function' && define.amd ? define('file', ['exports','some-side-effect','/local-dep','@angular/core','@angular/core','@angular/common'], factory) :`); + }); + + it('should append the given imports into the global initialization', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `(factory(global.file,global.someSideEffect,global.localDep,global.ng.core,global.ng.core,global.ng.common));`); + }); + + it('should append the given imports as parameters into the factory function definition', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain(`(function (exports,someSideEffect,localDep,core,i0,i1) {'use strict';`); + }); + }); + + describe('addExports', () => { + it('should insert the given exports at the end of the source file', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const generateNamedImportSpy = spyOn(importManager, 'generateNamedImport').and.callThrough(); + renderer.addExports( + output, PROGRAM.name.replace(/\.js$/, ''), + [ + {from: _('/some/a.js'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), identifier: 'ComponentB'}, + {from: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + + expect(output.toString()).toContain(` +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife; +exports.ComponentA1 = i0.ComponentA1; +exports.ComponentA2 = i0.ComponentA2; +exports.ComponentB = i1.ComponentB; +exports.TopLevelComponent = TopLevelComponent; +})));`); + + expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA1'); + expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA2'); + expect(generateNamedImportSpy).toHaveBeenCalledWith('./foo/b', 'ComponentB'); + }); + + it('should not insert alias exports in js output', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, PROGRAM.name.replace(/\.js$/, ''), + [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + const outputString = output.toString(); + expect(outputString).not.toContain(`eComponentA1`); + expect(outputString).not.toContain(`eComponentB`); + expect(outputString).not.toContain(`eTopLevelComponent`); + }); + }); + + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + expect(output.toString()).toContain(` +}(this, (function (exports,someSideEffect,localDep,core) { +var x = 3; +'use strict'; +var A = (function() {`); + }); + + it('should insert constants after inserted imports', + () => { + // This test (from ESM5) is not needed as constants go in the body + // of the UMD IIFE, so cannot come before imports. + }); + }); + + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); + expect(output.toString()) + .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); + expect(output.toString()) + .toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); + }); + }); + + describe('addDefinitions', () => { + it('should insert the definitions directly before the return statement of the class IIFE', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()).toContain(` + A.prototype.ngDoCheck = function() { + // + }; +SOME DEFINITION TEXT + return A; +`); + }); + + it('should error if the compiledClass is not valid', () => { + const {renderer, sourceFile, program} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + + const noIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); + const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; + expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); + + const badIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); + const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'}; + expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); + }); + }); + + describe('removeDecorators', () => { + + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).not.toContain(`C.decorators`); + }); + + }); + + describe('[__decorate declarations]', () => { + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[c]' })`); + }); + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[c]' })`); + }); + + + it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`function C() {\n }\n return C;`); + }); + }); +}); From 8504128c35307e4dd173ebfd1a320e05ef2515cd Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:35 +0100 Subject: [PATCH 12/15] feat(ivy): ngcc - turn on UMD processing --- .../ngcc/src/dependencies/dependency_resolver.ts | 5 +++-- packages/compiler-cli/ngcc/src/main.ts | 2 +- .../compiler-cli/ngcc/src/packages/transformer.ts | 10 ++++++++++ .../test/dependencies/dependency_resolver_spec.ts | 4 ++-- .../compiler-cli/ngcc/test/integration/ngcc_spec.ts | 12 +++++++----- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts index d6e344b5b2a4..a4f532375566 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts @@ -170,12 +170,13 @@ function getEntryPointPath(entryPoint: EntryPoint): AbsoluteFsPath { const property = properties[i] as EntryPointJsonProperty; const format = getEntryPointFormat(property); - if (format === 'esm2015' || format === 'esm5') { + if (format === 'esm2015' || format === 'esm5' || format === 'umd') { const formatPath = entryPoint.packageJson[property] !; return AbsoluteFsPath.resolve(entryPoint.path, formatPath); } } - throw new Error(`There is no format with import statements in '${entryPoint.path}' entry-point.`); + throw new Error( + `There is no appropriate source code format in '${entryPoint.path}' entry-point.`); } interface DependencyGraph extends DependencyDiagnostics { diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 4a760b3e630a..85d4adbf25fd 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -63,7 +63,7 @@ export interface NgccOptions { pathMappings?: PathMappings; } -const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; +const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015', 'umd']; /** * This is the main entry-point into ngcc (aNGular Compatibility Compiler). diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index 3abc7078a3a0..a17f017294bc 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -16,10 +16,12 @@ import {FileSystem} from '../file_system/file_system'; import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm5ReflectionHost} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; +import {UmdReflectionHost} from '../host/umd_host'; import {Logger} from '../logging/logger'; import {Esm5Renderer} from '../rendering/esm5_renderer'; import {EsmRenderer} from '../rendering/esm_renderer'; import {FileInfo, Renderer} from '../rendering/renderer'; +import {UmdRenderer} from '../rendering/umd_renderer'; import {EntryPointBundle} from './entry_point_bundle'; @@ -78,6 +80,9 @@ export class Transformer { return new Esm2015ReflectionHost(this.logger, isCore, typeChecker, bundle.dts); case 'esm5': return new Esm5ReflectionHost(this.logger, isCore, typeChecker, bundle.dts); + case 'umd': + return new UmdReflectionHost( + this.logger, isCore, bundle.src.program, bundle.src.host, bundle.dts); default: throw new Error(`Reflection host for "${bundle.format}" not yet implemented.`); } @@ -89,6 +94,11 @@ export class Transformer { return new EsmRenderer(this.fs, this.logger, host, isCore, bundle); case 'esm5': return new Esm5Renderer(this.fs, this.logger, host, isCore, bundle); + case 'umd': + if (!(host instanceof UmdReflectionHost)) { + throw new Error('UmdRenderer requires a UmdReflectionHost'); + } + return new UmdRenderer(this.fs, this.logger, host, isCore, bundle); default: throw new Error(`Renderer for "${bundle.format}" not yet implemented.`); } diff --git a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts index 65e42dcb93dd..288793fe6c4a 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -106,10 +106,10 @@ describe('DependencyResolver', () => { ]); }); - it('should error if the entry point does not have either the esm5 nor esm2015 formats', () => { + it('should error if the entry point does not have a suitable format', () => { expect(() => resolver.sortEntryPointsByDependency([ { path: '/first', packageJson: {}, compiledByAngular: true } as EntryPoint - ])).toThrowError(`There is no format with import statements in '/first' entry-point.`); + ])).toThrowError(`There is no appropriate source code format in '/first' entry-point.`); }); it('should capture any dependencies that were ignored', () => { diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 95e33310f380..b92b7667d09a 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -41,6 +41,7 @@ describe('ngcc main()', () => { describe('with targetEntryPointPath', () => { it('should only compile the given package entry-point (and its dependencies).', () => { const STANDARD_MARKERS = { + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', esm5: '0.0.0-PLACEHOLDER', @@ -162,29 +163,31 @@ describe('ngcc main()', () => { }); - // * the `main` property is UMD, which is not yet supported. - // * none of the ES2015 formats are compiled as they are not on the `propertiesToConsider` - // list. + // The ES2015 formats are not compiled as they are not in `propertiesToConsider`. expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', @@ -196,12 +199,11 @@ describe('ngcc main()', () => { it('should only compile the first matching format', () => { mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['main', 'module', 'fesm5', 'esm5'], + propertiesToConsider: ['module', 'fesm5', 'esm5'], compileAllFormats: false, logger: new MockLogger(), }); - // * The `main` is UMD, which is not yet supported, and so is not compiled. // * In the Angular packages fesm5 and module have the same underlying format, // so both are marked as compiled. // * The `esm5` is not compiled because we stopped after the `fesm5` format. From f8e1f3187dbe248366b80e4a3e8ac07687ae94bc Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:35 +0100 Subject: [PATCH 13/15] fix(ivy): ngcc - separate typings rendering from src rendering Previously the same `Renderer` was used to render typings (.d.ts) files. But the new `UmdRenderer` is not able to render typings files correctly. This commit splits out the typings rendering from the src rendering. To achieve this the previous renderers have been refactored from sub-classes of the abstract `Renderer` class to classes that implement the `RenderingFormatter` interface, which are then passed to the `Renderer` and `DtsRenderer` to modify its rendering behaviour. Along the way a few utility interfaces and classes have been moved around and renamed for clarity. --- .../ngcc/src/packages/transformer.ts | 41 +- .../ngcc/src/rendering/dts_renderer.ts | 161 +++++++ ...enderer.ts => esm5_rendering_formatter.ts} | 20 +- .../ngcc/src/rendering/esm_renderer.ts | 140 ------ .../src/rendering/esm_rendering_formatter.ts | 218 +++++++++ .../ngcc/src/rendering/renderer.ts | 414 ++---------------- .../ngcc/src/rendering/rendering_formatter.ts | 42 ++ .../ngcc/src/rendering/source_maps.ts | 137 ++++++ ...renderer.ts => umd_rendering_formatter.ts} | 53 ++- .../compiler-cli/ngcc/src/rendering/utils.ts | 39 ++ .../ngcc/src/writing/file_writer.ts | 5 +- .../ngcc/src/writing/in_place_file_writer.ts | 6 +- .../writing/new_entry_point_file_writer.ts | 6 +- .../ngcc/test/rendering/dts_renderer_spec.ts | 181 ++++++++ ...ec.ts => esm5_rendering_formatter_spec.ts} | 6 +- ...pec.ts => esm_rendering_formatter_spec.ts} | 255 ++++++++--- .../ngcc/test/rendering/renderer_spec.ts | 351 +++------------ ...pec.ts => umd_rendering_formatter_spec.ts} | 6 +- 18 files changed, 1154 insertions(+), 927 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts rename packages/compiler-cli/ngcc/src/rendering/{esm5_renderer.ts => esm5_rendering_formatter.ts} (68%) delete mode 100644 packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts create mode 100644 packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts create mode 100644 packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts create mode 100644 packages/compiler-cli/ngcc/src/rendering/source_maps.ts rename packages/compiler-cli/ngcc/src/rendering/{umd_renderer.ts => umd_rendering_formatter.ts} (83%) create mode 100644 packages/compiler-cli/ngcc/src/rendering/utils.ts create mode 100644 packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts rename packages/compiler-cli/ngcc/test/rendering/{esm5_renderer_spec.ts => esm5_rendering_formatter_spec.ts} (98%) rename packages/compiler-cli/ngcc/test/rendering/{esm2015_renderer_spec.ts => esm_rendering_formatter_spec.ts} (65%) rename packages/compiler-cli/ngcc/test/rendering/{umd_renderer_spec.ts => umd_rendering_formatter_spec.ts} (99%) diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index a17f017294bc..4b87430c4035 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -18,10 +18,13 @@ import {Esm5ReflectionHost} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; import {UmdReflectionHost} from '../host/umd_host'; import {Logger} from '../logging/logger'; -import {Esm5Renderer} from '../rendering/esm5_renderer'; -import {EsmRenderer} from '../rendering/esm_renderer'; -import {FileInfo, Renderer} from '../rendering/renderer'; -import {UmdRenderer} from '../rendering/umd_renderer'; +import {DtsRenderer} from '../rendering/dts_renderer'; +import {Esm5RenderingFormatter} from '../rendering/esm5_rendering_formatter'; +import {EsmRenderingFormatter} from '../rendering/esm_rendering_formatter'; +import {Renderer} from '../rendering/renderer'; +import {RenderingFormatter} from '../rendering/rendering_formatter'; +import {UmdRenderingFormatter} from '../rendering/umd_rendering_formatter'; +import {FileToWrite} from '../rendering/utils'; import {EntryPointBundle} from './entry_point_bundle'; @@ -56,7 +59,7 @@ export class Transformer { * @param bundle the bundle to transform. * @returns information about the files that were transformed. */ - transform(bundle: EntryPointBundle): FileInfo[] { + transform(bundle: EntryPointBundle): FileToWrite[] { const isCore = bundle.isCore; const reflectionHost = this.getHost(isCore, bundle); @@ -65,10 +68,21 @@ export class Transformer { moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle); // Transform the source files and source maps. - const renderer = this.getRenderer(reflectionHost, isCore, bundle); - const renderedFiles = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + const srcFormatter = this.getRenderingFormatter(reflectionHost, isCore, bundle); + + const renderer = + new Renderer(srcFormatter, this.fs, this.logger, reflectionHost, isCore, bundle); + let renderedFiles = renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + + if (bundle.dts) { + const dtsFormatter = new EsmRenderingFormatter(reflectionHost, isCore); + const dtsRenderer = + new DtsRenderer(dtsFormatter, this.fs, this.logger, reflectionHost, isCore, bundle); + const renderedDtsFiles = dtsRenderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + renderedFiles = renderedFiles.concat(renderedDtsFiles); + } return renderedFiles; } @@ -88,17 +102,18 @@ export class Transformer { } } - getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer { + getRenderingFormatter(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): + RenderingFormatter { switch (bundle.format) { case 'esm2015': - return new EsmRenderer(this.fs, this.logger, host, isCore, bundle); + return new EsmRenderingFormatter(host, isCore); case 'esm5': - return new Esm5Renderer(this.fs, this.logger, host, isCore, bundle); + return new Esm5RenderingFormatter(host, isCore); case 'umd': if (!(host instanceof UmdReflectionHost)) { throw new Error('UmdRenderer requires a UmdReflectionHost'); } - return new UmdRenderer(this.fs, this.logger, host, isCore, bundle); + return new UmdRenderingFormatter(host, isCore); default: throw new Error(`Renderer for "${bundle.format}" not yet implemented.`); } diff --git a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts new file mode 100644 index 000000000000..63295061361a --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.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 MagicString from 'magic-string'; +import * as ts from 'typescript'; + +import {translateType, ImportManager} from '../../../src/ngtsc/translator'; +import {DecorationAnalyses} from '../analysis/decoration_analyzer'; +import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer'; +import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer'; +import {IMPORT_PREFIX} from '../constants'; +import {FileSystem} from '../file_system/file_system'; +import {NgccReflectionHost} from '../host/ngcc_host'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {Logger} from '../logging/logger'; +import {FileToWrite, getImportRewriter} from './utils'; +import {RenderingFormatter} from './rendering_formatter'; +import {extractSourceMap, renderSourceAndMap} from './source_maps'; +import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform'; + +/** + * A structure that captures information about what needs to be rendered + * in a typings file. + * + * It is created as a result of processing the analysis passed to the renderer. + * + * The `renderDtsFile()` method consumes it when rendering a typings file. + */ +class DtsRenderInfo { + classInfo: DtsClassInfo[] = []; + moduleWithProviders: ModuleWithProvidersInfo[] = []; + privateExports: ExportInfo[] = []; +} + + +/** + * Information about a class in a typings file. + */ +export interface DtsClassInfo { + dtsDeclaration: ts.Declaration; + compilation: CompileResult[]; +} + +/** + * A base-class for rendering an `AnalyzedFile`. + * + * Package formats have output files that must be rendered differently. Concrete sub-classes must + * implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods. + */ +export class DtsRenderer { + constructor( + private dtsFormatter: RenderingFormatter, private fs: FileSystem, private logger: Logger, + private host: NgccReflectionHost, private isCore: boolean, private bundle: EntryPointBundle) { + } + + renderProgram( + decorationAnalyses: DecorationAnalyses, + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, + moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileToWrite[] { + const renderedFiles: FileToWrite[] = []; + + // Transform the .d.ts files + if (this.bundle.dts) { + const dtsFiles = this.getTypingsFilesToRender( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + // If the dts entry-point is not already there (it did not have compiled classes) + // then add it now, to ensure it gets its extra exports rendered. + if (!dtsFiles.has(this.bundle.dts.file)) { + dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo()); + } + dtsFiles.forEach( + (renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo))); + } + + return renderedFiles; + } + + renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileToWrite[] { + const input = extractSourceMap(this.fs, this.logger, dtsFile); + const outputText = new MagicString(input.source); + const printer = ts.createPrinter(); + const importManager = new ImportManager( + getImportRewriter(this.bundle.dts !.r3SymbolsFile, this.isCore, false), IMPORT_PREFIX); + + renderInfo.classInfo.forEach(dtsClass => { + const endOfClass = dtsClass.dtsDeclaration.getEnd(); + dtsClass.compilation.forEach(declaration => { + const type = translateType(declaration.type, importManager); + const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile); + const newStatement = ` static ${declaration.name}: ${typeStr};\n`; + outputText.appendRight(endOfClass - 1, newStatement); + }); + }); + + this.dtsFormatter.addModuleWithProvidersParams( + outputText, renderInfo.moduleWithProviders, importManager); + this.dtsFormatter.addExports( + outputText, dtsFile.fileName, renderInfo.privateExports, importManager, dtsFile); + this.dtsFormatter.addImports( + outputText, importManager.getAllImports(dtsFile.fileName), dtsFile); + + + + return renderSourceAndMap(dtsFile, input, outputText); + } + + private getTypingsFilesToRender( + decorationAnalyses: DecorationAnalyses, + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, + moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses| + null): Map { + const dtsMap = new Map(); + + // Capture the rendering info from the decoration analyses + decorationAnalyses.forEach(compiledFile => { + compiledFile.compiledClasses.forEach(compiledClass => { + const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); + if (dtsDeclaration) { + const dtsFile = dtsDeclaration.getSourceFile(); + const renderInfo = dtsMap.has(dtsFile) ? dtsMap.get(dtsFile) ! : new DtsRenderInfo(); + renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation}); + dtsMap.set(dtsFile, renderInfo); + } + }); + }); + + // Capture the ModuleWithProviders functions/methods that need updating + if (moduleWithProvidersAnalyses !== null) { + moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => { + const renderInfo = dtsMap.has(dtsFile) ? dtsMap.get(dtsFile) ! : new DtsRenderInfo(); + renderInfo.moduleWithProviders = moduleWithProvidersToFix; + dtsMap.set(dtsFile, renderInfo); + }); + } + + // Capture the private declarations that need to be re-exported + if (privateDeclarationsAnalyses.length) { + privateDeclarationsAnalyses.forEach(e => { + if (!e.dtsFrom && !e.alias) { + throw new Error( + `There is no typings path for ${e.identifier} in ${e.from}.\n` + + `We need to add an export for this class to a .d.ts typings file because ` + + `Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` + + `The simplest fix for this is to ensure that this class is exported from the package's entry-point.`); + } + }); + const dtsEntryPoint = this.bundle.dts !.file; + const renderInfo = + dtsMap.has(dtsEntryPoint) ? dtsMap.get(dtsEntryPoint) ! : new DtsRenderInfo(); + renderInfo.privateExports = privateDeclarationsAnalyses; + dtsMap.set(dtsEntryPoint, renderInfo); + } + + return dtsMap; + } +} diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts similarity index 68% rename from packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts rename to packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts index 368c93b44f91..8b22f7519fad 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts @@ -8,22 +8,16 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {CompiledClass} from '../analysis/decoration_analyzer'; -import {FileSystem} from '../file_system/file_system'; import {getIifeBody} from '../host/esm5_host'; -import {NgccReflectionHost} from '../host/ngcc_host'; -import {Logger} from '../logging/logger'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {EsmRenderer} from './esm_renderer'; - -export class Esm5Renderer extends EsmRenderer { - constructor( - fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, host, isCore, bundle); - } +import {EsmRenderingFormatter} from './esm_rendering_formatter'; +/** + * A RenderingFormatter that works with files that use ECMAScript Module `import` and `export` + * statements, but instead of `class` declarations it uses ES5 `function` wrappers for classes. + */ +export class Esm5RenderingFormatter extends EsmRenderingFormatter { /** - * Add the definitions to each decorated class + * Add the definitions inside the IIFE of each decorated class */ addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { const iifeBody = getIifeBody(compiledClass.declaration); diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts deleted file mode 100644 index 3b2be815d3be..000000000000 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ /dev/null @@ -1,140 +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 MagicString from 'magic-string'; -import * as ts from 'typescript'; -import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; -import {Import, ImportManager} from '../../../src/ngtsc/translator'; -import {CompiledClass} from '../analysis/decoration_analyzer'; -import {ExportInfo} from '../analysis/private_declarations_analyzer'; -import {FileSystem} from '../file_system/file_system'; -import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; -import {Logger} from '../logging/logger'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer'; - -export class EsmRenderer extends Renderer { - constructor( - fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, host, isCore, bundle); - } - - /** - * Add the imports at the top of the file - */ - addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void { - const insertionPoint = findEndOfImports(sf); - const renderedImports = - imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join(''); - output.appendLeft(insertionPoint, renderedImports); - } - - addExports( - output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], - importManager: ImportManager, file: ts.SourceFile): void { - exports.forEach(e => { - let exportFrom = ''; - const isDtsFile = isDtsPath(entryPointBasePath); - const from = isDtsFile ? e.dtsFrom : e.from; - - if (from) { - const basePath = stripExtension(from); - const relativePath = - './' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath); - exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : ''; - } - - // aliases should only be added in dts files as these are lost when rolling up dts file. - const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier; - const exportStr = `\nexport {${exportStatement}}${exportFrom};`; - output.append(exportStr); - }); - } - - addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { - if (constants === '') { - return; - } - const insertionPoint = findEndOfImports(file); - - // Append the constants to the right of the insertion point, to ensure they get ordered after - // added imports (those are appended left to the insertion point). - output.appendRight(insertionPoint, '\n' + constants + '\n'); - } - - /** - * Add the definitions to each decorated class - */ - addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { - const classSymbol = this.host.getClassSymbol(compiledClass.declaration); - if (!classSymbol) { - throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`); - } - const insertionPoint = classSymbol.valueDeclaration !.getEnd(); - output.appendLeft(insertionPoint, '\n' + definitions); - } - - /** - * Remove static decorator properties from classes - */ - removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void { - decoratorsToRemove.forEach((nodesToRemove, containerNode) => { - if (ts.isArrayLiteralExpression(containerNode)) { - const items = containerNode.elements; - if (items.length === nodesToRemove.length) { - // Remove the entire statement - const statement = findStatement(containerNode); - if (statement) { - output.remove(statement.getFullStart(), statement.getEnd()); - } - } else { - nodesToRemove.forEach(node => { - // remove any trailing comma - const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ? - node.getEnd() + 1 : - node.getEnd(); - output.remove(node.getFullStart(), end); - }); - } - } - }); - } - - rewriteSwitchableDeclarations( - outputText: MagicString, sourceFile: ts.SourceFile, - declarations: SwitchableVariableDeclaration[]): void { - declarations.forEach(declaration => { - const start = declaration.initializer.getStart(); - const end = declaration.initializer.getEnd(); - const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER); - outputText.overwrite(start, end, replacement); - }); - } -} - -function findEndOfImports(sf: ts.SourceFile): number { - for (const stmt of sf.statements) { - if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) && - !ts.isNamespaceImport(stmt)) { - return stmt.getStart(); - } - } - - return 0; -} - -function findStatement(node: ts.Node) { - while (node) { - if (ts.isExpressionStatement(node)) { - return node; - } - node = node.parent; - } - return undefined; -} diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts new file mode 100644 index 000000000000..579fde535b12 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts @@ -0,0 +1,218 @@ +/** + * @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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; +import {CompiledClass} from '../analysis/decoration_analyzer'; +import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; +import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter'; +import {stripExtension} from './utils'; + +/** + * A RenderingFormatter that works with ECMAScript Module import and export statements. + */ +export class EsmRenderingFormatter implements RenderingFormatter { + constructor(protected host: NgccReflectionHost, protected isCore: boolean) {} + + /** + * Add the imports at the top of the file, after any imports that are already there. + */ + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void { + const insertionPoint = this.findEndOfImports(sf); + const renderedImports = + imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join(''); + output.appendLeft(insertionPoint, renderedImports); + } + + /** + * Add the exports to the end of the file. + */ + addExports( + output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { + exports.forEach(e => { + let exportFrom = ''; + const isDtsFile = isDtsPath(entryPointBasePath); + const from = isDtsFile ? e.dtsFrom : e.from; + + if (from) { + const basePath = stripExtension(from); + const relativePath = + './' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath); + exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : ''; + } + + // aliases should only be added in dts files as these are lost when rolling up dts file. + const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier; + const exportStr = `\nexport {${exportStatement}}${exportFrom};`; + output.append(exportStr); + }); + } + + /** + * Add the constants directly after the imports. + */ + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + if (constants === '') { + return; + } + const insertionPoint = this.findEndOfImports(file); + + // Append the constants to the right of the insertion point, to ensure they get ordered after + // added imports (those are appended left to the insertion point). + output.appendRight(insertionPoint, '\n' + constants + '\n'); + } + + /** + * Add the definitions directly after their decorated class. + */ + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { + const classSymbol = this.host.getClassSymbol(compiledClass.declaration); + if (!classSymbol) { + throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`); + } + const insertionPoint = classSymbol.valueDeclaration !.getEnd(); + output.appendLeft(insertionPoint, '\n' + definitions); + } + + /** + * Remove static decorator properties from classes. + */ + removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void { + decoratorsToRemove.forEach((nodesToRemove, containerNode) => { + if (ts.isArrayLiteralExpression(containerNode)) { + const items = containerNode.elements; + if (items.length === nodesToRemove.length) { + // Remove the entire statement + const statement = findStatement(containerNode); + if (statement) { + output.remove(statement.getFullStart(), statement.getEnd()); + } + } else { + nodesToRemove.forEach(node => { + // remove any trailing comma + const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ? + node.getEnd() + 1 : + node.getEnd(); + output.remove(node.getFullStart(), end); + }); + } + } + }); + } + + /** + * Rewrite the the IVY switch markers to indicate we are in IVY mode. + */ + rewriteSwitchableDeclarations( + outputText: MagicString, sourceFile: ts.SourceFile, + declarations: SwitchableVariableDeclaration[]): void { + declarations.forEach(declaration => { + const start = declaration.initializer.getStart(); + const end = declaration.initializer.getEnd(); + const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER); + outputText.overwrite(start, end, replacement); + }); + } + + + /** + * Add the type parameters to the appropriate functions that return `ModuleWithProviders` + * structures. + * + * This function will only get called on typings files. + */ + addModuleWithProvidersParams( + outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + moduleWithProviders.forEach(info => { + const ngModuleName = info.ngModule.node.name.text; + const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile()); + const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile()); + const importPath = info.ngModule.viaModule || + (declarationFile !== ngModuleFile ? + stripExtension( + `./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) : + null); + const ngModule = generateImportString(importManager, importPath, ngModuleName); + + if (info.declaration.type) { + const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ? + info.declaration.type.typeName : + null; + if (this.isCoreModuleWithProvidersType(typeName)) { + // The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type + // parameter adding. + outputText.overwrite( + info.declaration.type.getStart(), info.declaration.type.getEnd(), + `ModuleWithProviders<${ngModule}>`); + } else { + // The declaration returns an unknown type so we need to convert it to a union that + // includes the ngModule property. + const originalTypeString = info.declaration.type.getText(); + outputText.overwrite( + info.declaration.type.getStart(), info.declaration.type.getEnd(), + `(${originalTypeString})&{ngModule:${ngModule}}`); + } + } else { + // The declaration has no return type so provide one. + const lastToken = info.declaration.getLastToken(); + const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ? + lastToken.getStart() : + info.declaration.getEnd(); + outputText.appendLeft( + insertPoint, + `: ${generateImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`); + } + }); + } + + protected findEndOfImports(sf: ts.SourceFile): number { + for (const stmt of sf.statements) { + if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) && + !ts.isNamespaceImport(stmt)) { + return stmt.getStart(); + } + } + return 0; + } + + + + /** + * Check whether the given type is the core Angular `ModuleWithProviders` interface. + * @param typeName The type to check. + * @returns true if the type is the core Angular `ModuleWithProviders` interface. + */ + private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) { + const id = + typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null; + return ( + id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core')); + } +} + +function findStatement(node: ts.Node) { + while (node) { + if (ts.isExpressionStatement(node)) { + return node; + } + node = node.parent; + } + return undefined; +} + +function generateImportString( + importManager: ImportManager, importPath: string | null, importName: string) { + const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null; + return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index f2dbf5b276b8..136c401feeb7 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -6,72 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler'; -import {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; import MagicString from 'magic-string'; -import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map'; import * as ts from 'typescript'; -import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; -import {CompileResult} from '../../../src/ngtsc/transform'; -import {translateStatement, translateType, Import, ImportManager} from '../../../src/ngtsc/translator'; +import {NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports'; +import {translateStatement, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer'; -import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer'; -import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer'; +import {PrivateDeclarationsAnalyses} from '../analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../constants'; import {FileSystem} from '../file_system/file_system'; -import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host'; -import {Logger} from '../logging/logger'; +import {NgccReflectionHost} from '../host/ngcc_host'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; - -interface SourceMapInfo { - source: string; - map: SourceMapConverter|null; - isInline: boolean; -} - -/** - * Information about a file that has been rendered. - */ -export interface FileInfo { - /** - * Path to where the file should be written. - */ - path: AbsoluteFsPath; - /** - * The contents of the file to be be written. - */ - contents: string; -} - -interface DtsClassInfo { - dtsDeclaration: ts.Declaration; - compilation: CompileResult[]; -} - -/** - * A structure that captures information about what needs to be rendered - * in a typings file. - * - * It is created as a result of processing the analysis passed to the renderer. - * - * The `renderDtsFile()` method consumes it when rendering a typings file. - */ -class DtsRenderInfo { - classInfo: DtsClassInfo[] = []; - moduleWithProviders: ModuleWithProvidersInfo[] = []; - privateExports: ExportInfo[] = []; -} - -/** - * The collected decorators that have become redundant after the compilation - * of Ivy static fields. The map is keyed by the container node, such that we - * can tell if we should remove the entire decorator property - */ -export type RedundantDecoratorMap = Map; -export const RedundantDecoratorMap = Map; +import {Logger} from '../logging/logger'; +import {FileToWrite, getImportRewriter, stripExtension} from './utils'; +import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter'; +import {extractSourceMap, renderSourceAndMap} from './source_maps'; /** * A base-class for rendering an `AnalyzedFile`. @@ -79,42 +29,28 @@ export const RedundantDecoratorMap = Map; * Package formats have output files that must be rendered differently. Concrete sub-classes must * implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods. */ -export abstract class Renderer { +export class Renderer { constructor( - protected fs: FileSystem, protected logger: Logger, protected host: NgccReflectionHost, - protected isCore: boolean, protected bundle: EntryPointBundle) {} + private srcFormatter: RenderingFormatter, private fs: FileSystem, private logger: Logger, + private host: NgccReflectionHost, private isCore: boolean, private bundle: EntryPointBundle) { + } renderProgram( decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses, - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, - moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileInfo[] { - const renderedFiles: FileInfo[] = []; + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileToWrite[] { + const renderedFiles: FileToWrite[] = []; // Transform the source files. this.bundle.src.program.getSourceFiles().forEach(sourceFile => { - const compiledFile = decorationAnalyses.get(sourceFile); - const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile); - - if (compiledFile || switchMarkerAnalysis || sourceFile === this.bundle.src.file) { + if (decorationAnalyses.has(sourceFile) || switchMarkerAnalyses.has(sourceFile) || + sourceFile === this.bundle.src.file) { + const compiledFile = decorationAnalyses.get(sourceFile); + const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile); renderedFiles.push(...this.renderFile( sourceFile, compiledFile, switchMarkerAnalysis, privateDeclarationsAnalyses)); } }); - // Transform the .d.ts files - if (this.bundle.dts) { - const dtsFiles = this.getTypingsFilesToRender( - decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); - - // If the dts entry-point is not already there (it did not have compiled classes) - // then add it now, to ensure it gets its extra exports rendered. - if (!dtsFiles.has(this.bundle.dts.file)) { - dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo()); - } - dtsFiles.forEach( - (renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo))); - } - return renderedFiles; } @@ -126,32 +62,32 @@ export abstract class Renderer { renderFile( sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined, switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] { + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileToWrite[] { const isEntryPoint = sourceFile === this.bundle.src.file; - const input = this.extractSourceMap(sourceFile); + const input = extractSourceMap(this.fs, this.logger, sourceFile); const outputText = new MagicString(input.source); if (switchMarkerAnalysis) { - this.rewriteSwitchableDeclarations( + this.srcFormatter.rewriteSwitchableDeclarations( outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations); } const importManager = new ImportManager( - this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlatCore), + getImportRewriter(this.bundle.src.r3SymbolsFile, this.isCore, this.bundle.isFlatCore), IMPORT_PREFIX); if (compiledFile) { // TODO: remove constructor param metadata and property decorators (we need info from the // handlers to do this) const decoratorsToRemove = this.computeDecoratorsToRemove(compiledFile.compiledClasses); - this.removeDecorators(outputText, decoratorsToRemove); + this.srcFormatter.removeDecorators(outputText, decoratorsToRemove); compiledFile.compiledClasses.forEach(clazz => { const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager); - this.addDefinitions(outputText, clazz, renderedDefinition); + this.srcFormatter.addDefinitions(outputText, clazz, renderedDefinition); }); - this.addConstants( + this.srcFormatter.addConstants( outputText, renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager), compiledFile.sourceFile); @@ -160,115 +96,22 @@ export abstract class Renderer { // Add exports to the entry-point file if (isEntryPoint) { const entryPointBasePath = stripExtension(this.bundle.src.path); - this.addExports( + this.srcFormatter.addExports( outputText, entryPointBasePath, privateDeclarationsAnalyses, importManager, sourceFile); } if (isEntryPoint || compiledFile) { - this.addImports(outputText, importManager.getAllImports(sourceFile.fileName), sourceFile); + this.srcFormatter.addImports( + outputText, importManager.getAllImports(sourceFile.fileName), sourceFile); } if (compiledFile || switchMarkerAnalysis || isEntryPoint) { - return this.renderSourceAndMap(sourceFile, input, outputText); + return renderSourceAndMap(sourceFile, input, outputText); } else { return []; } } - renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] { - const input = this.extractSourceMap(dtsFile); - const outputText = new MagicString(input.source); - const printer = createPrinter(); - const importManager = new ImportManager( - this.getImportRewriter(this.bundle.dts !.r3SymbolsFile, false), IMPORT_PREFIX); - - renderInfo.classInfo.forEach(dtsClass => { - const endOfClass = dtsClass.dtsDeclaration.getEnd(); - dtsClass.compilation.forEach(declaration => { - const type = translateType(declaration.type, importManager); - const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile); - const newStatement = ` static ${declaration.name}: ${typeStr};\n`; - outputText.appendRight(endOfClass - 1, newStatement); - }); - }); - - this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager); - this.addImports(outputText, importManager.getAllImports(dtsFile.fileName), dtsFile); - - this.addExports( - outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports, - importManager, dtsFile); - - - return this.renderSourceAndMap(dtsFile, input, outputText); - } - - /** - * Add the type parameters to the appropriate functions that return `ModuleWithProviders` - * structures. - * - * This function only gets called on typings files, so it doesn't need different implementations - * for each bundle format. - */ - protected addModuleWithProvidersParams( - outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], - importManager: ImportManager): void { - moduleWithProviders.forEach(info => { - const ngModuleName = info.ngModule.node.name.text; - const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile()); - const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile()); - const importPath = info.ngModule.viaModule || - (declarationFile !== ngModuleFile ? - stripExtension( - `./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) : - null); - const ngModule = getImportString(importManager, importPath, ngModuleName); - - if (info.declaration.type) { - const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ? - info.declaration.type.typeName : - null; - if (this.isCoreModuleWithProvidersType(typeName)) { - // The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type - // parameter adding. - outputText.overwrite( - info.declaration.type.getStart(), info.declaration.type.getEnd(), - `ModuleWithProviders<${ngModule}>`); - } else { - // The declaration returns an unknown type so we need to convert it to a union that - // includes the ngModule property. - const originalTypeString = info.declaration.type.getText(); - outputText.overwrite( - info.declaration.type.getStart(), info.declaration.type.getEnd(), - `(${originalTypeString})&{ngModule:${ngModule}}`); - } - } else { - // The declaration has no return type so provide one. - const lastToken = info.declaration.getLastToken(); - const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ? - lastToken.getStart() : - info.declaration.getEnd(); - outputText.appendLeft( - insertPoint, - `: ${getImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`); - } - }); - } - - protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile): - void; - protected abstract addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void; - protected abstract addExports( - output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], - importManager: ImportManager, file: ts.SourceFile): void; - protected abstract addDefinitions( - output: MagicString, compiledClass: CompiledClass, definitions: string): void; - protected abstract removeDecorators( - output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void; - protected abstract rewriteSwitchableDeclarations( - outputText: MagicString, sourceFile: ts.SourceFile, - declarations: SwitchableVariableDeclaration[]): void; - /** * From the given list of classes, computes a map of decorators that should be removed. * The decorators to remove are keyed by their container node, such that we can tell if @@ -276,7 +119,7 @@ export abstract class Renderer { * @param classes The list of classes that may have decorators to remove. * @returns A map of decorators to remove, keyed by their container node. */ - protected computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap { + private computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap { const decoratorsToRemove = new RedundantDecoratorMap(); classes.forEach(clazz => { clazz.decorators.forEach(dec => { @@ -290,191 +133,6 @@ export abstract class Renderer { }); return decoratorsToRemove; } - - /** - * Get the map from the source (note whether it is inline or external) - */ - protected extractSourceMap(file: ts.SourceFile): SourceMapInfo { - const inline = commentRegex.test(file.text); - const external = mapFileCommentRegex.exec(file.text); - - if (inline) { - const inlineSourceMap = fromSource(file.text); - return { - source: removeComments(file.text).replace(/\n\n$/, '\n'), - map: inlineSourceMap, - isInline: true, - }; - } else if (external) { - let externalSourceMap: SourceMapConverter|null = null; - try { - const fileName = external[1] || external[2]; - const filePath = AbsoluteFsPath.resolve( - AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName); - const mappingFile = this.fs.readFile(filePath); - externalSourceMap = fromJSON(mappingFile); - } catch (e) { - if (e.code === 'ENOENT') { - this.logger.warn( - `The external map file specified in the source code comment "${e.path}" was not found on the file system.`); - const mapPath = AbsoluteFsPath.fromUnchecked(file.fileName + '.map'); - if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) && - this.fs.stat(mapPath).isFile()) { - this.logger.warn( - `Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`); - try { - externalSourceMap = fromObject(JSON.parse(this.fs.readFile(mapPath))); - } catch (e) { - this.logger.error(e); - } - } - } - } - return { - source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'), - map: externalSourceMap, - isInline: false, - }; - } else { - return {source: file.text, map: null, isInline: false}; - } - } - - /** - * Merge the input and output source-maps, replacing the source-map comment in the output file - * with an appropriate source-map comment pointing to the merged source-map. - */ - protected renderSourceAndMap( - sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] { - const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile); - const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`); - const relativeSourcePath = PathSegment.basename(outputPath); - const relativeMapPath = `${relativeSourcePath}.map`; - - const outputMap = output.generateMap({ - source: outputPath, - includeContent: true, - // hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix - // the merge algorithm. - }); - - // we must set this after generation as magic string does "manipulation" on the path - outputMap.file = relativeSourcePath; - - const mergedMap = - mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString())); - - const result: FileInfo[] = []; - if (input.isInline) { - result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`}); - } else { - result.push({ - path: outputPath, - contents: `${output.toString()}\n${generateMapFileComment(relativeMapPath)}` - }); - result.push({path: outputMapPath, contents: mergedMap.toJSON()}); - } - return result; - } - - protected getTypingsFilesToRender( - decorationAnalyses: DecorationAnalyses, - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, - moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses| - null): Map { - const dtsMap = new Map(); - - // Capture the rendering info from the decoration analyses - decorationAnalyses.forEach(compiledFile => { - compiledFile.compiledClasses.forEach(compiledClass => { - const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); - if (dtsDeclaration) { - const dtsFile = dtsDeclaration.getSourceFile(); - const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo(); - renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation}); - dtsMap.set(dtsFile, renderInfo); - } - }); - }); - - // Capture the ModuleWithProviders functions/methods that need updating - if (moduleWithProvidersAnalyses !== null) { - moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => { - const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo(); - renderInfo.moduleWithProviders = moduleWithProvidersToFix; - dtsMap.set(dtsFile, renderInfo); - }); - } - - // Capture the private declarations that need to be re-exported - if (privateDeclarationsAnalyses.length) { - privateDeclarationsAnalyses.forEach(e => { - if (!e.dtsFrom && !e.alias) { - throw new Error( - `There is no typings path for ${e.identifier} in ${e.from}.\n` + - `We need to add an export for this class to a .d.ts typings file because ` + - `Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` + - `The simplest fix for this is to ensure that this class is exported from the package's entry-point.`); - } - }); - const dtsEntryPoint = this.bundle.dts !.file; - const renderInfo = dtsMap.get(dtsEntryPoint) || new DtsRenderInfo(); - renderInfo.privateExports = privateDeclarationsAnalyses; - dtsMap.set(dtsEntryPoint, renderInfo); - } - - return dtsMap; - } - - /** - * Check whether the given type is the core Angular `ModuleWithProviders` interface. - * @param typeName The type to check. - * @returns true if the type is the core Angular `ModuleWithProviders` interface. - */ - private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) { - const id = - typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null; - return ( - id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core')); - } - - private getImportRewriter(r3SymbolsFile: ts.SourceFile|null, isFlat: boolean): ImportRewriter { - if (this.isCore && isFlat) { - return new NgccFlatImportRewriter(); - } else if (this.isCore) { - return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName); - } else { - return new NoopImportRewriter(); - } - } -} - -/** - * Merge the two specified source-maps into a single source-map that hides the intermediate - * source-map. - * E.g. Consider these mappings: - * - * ``` - * OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC - * ``` - * - * this will be replaced with: - * - * ``` - * OLD_SRC -> MERGED_MAP -> NEW_SRC - * ``` - */ -export function mergeSourceMaps( - oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter { - if (!oldMap) { - return fromObject(newMap); - } - const oldMapConsumer = new SourceMapConsumer(oldMap); - const newMapConsumer = new SourceMapConsumer(newMap); - const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer); - mergedMapGenerator.applySourceMap(oldMapConsumer); - const merged = fromJSON(mergedMapGenerator.toString()); - return merged; } /** @@ -515,10 +173,6 @@ export function renderDefinitions( return definitions; } -export function stripExtension(filePath: T): T { - return filePath.replace(/\.(js|d\.ts)$/, '') as T; -} - /** * Create an Angular AST statement node that contains the assignment of the * compiled decorator to be applied to the class. @@ -530,12 +184,6 @@ function createAssignmentStatement( return new WritePropExpr(receiver, propName, initializer).toStmt(); } -function getImportString( - importManager: ImportManager, importPath: string | null, importName: string) { - const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null; - return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`; -} - function createPrinter(): ts.Printer { return ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); -} \ No newline at end of file +} diff --git a/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts new file mode 100644 index 000000000000..b2a0ad2ca3bd --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {CompiledClass} from '../analysis/decoration_analyzer'; +import {SwitchableVariableDeclaration} from '../host/ngcc_host'; +import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer'; + +/** + * The collected decorators that have become redundant after the compilation + * of Ivy static fields. The map is keyed by the container node, such that we + * can tell if we should remove the entire decorator property + */ +export type RedundantDecoratorMap = Map; +export const RedundantDecoratorMap = Map; + +/** + * Implement this interface with methods that know how to render a specific format, + * such as ESM5 or UMD. + */ +export interface RenderingFormatter { + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void; + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void; + addExports( + output: MagicString, entryPointBasePath: string, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void; + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void; + removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void; + rewriteSwitchableDeclarations( + outputText: MagicString, sourceFile: ts.SourceFile, + declarations: SwitchableVariableDeclaration[]): void; + addModuleWithProvidersParams( + outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/source_maps.ts b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts new file mode 100644 index 000000000000..23be4392c876 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts @@ -0,0 +1,137 @@ +/** + * @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 {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; +import MagicString from 'magic-string'; +import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map'; +import * as ts from 'typescript'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; +import {Logger} from '../logging/logger'; +import {FileToWrite} from './utils'; + +export interface SourceMapInfo { + source: string; + map: SourceMapConverter|null; + isInline: boolean; +} + +/** + * Get the map from the source (note whether it is inline or external) + */ +export function extractSourceMap( + fs: FileSystem, logger: Logger, file: ts.SourceFile): SourceMapInfo { + const inline = commentRegex.test(file.text); + const external = mapFileCommentRegex.exec(file.text); + + if (inline) { + const inlineSourceMap = fromSource(file.text); + return { + source: removeComments(file.text).replace(/\n\n$/, '\n'), + map: inlineSourceMap, + isInline: true, + }; + } else if (external) { + let externalSourceMap: SourceMapConverter|null = null; + try { + const fileName = external[1] || external[2]; + const filePath = AbsoluteFsPath.resolve( + AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName); + const mappingFile = fs.readFile(filePath); + externalSourceMap = fromJSON(mappingFile); + } catch (e) { + if (e.code === 'ENOENT') { + logger.warn( + `The external map file specified in the source code comment "${e.path}" was not found on the file system.`); + const mapPath = AbsoluteFsPath.fromUnchecked(file.fileName + '.map'); + if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) && fs.exists(mapPath) && + fs.stat(mapPath).isFile()) { + logger.warn( + `Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`); + try { + externalSourceMap = fromObject(JSON.parse(fs.readFile(mapPath))); + } catch (e) { + logger.error(e); + } + } + } + } + return { + source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'), + map: externalSourceMap, + isInline: false, + }; + } else { + return {source: file.text, map: null, isInline: false}; + } +} + +/** + * Merge the input and output source-maps, replacing the source-map comment in the output file + * with an appropriate source-map comment pointing to the merged source-map. + */ +export function renderSourceAndMap( + sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileToWrite[] { + const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile); + const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`); + const relativeSourcePath = PathSegment.basename(outputPath); + const relativeMapPath = `${relativeSourcePath}.map`; + + const outputMap = output.generateMap({ + source: outputPath, + includeContent: true, + // hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix + // the merge algorithm. + }); + + // we must set this after generation as magic string does "manipulation" on the path + outputMap.file = relativeSourcePath; + + const mergedMap = + mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString())); + + const result: FileToWrite[] = []; + if (input.isInline) { + result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`}); + } else { + result.push({ + path: outputPath, + contents: `${output.toString()}\n${generateMapFileComment(relativeMapPath)}` + }); + result.push({path: outputMapPath, contents: mergedMap.toJSON()}); + } + return result; +} + + +/** + * Merge the two specified source-maps into a single source-map that hides the intermediate + * source-map. + * E.g. Consider these mappings: + * + * ``` + * OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC + * ``` + * + * this will be replaced with: + * + * ``` + * OLD_SRC -> MERGED_MAP -> NEW_SRC + * ``` + */ +export function mergeSourceMaps( + oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter { + if (!oldMap) { + return fromObject(newMap); + } + const oldMapConsumer = new SourceMapConsumer(oldMap); + const newMapConsumer = new SourceMapConsumer(newMap); + const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer); + mergedMapGenerator.applySourceMap(oldMapConsumer); + const merged = fromJSON(mergedMapGenerator.toString()); + return merged; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts similarity index 83% rename from packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts rename to packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts index e3ccfff46fc8..f15e23b581c1 100644 --- a/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts @@ -10,25 +10,23 @@ import * as ts from 'typescript'; import MagicString from 'magic-string'; import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {ExportInfo} from '../analysis/private_declarations_analyzer'; -import {FileSystem} from '../file_system/file_system'; import {UmdReflectionHost} from '../host/umd_host'; -import {Logger} from '../logging/logger'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {Esm5Renderer} from './esm5_renderer'; -import {stripExtension} from './renderer'; +import {Esm5RenderingFormatter} from './esm5_rendering_formatter'; +import {stripExtension} from './utils'; type CommonJsConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; type AmdConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; -export class UmdRenderer extends Esm5Renderer { - constructor( - fs: FileSystem, logger: Logger, protected umdHost: UmdReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, umdHost, isCore, bundle); - } +/** + * A RenderingFormatter that works with UMD files, instead of `import` and `export` statements + * the module is an IIFE with a factory function call with dependencies, which are defined in a + * wrapper function for AMD, CommonJS and global module formats. + */ +export class UmdRenderingFormatter extends Esm5RenderingFormatter { + constructor(protected umdHost: UmdReflectionHost, isCore: boolean) { super(umdHost, isCore); } /** - * Add the imports at the top of the file + * Add the imports to the UMD module IIFE. */ addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void { // Assume there is only one UMD module in the file @@ -46,6 +44,9 @@ export class UmdRenderer extends Esm5Renderer { renderFactoryParameters(output, wrapperFunction, imports); } + /** + * Add the exports to the bottom of the UMD module factory function. + */ addExports( output: MagicString, entryPointBasePath: string, exports: ExportInfo[], importManager: ImportManager, file: ts.SourceFile): void { @@ -70,6 +71,9 @@ export class UmdRenderer extends Esm5Renderer { }); } + /** + * Add the constants to the top of the UMD factory function. + */ addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { if (constants === '') { return; @@ -86,6 +90,9 @@ export class UmdRenderer extends Esm5Renderer { } } +/** + * Add dependencies to the CommonJS part of the UMD wrapper function. + */ function renderCommonJsDependencies( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const conditional = find(wrapperFunction.body.statements[0], isCommonJSConditional); @@ -98,6 +105,9 @@ function renderCommonJsDependencies( imports.forEach(i => output.appendLeft(injectionPoint, `,require('${i.specifier}')`)); } +/** + * Add dependencies to the AMD part of the UMD wrapper function. + */ function renderAmdDependencies( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const conditional = find(wrapperFunction.body.statements[0], isAmdConditional); @@ -113,17 +123,23 @@ function renderAmdDependencies( imports.forEach(i => output.appendLeft(injectionPoint, `,'${i.specifier}'`)); } +/** + * Add dependencies to the global part of the UMD wrapper function. + */ function renderGlobalDependencies( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const globalFactoryCall = find(wrapperFunction.body.statements[0], isGlobalFactoryCall); if (!globalFactoryCall) { return; } - const injectionPoint = globalFactoryCall.getEnd() - - 1; // Backup one char to account for the closing parenthesis on the call + // Backup one char to account for the closing parenthesis after the argument list of the call. + const injectionPoint = globalFactoryCall.getEnd() - 1; imports.forEach(i => output.appendLeft(injectionPoint, `,global.${getGlobalIdentifier(i)}`)); } +/** + * Add dependency parameters to the UMD factory function. + */ function renderFactoryParameters( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const wrapperCall = wrapperFunction.parent as ts.CallExpression; @@ -143,6 +159,9 @@ function renderFactoryParameters( imports.forEach(i => output.appendLeft(injectionPoint, `,${i.qualifier}`)); } +/** + * Is this node the CommonJS conditional expression in the UMD wrapper? + */ function isCommonJSConditional(value: ts.Node): value is CommonJsConditional { if (!ts.isConditionalExpression(value)) { return false; @@ -160,6 +179,9 @@ function isCommonJSConditional(value: ts.Node): value is CommonJsConditional { return value.whenTrue.expression.text === 'factory'; } +/** + * Is this node the AMD conditional expression in the UMD wrapper? + */ function isAmdConditional(value: ts.Node): value is AmdConditional { if (!ts.isConditionalExpression(value)) { return false; @@ -177,6 +199,9 @@ function isAmdConditional(value: ts.Node): value is AmdConditional { return value.whenTrue.expression.text === 'define'; } +/** + * Is this node the call to setup the global dependencies in the UMD wrapper? + */ function isGlobalFactoryCall(value: ts.Node): value is ts.CallExpression { if (ts.isCallExpression(value) && !!value.parent) { // Be resilient to the value being inside parentheses diff --git a/packages/compiler-cli/ngcc/src/rendering/utils.ts b/packages/compiler-cli/ngcc/src/rendering/utils.ts new file mode 100644 index 000000000000..8392a094256d --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/utils.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; + +/** + * Information about a file that has been rendered. + */ +export interface FileToWrite { + /** Path to where the file should be written. */ + path: AbsoluteFsPath; + /** The contents of the file to be be written. */ + contents: string; +} + +/** + * Create an appropriate ImportRewriter given the parameters. + */ +export function getImportRewriter( + r3SymbolsFile: ts.SourceFile | null, isCore: boolean, isFlat: boolean): ImportRewriter { + if (isCore && isFlat) { + return new NgccFlatImportRewriter(); + } else if (isCore) { + return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName); + } else { + return new NoopImportRewriter(); + } +} + +export function stripExtension(filePath: T): T { + return filePath.replace(/\.(js|d\.ts)$/, '') as T; +} diff --git a/packages/compiler-cli/ngcc/src/writing/file_writer.ts b/packages/compiler-cli/ngcc/src/writing/file_writer.ts index 0d7ee1e8a6f4..5b178e535596 100644 --- a/packages/compiler-cli/ngcc/src/writing/file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/file_writer.ts @@ -8,12 +8,13 @@ */ import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; /** * Responsible for writing out the transformed files to disk. */ export interface FileWriter { - writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]): void; + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]): + void; } diff --git a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts index 6bf43e94caa9..adc7f396c5f9 100644 --- a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts @@ -10,7 +10,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../file_system/file_system'; import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; import {FileWriter} from './file_writer'; /** @@ -20,11 +20,11 @@ import {FileWriter} from './file_writer'; export class InPlaceFileWriter implements FileWriter { constructor(protected fs: FileSystem) {} - writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileToWrite[]) { transformedFiles.forEach(file => this.writeFileAndBackup(file)); } - protected writeFileAndBackup(file: FileInfo): void { + protected writeFileAndBackup(file: FileToWrite): void { this.fs.ensureDir(AbsoluteFsPath.dirname(file.path)); const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`); if (this.fs.exists(backPath)) { diff --git a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts index 10414ebdfc0c..59ff4e67c1f9 100644 --- a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts @@ -10,7 +10,7 @@ import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; import {InPlaceFileWriter} from './in_place_file_writer'; @@ -25,7 +25,7 @@ const NGCC_DIRECTORY = '__ivy_ngcc__'; * `InPlaceFileWriter`). */ export class NewEntryPointFileWriter extends InPlaceFileWriter { - writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]) { // The new folder is at the root of the overall package const ngccFolder = AbsoluteFsPath.join(entryPoint.package, NGCC_DIRECTORY); this.copyBundle(bundle, entryPoint.package, ngccFolder); @@ -47,7 +47,7 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter { }); } - protected writeFile(file: FileInfo, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath): + protected writeFile(file: FileToWrite, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath): void { if (isDtsPath(file.path.replace(/\.map$/, ''))) { // This is either `.d.ts` or `.d.ts.map` file diff --git a/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts new file mode 100644 index 000000000000..3fea099f44d4 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts @@ -0,0 +1,181 @@ +/** + * @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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {fromObject} from 'convert-source-map'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {ModuleWithProvidersAnalyzer, ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer'; +import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {DtsRenderer} from '../../src/rendering/dts_renderer'; +import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; +import {MockLogger} from '../helpers/mock_logger'; +import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter'; +import {MockFileSystem} from '../helpers/mock_file_system'; +import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; + +const _ = AbsoluteFsPath.fromUnchecked; + +class TestRenderingFormatter implements RenderingFormatter { + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { + output.prepend('\n// ADD IMPORTS\n'); + } + addExports(output: MagicString, baseEntryPointPath: string, exports: ExportInfo[]) { + output.prepend('\n// ADD EXPORTS\n'); + } + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + output.prepend('\n// ADD CONSTANTS\n'); + } + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) { + output.prepend('\n// ADD DEFINITIONS\n'); + } + removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap) { + output.prepend('\n// REMOVE DECORATORS\n'); + } + rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { + output.prepend('\n// REWRITTEN DECLARATIONS\n'); + } + addModuleWithProvidersParams( + output: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + output.prepend('\n// ADD MODUlE WITH PROVIDERS PARAMS\n'); + } +} + +function createTestRenderer( + packageName: string, files: {name: string, contents: string}[], + dtsFiles?: {name: string, contents: string}[], + mappingFiles?: {name: string, contents: string}[]) { + const logger = new MockLogger(); + const fs = new MockFileSystem(createFileSystemFromProgramFiles(files, dtsFiles, mappingFiles)); + const isCore = packageName === '@angular/core'; + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', isCore, files, dtsFiles); + const typeChecker = bundle.src.program.getTypeChecker(); + const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, + typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) + .analyzeProgram(); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); + const privateDeclarationsAnalyses = + new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); + const testFormatter = new TestRenderingFormatter(); + spyOn(testFormatter, 'addExports').and.callThrough(); + spyOn(testFormatter, 'addImports').and.callThrough(); + spyOn(testFormatter, 'addDefinitions').and.callThrough(); + spyOn(testFormatter, 'addConstants').and.callThrough(); + spyOn(testFormatter, 'removeDecorators').and.callThrough(); + spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough(); + spyOn(testFormatter, 'addModuleWithProvidersParams').and.callThrough(); + + const renderer = new DtsRenderer(testFormatter, fs, logger, host, isCore, bundle); + + return {renderer, + testFormatter, + decorationAnalyses, + moduleWithProvidersAnalyses, + privateDeclarationsAnalyses, + bundle}; +} + + +describe('DtsRenderer', () => { + const INPUT_PROGRAM = { + name: '/src/file.js', + contents: + `import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n` + }; + const INPUT_DTS_PROGRAM = { + name: '/typings/file.d.ts', + contents: `export declare class A {\nfoo(x: number): number;\n}\n` + }; + + const INPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'file': '/src/file.js', + 'sourceRoot': '', + 'sources': ['/src/file.ts'], + 'names': [], + 'mappings': + 'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC', + 'sourcesContent': [INPUT_PROGRAM.contents] + }); + + const RENDERED_CONTENTS = ` +// ADD IMPORTS + +// ADD EXPORTS + +// ADD CONSTANTS + +// ADD DEFINITIONS + +// REMOVE DECORATORS +` + INPUT_PROGRAM.contents; + + const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'sources': ['/src/file.ts'], + 'names': [], + 'mappings': ';;;;;;;;;;AAAA', + 'file': 'file.js', + 'sourcesContent': [INPUT_PROGRAM.contents] + }); + + it('should render extract types into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents) + .toContain( + 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ΔDirectiveDefWithMeta'); + }); + + it('should render imports into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); + }); + + it('should render exports into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + + // Add a mock export to trigger export rendering + privateDeclarationsAnalyses.push( + {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); + + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`); + }); + + it('should render ModuleWithProviders type params', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD MODUlE WITH PROVIDERS PARAMS\n`); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts similarity index 98% rename from packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts index fc57b1e65334..4f8e69e5ca5a 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts @@ -15,7 +15,7 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../../src/constants'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; -import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; +import {Esm5RenderingFormatter} from '../../src/rendering/esm5_rendering_formatter'; import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; @@ -35,7 +35,7 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new Esm5Renderer(fs, logger, host, false, bundle); + const renderer = new Esm5RenderingFormatter(host, false); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, @@ -155,7 +155,7 @@ export { D }; // Some other content` }; -describe('Esm5Renderer', () => { +describe('Esm5RenderingFormatter', () => { describe('addImports', () => { it('should insert the given imports after existing imports of the source file', () => { diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts similarity index 65% rename from packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts index df75d709b176..fc68fee711bb 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts @@ -15,29 +15,33 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../../src/constants'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {EsmRenderer} from '../../src/rendering/esm_renderer'; +import {EsmRenderingFormatter} from '../../src/rendering/esm_rendering_formatter'; import {makeTestEntryPointBundle} from '../helpers/utils'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; +import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; const _ = AbsoluteFsPath.fromUnchecked; -function setup(file: {name: AbsoluteFsPath, contents: string}) { +function setup( + files: {name: string, contents: string}[], + dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]) { const fs = new MockFileSystem(); const logger = new MockLogger(); - const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !; + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, files, dtsFiles) !; const typeChecker = bundle.src.program.getTypeChecker(); - const host = new Esm2015ReflectionHost(logger, false, typeChecker); + const host = new Esm2015ReflectionHost(logger, false, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); const decorationAnalyses = new DecorationAnalyzer( fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, referencesRegistry, [_('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new EsmRenderer(fs, logger, host, false, bundle); + const renderer = new EsmRenderingFormatter(host, false); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, + bundle, program: bundle.src.program, sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager, }; @@ -79,49 +83,11 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { // Some other content` }; -const PROGRAM_DECORATE_HELPER = { - name: _('/some/file.js'), - contents: ` -import * as tslib_1 from "tslib"; -var D_1; -/* A copyright notice */ -import { Directive } from '@angular/core'; -const OtherA = () => (node) => { }; -const OtherB = () => (node) => { }; -let A = class A { -}; -A = tslib_1.__decorate([ - Directive({ selector: '[a]' }), - OtherA() -], A); -export { A }; -let B = class B { -}; -B = tslib_1.__decorate([ - OtherB(), - Directive({ selector: '[b]' }) -], B); -export { B }; -let C = class C { -}; -C = tslib_1.__decorate([ - Directive({ selector: '[c]' }) -], C); -export { C }; -let D = D_1 = class D { -}; -D = D_1 = tslib_1.__decorate([ - Directive({ selector: '[d]', providers: [D_1] }) -], D); -export { D }; -// Some other content` -}; - -describe('Esm2015Renderer', () => { +describe('EsmRenderingFormatter', () => { describe('addImports', () => { it('should insert the given imports after existing imports of the source file', () => { - const {renderer, sourceFile} = setup(PROGRAM); + const {renderer, sourceFile} = setup([PROGRAM]); const output = new MagicString(PROGRAM.contents); renderer.addImports( output, @@ -140,7 +106,7 @@ import * as i1 from '@angular/common';`); describe('addExports', () => { it('should insert the given exports at the end of the source file', () => { - const {importManager, renderer, sourceFile} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup([PROGRAM]); const output = new MagicString(PROGRAM.contents); renderer.addExports( output, _(PROGRAM.name.replace(/\.js$/, '')), @@ -160,7 +126,7 @@ export {TopLevelComponent};`); }); it('should not insert alias exports in js output', () => { - const {importManager, renderer, sourceFile} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup([PROGRAM]); const output = new MagicString(PROGRAM.contents); renderer.addExports( output, _(PROGRAM.name.replace(/\.js$/, '')), @@ -180,7 +146,7 @@ export {TopLevelComponent};`); describe('addConstants', () => { it('should insert the given constants after imports in the source file', () => { - const {renderer, program} = setup(PROGRAM); + const {renderer, program} = setup([PROGRAM]); const file = program.getSourceFile('some/file.js'); if (file === undefined) { throw new Error(`Could not find source file`); @@ -195,7 +161,7 @@ export class A {}`); }); it('should insert constants after inserted imports', () => { - const {renderer, program} = setup(PROGRAM); + const {renderer, program} = setup([PROGRAM]); const file = program.getSourceFile('some/file.js'); if (file === undefined) { throw new Error(`Could not find source file`); @@ -214,7 +180,7 @@ export class A {`); describe('rewriteSwitchableDeclarations', () => { it('should switch marked declaration initializers', () => { - const {renderer, program, switchMarkerAnalyses, sourceFile} = setup(PROGRAM); + const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]); const file = program.getSourceFile('some/file.js'); if (file === undefined) { throw new Error(`Could not find source file`); @@ -237,7 +203,7 @@ export class A {`); describe('addDefinitions', () => { it('should insert the definitions directly after the class declaration', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]); const output = new MagicString(PROGRAM.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; @@ -256,7 +222,7 @@ A.decorators = [ describe('[static property declaration]', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); const output = new MagicString(PROGRAM.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; @@ -275,7 +241,7 @@ A.decorators = [ it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); const output = new MagicString(PROGRAM.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; @@ -294,7 +260,7 @@ A.decorators = [ it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); const output = new MagicString(PROGRAM.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; @@ -314,8 +280,47 @@ A.decorators = [ }); describe('[__decorate declarations]', () => { + + const PROGRAM_DECORATE_HELPER = { + name: '/some/file.js', + contents: ` +import * as tslib_1 from "tslib"; +var D_1; +/* A copyright notice */ +import { Directive } from '@angular/core'; +const OtherA = () => (node) => { }; +const OtherB = () => (node) => { }; +let A = class A { +}; +A = tslib_1.__decorate([ + Directive({ selector: '[a]' }), + OtherA() +], A); +export { A }; +let B = class B { +}; +B = tslib_1.__decorate([ + OtherB(), + Directive({ selector: '[b]' }) +], B); +export { B }; +let C = class C { +}; +C = tslib_1.__decorate([ + Directive({ selector: '[c]' }) +], C); +export { C }; +let D = D_1 = class D { +}; +D = D_1 = tslib_1.__decorate([ + Directive({ selector: '[d]', providers: [D_1] }) +], D); +export { D }; +// Some other content` + }; + it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; @@ -332,7 +337,7 @@ A.decorators = [ it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; @@ -350,7 +355,7 @@ A.decorators = [ it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; @@ -367,4 +372,140 @@ A.decorators = [ expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`); }); }); + + describe('addModuleWithProvidersParams', () => { + const MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: '/src/index.js', + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export class SomeClass {} + export class SomeModule { + static withProviders1() { return {ngModule: SomeModule}; } + static withProviders2() { return {ngModule: SomeModule}; } + static withProviders3() { return {ngModule: SomeClass}; } + static withProviders4() { return {ngModule: ExternalModule}; } + static withProviders5() { return {ngModule: ExternalModule}; } + static withProviders6() { return {ngModule: LibraryModule}; } + static withProviders7() { return {ngModule: SomeModule, providers: []}; }; + static withProviders8() { return {ngModule: SomeModule}; } + } + export function withProviders1() { return {ngModule: SomeModule}; } + export function withProviders2() { return {ngModule: SomeModule}; } + export function withProviders3() { return {ngModule: SomeClass}; } + export function withProviders4() { return {ngModule: ExternalModule}; } + export function withProviders5() { return {ngModule: ExternalModule}; } + export function withProviders6() { return {ngModule: LibraryModule}; } + export function withProviders7() { return {ngModule: SomeModule, providers: []}; }; + export function withProviders8() { return {ngModule: SomeModule}; }`, + }, + { + name: '/src/module.js', + contents: ` + export class ExternalModule { + static withProviders1() { return {ngModule: ExternalModule}; } + static withProviders2() { return {ngModule: ExternalModule}; } + }` + }, + { + name: '/node_modules/some-library/index.d.ts', + contents: 'export declare class LibraryModule {}' + }, + ]; + const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [ + { + name: '/typings/index.d.ts', + contents: ` + import {ModuleWithProviders} from '@angular/core'; + export declare class SomeClass {} + export interface MyModuleWithProviders extends ModuleWithProviders {} + export declare class SomeModule { + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders; + static withProviders5(); + static withProviders6(): ModuleWithProviders; + static withProviders7(): {ngModule: SomeModule, providers: any[]}; + static withProviders8(): MyModuleWithProviders; + } + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders; + export declare function withProviders5(); + export declare function withProviders6(): ModuleWithProviders; + export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; + export declare function withProviders8(): MyModuleWithProviders;` + }, + { + name: '/typings/module.d.ts', + contents: ` + export interface ModuleWithProviders {} + export declare class ExternalModule { + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + }` + }, + { + name: '/node_modules/some-library/index.d.ts', + contents: 'export declare class LibraryModule {}' + }, + ]; + + it('should fixup functions/methods that return ModuleWithProviders structures', () => { + const {bundle, renderer, host} = + setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); + + const referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = bundle.dts !.program.getSourceFile('/typings/index.d.ts') !; + const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; + + const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[0].contents); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); + + expect(output.toString()).toContain(` + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders; + static withProviders5(): i1.ModuleWithProviders; + static withProviders6(): ModuleWithProviders; + static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + expect(output.toString()).toContain(` + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders; + export declare function withProviders5(): i1.ModuleWithProviders; + export declare function withProviders6(): ModuleWithProviders; + export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + }); + + it('should not mistake `ModuleWithProviders` types that are not imported from `@angular/core', + () => { + const {bundle, renderer, host} = + setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); + + const referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = bundle.dts !.program.getSourceFile('/typings/module.d.ts') !; + const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; + + const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[1].contents); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); + expect(output.toString()).toContain(` + static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule}; + static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`); + }); + }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 81b5f97575c3..83ff27c7ac6a 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -9,29 +9,22 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {fromObject, generateMapFileComment} from 'convert-source-map'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {Import} from '../../../src/ngtsc/translator'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; -import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; +import {ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer'; import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer'; -import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; +const _ = AbsoluteFsPath.fromUnchecked; + +import {Renderer} from '../../src/rendering/renderer'; +import {MockLogger} from '../helpers/mock_logger'; +import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter'; import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; -import {Logger} from '../../src/logging/logger'; import {MockFileSystem} from '../helpers/mock_file_system'; -import {MockLogger} from '../helpers/mock_logger'; -import {FileSystem} from '../../src/file_system/file_system'; -const _ = AbsoluteFsPath.fromUnchecked; - -class TestRenderer extends Renderer { - constructor( - fs: FileSystem, logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, host, isCore, bundle); - } +class TestRenderingFormatter implements RenderingFormatter { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { output.prepend('\n// ADD IMPORTS\n'); } @@ -50,6 +43,11 @@ class TestRenderer extends Renderer { rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { output.prepend('\n// REWRITTEN DECLARATIONS\n'); } + addModuleWithProvidersParams( + output: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + output.prepend('\n// ADD MODUlE WITH PROVIDERS PARAMS\n'); + } } function createTestRenderer( @@ -68,21 +66,23 @@ function createTestRenderer( typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const moduleWithProvidersAnalyses = - new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); const privateDeclarationsAnalyses = new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); - const renderer = new TestRenderer(fs, logger, host, isCore, bundle); - spyOn(renderer, 'addExports').and.callThrough(); - spyOn(renderer, 'addImports').and.callThrough(); - spyOn(renderer, 'addDefinitions').and.callThrough(); - spyOn(renderer, 'addConstants').and.callThrough(); - spyOn(renderer, 'removeDecorators').and.callThrough(); + const testFormatter = new TestRenderingFormatter(); + spyOn(testFormatter, 'addExports').and.callThrough(); + spyOn(testFormatter, 'addImports').and.callThrough(); + spyOn(testFormatter, 'addDefinitions').and.callThrough(); + spyOn(testFormatter, 'addConstants').and.callThrough(); + spyOn(testFormatter, 'removeDecorators').and.callThrough(); + spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough(); + spyOn(testFormatter, 'addModuleWithProvidersParams').and.callThrough(); + + const renderer = new Renderer(testFormatter, fs, logger, host, isCore, bundle); return {renderer, + testFormatter, decorationAnalyses, switchMarkerAnalyses, - moduleWithProvidersAnalyses, privateDeclarationsAnalyses, bundle}; } @@ -94,10 +94,6 @@ describe('Renderer', () => { contents: `import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n` }; - const INPUT_DTS_PROGRAM = { - name: '/typings/file.d.ts', - contents: `export declare class A {\nfoo(x: number): number;\n}\n` - }; const COMPONENT_PROGRAM = { name: '/src/component.js', @@ -149,11 +145,10 @@ describe('Renderer', () => { describe('renderProgram()', () => { it('should render the modified contents; and a new map file, if the original provided no map file.', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); @@ -164,11 +159,9 @@ describe('Renderer', () => { it('should render as JavaScript', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); - renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + testFormatter} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); + renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toEqual( `A.ngComponentDef = ɵngcc0.ΔdefineComponent({ type: A, selectors: [["a"]], factory: function A_Factory(t) { return new (t || A)(); }, consts: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) { @@ -184,16 +177,14 @@ describe('Renderer', () => { }); - describe('calling abstract methods', () => { + describe('calling RenderingFormatter methods', () => { it('should call addImports with the source code and info about the core Angular library.', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addImportsSpy = renderer.addImports as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addImportsSpy.calls.first().args[1]).toEqual([ {specifier: '@angular/core', qualifier: 'ɵngcc0'} @@ -203,12 +194,10 @@ describe('Renderer', () => { it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({ name: _('A'), @@ -226,12 +215,10 @@ describe('Renderer', () => { it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const removeDecoratorsSpy = testFormatter.removeDecorators as jasmine.Spy; expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); // Each map key is the TS node of the decorator container @@ -251,14 +238,13 @@ describe('Renderer', () => { it('should call renderImports after other abstract methods', () => { // This allows the other methods to add additional imports if necessary const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); - const addExportsSpy = renderer.addExports as jasmine.Spy; - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; - const addConstantsSpy = renderer.addConstants as jasmine.Spy; - const addImportsSpy = renderer.addImports as jasmine.Spy; + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const addExportsSpy = testFormatter.addExports as jasmine.Spy; + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; + const addConstantsSpy = testFormatter.addConstants as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); @@ -268,16 +254,14 @@ describe('Renderer', () => { describe('source map merging', () => { it('should merge any inline source map from the original file and write the output as an inline source map', () => { - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = createTestRenderer( 'test-package', [{ ...INPUT_PROGRAM, contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() }]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); @@ -292,12 +276,10 @@ describe('Renderer', () => { }]; const mappingFiles = [{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}]; - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); @@ -320,15 +302,13 @@ describe('Renderer', () => { }; // The package name of `@angular/core` indicates that we are compiling the core library. const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`); - const addImportsSpy = renderer.addImports as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[1]).toEqual([ {specifier: './r3_symbols', qualifier: 'ɵngcc0'} ]); @@ -342,230 +322,15 @@ describe('Renderer', () => { }; const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]); + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toContain(`/*@__PURE__*/ setClassMetadata(`); - const addImportsSpy = renderer.addImports as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[1]).toEqual([]); }); }); - - describe('rendering typings', () => { - it('should render extract types into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents) - .toContain( - 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ΔDirectiveDefWithMeta'); - }); - - it('should render imports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); - }); - - it('should render exports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - - // Add a mock export to trigger export rendering - privateDeclarationsAnalyses.push( - {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); - - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`); - }); - - it('should fixup functions/methods that return ModuleWithProviders structures', () => { - const MODULE_WITH_PROVIDERS_PROGRAM = [ - { - name: '/src/index.js', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export class SomeClass {} - export class SomeModule { - static withProviders1() { - return {ngModule: SomeModule}; - } - static withProviders2() { - return {ngModule: SomeModule}; - } - static withProviders3() { - return {ngModule: SomeClass}; - } - static withProviders4() { - return {ngModule: ExternalModule}; - } - static withProviders5() { - return {ngModule: ExternalModule}; - } - static withProviders6() { - return {ngModule: LibraryModule}; - } - static withProviders7() { - return {ngModule: SomeModule, providers: []}; - }; - static withProviders8() { - return {ngModule: SomeModule}; - } - } - export function withProviders1() { - return {ngModule: SomeModule}; - } - export function withProviders2() { - return {ngModule: SomeModule}; - } - export function withProviders3() { - return {ngModule: SomeClass}; - } - export function withProviders4() { - return {ngModule: ExternalModule}; - } - export function withProviders5() { - return {ngModule: ExternalModule}; - } - export function withProviders6() { - return {ngModule: LibraryModule}; - } - export function withProviders7() { - return {ngModule: SomeModule, providers: []}; - }; - export function withProviders8() { - return {ngModule: SomeModule}; - }`, - }, - { - name: '/src/module.js', - contents: ` - export class ExternalModule { - static withProviders1() { - return {ngModule: ExternalModule}; - } - static withProviders2() { - return {ngModule: ExternalModule}; - } - }` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, - ]; - const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [ - { - name: '/typings/index.d.ts', - contents: ` - import {ModuleWithProviders} from '@angular/core'; - export declare class SomeClass {} - export interface MyModuleWithProviders extends ModuleWithProviders {} - export declare class SomeModule { - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - static withProviders3(): ModuleWithProviders; - static withProviders4(): ModuleWithProviders; - static withProviders5(); - static withProviders6(): ModuleWithProviders; - static withProviders7(): {ngModule: SomeModule, providers: any[]}; - static withProviders8(): MyModuleWithProviders; - } - export declare function withProviders1(): ModuleWithProviders; - export declare function withProviders2(): ModuleWithProviders; - export declare function withProviders3(): ModuleWithProviders; - export declare function withProviders4(): ModuleWithProviders; - export declare function withProviders5(); - export declare function withProviders6(): ModuleWithProviders; - export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; - export declare function withProviders8(): MyModuleWithProviders;` - }, - { - name: '/typings/module.d.ts', - contents: ` - export interface ModuleWithProviders {} - export declare class ExternalModule { - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - }` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, - ]; - const {renderer, - decorationAnalyses, - switchMarkerAnalyses, - privateDeclarationsAnalyses, - moduleWithProvidersAnalyses, - bundle} = - createTestRenderer( - 'test-package', MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); - - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/index.d.ts') !; - - expect(typingsFile.contents).toContain(` - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - static withProviders3(): ModuleWithProviders; - static withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; - static withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; - static withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; - static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; - static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); - expect(typingsFile.contents).toContain(` - export declare function withProviders1(): ModuleWithProviders; - export declare function withProviders2(): ModuleWithProviders; - export declare function withProviders3(): ModuleWithProviders; - export declare function withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; - export declare function withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; - export declare function withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; - export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; - export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); - - expect(renderer.addImports) - .toHaveBeenCalledWith( - jasmine.any(MagicString), - [ - {specifier: './module', qualifier: 'ɵngcc0'}, - {specifier: '@angular/core', qualifier: 'ɵngcc1'}, - {specifier: 'some-library', qualifier: 'ɵngcc2'}, - ], - bundle.dts !.file); - - - // The following expectation checks that we do not mistake `ModuleWithProviders` types - // that are not imported from `@angular/core`. - const typingsFile2 = result.find(f => f.path === '/typings/module.d.ts') !; - expect(typingsFile2.contents).toContain(` - static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule}; - static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`); - }); - }); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts similarity index 99% rename from packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts index e3dd4df9a6ee..8d6e09d6b10f 100644 --- a/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts @@ -14,8 +14,8 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {UmdReflectionHost} from '../../src/host/umd_host'; import {ImportManager} from '../../../src/ngtsc/translator'; -import {UmdRenderer} from '../../src/rendering/umd_renderer'; import {MockFileSystem} from '../helpers/mock_file_system'; +import {UmdRenderingFormatter} from '../../src/rendering/umd_rendering_formatter'; import {MockLogger} from '../helpers/mock_logger'; import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; @@ -34,7 +34,7 @@ function setup(file: {name: string, contents: string}) { referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); - const renderer = new UmdRenderer(fs, logger, host, false, bundle); + const renderer = new UmdRenderingFormatter(host, false); const importManager = new ImportManager(new NoopImportRewriter(), 'i'); return { decorationAnalyses, @@ -169,7 +169,7 @@ typeof define === 'function' && define.amd ? define('file', ['exports','/tslib', })));` }; -describe('UmdRenderer', () => { +describe('UmdRenderingFormatter', () => { describe('addImports', () => { it('should append the given imports into the CommonJS factory call', () => { From 24ce6c8595f25640f1490635548201334370aa7c Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:35 +0100 Subject: [PATCH 14/15] feat(ivy): ngcc - implement UmdDependencyHost The dependency resolution that works out the order in which to process entry-points must also understand UMD formats. --- .../src/dependencies/dependency_resolver.ts | 23 ++- .../src/dependencies/umd_dependency_host.ts | 111 ++++++++++ packages/compiler-cli/ngcc/src/main.ts | 7 +- .../dependencies/dependency_resolver_spec.ts | 40 +++- .../dependencies/esm_dependency_host_spec.ts | 2 +- .../dependencies/umd_dependency_host_spec.ts | 192 ++++++++++++++++++ .../test/packages/entry_point_finder_spec.ts | 4 +- 7 files changed, 361 insertions(+), 18 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts create mode 100644 packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts index a4f532375566..d4338b5d7e16 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts @@ -7,15 +7,11 @@ */ import {DepGraph} from 'dependency-graph'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {Logger} from '../logging/logger'; -import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point'; - +import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point'; import {DependencyHost} from './dependency_host'; - - /** * Holds information about entry points that are removed because * they have dependencies that are missing (directly or transitively). @@ -68,7 +64,8 @@ export interface SortedEntryPointsInfo extends DependencyDiagnostics { entryPoin * A class that resolves dependencies between entry-points. */ export class DependencyResolver { - constructor(private logger: Logger, private host: DependencyHost) {} + constructor( + private logger: Logger, private hosts: Partial>) {} /** * Sort the array of entry points so that the dependant entry points always come later than * their dependencies in the array. @@ -118,8 +115,13 @@ export class DependencyResolver { // Now add the dependencies between them angularEntryPoints.forEach(entryPoint => { - const entryPointPath = getEntryPointPath(entryPoint); - const {dependencies, missing, deepImports} = this.host.findDependencies(entryPointPath); + const formatInfo = getEntryPointFormatInfo(entryPoint); + const host = this.hosts[formatInfo.format]; + if (!host) { + throw new Error( + `Could not find a suitable format for computing dependencies of entry-point: '${entryPoint.path}'.`); + } + const {dependencies, missing, deepImports} = host.findDependencies(formatInfo.path); if (missing.size > 0) { // This entry point has dependencies that are missing @@ -164,7 +166,8 @@ export class DependencyResolver { } } -function getEntryPointPath(entryPoint: EntryPoint): AbsoluteFsPath { +function getEntryPointFormatInfo(entryPoint: EntryPoint): + {format: EntryPointFormat, path: AbsoluteFsPath} { const properties = Object.keys(entryPoint.packageJson); for (let i = 0; i < properties.length; i++) { const property = properties[i] as EntryPointJsonProperty; @@ -172,7 +175,7 @@ function getEntryPointPath(entryPoint: EntryPoint): AbsoluteFsPath { if (format === 'esm2015' || format === 'esm5' || format === 'umd') { const formatPath = entryPoint.packageJson[property] !; - return AbsoluteFsPath.resolve(entryPoint.path, formatPath); + return {format, path: AbsoluteFsPath.resolve(entryPoint.path, formatPath)}; } } throw new Error( diff --git a/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts new file mode 100644 index 000000000000..f8ad0ae9d667 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; + +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; +import {getImportsOfUmdModule, parseStatementForUmdModule} from '../host/umd_host'; + +import {DependencyHost, DependencyInfo} from './dependency_host'; +import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; + + + +/** + * Helper functions for computing dependencies. + */ +export class UmdDependencyHost implements DependencyHost { + constructor(private fs: FileSystem, private moduleResolver: ModuleResolver) {} + + /** + * Find all the dependencies for the entry-point at the given path. + * + * @param entryPointPath The absolute path to the JavaScript file that represents an entry-point. + * @returns Information about the dependencies of the entry-point, including those that were + * missing or deep imports into other entry-points. + */ + findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo { + const dependencies = new Set(); + const missing = new Set(); + const deepImports = new Set(); + const alreadySeen = new Set(); + this.recursivelyFindDependencies( + entryPointPath, dependencies, missing, deepImports, alreadySeen); + return {dependencies, missing, deepImports}; + } + + /** + * Compute the dependencies of the given file. + * + * @param file An absolute path to the file whose dependencies we want to get. + * @param dependencies A set that will have the absolute paths of resolved entry points added to + * it. + * @param missing A set that will have the dependencies that could not be found added to it. + * @param deepImports A set that will have the import paths that exist but cannot be mapped to + * entry-points, i.e. deep-imports. + * @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck + * in a + * circular dependency loop. + */ + private recursivelyFindDependencies( + file: AbsoluteFsPath, dependencies: Set, missing: Set, + deepImports: Set, alreadySeen: Set): void { + const fromContents = this.fs.readFile(file); + if (!this.hasRequireCalls(fromContents)) { + // Avoid parsing the source file as there are no require calls. + return; + } + + // Parse the source into a TypeScript AST and then walk it looking for imports and re-exports. + const sf = + ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS); + if (sf.statements.length !== 1) { + return; + } + + const umdModule = parseStatementForUmdModule(sf.statements[0]); + const umdImports = umdModule && getImportsOfUmdModule(umdModule); + if (umdImports === null) { + return; + } + + umdImports.forEach(umdImport => { + const resolvedModule = this.moduleResolver.resolveModuleImport(umdImport.path, file); + if (resolvedModule) { + if (resolvedModule instanceof ResolvedRelativeModule) { + const internalDependency = resolvedModule.modulePath; + if (!alreadySeen.has(internalDependency)) { + alreadySeen.add(internalDependency); + this.recursivelyFindDependencies( + internalDependency, dependencies, missing, deepImports, alreadySeen); + } + } else { + if (resolvedModule instanceof ResolvedDeepImport) { + deepImports.add(resolvedModule.importPath); + } else { + dependencies.add(resolvedModule.entryPointPath); + } + } + } else { + missing.add(umdImport.path); + } + }); + } + + /** + * Check whether a source file needs to be parsed for imports. + * This is a performance short-circuit, which saves us from creating + * a TypeScript AST unnecessarily. + * + * @param source The content of the source file to check. + * + * @returns false if there are definitely no require calls + * in this file, true otherwise. + */ + hasRequireCalls(source: string): boolean { return /require\(['"]/.test(source); } +} diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 85d4adbf25fd..bf98e04b1cfb 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -10,6 +10,7 @@ import {AbsoluteFsPath} from '../../src/ngtsc/path'; import {DependencyResolver} from './dependencies/dependency_resolver'; import {EsmDependencyHost} from './dependencies/esm_dependency_host'; import {ModuleResolver} from './dependencies/module_resolver'; +import {UmdDependencyHost} from './dependencies/umd_dependency_host'; import {FileSystem} from './file_system/file_system'; import {NodeJSFileSystem} from './file_system/node_js_file_system'; import {ConsoleLogger, LogLevel} from './logging/console_logger'; @@ -80,8 +81,10 @@ export function mainNgcc( const fs = new NodeJSFileSystem(); const transformer = new Transformer(fs, logger); const moduleResolver = new ModuleResolver(fs, pathMappings); - const host = new EsmDependencyHost(fs, moduleResolver); - const resolver = new DependencyResolver(logger, host); + const esmDependencyHost = new EsmDependencyHost(fs, moduleResolver); + const umdDependencyHost = new UmdDependencyHost(fs, moduleResolver); + const resolver = new DependencyResolver( + logger, {esm5: esmDependencyHost, esm2015: esmDependencyHost, umd: umdDependencyHost}); const finder = new EntryPointFinder(fs, logger, resolver); const fileWriter = getFileWriter(fs, createNewEntryPointFormats); diff --git a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts index 288793fe6c4a..89538b716975 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -9,6 +9,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {FileSystem} from '../../src/file_system/file_system'; import {EntryPoint} from '../../src/packages/entry_point'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; @@ -18,10 +19,13 @@ const _ = AbsoluteFsPath.from; describe('DependencyResolver', () => { let host: EsmDependencyHost; let resolver: DependencyResolver; + let fs: FileSystem; + let moduleResolver: ModuleResolver; beforeEach(() => { - const fs = new MockFileSystem(); - host = new EsmDependencyHost(fs, new ModuleResolver(fs)); - resolver = new DependencyResolver(new MockLogger(), host); + fs = new MockFileSystem(); + moduleResolver = new ModuleResolver(fs); + host = new EsmDependencyHost(fs, moduleResolver); + resolver = new DependencyResolver(new MockLogger(), {esm5: host, esm2015: host}); }); describe('sortEntryPointsByDependency()', () => { const first = { @@ -112,6 +116,13 @@ describe('DependencyResolver', () => { ])).toThrowError(`There is no appropriate source code format in '/first' entry-point.`); }); + it('should error if there is no appropriate DependencyHost for the given formats', () => { + resolver = new DependencyResolver(new MockLogger(), {esm2015: host}); + expect(() => resolver.sortEntryPointsByDependency([first])) + .toThrowError( + `Could not find a suitable format for computing dependencies of entry-point: '/first'.`); + }); + it('should capture any dependencies that were ignored', () => { spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); @@ -138,6 +149,29 @@ describe('DependencyResolver', () => { expect(sorted.entryPoints).toEqual([fifth]); }); + it('should use the appropriate DependencyHost for each entry-point', () => { + const esm5Host = new EsmDependencyHost(fs, moduleResolver); + const esm2015Host = new EsmDependencyHost(fs, moduleResolver); + resolver = new DependencyResolver(new MockLogger(), {esm5: esm5Host, esm2015: esm2015Host}); + spyOn(esm5Host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + spyOn(esm2015Host, 'findDependencies') + .and.callFake(createFakeComputeDependencies(dependencies)); + const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); + expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]); + + expect(esm5Host.findDependencies).toHaveBeenCalledWith('/first/index.js'); + expect(esm5Host.findDependencies).not.toHaveBeenCalledWith('/second/sub/index.js'); + expect(esm5Host.findDependencies).toHaveBeenCalledWith('/third/index.js'); + expect(esm5Host.findDependencies).not.toHaveBeenCalledWith('/fourth/sub2/index.js'); + expect(esm5Host.findDependencies).toHaveBeenCalledWith('/fifth/index.js'); + + expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith('/first/index.js'); + expect(esm2015Host.findDependencies).toHaveBeenCalledWith('/second/sub/index.js'); + expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith('/third/index.js'); + expect(esm2015Host.findDependencies).toHaveBeenCalledWith('/fourth/sub2/index.js'); + expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith('/fifth/index.js'); + }); + interface DepMap { [path: string]: {resolved: string[], missing: string[]}; } diff --git a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts index 0d10e82da05a..69ec54d5a553 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts @@ -14,7 +14,7 @@ import {MockFileSystem} from '../helpers/mock_file_system'; const _ = AbsoluteFsPath.from; -describe('DependencyHost', () => { +describe('EsmDependencyHost', () => { let host: EsmDependencyHost; beforeEach(() => { const fs = createMockFileSystem(); diff --git a/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts new file mode 100644 index 000000000000..5423506e20d8 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts @@ -0,0 +1,192 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; + +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {UmdDependencyHost} from '../../src/dependencies/umd_dependency_host'; +import {MockFileSystem} from '../helpers/mock_file_system'; + +const _ = AbsoluteFsPath.from; + +describe('UmdDependencyHost', () => { + let host: UmdDependencyHost; + beforeEach(() => { + const fs = createMockFileSystem(); + host = new UmdDependencyHost(fs, new ModuleResolver(fs)); + }); + + describe('getDependencies()', () => { + it('should not generate a TS AST if the source does not contain any require calls', () => { + spyOn(ts, 'createSourceFile'); + host.findDependencies(_('/no/imports/or/re-exports/index.js')); + expect(ts.createSourceFile).not.toHaveBeenCalled(); + }); + + it('should resolve all the external imports of the source file', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/imports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + }); + + it('should resolve all the external re-exports of the source file', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/re-exports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + }); + + it('should capture missing external imports', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/imports-missing/index.js')); + + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(missing.size).toBe(1); + expect(missing.has('missing')).toBe(true); + expect(deepImports.size).toBe(0); + }); + + it('should not register deep imports as missing', () => { + // This scenario verifies the behavior of the dependency analysis when an external import + // is found that does not map to an entry-point but still exists on disk, i.e. a deep import. + // Such deep imports are captured for diagnostics purposes. + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/deep-import/index.js')); + + expect(dependencies.size).toBe(0); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(1); + expect(deepImports.has('/node_modules/lib_1/deep/import')).toBe(true); + }); + + it('should recurse into internal dependencies', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/internal/outer/index.js')); + + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + + it('should handle circular internal dependencies', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/internal/circular_a/index.js')); + expect(dependencies.size).toBe(2); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + + it('should support `paths` alias mappings when resolving modules', () => { + const fs = createMockFileSystem(); + host = new UmdDependencyHost(fs, new ModuleResolver(fs, { + baseUrl: '/dist', + paths: { + '@app/*': ['*'], + '@lib/*/test': ['lib/*/test'], + } + })); + const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); + expect(dependencies.size).toBe(4); + expect(dependencies.has(_('/dist/components'))).toBe(true); + expect(dependencies.has(_('/dist/shared'))).toBe(true); + expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + }); + + function createMockFileSystem() { + return new MockFileSystem({ + '/no/imports/or/re-exports/index.js': '// some text but no import-like statements', + '/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', + '/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports/index.js': umd('imports_index', ['lib_1', 'lib_1/sub_1']), + '/external/imports/package.json': '{"esm2015": "./index.js"}', + '/external/imports/index.metadata.json': 'MOCK METADATA', + '/external/re-exports/index.js': + umd('imports_index', ['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']), + '/external/re-exports/package.json': '{"esm2015": "./index.js"}', + '/external/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports-missing/index.js': umd('imports_missing', ['lib_1', 'missing']), + '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', + '/external/imports-missing/index.metadata.json': 'MOCK METADATA', + '/external/deep-import/index.js': umd('deep_import', ['lib_1/deep/import']), + '/external/deep-import/package.json': '{"esm2015": "./index.js"}', + '/external/deep-import/index.metadata.json': 'MOCK METADATA', + '/internal/outer/index.js': umd('outer', ['../inner']), + '/internal/outer/package.json': '{"esm2015": "./index.js"}', + '/internal/outer/index.metadata.json': 'MOCK METADATA', + '/internal/inner/index.js': umd('inner', ['lib_1/sub_1'], ['X']), + '/internal/circular_a/index.js': umd('circular_a', ['../circular_b', 'lib_1/sub_1'], ['Y']), + '/internal/circular_b/index.js': umd('circular_b', ['../circular_a', 'lib_1'], ['X']), + '/internal/circular_a/package.json': '{"esm2015": "./index.js"}', + '/internal/circular_a/index.metadata.json': 'MOCK METADATA', + '/re-directed/index.js': umd('re_directed', ['lib_1/sub_2']), + '/re-directed/package.json': '{"esm2015": "./index.js"}', + '/re-directed/index.metadata.json': 'MOCK METADATA', + '/path-alias/index.js': + umd('path_alias', ['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']), + '/path-alias/package.json': '{"esm2015": "./index.js"}', + '/path-alias/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/index.d.ts': 'export declare class X {}', + '/node_modules/lib_1/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/node_modules/lib_1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/deep/import/index.js': 'export class DeepImport {}', + '/node_modules/lib_1/sub_1/index.d.ts': 'export declare class Y {}', + '/node_modules/lib_1/sub_1/package.json': + '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/node_modules/lib_1/sub_1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/sub_2.d.ts': `export * from './sub_2/sub_2';`, + '/node_modules/lib_1/sub_2/sub_2.d.ts': `export declare class Z {}';`, + '/node_modules/lib_1/sub_2/package.json': + '{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}', + '/node_modules/lib_1/sub_2/sub_2.metadata.json': 'MOCK METADATA', + '/dist/components/index.d.ts': `export declare class MyComponent {};`, + '/dist/components/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/components/index.metadata.json': 'MOCK METADATA', + '/dist/shared/index.d.ts': `import {X} from 'lib_1';\nexport declare class Service {}`, + '/dist/shared/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/shared/index.metadata.json': 'MOCK METADATA', + '/dist/lib/shared/test/index.d.ts': `export class TestHelper {}`, + '/dist/lib/shared/test/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', + }); + } +}); + +function umd(moduleName: string, importPaths: string[], exportNames: string[] = []) { + const commonJsRequires = importPaths.map(p => `,require('${p}')`).join(''); + const amdDeps = importPaths.map(p => `,'${p}'`).join(''); + const globalParams = + importPaths.map(p => `,global.${p.replace('@angular/', 'ng.').replace(/\//g, '')}`).join(''); + const params = + importPaths.map(p => `,${p.replace('@angular/', '').replace(/\.?\.?\//g, '')}`).join(''); + const exportStatements = + exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n'); + return ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports${commonJsRequires}) : + typeof define === 'function' && define.amd ? define('${moduleName}', ['exports'${amdDeps}], factory) : + (factory(global.${moduleName}${globalParams})); +}(this, (function (exports${params}) { 'use strict'; +${exportStatements} +}))); + `; +} diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts index e93af3918566..4669e206c33c 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts @@ -22,8 +22,8 @@ describe('findEntryPoints()', () => { let finder: EntryPointFinder; beforeEach(() => { const fs = createMockFileSystem(); - resolver = - new DependencyResolver(new MockLogger(), new EsmDependencyHost(fs, new ModuleResolver(fs))); + resolver = new DependencyResolver( + new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))}); spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; }); From c52551a2d9669e5c5d8e9e24757657e2d41781d2 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 14 May 2019 08:36:57 +0100 Subject: [PATCH 15/15] refactor(ivy): ngcc - use `.has()` to check Map membership Previously we were relying upon the `.get()` method to return `undefined` but it is clearer and safer to always check with `.has()` first. --- .../module_with_providers_analyzer.ts | 2 +- .../analysis/private_declarations_analyzer.ts | 6 ++-- .../ngcc/src/host/esm2015_host.ts | 28 ++++++++++++------- .../compiler-cli/ngcc/src/host/esm5_host.ts | 5 ++-- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts index 11894f9d133e..6de29258cf4b 100644 --- a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -63,7 +63,7 @@ export class ModuleWithProvidersAnalyzer { ngModule = {node: dtsNgModule, viaModule: null}; } const dtsFile = dtsFn.getSourceFile(); - const analysis = analyses.get(dtsFile) || []; + const analysis = analyses.has(dtsFile) ? analyses.get(dtsFile) : []; analysis.push({declaration: dtsFn, ngModule}); analyses.set(dtsFile, analysis); } diff --git a/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts index 72698b55824c..8ca9d7ed3ba9 100644 --- a/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts @@ -49,8 +49,8 @@ export class PrivateDeclarationsAnalyzer { if (exports) { exports.forEach((declaration, exportedName) => { if (hasNameIdentifier(declaration.node)) { - const privateDeclaration = privateDeclarations.get(declaration.node.name); - if (privateDeclaration) { + if (privateDeclarations.has(declaration.node.name)) { + const privateDeclaration = privateDeclarations.get(declaration.node.name) !; if (privateDeclaration.node !== declaration.node) { throw new Error(`${declaration.node.name.text} is declared multiple times.`); } @@ -96,7 +96,7 @@ export class PrivateDeclarationsAnalyzer { return Array.from(privateDeclarations.keys()).map(id => { const from = AbsoluteFsPath.fromSourceFile(id.getSourceFile()); const declaration = privateDeclarations.get(id) !; - const alias = exportAliasDeclarations.get(id) || null; + const alias = exportAliasDeclarations.has(id) ? exportAliasDeclarations.get(id) ! : null; const dtsDeclaration = this.host.getDtsDeclaration(declaration.node); const dtsFrom = dtsDeclaration && AbsoluteFsPath.fromSourceFile(dtsDeclaration.getSourceFile()); diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index d65a6f32d65a..b30074c49e61 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -366,7 +366,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N throw new Error( `Cannot get the dts file for a declaration that has no name: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`); } - return this.dtsDeclarationMap.get(declaration.name.text) || null; + return this.dtsDeclarationMap.has(declaration.name.text) ? + this.dtsDeclarationMap.get(declaration.name.text) ! : + null; } /** @@ -419,7 +421,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ protected resolveAliasedClassIdentifier(declaration: ts.Declaration): ts.Identifier|null { this.ensurePreprocessed(declaration.getSourceFile()); - return this.aliasedClassDeclarations.get(declaration) || null; + return this.aliasedClassDeclarations.has(declaration) ? + this.aliasedClassDeclarations.get(declaration) ! : + null; } /** @@ -738,7 +742,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N helperCall, makeMemberTargetFilter(classSymbol.name)); memberDecorators.forEach((decorators, memberName) => { if (memberName) { - const memberDecorators = memberDecoratorMap.get(memberName) || []; + const memberDecorators = + memberDecoratorMap.has(memberName) ? memberDecoratorMap.get(memberName) ! : []; const coreDecorators = decorators.filter(decorator => this.isFromCore(decorator)); memberDecoratorMap.set(memberName, memberDecorators.concat(coreDecorators)); } @@ -775,7 +780,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N if (keyName === undefined) { classDecorators.push(decorator); } else { - const decorators = memberDecorators.get(keyName) || []; + const decorators = + memberDecorators.has(keyName) ? memberDecorators.get(keyName) ! : []; decorators.push(decorator); memberDecorators.set(keyName, decorators); } @@ -874,8 +880,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N const decorator = reflectObjectLiteral(node); // Is the value of the `type` property an identifier? - let typeIdentifier = decorator.get('type'); - if (typeIdentifier) { + if (decorator.has('type')) { + let typeIdentifier = decorator.get('type') !; if (ts.isPropertyAccessExpression(typeIdentifier)) { // the type is in a namespace, e.g. `core.Directive` typeIdentifier = typeIdentifier.name; @@ -1036,8 +1042,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ protected getConstructorParameterDeclarations(classSymbol: ClassSymbol): ts.ParameterDeclaration[]|null { - const constructorSymbol = classSymbol.members && classSymbol.members.get(CONSTRUCTOR); - if (constructorSymbol) { + if (classSymbol.members && classSymbol.members.has(CONSTRUCTOR)) { + const constructorSymbol = classSymbol.members.get(CONSTRUCTOR) !; // For some reason the constructor does not have a `valueDeclaration` ?!? const constructor = constructorSymbol.declarations && constructorSymbol.declarations[0] as ts.ConstructorDeclaration | undefined; @@ -1113,8 +1119,10 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N element => ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null) .map(paramInfo => { - const typeExpression = paramInfo && paramInfo.get('type') || null; - const decoratorInfo = paramInfo && paramInfo.get('decorators') || null; + const typeExpression = + paramInfo && paramInfo.has('type') ? paramInfo.get('type') ! : null; + const decoratorInfo = + paramInfo && paramInfo.has('decorators') ? paramInfo.get('decorators') ! : null; const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo) .filter(decorator => this.isFromCore(decorator)); diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index a2354a40dbf2..6fcfb09baa0d 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -339,8 +339,9 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { if (expression && ts.isArrayLiteralExpression(expression)) { const elements = expression.elements; return elements.map(reflectArrayElement).map(paramInfo => { - const typeExpression = paramInfo && paramInfo.get('type') || null; - const decoratorInfo = paramInfo && paramInfo.get('decorators') || null; + const typeExpression = paramInfo && paramInfo.has('type') ? paramInfo.get('type') ! : null; + const decoratorInfo = + paramInfo && paramInfo.has('decorators') ? paramInfo.get('decorators') ! : null; const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo); return {typeExpression, decorators}; });