From 7186f9c0162601828a0735919eace3a73da47a46 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 6 Jun 2019 20:22:32 +0100 Subject: [PATCH] refactor(ivy): implement a virtual file-system layer in ngtsc + ngcc (#30921) To improve cross platform support, all file access (and path manipulation) is now done through a well known interface (`FileSystem`). For testing a number of `MockFileSystem` implementations are provided. These provide an in-memory file-system which emulates operating systems like OS/X, Unix and Windows. The current file system is always available via the static method, `FileSystem.getFileSystem()`. This is also used by a number of static methods on `AbsoluteFsPath` and `PathSegment`, to avoid having to pass `FileSystem` objects around all the time. The result of this is that one must be careful to ensure that the file-system has been initialized before using any of these static methods. To prevent this happening accidentally the current file system always starts out as an instance of `InvalidFileSystem`, which will throw an error if any of its methods are called. You can set the current file-system by calling `FileSystem.setFileSystem()`. During testing you can call the helper function `initMockFileSystem(os)` which takes a string name of the OS to emulate, and will also monkey-patch aspects of the TypeScript library to ensure that TS is also using the current file-system. Finally there is the `NgtscCompilerHost` to be used for any TypeScript compilation, which uses a given file-system. All tests that interact with the file-system should be tested against each of the mock file-systems. A series of helpers have been provided to support such tests: * `runInEachFileSystem()` - wrap your tests in this helper to run all the wrapped tests in each of the mock file-systems. * `addTestFilesToFileSystem()` - use this to add files and their contents to the mock file system for testing. * `loadTestFilesFromDisk()` - use this to load a mirror image of files on disk into the in-memory mock file-system. * `loadFakeCore()` - use this to load a fake version of `@angular/core` into the mock file-system. All ngcc and ngtsc source and tests now use this virtual file-system setup. PR Close #30921 --- packages/compiler-cli/BUILD.bazel | 2 +- packages/compiler-cli/index.ts | 4 + packages/compiler-cli/ngcc/BUILD.bazel | 4 +- packages/compiler-cli/ngcc/index.ts | 4 + packages/compiler-cli/ngcc/main-ngcc.ts | 7 +- .../ngcc/src/analysis/decoration_analyzer.ts | 7 +- .../analysis/private_declarations_analyzer.ts | 7 +- .../dependencies/commonjs_dependency_host.ts | 5 +- .../ngcc/src/dependencies/dependency_host.ts | 2 +- .../src/dependencies/dependency_resolver.ts | 5 +- .../src/dependencies/esm_dependency_host.ts | 5 +- .../ngcc/src/dependencies/module_resolver.ts | 33 +- .../src/dependencies/umd_dependency_host.ts | 7 +- .../ngcc/src/file_system/file_system.ts | 38 - .../src/file_system/node_js_file_system.ts | 30 - .../ngcc/src/host/commonjs_host.ts | 6 +- .../ngcc/src/host/esm2015_host.ts | 3 +- .../compiler-cli/ngcc/src/host/umd_host.ts | 5 +- packages/compiler-cli/ngcc/src/main.ts | 24 +- .../ngcc/src/packages/build_marker.ts | 4 +- .../ngcc/src/packages/bundle_program.ts | 17 +- .../ngcc/src/packages/entry_point.ts | 12 +- .../ngcc/src/packages/entry_point_bundle.ts | 23 +- .../ngcc/src/packages/entry_point_finder.ts | 18 +- .../ngcc/src/packages/ngcc_compiler_host.ts | 61 +- .../ngcc/src/packages/transformer.ts | 6 +- .../ngcc/src/rendering/dts_renderer.ts | 5 +- .../src/rendering/esm_rendering_formatter.ts | 12 +- .../ngcc/src/rendering/renderer.ts | 5 +- .../ngcc/src/rendering/source_maps.ts | 18 +- .../compiler-cli/ngcc/src/rendering/utils.ts | 2 +- .../ngcc/src/writing/file_writer.ts | 1 - .../ngcc/src/writing/in_place_file_writer.ts | 7 +- .../writing/new_entry_point_file_writer.ts | 31 +- packages/compiler-cli/ngcc/test/BUILD.bazel | 24 +- .../test/analysis/decoration_analyzer_spec.ts | 445 +-- .../module_with_providers_analyzer_spec.ts | 780 ++--- .../private_declarations_analyzer_spec.ts | 443 +-- .../test/analysis/references_registry_spec.ts | 68 +- .../analysis/switch_marker_analyzer_spec.ts | 122 +- .../commonjs_dependency_host_spec.ts | 350 +- .../dependencies/dependency_resolver_spec.ts | 362 +- .../dependencies/esm_dependency_host_spec.ts | 452 +-- .../test/dependencies/module_resolver_spec.ts | 348 +- .../dependencies/umd_dependency_host_spec.ts | 390 ++- .../ngcc/test/helpers/BUILD.bazel | 4 +- .../ngcc/test/helpers/mock_file_system.ts | 174 - .../compiler-cli/ngcc/test/helpers/utils.ts | 113 +- .../ngcc/test/host/commonjs_host_spec.ts | 2558 +++++++------- .../host/esm2015_host_import_helper_spec.ts | 646 ++-- .../ngcc/test/host/esm2015_host_spec.ts | 2574 +++++++------- .../test/host/esm5_host_import_helper_spec.ts | 750 ++-- .../ngcc/test/host/esm5_host_spec.ts | 2829 +++++++-------- .../ngcc/test/host/umd_host_spec.ts | 2212 ++++++------ packages/compiler-cli/ngcc/test/host/util.ts | 2 - .../ngcc/test/integration/ngcc_spec.ts | 747 ++-- .../ngcc/test/packages/build_marker_spec.ts | 295 +- .../test/packages/entry_point_bundle_spec.ts | 225 +- .../test/packages/entry_point_finder_spec.ts | 374 +- .../ngcc/test/packages/entry_point_spec.ts | 281 +- .../commonjs_rendering_formatter_spec.ts | 530 +-- .../ngcc/test/rendering/dts_renderer_spec.ts | 166 +- .../esm5_rendering_formatter_spec.ts | 488 +-- .../rendering/esm_rendering_formatter_spec.ts | 605 ++-- .../ngcc/test/rendering/renderer_spec.ts | 491 +-- .../rendering/umd_rendering_formatter_spec.ts | 591 ++-- .../test/writing/in_place_file_writer_spec.ts | 133 +- .../new_entry_point_file_writer_spec.ts | 647 ++-- packages/compiler-cli/src/extract_i18n.ts | 6 +- packages/compiler-cli/src/main.ts | 5 +- packages/compiler-cli/src/ngtools_api2.ts | 1 - .../src/ngtsc/annotations/BUILD.bazel | 1 + .../src/ngtsc/annotations/src/component.ts | 11 +- .../annotations/src/references_registry.ts | 1 - .../src/ngtsc/annotations/test/BUILD.bazel | 4 +- .../ngtsc/annotations/test/component_spec.ts | 104 +- .../ngtsc/annotations/test/directive_spec.ts | 105 +- .../ngtsc/annotations/test/metadata_spec.ts | 109 +- .../ngtsc/annotations/test/ng_module_spec.ts | 87 +- .../src/ngtsc/cycles/test/BUILD.bazel | 2 + .../src/ngtsc/cycles/test/analyzer_spec.ts | 104 +- .../src/ngtsc/cycles/test/imports_spec.ts | 102 +- .../src/ngtsc/cycles/test/util.ts | 12 +- .../src/ngtsc/entry_point/BUILD.bazel | 2 +- .../src/ngtsc/entry_point/src/generator.ts | 9 +- .../src/ngtsc/entry_point/src/logic.ts | 9 +- .../src/ngtsc/entry_point/test/BUILD.bazel | 3 +- .../entry_point/test/entry_point_spec.ts | 27 +- .../ngtsc/{path => file_system}/BUILD.bazel | 2 +- .../src/ngtsc/file_system/README.md | 42 + .../src/ngtsc/file_system/index.ts | 13 + .../ngtsc/file_system/src/compiler_host.ts | 71 + .../src/ngtsc/file_system/src/helpers.ts | 88 + .../file_system/src/invalid_file_system.ts | 48 + .../{path => file_system}/src/logical.ts | 15 +- .../file_system/src/node_js_file_system.ts | 81 + .../src/ngtsc/file_system/src/types.ts | 74 + .../ngtsc/{path => file_system}/src/util.ts | 18 +- .../{path => file_system}/test/BUILD.bazel | 3 +- .../ngtsc/file_system/test/helpers_spec.ts | 47 + .../ngtsc/file_system/test/logical_spec.ts | 65 + .../test/node_js_file_system_spec.ts | 148 + .../src/ngtsc/file_system/testing/BUILD.bazel | 16 + .../src/ngtsc/file_system/testing/index.ts | 13 + .../testing/src/mock_file_system.ts | 239 ++ .../testing/src/mock_file_system_native.ts | 48 + .../testing/src/mock_file_system_posix.ts | 41 + .../testing/src/mock_file_system_windows.ts | 41 + .../file_system/testing/src/test_helper.ts | 149 + .../src/ngtsc/imports/BUILD.bazel | 2 +- .../src/ngtsc/imports/src/emitter.ts | 13 +- .../src/ngtsc/imports/src/resolver.ts | 7 +- .../src/ngtsc/imports/test/BUILD.bazel | 2 + .../src/ngtsc/imports/test/default_spec.ts | 161 +- .../src/ngtsc/indexer/test/BUILD.bazel | 2 + .../src/ngtsc/indexer/test/context_spec.ts | 42 +- .../src/ngtsc/indexer/test/transform_spec.ts | 166 +- .../src/ngtsc/indexer/test/util.ts | 13 +- .../ngtsc/partial_evaluator/test/BUILD.bazel | 3 +- .../partial_evaluator/test/evaluator_spec.ts | 788 ++--- .../src/ngtsc/partial_evaluator/test/utils.ts | 20 +- .../compiler-cli/src/ngtsc/path/README.md | 45 - .../compiler-cli/src/ngtsc/path/src/types.ts | 122 - .../src/ngtsc/path/test/logical_spec.ts | 63 - .../src/ngtsc/path/test/types_spec.ts | 26 - .../compiler-cli/src/ngtsc/perf/BUILD.bazel | 1 + .../src/ngtsc/perf/src/tracking.ts | 9 +- packages/compiler-cli/src/ngtsc/program.ts | 12 +- .../src/ngtsc/reflection/test/BUILD.bazel | 2 + .../src/ngtsc/reflection/test/ts_host_spec.ts | 406 +-- .../compiler-cli/src/ngtsc/resource_loader.ts | 7 +- .../src/ngtsc/scope/test/BUILD.bazel | 2 + .../src/ngtsc/scope/test/dependency_spec.ts | 192 +- .../compiler-cli/src/ngtsc/shims/BUILD.bazel | 2 +- .../src/ngtsc/shims/src/factory_generator.ts | 12 +- .../compiler-cli/src/ngtsc/shims/src/host.ts | 7 +- .../src/ngtsc/shims/src/summary_generator.ts | 7 +- .../src/ngtsc/shims/src/typecheck_shim.ts | 2 +- .../src/ngtsc/testing/BUILD.bazel | 1 + .../src/ngtsc/testing/in_memory_typescript.ts | 157 - .../src/ngtsc/{path => testing}/index.ts | 4 +- .../src/ngtsc/testing/src/utils.ts | 100 + .../src/ngtsc/typecheck/BUILD.bazel | 2 +- .../src/ngtsc/typecheck/src/context.ts | 2 +- .../ngtsc/typecheck/src/type_check_file.ts | 7 +- .../src/ngtsc/typecheck/test/BUILD.bazel | 4 +- .../typecheck/test/type_constructor_spec.ts | 168 +- .../compiler-cli/src/ngtsc/util/BUILD.bazel | 2 +- .../compiler-cli/src/ngtsc/util/src/path.ts | 15 +- .../src/ngtsc/util/src/typescript.ts | 10 +- .../src/ngtsc/util/test/BUILD.bazel | 2 + .../src/ngtsc/util/test/visitor_spec.ts | 69 +- packages/compiler-cli/src/perform_compile.ts | 39 +- .../src/transformers/compiler_host.ts | 7 +- packages/compiler-cli/test/BUILD.bazel | 4 +- .../compiler-cli/test/compliance/BUILD.bazel | 1 + .../test/compliance/mock_compile.ts | 7 +- .../test/diagnostics/check_types_spec.ts | 4 +- .../compiler-cli/test/diagnostics/mocks.ts | 2 - .../compiler-cli/test/extract_i18n_spec.ts | 2 +- .../compiler-cli/test/helpers/BUILD.bazel | 17 + packages/compiler-cli/test/helpers/index.ts | 9 + .../test/helpers/src/mock_file_loading.ts | 82 + .../test/{ => helpers/src}/runfile_helpers.ts | 1 + .../test/metadata/evaluator_spec.ts | 2 - packages/compiler-cli/test/ngc_spec.ts | 2 +- packages/compiler-cli/test/ngtsc/BUILD.bazel | 5 +- .../test/ngtsc/component_indexing_spec.ts | 189 +- packages/compiler-cli/test/ngtsc/env.ts | 89 +- .../test/ngtsc/incremental_spec.ts | 292 +- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 3038 ++++++++--------- .../compiler-cli/test/ngtsc/scope_spec.ts | 196 +- .../test/ngtsc/sourcemap_utils.ts | 1 - .../test/ngtsc/template_mapping_spec.ts | 986 +++--- .../test/ngtsc/template_typecheck_spec.ts | 156 +- packages/compiler-cli/test/test_support.ts | 9 +- .../test/transformers/program_spec.ts | 3 +- 177 files changed, 16598 insertions(+), 14829 deletions(-) delete mode 100644 packages/compiler-cli/ngcc/src/file_system/file_system.ts delete mode 100644 packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts delete mode 100644 packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts rename packages/compiler-cli/src/ngtsc/{path => file_system}/BUILD.bazel (91%) create mode 100644 packages/compiler-cli/src/ngtsc/file_system/README.md create mode 100644 packages/compiler-cli/src/ngtsc/file_system/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts rename packages/compiler-cli/src/ngtsc/{path => file_system}/src/logical.ts (91%) create mode 100644 packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/src/types.ts rename packages/compiler-cli/src/ngtsc/{path => file_system}/src/util.ts (60%) rename packages/compiler-cli/src/ngtsc/{path => file_system}/test/BUILD.bazel (79%) create mode 100644 packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/testing/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/file_system/testing/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts create mode 100644 packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts delete mode 100644 packages/compiler-cli/src/ngtsc/path/README.md delete mode 100644 packages/compiler-cli/src/ngtsc/path/src/types.ts delete mode 100644 packages/compiler-cli/src/ngtsc/path/test/logical_spec.ts delete mode 100644 packages/compiler-cli/src/ngtsc/path/test/types_spec.ts delete mode 100644 packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts rename packages/compiler-cli/src/ngtsc/{path => testing}/index.ts (61%) create mode 100644 packages/compiler-cli/src/ngtsc/testing/src/utils.ts create mode 100644 packages/compiler-cli/test/helpers/BUILD.bazel create mode 100644 packages/compiler-cli/test/helpers/index.ts create mode 100644 packages/compiler-cli/test/helpers/src/mock_file_loading.ts rename packages/compiler-cli/test/{ => helpers/src}/runfile_helpers.ts (98%) diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index ce8922eea7..823adbe66b 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -27,12 +27,12 @@ ts_library( "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/entry_point", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", - "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index 990747d90c..d203ea81b7 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -5,6 +5,8 @@ * 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 {NodeJSFileSystem, setFileSystem} from './src/ngtsc/file_system'; + export {AotCompilerHost, AotCompilerHost as StaticReflectorHost, StaticReflector, StaticSymbol} from '@angular/compiler'; export {DiagnosticTemplateInfo, getExpressionScope, getTemplateExpressionDiagnostics} from './src/diagnostics/expression_diagnostics'; export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expression_type'; @@ -26,3 +28,5 @@ export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools export {ngToTsDiagnostic} from './src/transformers/util'; export {NgTscPlugin} from './src/ngtsc/tsc_plugin'; + +setFileSystem(new NodeJSFileSystem()); diff --git a/packages/compiler-cli/ngcc/BUILD.bazel b/packages/compiler-cli/ngcc/BUILD.bazel index 5084e4988a..1cceb5eb51 100644 --- a/packages/compiler-cli/ngcc/BUILD.bazel +++ b/packages/compiler-cli/ngcc/BUILD.bazel @@ -8,15 +8,16 @@ ts_library( "*.ts", "**/*.ts", ]), + tsconfig = "//packages/compiler-cli:tsconfig", deps = [ "//packages:types", "//packages/compiler", "//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/cycles", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", - "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/scope", @@ -25,7 +26,6 @@ ts_library( "//packages/compiler-cli/src/ngtsc/util", "@npm//@types/convert-source-map", "@npm//@types/node", - "@npm//@types/shelljs", "@npm//@types/source-map", "@npm//@types/yargs", "@npm//canonical-path", diff --git a/packages/compiler-cli/ngcc/index.ts b/packages/compiler-cli/ngcc/index.ts index 8b131672ca..cbdb7db690 100644 --- a/packages/compiler-cli/ngcc/index.ts +++ b/packages/compiler-cli/ngcc/index.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system'; import {hasBeenProcessed as _hasBeenProcessed} from './src/packages/build_marker'; import {EntryPointJsonProperty, EntryPointPackageJson} from './src/packages/entry_point'; @@ -18,3 +19,6 @@ export function hasBeenProcessed(packageJson: object, format: string) { // We are wrapping this function to hide the internal types. return _hasBeenProcessed(packageJson as EntryPointPackageJson, format as EntryPointJsonProperty); } + +// Configure the file-system for external users. +setFileSystem(new NodeJSFileSystem()); diff --git a/packages/compiler-cli/ngcc/main-ngcc.ts b/packages/compiler-cli/ngcc/main-ngcc.ts index e47904e58d..140a91746c 100644 --- a/packages/compiler-cli/ngcc/main-ngcc.ts +++ b/packages/compiler-cli/ngcc/main-ngcc.ts @@ -8,7 +8,7 @@ */ import * as yargs from 'yargs'; -import {AbsoluteFsPath} from '../src/ngtsc/path'; +import {resolve, setFileSystem, NodeJSFileSystem} from '../src/ngtsc/file_system'; import {mainNgcc} from './src/main'; import {ConsoleLogger, LogLevel} from './src/logging/console_logger'; @@ -56,7 +56,10 @@ if (require.main === module) { 'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.'); process.exit(1); } - const baseSourcePath = AbsoluteFsPath.resolve(options['s'] || './node_modules'); + + setFileSystem(new NodeJSFileSystem()); + + const baseSourcePath = resolve(options['s'] || './node_modules'); const propertiesToConsider: string[] = options['p']; const targetEntryPointPath = options['t'] ? options['t'] : undefined; const compileAllFormats = !options['first-only']; diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index d483365ba7..12b832e78c 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -10,14 +10,13 @@ import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations'; import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles'; +import {AbsoluteFsPath, FileSystem, LogicalFileSystem, absoluteFrom, dirname, resolve} from '../../../src/ngtsc/file_system'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../../src/ngtsc/imports'; import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; -import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path'; import {ClassDeclaration, ClassSymbol, Decorator} from '../../../src/ngtsc/reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform'; -import {FileSystem} from '../file_system/file_system'; import {NgccReflectionHost} from '../host/ngcc_host'; import {isDefined} from '../utils'; @@ -57,9 +56,9 @@ class NgccResourceLoader implements ResourceLoader { constructor(private fs: FileSystem) {} canPreload = false; preload(): undefined|Promise { throw new Error('Not implemented.'); } - load(url: string): string { return this.fs.readFile(AbsoluteFsPath.resolve(url)); } + load(url: string): string { return this.fs.readFile(resolve(url)); } resolve(url: string, containingFile: string): string { - return AbsoluteFsPath.resolve(AbsoluteFsPath.dirname(AbsoluteFsPath.from(containingFile)), url); + return resolve(dirname(absoluteFrom(containingFile)), url); } } 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 8ca9d7ed3b..53fc3e60d2 100644 --- a/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts @@ -7,7 +7,7 @@ */ import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, absoluteFromSourceFile} from '../../../src/ngtsc/file_system'; import {Declaration} from '../../../src/ngtsc/reflection'; import {NgccReflectionHost} from '../host/ngcc_host'; import {hasNameIdentifier, isDefined} from '../utils'; @@ -94,12 +94,11 @@ export class PrivateDeclarationsAnalyzer { }); return Array.from(privateDeclarations.keys()).map(id => { - const from = AbsoluteFsPath.fromSourceFile(id.getSourceFile()); + const from = absoluteFromSourceFile(id.getSourceFile()); const declaration = privateDeclarations.get(id) !; const alias = exportAliasDeclarations.has(id) ? exportAliasDeclarations.get(id) ! : null; const dtsDeclaration = this.host.getDtsDeclaration(declaration.node); - const dtsFrom = - dtsDeclaration && AbsoluteFsPath.fromSourceFile(dtsDeclaration.getSourceFile()); + const dtsFrom = dtsDeclaration && absoluteFromSourceFile(dtsDeclaration.getSourceFile()); return {identifier: id.text, from, dtsFrom, alias}; }); diff --git a/packages/compiler-cli/ngcc/src/dependencies/commonjs_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/commonjs_dependency_host.ts index 352844d282..e4269c2c6e 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/commonjs_dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/commonjs_dependency_host.ts @@ -6,11 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; - -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system'; import {isRequireCall} from '../host/commonjs_host'; - import {DependencyHost, DependencyInfo} from './dependency_host'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts index c2a6924c6f..c63a5ce2f2 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/file_system'; export interface DependencyHost { findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo; diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts index 092d1f65b8..cd83402d2e 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts @@ -7,8 +7,7 @@ */ import {DepGraph} from 'dependency-graph'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {AbsoluteFsPath, FileSystem, resolve} from '../../../src/ngtsc/file_system'; import {Logger} from '../logging/logger'; import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point'; import {DependencyHost} from './dependency_host'; @@ -176,7 +175,7 @@ export class DependencyResolver { if (format === 'esm2015' || format === 'esm5' || format === 'umd' || format === 'commonjs') { const formatPath = entryPoint.packageJson[property] !; - return {format, path: AbsoluteFsPath.resolve(entryPoint.path, formatPath)}; + return {format, path: resolve(entryPoint.path, formatPath)}; } } throw new Error( diff --git a/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts index 0b164fac20..75c463662d 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts @@ -6,13 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; - -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system'; import {DependencyHost, DependencyInfo} from './dependency_host'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; - /** * Helper functions for computing dependencies. */ diff --git a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts index c558a2bb97..98cc8e54fa 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, isRoot, join, resolve} from '../../../src/ngtsc/file_system'; import {PathMappings, isRelativePath} from '../utils'; + + /** * This is a very cut-down implementation of the TypeScript module resolution strategy. * @@ -55,7 +56,7 @@ export class ModuleResolver { * Convert the `pathMappings` into a collection of `PathMapper` functions. */ private processPathMappings(pathMappings: PathMappings): ProcessedPathMapping[] { - const baseUrl = AbsoluteFsPath.from(pathMappings.baseUrl); + const baseUrl = absoluteFrom(pathMappings.baseUrl); return Object.keys(pathMappings.paths).map(pathPattern => { const matcher = splitOnStar(pathPattern); const templates = pathMappings.paths[pathPattern].map(splitOnStar); @@ -71,9 +72,8 @@ export class ModuleResolver { * If neither of these files exist then the method returns `null`. */ private resolveAsRelativePath(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { - const resolvedPath = this.resolvePath( - AbsoluteFsPath.resolve(AbsoluteFsPath.dirname(fromPath), moduleName), - this.relativeExtensions); + const resolvedPath = + this.resolvePath(resolve(dirname(fromPath), moduleName), this.relativeExtensions); return resolvedPath && new ResolvedRelativeModule(resolvedPath); } @@ -118,13 +118,13 @@ export class ModuleResolver { */ private resolveAsEntryPoint(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { let folder = fromPath; - while (!AbsoluteFsPath.isRoot(folder)) { - folder = AbsoluteFsPath.dirname(folder); + while (!isRoot(folder)) { + folder = dirname(folder); if (folder.endsWith('node_modules')) { // Skip up if the folder already ends in node_modules - folder = AbsoluteFsPath.dirname(folder); + folder = dirname(folder); } - const modulePath = AbsoluteFsPath.resolve(folder, 'node_modules', moduleName); + const modulePath = resolve(folder, 'node_modules', moduleName); if (this.isEntryPoint(modulePath)) { return new ResolvedExternalModule(modulePath); } else if (this.resolveAsRelativePath(modulePath, fromPath)) { @@ -141,7 +141,7 @@ export class ModuleResolver { */ private resolvePath(path: AbsoluteFsPath, postFixes: string[]): AbsoluteFsPath|null { for (const postFix of postFixes) { - const testPath = AbsoluteFsPath.fromUnchecked(path + postFix); + const testPath = absoluteFrom(path + postFix); if (this.fs.exists(testPath)) { return testPath; } @@ -155,7 +155,7 @@ export class ModuleResolver { * This is achieved by checking for the existence of `${modulePath}/package.json`. */ private isEntryPoint(modulePath: AbsoluteFsPath): boolean { - return this.fs.exists(AbsoluteFsPath.join(modulePath, 'package.json')); + return this.fs.exists(join(modulePath, 'package.json')); } /** @@ -215,8 +215,7 @@ export class ModuleResolver { */ private computeMappedTemplates(mapping: ProcessedPathMapping, match: string) { return mapping.templates.map( - template => - AbsoluteFsPath.resolve(mapping.baseUrl, template.prefix + match + template.postfix)); + template => resolve(mapping.baseUrl, template.prefix + match + template.postfix)); } /** @@ -225,9 +224,9 @@ export class ModuleResolver { */ private findPackagePath(path: AbsoluteFsPath): AbsoluteFsPath|null { let folder = path; - while (!AbsoluteFsPath.isRoot(folder)) { - folder = AbsoluteFsPath.dirname(folder); - if (this.fs.exists(AbsoluteFsPath.join(folder, 'package.json'))) { + while (!isRoot(folder)) { + folder = dirname(folder); + if (this.fs.exists(join(folder, 'package.json'))) { return folder; } } diff --git a/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts index d00790a494..7828e3db88 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts @@ -6,16 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; - -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/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. */ diff --git a/packages/compiler-cli/ngcc/src/file_system/file_system.ts b/packages/compiler-cli/ngcc/src/file_system/file_system.ts deleted file mode 100644 index 9c9653b48b..0000000000 --- a/packages/compiler-cli/ngcc/src/file_system/file_system.ts +++ /dev/null @@ -1,38 +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 {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; - -/** - * A basic interface to abstract the underlying file-system. - * - * This makes it easier to provide mock file-systems in unit tests, - * but also to create clever file-systems that have features such as caching. - */ -export interface FileSystem { - exists(path: AbsoluteFsPath): boolean; - readFile(path: AbsoluteFsPath): string; - writeFile(path: AbsoluteFsPath, data: string): void; - readdir(path: AbsoluteFsPath): PathSegment[]; - lstat(path: AbsoluteFsPath): FileStats; - stat(path: AbsoluteFsPath): FileStats; - pwd(): AbsoluteFsPath; - copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void; - moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void; - ensureDir(path: AbsoluteFsPath): void; -} - -/** - * Information about an object in the FileSystem. - * This is analogous to the `fs.Stats` class in Node.js. - */ -export interface FileStats { - isFile(): boolean; - isDirectory(): boolean; - isSymbolicLink(): boolean; -} diff --git a/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts b/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts deleted file mode 100644 index 8f4e7c797c..0000000000 --- a/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts +++ /dev/null @@ -1,30 +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 * as fs from 'fs'; -import {cp, mkdir, mv} from 'shelljs'; -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; -import {FileSystem} from './file_system'; - -/** - * A wrapper around the Node.js file-system (i.e the `fs` package). - */ -export class NodeJSFileSystem implements FileSystem { - exists(path: AbsoluteFsPath): boolean { return fs.existsSync(path); } - readFile(path: AbsoluteFsPath): string { return fs.readFileSync(path, 'utf8'); } - writeFile(path: AbsoluteFsPath, data: string): void { - return fs.writeFileSync(path, data, 'utf8'); - } - readdir(path: AbsoluteFsPath): PathSegment[] { return fs.readdirSync(path) as PathSegment[]; } - lstat(path: AbsoluteFsPath): fs.Stats { return fs.lstatSync(path); } - stat(path: AbsoluteFsPath): fs.Stats { return fs.statSync(path); } - pwd() { return AbsoluteFsPath.from(process.cwd()); } - copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { cp(from, to); } - moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { mv(from, to); } - ensureDir(path: AbsoluteFsPath): void { mkdir('-p', path); } -} diff --git a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts index 979f6604ea..9a48691721 100644 --- a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts +++ b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts @@ -7,7 +7,7 @@ */ import * as ts from 'typescript'; - +import {absoluteFrom} from '../../../src/ngtsc/file_system'; import {Declaration, Import} from '../../../src/ngtsc/reflection'; import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; @@ -122,13 +122,13 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { if (this.compilerHost.resolveModuleNames) { const moduleInfo = this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0]; - return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName); + return moduleInfo && this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedFileName)); } else { const moduleInfo = ts.resolveModuleName( moduleName, containingFile.fileName, this.program.getCompilerOptions(), this.compilerHost); return moduleInfo.resolvedModule && - this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName); + this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedModule.resolvedFileName)); } } } diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 4949a3ef65..8d75c2f3f9 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -8,6 +8,7 @@ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../src/ngtsc/file_system'; import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; @@ -1281,7 +1282,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * @param dtsProgram The program containing all the typings files. * @returns a map of class names to class declarations. */ - protected computeDtsDeclarationMap(dtsRootFileName: string, dtsProgram: ts.Program): + protected computeDtsDeclarationMap(dtsRootFileName: AbsoluteFsPath, dtsProgram: ts.Program): Map { const dtsDeclarationMap = new Map(); const checker = dtsProgram.getTypeChecker(); diff --git a/packages/compiler-cli/ngcc/src/host/umd_host.ts b/packages/compiler-cli/ngcc/src/host/umd_host.ts index 33283a6380..89b401106d 100644 --- a/packages/compiler-cli/ngcc/src/host/umd_host.ts +++ b/packages/compiler-cli/ngcc/src/host/umd_host.ts @@ -8,6 +8,7 @@ import * as ts from 'typescript'; +import {absoluteFrom} from '../../../src/ngtsc/file_system'; import {Declaration, Import} from '../../../src/ngtsc/reflection'; import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; @@ -154,13 +155,13 @@ export class UmdReflectionHost extends Esm5ReflectionHost { if (this.compilerHost.resolveModuleNames) { const moduleInfo = this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0]; - return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName); + return moduleInfo && this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedFileName)); } else { const moduleInfo = ts.resolveModuleName( moduleName, containingFile.fileName, this.program.getCompilerOptions(), this.compilerHost); return moduleInfo.resolvedModule && - this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName); + this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedModule.resolvedFileName)); } } } diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index ec03384e23..d7d3a0ec95 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -5,15 +5,12 @@ * 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 {AbsoluteFsPath} from '../../src/ngtsc/path'; - +import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, resolve} from '../../src/ngtsc/file_system'; import {CommonJsDependencyHost} from './dependencies/commonjs_dependency_host'; 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'; import {Logger} from './logging/logger'; import {hasBeenProcessed, markAsProcessed} from './packages/build_marker'; @@ -79,7 +76,7 @@ export function mainNgcc( {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, compileAllFormats = true, createNewEntryPointFormats = false, logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { - const fs = new NodeJSFileSystem(); + const fs = getFileSystem(); const transformer = new Transformer(fs, logger); const moduleResolver = new ModuleResolver(fs, pathMappings); const esmDependencyHost = new EsmDependencyHost(fs, moduleResolver); @@ -95,7 +92,7 @@ export function mainNgcc( const fileWriter = getFileWriter(fs, createNewEntryPointFormats); const absoluteTargetEntryPointPath = - targetEntryPointPath ? AbsoluteFsPath.resolve(basePath, targetEntryPointPath) : undefined; + targetEntryPointPath ? resolve(basePath, targetEntryPointPath) : undefined; if (absoluteTargetEntryPointPath && hasProcessedTargetEntryPoint( @@ -104,8 +101,8 @@ export function mainNgcc( return; } - const {entryPoints, invalidEntryPoints} = finder.findEntryPoints( - AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath, pathMappings); + const {entryPoints, invalidEntryPoints} = + finder.findEntryPoints(absoluteFrom(basePath), absoluteTargetEntryPointPath, pathMappings); invalidEntryPoints.forEach(invalidEntryPoint => { logger.debug( @@ -119,14 +116,13 @@ export function mainNgcc( return; } - entryPoints.forEach(entryPoint => { + for (const entryPoint of entryPoints) { // Are we compiling the Angular core? const isCore = entryPoint.name === '@angular/core'; const compiledFormats = new Set(); const entryPointPackageJson = entryPoint.packageJson; - const entryPointPackageJsonPath = - AbsoluteFsPath.fromUnchecked(`${entryPoint.path}/package.json`); + const entryPointPackageJsonPath = fs.resolve(entryPoint.path, 'package.json'); const hasProcessedDts = hasBeenProcessed(entryPointPackageJson, 'typings'); @@ -180,7 +176,7 @@ export function mainNgcc( throw new Error( `Failed to compile any formats for entry-point at (${entryPoint.path}). Tried ${propertiesToConsider}.`); } - }); + } } function getFileWriter(fs: FileSystem, createNewEntryPointFormats: boolean): FileWriter { @@ -190,7 +186,7 @@ function getFileWriter(fs: FileSystem, createNewEntryPointFormats: boolean): Fil function hasProcessedTargetEntryPoint( fs: FileSystem, targetPath: AbsoluteFsPath, propertiesToConsider: string[], compileAllFormats: boolean) { - const packageJsonPath = AbsoluteFsPath.resolve(targetPath, 'package.json'); + const packageJsonPath = resolve(targetPath, 'package.json'); const packageJson = JSON.parse(fs.readFile(packageJsonPath)); for (const property of propertiesToConsider) { @@ -221,7 +217,7 @@ function hasProcessedTargetEntryPoint( */ function markNonAngularPackageAsProcessed( fs: FileSystem, path: AbsoluteFsPath, propertiesToConsider: string[]) { - const packageJsonPath = AbsoluteFsPath.resolve(path, 'package.json'); + const packageJsonPath = resolve(path, 'package.json'); const packageJson = JSON.parse(fs.readFile(packageJsonPath)); propertiesToConsider.forEach(formatProperty => { if (packageJson[formatProperty]) diff --git a/packages/compiler-cli/ngcc/src/packages/build_marker.ts b/packages/compiler-cli/ngcc/src/packages/build_marker.ts index 8445854e04..9223a2036b 100644 --- a/packages/compiler-cli/ngcc/src/packages/build_marker.ts +++ b/packages/compiler-cli/ngcc/src/packages/build_marker.ts @@ -5,9 +5,7 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; import {EntryPointJsonProperty, EntryPointPackageJson} from './entry_point'; export const NGCC_VERSION = '0.0.0-PLACEHOLDER'; diff --git a/packages/compiler-cli/ngcc/src/packages/bundle_program.ts b/packages/compiler-cli/ngcc/src/packages/bundle_program.ts index a1e34dde9c..74e058b113 100644 --- a/packages/compiler-cli/ngcc/src/packages/bundle_program.ts +++ b/packages/compiler-cli/ngcc/src/packages/bundle_program.ts @@ -6,9 +6,7 @@ * 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 {AbsoluteFsPath, FileSystem, dirname, resolve} from '../../../src/ngtsc/file_system'; import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from './patch_ts_expando_initializer'; /** @@ -35,12 +33,14 @@ export interface BundleProgram { export function makeBundleProgram( fs: FileSystem, isCore: boolean, path: AbsoluteFsPath, r3FileName: string, options: ts.CompilerOptions, host: ts.CompilerHost): BundleProgram { - const r3SymbolsPath = - isCore ? findR3SymbolsPath(fs, AbsoluteFsPath.dirname(path), r3FileName) : null; + const r3SymbolsPath = isCore ? findR3SymbolsPath(fs, dirname(path), r3FileName) : null; const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path]; const originalGetExpandoInitializer = patchTsGetExpandoInitializer(); const program = ts.createProgram(rootPaths, options, host); + // Ask for the typeChecker to trigger the binding phase of the compilation. + // This will then exercise the patched function. + program.getTypeChecker(); restoreGetExpandoInitializer(originalGetExpandoInitializer); const file = program.getSourceFile(path) !; @@ -54,7 +54,7 @@ export function makeBundleProgram( */ export function findR3SymbolsPath( fs: FileSystem, directory: AbsoluteFsPath, filename: string): AbsoluteFsPath|null { - const r3SymbolsFilePath = AbsoluteFsPath.resolve(directory, filename); + const r3SymbolsFilePath = resolve(directory, filename); if (fs.exists(r3SymbolsFilePath)) { return r3SymbolsFilePath; } @@ -67,13 +67,12 @@ export function findR3SymbolsPath( .filter(p => p !== 'node_modules') // Only interested in directories (and only those that are not symlinks) .filter(p => { - const stat = fs.lstat(AbsoluteFsPath.resolve(directory, p)); + const stat = fs.lstat(resolve(directory, p)); return stat.isDirectory() && !stat.isSymbolicLink(); }); for (const subDirectory of subDirectories) { - const r3SymbolsFilePath = - findR3SymbolsPath(fs, AbsoluteFsPath.resolve(directory, subDirectory), filename); + const r3SymbolsFilePath = findR3SymbolsPath(fs, resolve(directory, subDirectory), filename); if (r3SymbolsFilePath) { return r3SymbolsFilePath; } diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index b7a720755b..7d7f063e0d 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -6,8 +6,7 @@ * 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 {AbsoluteFsPath, FileSystem, join, resolve} from '../../../src/ngtsc/file_system'; import {parseStatementForUmdModule} from '../host/umd_host'; import {Logger} from '../logging/logger'; @@ -70,7 +69,7 @@ export const SUPPORTED_FORMAT_PROPERTIES: EntryPointJsonProperty[] = export function getEntryPointInfo( fs: FileSystem, logger: Logger, packagePath: AbsoluteFsPath, entryPointPath: AbsoluteFsPath): EntryPoint|null { - const packageJsonPath = AbsoluteFsPath.resolve(entryPointPath, 'package.json'); + const packageJsonPath = resolve(entryPointPath, 'package.json'); if (!fs.exists(packageJsonPath)) { return null; } @@ -88,15 +87,14 @@ export function getEntryPointInfo( } // Also there must exist a `metadata.json` file next to the typings entry-point. - const metadataPath = - AbsoluteFsPath.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json'); + const metadataPath = resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json'); const entryPointInfo: EntryPoint = { name: entryPointPackageJson.name, packageJson: entryPointPackageJson, package: packagePath, path: entryPointPath, - typings: AbsoluteFsPath.resolve(entryPointPath, typings), + typings: resolve(entryPointPath, typings), compiledByAngular: fs.exists(metadataPath), }; @@ -127,7 +125,7 @@ export function getEntryPointFormat( if (mainFile === undefined) { return undefined; } - const pathToMain = AbsoluteFsPath.join(entryPoint.path, mainFile); + const pathToMain = join(entryPoint.path, mainFile); return isUmdModule(fs, pathToMain) ? 'umd' : 'commonjs'; case 'module': return 'esm5'; diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts index 166873090e..a8d14a6704 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -6,13 +6,13 @@ * 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 {AbsoluteFsPath, FileSystem, absoluteFrom, resolve} from '../../../src/ngtsc/file_system'; +import {NgtscCompilerHost} from '../../../src/ngtsc/file_system/src/compiler_host'; import {PathMappings} from '../utils'; import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from './entry_point'; -import {NgccCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host'; +import {NgccSourcesCompilerHost} from './ngcc_compiler_host'; + /** * A bundle of files and paths (and TS programs) that correspond to a particular @@ -49,17 +49,16 @@ export function makeEntryPointBundle( rootDir: entryPointPath, ...pathMappings }; const srcHost = new NgccSourcesCompilerHost(fs, options, entryPointPath); - const dtsHost = new NgccCompilerHost(fs, options); - const rootDirs = [AbsoluteFsPath.from(entryPointPath)]; + const dtsHost = new NgtscCompilerHost(fs, options); + const rootDirs = [absoluteFrom(entryPointPath)]; // Create the bundle programs, as necessary. const src = makeBundleProgram( - fs, isCore, AbsoluteFsPath.resolve(entryPointPath, formatPath), 'r3_symbols.js', options, - srcHost); - const dts = transformDts ? makeBundleProgram( - fs, isCore, AbsoluteFsPath.resolve(entryPointPath, typingsPath), - 'r3_symbols.d.ts', options, dtsHost) : - null; + fs, isCore, resolve(entryPointPath, formatPath), 'r3_symbols.js', options, srcHost); + const dts = transformDts ? + makeBundleProgram( + fs, isCore, resolve(entryPointPath, typingsPath), 'r3_symbols.d.ts', options, dtsHost) : + null; const isFlatCore = isCore && src.r3SymbolsFile === null; return {format, formatProperty, rootDirs, isCore, isFlatCore, src, dts}; diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts index b3972bb530..ef3abc79de 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts @@ -5,11 +5,11 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, FileSystem, join, resolve} from '../../../src/ngtsc/file_system'; import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver'; -import {FileSystem} from '../file_system/file_system'; import {Logger} from '../logging/logger'; import {PathMappings} from '../utils'; + import {EntryPoint, getEntryPointInfo} from './entry_point'; export class EntryPointFinder { @@ -55,9 +55,9 @@ export class EntryPointFinder { AbsoluteFsPath[] { const basePaths = [sourceDirectory]; if (pathMappings) { - const baseUrl = AbsoluteFsPath.resolve(pathMappings.baseUrl); + const baseUrl = resolve(pathMappings.baseUrl); values(pathMappings.paths).forEach(paths => paths.forEach(path => { - basePaths.push(AbsoluteFsPath.join(baseUrl, extractPathPrefix(path))); + basePaths.push(join(baseUrl, extractPathPrefix(path))); })); } basePaths.sort(); // Get the paths in order with the shorter ones first. @@ -84,17 +84,17 @@ export class EntryPointFinder { .filter(p => p !== 'node_modules') // Only interested in directories (and only those that are not symlinks) .filter(p => { - const stat = this.fs.lstat(AbsoluteFsPath.resolve(sourceDirectory, p)); + const stat = this.fs.lstat(resolve(sourceDirectory, p)); return stat.isDirectory() && !stat.isSymbolicLink(); }) .forEach(p => { // Either the directory is a potential package or a namespace containing packages (e.g // `@angular`). - const packagePath = AbsoluteFsPath.join(sourceDirectory, p); + const packagePath = join(sourceDirectory, p); entryPoints.push(...this.walkDirectoryForEntryPoints(packagePath)); // Also check for any nested node_modules in this package - const nestedNodeModulesPath = AbsoluteFsPath.join(packagePath, 'node_modules'); + const nestedNodeModulesPath = join(packagePath, 'node_modules'); if (this.fs.exists(nestedNodeModulesPath)) { entryPoints.push(...this.walkDirectoryForEntryPoints(nestedNodeModulesPath)); } @@ -145,11 +145,11 @@ export class EntryPointFinder { .filter(p => p !== 'node_modules') // Only interested in directories (and only those that are not symlinks) .filter(p => { - const stat = this.fs.lstat(AbsoluteFsPath.resolve(dir, p)); + const stat = this.fs.lstat(resolve(dir, p)); return stat.isDirectory() && !stat.isSymbolicLink(); }) .forEach(subDir => { - const resolvedSubDir = AbsoluteFsPath.resolve(dir, subDir); + const resolvedSubDir = resolve(dir, subDir); fn(resolvedSubDir); this.walkDirectory(resolvedSubDir, fn); }); diff --git a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts index d437a51945..161e34f7cc 100644 --- a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts +++ b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts @@ -5,74 +5,19 @@ * 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 os from 'os'; import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {FileSystem} from '../../../src/ngtsc/file_system'; +import {NgtscCompilerHost} from '../../../src/ngtsc/file_system/src/compiler_host'; import {isRelativePath} from '../utils'; -export class NgccCompilerHost implements ts.CompilerHost { - private _caseSensitive = this.fs.exists(AbsoluteFsPath.fromUnchecked(__filename.toUpperCase())); - - constructor(protected fs: FileSystem, protected options: ts.CompilerOptions) {} - - getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { - const text = this.readFile(fileName); - return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion) : undefined; - } - - getDefaultLibFileName(options: ts.CompilerOptions): string { - return this.getDefaultLibLocation() + '/' + ts.getDefaultLibFileName(options); - } - - getDefaultLibLocation(): string { - const nodeLibPath = AbsoluteFsPath.from(require.resolve('typescript')); - return AbsoluteFsPath.join(nodeLibPath, '..'); - } - - writeFile(fileName: string, data: string): void { - this.fs.writeFile(AbsoluteFsPath.fromUnchecked(fileName), data); - } - - getCurrentDirectory(): string { return this.fs.pwd(); } - - getCanonicalFileName(fileName: string): string { - return this.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); - } - - useCaseSensitiveFileNames(): boolean { return this._caseSensitive; } - - getNewLine(): string { - switch (this.options.newLine) { - case ts.NewLineKind.CarriageReturnLineFeed: - return '\r\n'; - case ts.NewLineKind.LineFeed: - return '\n'; - default: - return os.EOL; - } - } - - fileExists(fileName: string): boolean { - return this.fs.exists(AbsoluteFsPath.fromUnchecked(fileName)); - } - - readFile(fileName: string): string|undefined { - if (!this.fileExists(fileName)) { - return undefined; - } - return this.fs.readFile(AbsoluteFsPath.fromUnchecked(fileName)); - } -} - /** * Represents a compiler host that resolves a module import as a JavaScript source file if * available, instead of the .d.ts typings file that would have been resolved by TypeScript. This * is necessary for packages that have their typings in the same directory as the sources, which * would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file. */ -export class NgccSourcesCompilerHost extends NgccCompilerHost { +export class NgccSourcesCompilerHost extends NgtscCompilerHost { private cache = ts.createModuleResolutionCache( this.getCurrentDirectory(), file => this.getCanonicalFileName(file)); diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index 25a972f223..823600fb41 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -6,13 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; - +import {FileSystem} from '../../../src/ngtsc/file_system'; import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer'; import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analysis/module_with_providers_analyzer'; import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry'; import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; -import {FileSystem} from '../file_system/file_system'; import {CommonJsReflectionHost} from '../host/commonjs_host'; import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm5ReflectionHost} from '../host/esm5_host'; @@ -27,11 +26,8 @@ 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'; - - /** * A Package is stored in a directory on disk and that directory can contain one or more package * formats - e.g. fesm2015, UMD, etc. Additionally, each package provides typings (`.d.ts` files). diff --git a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts index 6329506136..dba0baac80 100644 --- a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts @@ -7,20 +7,19 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; - +import {FileSystem} from '../../../src/ngtsc/file_system'; +import {CompileResult} from '../../../src/ngtsc/transform'; 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 diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts index 579fde535b..895c8d8a52 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts @@ -7,7 +7,7 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; -import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {relative, dirname, AbsoluteFsPath, absoluteFromSourceFile} from '../../../src/ngtsc/file_system'; import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {CompiledClass} from '../analysis/decoration_analyzer'; @@ -46,8 +46,7 @@ export class EsmRenderingFormatter implements RenderingFormatter { if (from) { const basePath = stripExtension(from); - const relativePath = - './' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath); + const relativePath = './' + relative(dirname(entryPointBasePath), basePath); exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : ''; } @@ -136,12 +135,11 @@ export class EsmRenderingFormatter implements RenderingFormatter { 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 declarationFile = absoluteFromSourceFile(info.declaration.getSourceFile()); + const ngModuleFile = absoluteFromSourceFile(info.ngModule.node.getSourceFile()); const importPath = info.ngModule.viaModule || (declarationFile !== ngModuleFile ? - stripExtension( - `./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) : + stripExtension(`./${relative(dirname(declarationFile), ngModuleFile)}`) : null); const ngModule = generateImportString(importManager, importPath, ngModuleName); diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index 3624db0133..0f4b7d8a91 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -8,14 +8,13 @@ import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler'; import MagicString from 'magic-string'; import * as ts from 'typescript'; - -import {NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports'; +import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports'; import {translateStatement, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_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 {FileSystem} from '../../../src/ngtsc/file_system'; import {NgccReflectionHost} from '../host/ngcc_host'; import {EntryPointBundle} from '../packages/entry_point_bundle'; import {Logger} from '../logging/logger'; diff --git a/packages/compiler-cli/ngcc/src/rendering/source_maps.ts b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts index 23be4392c8..900a8e1c11 100644 --- a/packages/compiler-cli/ngcc/src/rendering/source_maps.ts +++ b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts @@ -9,8 +9,7 @@ import {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, gene 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 {resolve, FileSystem, absoluteFromSourceFile, dirname, basename, absoluteFrom} from '../../../src/ngtsc/file_system'; import {Logger} from '../logging/logger'; import {FileToWrite} from './utils'; @@ -39,19 +38,18 @@ export function extractSourceMap( let externalSourceMap: SourceMapConverter|null = null; try { const fileName = external[1] || external[2]; - const filePath = AbsoluteFsPath.resolve( - AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName); + const filePath = resolve(dirname(absoluteFromSourceFile(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) && + const mapPath = absoluteFrom(file.fileName + '.map'); + if (basename(e.path) !== basename(mapPath) && fs.exists(mapPath) && fs.stat(mapPath).isFile()) { logger.warn( - `Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`); + `Guessing the map file name from the source file name: "${basename(mapPath)}"`); try { externalSourceMap = fromObject(JSON.parse(fs.readFile(mapPath))); } catch (e) { @@ -76,9 +74,9 @@ export function extractSourceMap( */ 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 outputPath = absoluteFromSourceFile(sourceFile); + const outputMapPath = absoluteFrom(`${outputPath}.map`); + const relativeSourcePath = basename(outputPath); const relativeMapPath = `${relativeSourcePath}.map`; const outputMap = output.generateMap({ diff --git a/packages/compiler-cli/ngcc/src/rendering/utils.ts b/packages/compiler-cli/ngcc/src/rendering/utils.ts index 8392a09425..29cafaa785 100644 --- a/packages/compiler-cli/ngcc/src/rendering/utils.ts +++ b/packages/compiler-cli/ngcc/src/rendering/utils.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../src/ngtsc/file_system'; import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; /** diff --git a/packages/compiler-cli/ngcc/src/writing/file_writer.ts b/packages/compiler-cli/ngcc/src/writing/file_writer.ts index 5b178e5355..aea331f5eb 100644 --- a/packages/compiler-cli/ngcc/src/writing/file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/file_writer.ts @@ -10,7 +10,6 @@ import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; import {FileToWrite} from '../rendering/utils'; - /** * Responsible for writing out the transformed files to disk. */ 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 adc7f396c5..ca28c93b6f 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 @@ -6,8 +6,7 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {FileSystem} from '../file_system/file_system'; +import {FileSystem, absoluteFrom, dirname} from '../../../src/ngtsc/file_system'; import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; import {FileToWrite} from '../rendering/utils'; @@ -25,8 +24,8 @@ export class InPlaceFileWriter implements FileWriter { } protected writeFileAndBackup(file: FileToWrite): void { - this.fs.ensureDir(AbsoluteFsPath.dirname(file.path)); - const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`); + this.fs.ensureDir(dirname(file.path)); + const backPath = absoluteFrom(`${file.path}.__ivy_ngcc_bak`); if (this.fs.exists(backPath)) { throw new Error( `Tried to overwrite ${backPath} with an ngcc back up file, which is disallowed.`); 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 59ff4e67c1..a07e592881 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 @@ -6,7 +6,7 @@ * 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 {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, absoluteFromSourceFile, dirname, join, relative} from '../../../src/ngtsc/file_system'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; @@ -27,7 +27,7 @@ const NGCC_DIRECTORY = '__ivy_ngcc__'; export class NewEntryPointFileWriter extends InPlaceFileWriter { 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); + const ngccFolder = join(entryPoint.package, NGCC_DIRECTORY); this.copyBundle(bundle, entryPoint.package, ngccFolder); transformedFiles.forEach(file => this.writeFile(file, entryPoint.package, ngccFolder)); this.updatePackageJson(entryPoint, bundle.formatProperty, ngccFolder); @@ -36,13 +36,12 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter { protected copyBundle( bundle: EntryPointBundle, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath) { bundle.src.program.getSourceFiles().forEach(sourceFile => { - const relativePath = - PathSegment.relative(packagePath, AbsoluteFsPath.fromSourceFile(sourceFile)); + const relativePath = relative(packagePath, absoluteFromSourceFile(sourceFile)); const isOutsidePackage = relativePath.startsWith('..'); if (!sourceFile.isDeclarationFile && !isOutsidePackage) { - const newFilePath = AbsoluteFsPath.join(ngccFolder, relativePath); - this.fs.ensureDir(AbsoluteFsPath.dirname(newFilePath)); - this.fs.copyFile(AbsoluteFsPath.fromSourceFile(sourceFile), newFilePath); + const newFilePath = join(ngccFolder, relativePath); + this.fs.ensureDir(dirname(newFilePath)); + this.fs.copyFile(absoluteFromSourceFile(sourceFile), newFilePath); } }); } @@ -53,24 +52,20 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter { // This is either `.d.ts` or `.d.ts.map` file super.writeFileAndBackup(file); } else { - const relativePath = PathSegment.relative(packagePath, file.path); - const newFilePath = AbsoluteFsPath.join(ngccFolder, relativePath); - this.fs.ensureDir(AbsoluteFsPath.dirname(newFilePath)); + const relativePath = relative(packagePath, file.path); + const newFilePath = join(ngccFolder, relativePath); + this.fs.ensureDir(dirname(newFilePath)); this.fs.writeFile(newFilePath, file.contents); } } protected updatePackageJson( entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, ngccFolder: AbsoluteFsPath) { - const formatPath = - AbsoluteFsPath.join(entryPoint.path, entryPoint.packageJson[formatProperty] !); - const newFormatPath = - AbsoluteFsPath.join(ngccFolder, PathSegment.relative(entryPoint.package, formatPath)); + const formatPath = join(entryPoint.path, entryPoint.packageJson[formatProperty] !); + const newFormatPath = join(ngccFolder, relative(entryPoint.package, formatPath)); const newFormatProperty = formatProperty + '_ivy_ngcc'; - (entryPoint.packageJson as any)[newFormatProperty] = - PathSegment.relative(entryPoint.path, newFormatPath); + (entryPoint.packageJson as any)[newFormatProperty] = relative(entryPoint.path, newFormatPath); this.fs.writeFile( - AbsoluteFsPath.join(entryPoint.path, 'package.json'), - JSON.stringify(entryPoint.packageJson)); + join(entryPoint.path, 'package.json'), JSON.stringify(entryPoint.packageJson)); } } diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 4ed0921b8d..5e120e0871 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -12,17 +12,17 @@ ts_library( deps = [ "//packages/compiler-cli/ngcc", "//packages/compiler-cli/ngcc/test/helpers", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", - "//packages/compiler-cli/src/ngtsc/path", "//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", + "//packages/compiler-cli/test/helpers", "@npm//@types/convert-source-map", - "@npm//@types/mock-fs", - "@npm//canonical-path", + "@npm//convert-source-map", "@npm//magic-string", "@npm//typescript", ], @@ -31,12 +31,12 @@ ts_library( jasmine_node_test( name = "test", bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"], + data = [ + "//packages/compiler-cli/test/ngtsc/fake_core:npm_package", + ], deps = [ ":test_lib", "//tools/testing:node_no_angular", - "@npm//canonical-path", - "@npm//convert-source-map", - "@npm//shelljs", ], ) @@ -49,21 +49,24 @@ ts_library( deps = [ "//packages/compiler-cli/ngcc", "//packages/compiler-cli/ngcc/test/helpers", - "//packages/compiler-cli/src/ngtsc/path", - "//packages/compiler-cli/test:test_utils", - "@npm//@types/mock-fs", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", + "//packages/compiler-cli/src/ngtsc/testing", + "//packages/compiler-cli/test/helpers", "@npm//rxjs", ], ) jasmine_node_test( name = "integration", + timeout = "long", bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"], data = [ "//packages/common:npm_package", "//packages/core:npm_package", "@npm//rxjs", ], + shard_count = 4, tags = [ # Disabled in AOT mode because we want ngcc to compile non-AOT Angular packages. "no-ivy-aot", @@ -73,6 +76,5 @@ jasmine_node_test( "//tools/testing:node_no_angular", "@npm//canonical-path", "@npm//convert-source-map", - "@npm//shelljs", ], ) diff --git a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts index 55e9ce36e7..1baa420b22 100644 --- a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -7,238 +7,267 @@ */ import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {Decorator} from '../../../src/ngtsc/reflection'; import {DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform'; +import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {Folder, MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; -import {createFileSystemFromProgramFiles, makeTestBundleProgram} from '../helpers/utils'; - -const _ = AbsoluteFsPath.fromUnchecked; - -const TEST_PROGRAM = [ - { - name: _('/test.js'), - contents: ` - import {Component, Directive, Injectable} from '@angular/core'; - - export class MyComponent {} - MyComponent.decorators = [{type: Component}]; - - export class MyDirective {} - MyDirective.decorators = [{type: Directive}]; - - export class MyService {} - MyService.decorators = [{type: Injectable}]; - `, - }, - { - name: _('/other.js'), - contents: ` - import {Component} from '@angular/core'; - - export class MyOtherComponent {} - MyOtherComponent.decorators = [{type: Component}]; - `, - }, -]; - -const INTERNAL_COMPONENT_PROGRAM = [ - { - name: _('/entrypoint.js'), - contents: ` - import {Component, NgModule} from '@angular/core'; - import {ImportedComponent} from './component'; - - export class LocalComponent {} - LocalComponent.decorators = [{type: Component}]; - - export class MyModule {} - MyModule.decorators = [{type: NgModule, args: [{ - declarations: [ImportedComponent, LocalComponent], - exports: [ImportedComponent, LocalComponent], - },] }]; - ` - }, - { - name: _('/component.js'), - contents: ` - import {Component} from '@angular/core'; - export class ImportedComponent {} - ImportedComponent.decorators = [{type: Component}]; - `, - isRoot: false, - } -]; +import {getRootFiles, makeTestBundleProgram} from '../helpers/utils'; type DecoratorHandlerWithResolve = DecoratorHandler& { resolve: NonNullable['resolve']>; }; -describe('DecorationAnalyzer', () => { - describe('analyzeProgram()', () => { - let logs: string[]; - let program: ts.Program; - let testHandler: jasmine.SpyObj; - let result: DecorationAnalyses; +runInEachFileSystem(() => { + describe('DecorationAnalyzer', () => { + let _: typeof absoluteFrom; - // Helpers - const createTestHandler = () => { - const handler = jasmine.createSpyObj('TestDecoratorHandler', [ - 'detect', - 'analyze', - 'resolve', - 'compile', - ]); - // Only detect the Component and Directive decorators - handler.detect.and.callFake( - (node: ts.Declaration, decorators: Decorator[] | null): DetectResult| undefined => { - const className = (node as any).name.text; - if (decorators === null) { - logs.push(`detect: ${className} (no decorators)`); - } else { - logs.push(`detect: ${className}@${decorators.map(d => d.name)}`); - } - if (!decorators) { - return undefined; - } - const metadata = decorators.find(d => d.name === 'Component' || d.name === 'Directive'); - if (metadata === undefined) { - return undefined; - } else { - return { - metadata, - trigger: metadata.node, - }; - } - }); - // The "test" analysis is an object with the name of the decorator being analyzed - handler.analyze.and.callFake((decl: ts.Declaration, dec: Decorator) => { - logs.push(`analyze: ${(decl as any).name.text}@${dec.name}`); - return {analysis: {decoratorName: dec.name}, diagnostics: undefined}; - }); - // The "test" resolution is just setting `resolved: true` on the analysis - handler.resolve.and.callFake((decl: ts.Declaration, analysis: any) => { - logs.push(`resolve: ${(decl as any).name.text}@${analysis.decoratorName}`); - analysis.resolved = true; - }); - // The "test" compilation result is just the name of the decorator being compiled - // (suffixed with `(compiled)`) - handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => { - logs.push( - `compile: ${(decl as any).name.text}@${analysis.decoratorName} (resolved: ${analysis.resolved})`); - return `@${analysis.decoratorName} (compiled)`; - }); - return handler; - }; + beforeEach(() => { _ = absoluteFrom; }); - const setUpAndAnalyzeProgram = (...progArgs: Parameters) => { - logs = []; + describe('analyzeProgram()', () => { + let logs: string[]; + let program: ts.Program; + let testHandler: jasmine.SpyObj; + let result: DecorationAnalyses; - const {options, host, ...bundle} = makeTestBundleProgram(...progArgs); - program = bundle.program; + // Helpers + const createTestHandler = () => { + const handler = jasmine.createSpyObj('TestDecoratorHandler', [ + 'detect', + 'analyze', + 'resolve', + 'compile', + ]); + // Only detect the Component and Directive decorators + handler.detect.and.callFake( + (node: ts.Declaration, decorators: Decorator[] | null): DetectResult| + undefined => { + const className = (node as any).name.text; + if (decorators === null) { + logs.push(`detect: ${className} (no decorators)`); + } else { + logs.push(`detect: ${className}@${decorators.map(d => d.name)}`); + } + if (!decorators) { + return undefined; + } + const metadata = + decorators.find(d => d.name === 'Component' || d.name === 'Directive'); + if (metadata === undefined) { + return undefined; + } else { + return { + metadata, + trigger: metadata.node, + }; + } + }); + // The "test" analysis is an object with the name of the decorator being analyzed + handler.analyze.and.callFake((decl: ts.Declaration, dec: Decorator) => { + logs.push(`analyze: ${(decl as any).name.text}@${dec.name}`); + return {analysis: {decoratorName: dec.name}, diagnostics: undefined}; + }); + // The "test" resolution is just setting `resolved: true` on the analysis + handler.resolve.and.callFake((decl: ts.Declaration, analysis: any) => { + logs.push(`resolve: ${(decl as any).name.text}@${analysis.decoratorName}`); + analysis.resolved = true; + }); + // The "test" compilation result is just the name of the decorator being compiled + // (suffixed with `(compiled)`) + handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => { + logs.push( + `compile: ${(decl as any).name.text}@${analysis.decoratorName} (resolved: ${analysis.resolved})`); + return `@${analysis.decoratorName} (compiled)`; + }); + return handler; + }; - const reflectionHost = - new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const referencesRegistry = new NgccReferencesRegistry(reflectionHost); - const fs = new MockFileSystem(createFileSystemFromProgramFiles(...progArgs)); - const analyzer = new DecorationAnalyzer( - fs, program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, - [AbsoluteFsPath.fromUnchecked('/')], false); - testHandler = createTestHandler(); - analyzer.handlers = [testHandler]; - result = analyzer.analyzeProgram(); - }; + function setUpAndAnalyzeProgram(testFiles: TestFile[]) { + logs = []; + loadTestFiles(testFiles); + loadFakeCore(getFileSystem()); + const rootFiles = getRootFiles(testFiles); + const {options, host, ...bundle} = makeTestBundleProgram(rootFiles[0]); + program = bundle.program; - describe('basic usage', () => { - beforeEach(() => setUpAndAnalyzeProgram(TEST_PROGRAM)); + const reflectionHost = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const referencesRegistry = new NgccReferencesRegistry(reflectionHost); + const analyzer = new DecorationAnalyzer( + getFileSystem(), program, options, host, program.getTypeChecker(), reflectionHost, + referencesRegistry, [absoluteFrom('/')], false); + testHandler = createTestHandler(); + analyzer.handlers = [testHandler]; + result = analyzer.analyzeProgram(); + } - it('should return an object containing a reference to the original source file', () => { - TEST_PROGRAM.forEach(({name}) => { - const file = program.getSourceFile(name) !; - expect(result.get(file) !.sourceFile).toBe(file); + describe('basic usage', () => { + beforeEach(() => { + const TEST_PROGRAM = [ + { + name: _('/index.js'), + contents: ` + import * as test from './test'; + import * as other from './other'; + `, + }, + { + name: _('/test.js'), + contents: ` + import {Component, Directive, Injectable} from '@angular/core'; + + export class MyComponent {} + MyComponent.decorators = [{type: Component}]; + + export class MyDirective {} + MyDirective.decorators = [{type: Directive}]; + + export class MyService {} + MyService.decorators = [{type: Injectable}]; + `, + }, + { + name: _('/other.js'), + contents: ` + import {Component} from '@angular/core'; + + export class MyOtherComponent {} + MyOtherComponent.decorators = [{type: Component}]; + `, + }, + ]; + setUpAndAnalyzeProgram(TEST_PROGRAM); + }); + + it('should return an object containing a reference to the original source file', () => { + const testFile = getSourceFileOrError(program, _('/test.js')); + expect(result.get(testFile) !.sourceFile).toBe(testFile); + const otherFile = getSourceFileOrError(program, _('/other.js')); + expect(result.get(otherFile) !.sourceFile).toBe(otherFile); + }); + + it('should call detect on the decorator handlers with each class from the parsed file', + () => { + expect(testHandler.detect).toHaveBeenCalledTimes(11); + expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([ + null, + null, + null, + null, + null, + null, + null, + jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]), + jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]), + jasmine.arrayContaining([jasmine.objectContaining({name: 'Injectable'})]), + jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]), + ]); + }); + + it('should return an object containing the classes that were analyzed', () => { + const file1 = getSourceFileOrError(program, _('/test.js')); + const compiledFile1 = result.get(file1) !; + expect(compiledFile1.compiledClasses.length).toEqual(2); + expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({ + name: 'MyComponent', compilation: ['@Component (compiled)'], + } as unknown as CompiledClass)); + expect(compiledFile1.compiledClasses[1]).toEqual(jasmine.objectContaining({ + name: 'MyDirective', compilation: ['@Directive (compiled)'], + } as unknown as CompiledClass)); + + const file2 = getSourceFileOrError(program, _('/other.js')); + const compiledFile2 = result.get(file2) !; + expect(compiledFile2.compiledClasses.length).toEqual(1); + expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({ + name: 'MyOtherComponent', compilation: ['@Component (compiled)'], + } as unknown as CompiledClass)); + }); + + it('should analyze, resolve and compile the classes that are detected', () => { + expect(logs).toEqual([ + // Classes without decorators should also be detected. + 'detect: ChangeDetectorRef (no decorators)', + 'detect: ElementRef (no decorators)', + 'detect: Injector (no decorators)', + 'detect: TemplateRef (no decorators)', + 'detect: ViewContainerRef (no decorators)', + 'detect: Renderer2 (no decorators)', + 'detect: ɵNgModuleFactory (no decorators)', + // First detect and (potentially) analyze. + 'detect: MyComponent@Component', + 'analyze: MyComponent@Component', + 'detect: MyDirective@Directive', + 'analyze: MyDirective@Directive', + 'detect: MyService@Injectable', + 'detect: MyOtherComponent@Component', + 'analyze: MyOtherComponent@Component', + // The resolve. + 'resolve: MyComponent@Component', + 'resolve: MyDirective@Directive', + 'resolve: MyOtherComponent@Component', + // Finally compile. + 'compile: MyComponent@Component (resolved: true)', + 'compile: MyDirective@Directive (resolved: true)', + 'compile: MyOtherComponent@Component (resolved: true)', + ]); }); }); - it('should call detect on the decorator handlers with each class from the parsed file', - () => { - expect(testHandler.detect).toHaveBeenCalledTimes(5); - expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([ - null, - jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]), - jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]), - jasmine.arrayContaining([jasmine.objectContaining({name: 'Injectable'})]), - jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]), - ]); - }); + describe('internal components', () => { + beforeEach(() => { + const INTERNAL_COMPONENT_PROGRAM = [ + { + name: _('/entrypoint.js'), + contents: ` + import {Component, NgModule} from '@angular/core'; + import {ImportedComponent} from './component'; - it('should return an object containing the classes that were analyzed', () => { - const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !; - const compiledFile1 = result.get(file1) !; - expect(compiledFile1.compiledClasses.length).toEqual(2); - expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({ - name: 'MyComponent', compilation: ['@Component (compiled)'], - } as unknown as CompiledClass)); - expect(compiledFile1.compiledClasses[1]).toEqual(jasmine.objectContaining({ - name: 'MyDirective', compilation: ['@Directive (compiled)'], - } as unknown as CompiledClass)); + export class LocalComponent {} + LocalComponent.decorators = [{type: Component}]; - const file2 = program.getSourceFile(TEST_PROGRAM[1].name) !; - const compiledFile2 = result.get(file2) !; - expect(compiledFile2.compiledClasses.length).toEqual(1); - expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({ - name: 'MyOtherComponent', compilation: ['@Component (compiled)'], - } as unknown as CompiledClass)); - }); + export class MyModule {} + MyModule.decorators = [{type: NgModule, args: [{ + declarations: [ImportedComponent, LocalComponent], + exports: [ImportedComponent, LocalComponent], + },] }]; + ` + }, + { + name: _('/component.js'), + contents: ` + import {Component} from '@angular/core'; + export class ImportedComponent {} + ImportedComponent.decorators = [{type: Component}]; + `, + isRoot: false, + } + ]; + setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM); + }); - it('should analyze, resolve and compile the classes that are detected', () => { - expect(logs).toEqual([ - // Classes without decorators should also be detected. - 'detect: InjectionToken (no decorators)', - // First detect and (potentially) analyze. - 'detect: MyComponent@Component', - 'analyze: MyComponent@Component', - 'detect: MyDirective@Directive', - 'analyze: MyDirective@Directive', - 'detect: MyService@Injectable', - 'detect: MyOtherComponent@Component', - 'analyze: MyOtherComponent@Component', - // The resolve. - 'resolve: MyComponent@Component', - 'resolve: MyDirective@Directive', - 'resolve: MyOtherComponent@Component', - // Finally compile. - 'compile: MyComponent@Component (resolved: true)', - 'compile: MyDirective@Directive (resolved: true)', - 'compile: MyOtherComponent@Component (resolved: true)', - ]); - }); - }); + // The problem of exposing the type of these internal components in the .d.ts typing + // files is not yet solved. + it('should analyze an internally imported component, which is not publicly exported from the entry-point', + () => { + const file = getSourceFileOrError(program, _('/component.js')); + const analysis = result.get(file) !; + expect(analysis).toBeDefined(); + const ImportedComponent = + analysis.compiledClasses.find(f => f.name === 'ImportedComponent') !; + expect(ImportedComponent).toBeDefined(); + }); - describe('internal components', () => { - beforeEach(() => setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM)); - - // The problem of exposing the type of these internal components in the .d.ts typing files - // is not yet solved. - it('should analyze an internally imported component, which is not publicly exported from the entry-point', - () => { - const file = program.getSourceFile('component.js') !; - const analysis = result.get(file) !; - expect(analysis).toBeDefined(); - const ImportedComponent = - analysis.compiledClasses.find(f => f.name === 'ImportedComponent') !; - expect(ImportedComponent).toBeDefined(); - }); - - it('should analyze an internally defined component, which is not exported at all', () => { - const file = program.getSourceFile('entrypoint.js') !; - const analysis = result.get(file) !; - expect(analysis).toBeDefined(); - const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !; - expect(LocalComponent).toBeDefined(); + it('should analyze an internally defined component, which is not exported at all', () => { + const file = getSourceFileOrError(program, _('/entrypoint.js')); + const analysis = result.get(file) !; + expect(analysis).toBeDefined(); + const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !; + expect(LocalComponent).toBeDefined(); + }); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/analysis/module_with_providers_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/module_with_providers_analyzer_spec.ts index 22a3a93ea5..88d19f4aad 100644 --- a/packages/compiler-cli/ngcc/test/analysis/module_with_providers_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/module_with_providers_analyzer_spec.ts @@ -7,397 +7,415 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath, absoluteFrom, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {BundleProgram} from '../../src/packages/bundle_program'; import {MockLogger} from '../helpers/mock_logger'; -import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; +import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils'; -const TEST_PROGRAM = [ - { - name: '/src/entry-point.js', - contents: ` - export * from './explicit'; - export * from './any'; - export * from './implicit'; - export * from './no-providers'; - export * from './module'; - ` - }, - { - name: '/src/explicit.js', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export class ExplicitInternalModule {} - export function explicitInternalFunction() { - return { - ngModule: ExplicitInternalModule, - providers: [] - }; - } - export function explicitExternalFunction() { - return { - ngModule: ExternalModule, - providers: [] - }; - } - export function explicitLibraryFunction() { - return { - ngModule: LibraryModule, - providers: [] - }; - } - export class ExplicitClass { - static explicitInternalMethod() { - return { - ngModule: ExplicitInternalModule, - providers: [] - }; - } - static explicitExternalMethod() { - return { - ngModule: ExternalModule, - providers: [] - }; - } - static explicitLibraryMethod() { - return { - ngModule: LibraryModule, - providers: [] - }; - } - } - ` - }, - { - name: '/src/any.js', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export class AnyInternalModule {} - export function anyInternalFunction() { - return { - ngModule: AnyInternalModule, - providers: [] - }; - } - export function anyExternalFunction() { - return { - ngModule: ExternalModule, - providers: [] - }; - } - export function anyLibraryFunction() { - return { - ngModule: LibraryModule, - providers: [] - }; - } - export class AnyClass { - static anyInternalMethod() { - return { - ngModule: AnyInternalModule, - providers: [] - }; - } - static anyExternalMethod() { - return { - ngModule: ExternalModule, - providers: [] - }; - } - static anyLibraryMethod() { - return { - ngModule: LibraryModule, - providers: [] - }; - } - } - ` - }, - { - name: '/src/implicit.js', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export class ImplicitInternalModule {} - export function implicitInternalFunction() { - return { - ngModule: ImplicitInternalModule, - providers: [], - }; - } - export function implicitExternalFunction() { - return { - ngModule: ExternalModule, - providers: [], - }; - } - export function implicitLibraryFunction() { - return { - ngModule: LibraryModule, - providers: [], - }; - } - export class ImplicitClass { - static implicitInternalMethod() { - return { - ngModule: ImplicitInternalModule, - providers: [], - }; - } - static implicitExternalMethod() { - return { - ngModule: ExternalModule, - providers: [], - }; - } - static implicitLibraryMethod() { - return { - ngModule: LibraryModule, - providers: [], - }; - } - } - ` - }, - { - name: '/src/no-providers.js', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export class NoProvidersInternalModule {} - export function noProvExplicitInternalFunction() { - return {ngModule: NoProvidersInternalModule}; - } - export function noProvExplicitExternalFunction() { - return {ngModule: ExternalModule}; - } - export function noProvExplicitLibraryFunction() { - return {ngModule: LibraryModule}; - } - export function noProvAnyInternalFunction() { - return {ngModule: NoProvidersInternalModule}; - } - export function noProvAnyExternalFunction() { - return {ngModule: ExternalModule}; - } - export function noProvAnyLibraryFunction() { - return {ngModule: LibraryModule}; - } - export function noProvImplicitInternalFunction() { - return {ngModule: NoProvidersInternalModule}; - } - export function noProvImplicitExternalFunction() { - return {ngModule: ExternalModule}; - } - export function noProvImplicitLibraryFunction() { - return {ngModule: LibraryModule}; - } - ` - }, - { - name: '/src/module.js', - contents: ` - export class ExternalModule {} - ` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, -]; -const TEST_DTS_PROGRAM = [ - { - name: '/typings/entry-point.d.ts', - contents: ` - export * from './explicit'; - export * from './any'; - export * from './implicit'; - export * from './no-providers'; - export * from './module'; - ` - }, - { - name: '/typings/explicit.d.ts', - contents: ` - import {ModuleWithProviders} from './core'; - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export declare class ExplicitInternalModule {} - export declare function explicitInternalFunction(): ModuleWithProviders; - export declare function explicitExternalFunction(): ModuleWithProviders; - export declare function explicitLibraryFunction(): ModuleWithProviders; - export declare class ExplicitClass { - static explicitInternalMethod(): ModuleWithProviders; - static explicitExternalMethod(): ModuleWithProviders; - static explicitLibraryMethod(): ModuleWithProviders; - } - ` - }, - { - name: '/typings/any.d.ts', - contents: ` - import {ModuleWithProviders} from './core'; - export declare class AnyInternalModule {} - export declare function anyInternalFunction(): ModuleWithProviders; - export declare function anyExternalFunction(): ModuleWithProviders; - export declare function anyLibraryFunction(): ModuleWithProviders; - export declare class AnyClass { - static anyInternalMethod(): ModuleWithProviders; - static anyExternalMethod(): ModuleWithProviders; - static anyLibraryMethod(): ModuleWithProviders; - } - ` - }, - { - name: '/typings/implicit.d.ts', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export declare class ImplicitInternalModule {} - export declare function implicitInternalFunction(): { ngModule: typeof ImplicitInternalModule; providers: never[]; }; - export declare function implicitExternalFunction(): { ngModule: typeof ExternalModule; providers: never[]; }; - export declare function implicitLibraryFunction(): { ngModule: typeof LibraryModule; providers: never[]; }; - export declare class ImplicitClass { - static implicitInternalMethod(): { ngModule: typeof ImplicitInternalModule; providers: never[]; }; - static implicitExternalMethod(): { ngModule: typeof ExternalModule; providers: never[]; }; - static implicitLibraryMethod(): { ngModule: typeof LibraryModule; providers: never[]; }; - } - ` - }, - { - name: '/typings/no-providers.d.ts', - contents: ` - import {ModuleWithProviders} from './core'; - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export declare class NoProvidersInternalModule {} - export declare function noProvExplicitInternalFunction(): ModuleWithProviders; - export declare function noProvExplicitExternalFunction(): ModuleWithProviders; - export declare function noProvExplicitLibraryFunction(): ModuleWithProviders; - export declare function noProvAnyInternalFunction(): ModuleWithProviders; - export declare function noProvAnyExternalFunction(): ModuleWithProviders; - export declare function noProvAnyLibraryFunction(): ModuleWithProviders; - export declare function noProvImplicitInternalFunction(): { ngModule: typeof NoProvidersInternalModule; }; - export declare function noProvImplicitExternalFunction(): { ngModule: typeof ExternalModule; }; - export declare function noProvImplicitLibraryFunction(): { ngModule: typeof LibraryModule; }; - ` - }, - { - name: '/typings/module.d.ts', - contents: ` - export declare class ExternalModule {} - ` - }, - { - name: '/typings/core.d.ts', - contents: ` +runInEachFileSystem(() => { + describe('ModuleWithProvidersAnalyzer', () => { + describe('analyzeProgram()', () => { + let _: typeof absoluteFrom; + let analyses: ModuleWithProvidersAnalyses; + let program: ts.Program; + let dtsProgram: BundleProgram|null; + let referencesRegistry: NgccReferencesRegistry; - export declare interface Type { - new (...args: any[]): T - } - export declare type Provider = any; - export declare interface ModuleWithProviders { - ngModule: Type - providers?: Provider[] - } - ` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, -]; + beforeEach(() => { + _ = absoluteFrom; -describe('ModuleWithProvidersAnalyzer', () => { - describe('analyzeProgram()', () => { - let analyses: ModuleWithProvidersAnalyses; - let program: ts.Program; - let dtsProgram: BundleProgram; - let referencesRegistry: NgccReferencesRegistry; + const TEST_PROGRAM: TestFile[] = [ + { + name: _('/src/entry-point.js'), + contents: ` + export * from './explicit'; + export * from './any'; + export * from './implicit'; + export * from './no-providers'; + export * from './module'; + ` + }, + { + name: _('/src/explicit.js'), + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export class ExplicitInternalModule {} + export function explicitInternalFunction() { + return { + ngModule: ExplicitInternalModule, + providers: [] + }; + } + export function explicitExternalFunction() { + return { + ngModule: ExternalModule, + providers: [] + }; + } + export function explicitLibraryFunction() { + return { + ngModule: LibraryModule, + providers: [] + }; + } + export class ExplicitClass { + static explicitInternalMethod() { + return { + ngModule: ExplicitInternalModule, + providers: [] + }; + } + static explicitExternalMethod() { + return { + ngModule: ExternalModule, + providers: [] + }; + } + static explicitLibraryMethod() { + return { + ngModule: LibraryModule, + providers: [] + }; + } + } + ` + }, + { + name: _('/src/any.js'), + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export class AnyInternalModule {} + export function anyInternalFunction() { + return { + ngModule: AnyInternalModule, + providers: [] + }; + } + export function anyExternalFunction() { + return { + ngModule: ExternalModule, + providers: [] + }; + } + export function anyLibraryFunction() { + return { + ngModule: LibraryModule, + providers: [] + }; + } + export class AnyClass { + static anyInternalMethod() { + return { + ngModule: AnyInternalModule, + providers: [] + }; + } + static anyExternalMethod() { + return { + ngModule: ExternalModule, + providers: [] + }; + } + static anyLibraryMethod() { + return { + ngModule: LibraryModule, + providers: [] + }; + } + } + ` + }, + { + name: _('/src/implicit.js'), + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export class ImplicitInternalModule {} + export function implicitInternalFunction() { + return { + ngModule: ImplicitInternalModule, + providers: [], + }; + } + export function implicitExternalFunction() { + return { + ngModule: ExternalModule, + providers: [], + }; + } + export function implicitLibraryFunction() { + return { + ngModule: LibraryModule, + providers: [], + }; + } + export class ImplicitClass { + static implicitInternalMethod() { + return { + ngModule: ImplicitInternalModule, + providers: [], + }; + } + static implicitExternalMethod() { + return { + ngModule: ExternalModule, + providers: [], + }; + } + static implicitLibraryMethod() { + return { + ngModule: LibraryModule, + providers: [], + }; + } + } + ` + }, + { + name: _('/src/no-providers.js'), + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export class NoProvidersInternalModule {} + export function noProvExplicitInternalFunction() { + return {ngModule: NoProvidersInternalModule}; + } + export function noProvExplicitExternalFunction() { + return {ngModule: ExternalModule}; + } + export function noProvExplicitLibraryFunction() { + return {ngModule: LibraryModule}; + } + export function noProvAnyInternalFunction() { + return {ngModule: NoProvidersInternalModule}; + } + export function noProvAnyExternalFunction() { + return {ngModule: ExternalModule}; + } + export function noProvAnyLibraryFunction() { + return {ngModule: LibraryModule}; + } + export function noProvImplicitInternalFunction() { + return {ngModule: NoProvidersInternalModule}; + } + export function noProvImplicitExternalFunction() { + return {ngModule: ExternalModule}; + } + export function noProvImplicitLibraryFunction() { + return {ngModule: LibraryModule}; + } + ` + }, + { + name: _('/src/module.js'), + contents: ` + export class ExternalModule {} + ` + }, + { + name: _('/node_modules/some-library/index.d.ts'), + contents: 'export declare class LibraryModule {}' + }, + ]; + const TEST_DTS_PROGRAM: TestFile[] = [ + { + name: _('/typings/entry-point.d.ts'), + contents: ` + export * from './explicit'; + export * from './any'; + export * from './implicit'; + export * from './no-providers'; + export * from './module'; + ` + }, + { + name: _('/typings/explicit.d.ts'), + contents: ` + import {ModuleWithProviders} from './core'; + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export declare class ExplicitInternalModule {} + export declare function explicitInternalFunction(): ModuleWithProviders; + export declare function explicitExternalFunction(): ModuleWithProviders; + export declare function explicitLibraryFunction(): ModuleWithProviders; + export declare class ExplicitClass { + static explicitInternalMethod(): ModuleWithProviders; + static explicitExternalMethod(): ModuleWithProviders; + static explicitLibraryMethod(): ModuleWithProviders; + } + ` + }, + { + name: _('/typings/any.d.ts'), + contents: ` + import {ModuleWithProviders} from './core'; + export declare class AnyInternalModule {} + export declare function anyInternalFunction(): ModuleWithProviders; + export declare function anyExternalFunction(): ModuleWithProviders; + export declare function anyLibraryFunction(): ModuleWithProviders; + export declare class AnyClass { + static anyInternalMethod(): ModuleWithProviders; + static anyExternalMethod(): ModuleWithProviders; + static anyLibraryMethod(): ModuleWithProviders; + } + ` + }, + { + name: _('/typings/implicit.d.ts'), + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export declare class ImplicitInternalModule {} + export declare function implicitInternalFunction(): { ngModule: typeof ImplicitInternalModule; providers: never[]; }; + export declare function implicitExternalFunction(): { ngModule: typeof ExternalModule; providers: never[]; }; + export declare function implicitLibraryFunction(): { ngModule: typeof LibraryModule; providers: never[]; }; + export declare class ImplicitClass { + static implicitInternalMethod(): { ngModule: typeof ImplicitInternalModule; providers: never[]; }; + static implicitExternalMethod(): { ngModule: typeof ExternalModule; providers: never[]; }; + static implicitLibraryMethod(): { ngModule: typeof LibraryModule; providers: never[]; }; + } + ` + }, + { + name: _('/typings/no-providers.d.ts'), + contents: ` + import {ModuleWithProviders} from './core'; + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export declare class NoProvidersInternalModule {} + export declare function noProvExplicitInternalFunction(): ModuleWithProviders; + export declare function noProvExplicitExternalFunction(): ModuleWithProviders; + export declare function noProvExplicitLibraryFunction(): ModuleWithProviders; + export declare function noProvAnyInternalFunction(): ModuleWithProviders; + export declare function noProvAnyExternalFunction(): ModuleWithProviders; + export declare function noProvAnyLibraryFunction(): ModuleWithProviders; + export declare function noProvImplicitInternalFunction(): { ngModule: typeof NoProvidersInternalModule; }; + export declare function noProvImplicitExternalFunction(): { ngModule: typeof ExternalModule; }; + export declare function noProvImplicitLibraryFunction(): { ngModule: typeof LibraryModule; }; + ` + }, + { + name: _('/typings/module.d.ts'), + contents: ` + export declare class ExternalModule {} + ` + }, + { + name: _('/typings/core.d.ts'), + contents: ` - beforeAll(() => { - program = makeTestProgram(...TEST_PROGRAM); - dtsProgram = makeTestBundleProgram(TEST_DTS_PROGRAM); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dtsProgram); - referencesRegistry = new NgccReferencesRegistry(host); + export declare interface Type { + new (...args: any[]): T + } + export declare type Provider = any; + export declare interface ModuleWithProviders { + ngModule: Type + providers?: Provider[] + } + ` + }, + { + name: _('/node_modules/some-library/index.d.ts'), + contents: 'export declare class LibraryModule {}' + }, + ]; + loadTestFiles(TEST_PROGRAM); + loadTestFiles(TEST_DTS_PROGRAM); + const bundle = makeTestEntryPointBundle( + 'esm2015', 'esm2015', false, getRootFiles(TEST_PROGRAM), + getRootFiles(TEST_DTS_PROGRAM)); + program = bundle.src.program; + dtsProgram = bundle.dts; + const host = new Esm2015ReflectionHost( + new MockLogger(), false, program.getTypeChecker(), dtsProgram); + referencesRegistry = new NgccReferencesRegistry(host); - const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry); - analyses = analyzer.analyzeProgram(program); + const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry); + analyses = analyzer.analyzeProgram(program); + }); + + it('should ignore declarations that already have explicit NgModule type params', () => { + expect(getAnalysisDescription(analyses, _('/typings/explicit.d.ts'))).toEqual([]); + }); + + it('should find declarations that use `any` for the NgModule type param', () => { + const anyAnalysis = getAnalysisDescription(analyses, _('/typings/any.d.ts')); + expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]); + expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]); + expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']); + expect(anyAnalysis).toContain(['anyInternalMethod', 'AnyInternalModule', null]); + expect(anyAnalysis).toContain(['anyExternalMethod', 'ExternalModule', null]); + expect(anyAnalysis).toContain(['anyLibraryMethod', 'LibraryModule', 'some-library']); + }); + + it('should track internal module references in the references registry', () => { + const declarations = referencesRegistry.getDeclarationMap(); + const externalModuleDeclaration = getDeclaration( + program, absoluteFrom('/src/module.js'), 'ExternalModule', ts.isClassDeclaration); + const libraryModuleDeclaration = getDeclaration( + program, absoluteFrom('/node_modules/some-library/index.d.ts'), 'LibraryModule', + ts.isClassDeclaration); + expect(declarations.has(externalModuleDeclaration.name !)).toBe(true); + expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false); + }); + + it('should find declarations that have implicit return types', () => { + const anyAnalysis = getAnalysisDescription(analyses, _('/typings/implicit.d.ts')); + expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]); + expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]); + expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']); + expect(anyAnalysis).toContain(['implicitInternalMethod', 'ImplicitInternalModule', null]); + expect(anyAnalysis).toContain(['implicitExternalMethod', 'ExternalModule', null]); + expect(anyAnalysis).toContain(['implicitLibraryMethod', 'LibraryModule', 'some-library']); + }); + + it('should find declarations that do not specify a `providers` property in the return type', + () => { + const anyAnalysis = getAnalysisDescription(analyses, _('/typings/no-providers.d.ts')); + expect(anyAnalysis).not.toContain([ + 'noProvExplicitInternalFunction', 'NoProvidersInternalModule' + ]); + expect(anyAnalysis).not.toContain([ + 'noProvExplicitExternalFunction', 'ExternalModule', null + ]); + expect(anyAnalysis).toContain([ + 'noProvAnyInternalFunction', 'NoProvidersInternalModule', null + ]); + expect(anyAnalysis).toContain(['noProvAnyExternalFunction', 'ExternalModule', null]); + expect(anyAnalysis).toContain([ + 'noProvAnyLibraryFunction', 'LibraryModule', 'some-library' + ]); + expect(anyAnalysis).toContain([ + 'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null + ]); + expect(anyAnalysis).toContain([ + 'noProvImplicitExternalFunction', 'ExternalModule', null + ]); + expect(anyAnalysis).toContain([ + 'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library' + ]); + }); + + function getAnalysisDescription( + analyses: ModuleWithProvidersAnalyses, fileName: AbsoluteFsPath) { + const file = getSourceFileOrError(dtsProgram !.program, fileName); + const analysis = analyses.get(file); + return analysis ? + analysis.map( + info => + [info.declaration.name !.getText(), + (info.ngModule.node as ts.ClassDeclaration).name !.getText(), + info.ngModule.viaModule]) : + []; + } }); - - it('should ignore declarations that already have explicit NgModule type params', - () => { expect(getAnalysisDescription(analyses, '/typings/explicit.d.ts')).toEqual([]); }); - - it('should find declarations that use `any` for the NgModule type param', () => { - const anyAnalysis = getAnalysisDescription(analyses, '/typings/any.d.ts'); - expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]); - expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]); - expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']); - expect(anyAnalysis).toContain(['anyInternalMethod', 'AnyInternalModule', null]); - expect(anyAnalysis).toContain(['anyExternalMethod', 'ExternalModule', null]); - expect(anyAnalysis).toContain(['anyLibraryMethod', 'LibraryModule', 'some-library']); - }); - - it('should track internal module references in the references registry', () => { - const declarations = referencesRegistry.getDeclarationMap(); - const externalModuleDeclaration = - getDeclaration(program, '/src/module.js', 'ExternalModule', ts.isClassDeclaration); - const libraryModuleDeclaration = getDeclaration( - program, '/node_modules/some-library/index.d.ts', 'LibraryModule', ts.isClassDeclaration); - expect(declarations.has(externalModuleDeclaration.name !)).toBe(true); - expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false); - }); - - it('should find declarations that have implicit return types', () => { - const anyAnalysis = getAnalysisDescription(analyses, '/typings/implicit.d.ts'); - expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]); - expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]); - expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']); - expect(anyAnalysis).toContain(['implicitInternalMethod', 'ImplicitInternalModule', null]); - expect(anyAnalysis).toContain(['implicitExternalMethod', 'ExternalModule', null]); - expect(anyAnalysis).toContain(['implicitLibraryMethod', 'LibraryModule', 'some-library']); - }); - - it('should find declarations that do not specify a `providers` property in the return type', - () => { - const anyAnalysis = getAnalysisDescription(analyses, '/typings/no-providers.d.ts'); - expect(anyAnalysis).not.toContain([ - 'noProvExplicitInternalFunction', 'NoProvidersInternalModule' - ]); - expect(anyAnalysis).not.toContain([ - 'noProvExplicitExternalFunction', 'ExternalModule', null - ]); - expect(anyAnalysis).toContain([ - 'noProvAnyInternalFunction', 'NoProvidersInternalModule', null - ]); - expect(anyAnalysis).toContain(['noProvAnyExternalFunction', 'ExternalModule', null]); - expect(anyAnalysis).toContain([ - 'noProvAnyLibraryFunction', 'LibraryModule', 'some-library' - ]); - expect(anyAnalysis).toContain([ - 'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null - ]); - expect(anyAnalysis).toContain(['noProvImplicitExternalFunction', 'ExternalModule', null]); - expect(anyAnalysis).toContain([ - 'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library' - ]); - }); - - function getAnalysisDescription(analyses: ModuleWithProvidersAnalyses, fileName: string) { - const file = dtsProgram.program.getSourceFile(fileName) !; - const analysis = analyses.get(file); - return analysis ? - analysis.map( - info => - [info.declaration.name !.getText(), - (info.ngModule.node as ts.ClassDeclaration).name !.getText(), - info.ngModule.viaModule]) : - []; - } }); }); diff --git a/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts index e4bd4acb3b..4a408cf372 100644 --- a/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts @@ -5,241 +5,242 @@ * 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, absoluteFrom} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {Reference} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadTestFiles} from '../../../test/helpers/src/mock_file_loading'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; -import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; +import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils'; -const _ = AbsoluteFsPath.fromUnchecked; +runInEachFileSystem(() => { + describe('PrivateDeclarationsAnalyzer', () => { + describe('analyzeProgram()', () => { + it('should find all NgModule declarations that were not publicly exported from the entry-point', + () => { + const _ = absoluteFrom; -describe('PrivateDeclarationsAnalyzer', () => { - describe('analyzeProgram()', () => { - - const TEST_PROGRAM = [ - { - name: '/src/entry_point.js', - isRoot: true, - contents: ` - export {PublicComponent} from './a'; - export {ModuleA} from './mod'; - export {ModuleB} from './b'; - ` - }, - { - name: '/src/a.js', - isRoot: false, - contents: ` - import {Component} from '@angular/core'; - export class PublicComponent {} - PublicComponent.decorators = [ - {type: Component, args: [{selectors: 'a', template: ''}]} - ]; - ` - }, - { - name: '/src/b.js', - isRoot: false, - contents: ` - import {Component, NgModule} from '@angular/core'; - class PrivateComponent1 {} - PrivateComponent1.decorators = [ - {type: Component, args: [{selectors: 'b', template: ''}]} - ]; - class PrivateComponent2 {} - PrivateComponent2.decorators = [ - {type: Component, args: [{selectors: 'c', template: ''}]} - ]; - export class ModuleB {} - ModuleB.decorators = [ - {type: NgModule, args: [{declarations: [PrivateComponent1]}]} - ]; - ` - }, - { - name: '/src/c.js', - isRoot: false, - contents: ` - import {Component} from '@angular/core'; - export class InternalComponent1 {} - InternalComponent1.decorators = [ - {type: Component, args: [{selectors: 'd', template: ''}]} - ]; - export class InternalComponent2 {} - InternalComponent2.decorators = [ - {type: Component, args: [{selectors: 'e', template: ''}]} - ]; - ` - }, - { - name: '/src/mod.js', - isRoot: false, - contents: ` - import {Component, NgModule} from '@angular/core'; - import {PublicComponent} from './a'; - import {ModuleB} from './b'; - import {InternalComponent1} from './c'; - export class ModuleA {} - ModuleA.decorators = [ - {type: NgModule, args: [{ - declarations: [PublicComponent, InternalComponent1], - imports: [ModuleB] - }]} - ]; - ` - } - ]; - const TEST_DTS_PROGRAM = [ - { - name: '/typings/entry_point.d.ts', - isRoot: true, - contents: ` - export {PublicComponent} from './a'; - export {ModuleA} from './mod'; - export {ModuleB} from './b'; - ` - }, - { - name: '/typings/a.d.ts', - isRoot: false, - contents: ` - export declare class PublicComponent {} - ` - }, - { - name: '/typings/b.d.ts', - isRoot: false, - contents: ` - export declare class ModuleB {} - ` - }, - { - name: '/typings/c.d.ts', - isRoot: false, - contents: ` - export declare class InternalComponent1 {} - ` - }, - { - name: '/typings/mod.d.ts', - isRoot: false, - contents: ` - import {PublicComponent} from './a'; - import {ModuleB} from './b'; - import {InternalComponent1} from './c'; - export declare class ModuleA {} - ` - }, - ]; - - it('should find all NgModule declarations that were not publicly exported from the entry-point', - () => { - const {program, referencesRegistry, analyzer} = setup(TEST_PROGRAM, TEST_DTS_PROGRAM); - - addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'PublicComponent'); - addToReferencesRegistry(program, referencesRegistry, '/src/b.js', 'PrivateComponent1'); - addToReferencesRegistry(program, referencesRegistry, '/src/c.js', 'InternalComponent1'); - - const analyses = analyzer.analyzeProgram(program); - // Note that `PrivateComponent2` and `InternalComponent2` are not found because they are - // not added to the ReferencesRegistry (i.e. they were not declared in an NgModule). - expect(analyses.length).toEqual(2); - expect(analyses).toEqual([ - {identifier: 'PrivateComponent1', from: _('/src/b.js'), dtsFrom: null, alias: null}, - { - identifier: 'InternalComponent1', - from: _('/src/c.js'), - dtsFrom: _('/typings/c.d.ts'), - alias: null - }, - ]); - }); - - const ALIASED_EXPORTS_PROGRAM = [ - { - name: '/src/entry_point.js', - isRoot: true, - contents: ` - // This component is only exported as an alias. - export {ComponentOne as aliasedComponentOne} from './a'; - // This component is exported both as itself and an alias. - export {ComponentTwo as aliasedComponentTwo, ComponentTwo} from './a'; + const TEST_PROGRAM: TestFile[] = [ + { + name: _('/src/entry_point.js'), + isRoot: true, + contents: ` + export {PublicComponent} from './a'; + export {ModuleA} from './mod'; + export {ModuleB} from './b'; ` - }, - { - name: '/src/a.js', - isRoot: false, - contents: ` - import {Component} from '@angular/core'; - export class ComponentOne {} - ComponentOne.decorators = [ - {type: Component, args: [{selectors: 'a', template: ''}]} - ]; - - export class ComponentTwo {} - Component.decorators = [ - {type: Component, args: [{selectors: 'a', template: ''}]} - ]; - ` - } - ]; - const ALIASED_EXPORTS_DTS_PROGRAM = [ - { - name: '/typings/entry_point.d.ts', - isRoot: true, - contents: ` - export declare class aliasedComponentOne {} - export declare class ComponentTwo {} - export {ComponentTwo as aliasedComponentTwo} + }, + { + name: _('/src/a.js'), + isRoot: false, + contents: ` + import {Component} from '@angular/core'; + export class PublicComponent {} + PublicComponent.decorators = [ + {type: Component, args: [{selectors: 'a', template: ''}]} + ]; ` - }, - ]; + }, + { + name: _('/src/b.js'), + isRoot: false, + contents: ` + import {Component, NgModule} from '@angular/core'; + class PrivateComponent1 {} + PrivateComponent1.decorators = [ + {type: Component, args: [{selectors: 'b', template: ''}]} + ]; + class PrivateComponent2 {} + PrivateComponent2.decorators = [ + {type: Component, args: [{selectors: 'c', template: ''}]} + ]; + export class ModuleB {} + ModuleB.decorators = [ + {type: NgModule, args: [{declarations: [PrivateComponent1]}]} + ]; + ` + }, + { + name: _('/src/c.js'), + isRoot: false, + contents: ` + import {Component} from '@angular/core'; + export class InternalComponent1 {} + InternalComponent1.decorators = [ + {type: Component, args: [{selectors: 'd', template: ''}]} + ]; + export class InternalComponent2 {} + InternalComponent2.decorators = [ + {type: Component, args: [{selectors: 'e', template: ''}]} + ]; + ` + }, + { + name: _('/src/mod.js'), + isRoot: false, + contents: ` + import {Component, NgModule} from '@angular/core'; + import {PublicComponent} from './a'; + import {ModuleB} from './b'; + import {InternalComponent1} from './c'; + export class ModuleA {} + ModuleA.decorators = [ + {type: NgModule, args: [{ + declarations: [PublicComponent, InternalComponent1], + imports: [ModuleB] + }]} + ]; + ` + } + ]; + const TEST_DTS_PROGRAM = [ + { + name: _('/typings/entry_point.d.ts'), + isRoot: true, + contents: ` + export {PublicComponent} from './a'; + export {ModuleA} from './mod'; + export {ModuleB} from './b'; + ` + }, + { + name: _('/typings/a.d.ts'), + isRoot: false, + contents: ` + export declare class PublicComponent {} + ` + }, + { + name: _('/typings/b.d.ts'), + isRoot: false, + contents: ` + export declare class ModuleB {} + ` + }, + { + name: _('/typings/c.d.ts'), + isRoot: false, + contents: ` + export declare class InternalComponent1 {} + ` + }, + { + name: _('/typings/mod.d.ts'), + isRoot: false, + contents: ` + import {PublicComponent} from './a'; + import {ModuleB} from './b'; + import {InternalComponent1} from './c'; + export declare class ModuleA {} + ` + }, + ]; + const {program, referencesRegistry, analyzer} = setup(TEST_PROGRAM, TEST_DTS_PROGRAM); - it('should find all non-public declarations that were aliased', () => { - const {program, referencesRegistry, analyzer} = - setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM); + addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'PublicComponent'); + addToReferencesRegistry( + program, referencesRegistry, _('/src/b.js'), 'PrivateComponent1'); + addToReferencesRegistry( + program, referencesRegistry, _('/src/c.js'), 'InternalComponent1'); - addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentOne'); - addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentTwo'); + const analyses = analyzer.analyzeProgram(program); + // Note that `PrivateComponent2` and `InternalComponent2` are not found because they are + // not added to the ReferencesRegistry (i.e. they were not declared in an NgModule). + expect(analyses.length).toEqual(2); + expect(analyses).toEqual([ + {identifier: 'PrivateComponent1', from: _('/src/b.js'), dtsFrom: null, alias: null}, + { + identifier: 'InternalComponent1', + from: _('/src/c.js'), + dtsFrom: _('/typings/c.d.ts'), + alias: null + }, + ]); + }); - const analyses = analyzer.analyzeProgram(program); - expect(analyses).toEqual([{ - identifier: 'ComponentOne', - from: _('/src/a.js'), - dtsFrom: null, - alias: 'aliasedComponentOne', - }]); + it('should find all non-public declarations that were aliased', () => { + const _ = absoluteFrom; + const ALIASED_EXPORTS_PROGRAM = [ + { + name: _('/src/entry_point.js'), + isRoot: true, + contents: ` + // This component is only exported as an alias. + export {ComponentOne as aliasedComponentOne} from './a'; + // This component is exported both as itself and an alias. + export {ComponentTwo as aliasedComponentTwo, ComponentTwo} from './a'; + ` + }, + { + name: _('/src/a.js'), + isRoot: false, + contents: ` + import {Component} from '@angular/core'; + export class ComponentOne {} + ComponentOne.decorators = [ + {type: Component, args: [{selectors: 'a', template: ''}]} + ]; + + export class ComponentTwo {} + Component.decorators = [ + {type: Component, args: [{selectors: 'a', template: ''}]} + ]; + ` + } + ]; + const ALIASED_EXPORTS_DTS_PROGRAM = [ + { + name: _('/typings/entry_point.d.ts'), + isRoot: true, + contents: ` + export declare class aliasedComponentOne {} + export declare class ComponentTwo {} + export {ComponentTwo as aliasedComponentTwo} + ` + }, + ]; + const {program, referencesRegistry, analyzer} = + setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM); + + addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'ComponentOne'); + addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'ComponentTwo'); + + const analyses = analyzer.analyzeProgram(program); + expect(analyses).toEqual([{ + identifier: 'ComponentOne', + from: _('/src/a.js'), + dtsFrom: null, + alias: 'aliasedComponentOne', + }]); + }); }); }); + + function setup(jsProgram: TestFile[], dtsProgram: TestFile[]) { + loadTestFiles(jsProgram); + loadTestFiles(dtsProgram); + const {src: {program}, dts} = makeTestEntryPointBundle( + 'esm2015', 'esm2015', false, getRootFiles(jsProgram), getRootFiles(dtsProgram)); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); + const referencesRegistry = new NgccReferencesRegistry(host); + const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry); + return {program, referencesRegistry, analyzer}; + } + + /** + * Add up the named component to the references registry. + * + * This would normally be done by the decoration handlers in the `DecorationAnalyzer`. + */ + function addToReferencesRegistry( + program: ts.Program, registry: NgccReferencesRegistry, fileName: AbsoluteFsPath, + componentName: string) { + const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration); + registry.add(null !, new Reference(declaration)); + } }); - -type Files = { - name: string, - contents: string, isRoot?: boolean | undefined -}[]; - -function setup(jsProgram: Files, dtsProgram: Files) { - const program = makeTestProgram(...jsProgram); - const dts = makeTestBundleProgram(dtsProgram); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); - const referencesRegistry = new NgccReferencesRegistry(host); - const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry); - return {program, referencesRegistry, analyzer}; -} - -/** - * Add up the named component to the references registry. - * - * This would normally be done by the decoration handlers in the `DecorationAnalyzer`. - */ -function addToReferencesRegistry( - program: ts.Program, registry: NgccReferencesRegistry, fileName: string, - componentName: string) { - const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration); - registry.add(null !, new Reference(declaration)); -} diff --git a/packages/compiler-cli/ngcc/test/analysis/references_registry_spec.ts b/packages/compiler-cli/ngcc/test/analysis/references_registry_spec.ts index a6c92ce3d1..becddc8325 100644 --- a/packages/compiler-cli/ngcc/test/analysis/references_registry_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/references_registry_spec.ts @@ -5,51 +5,63 @@ * 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 {absoluteFrom} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {Reference} from '../../../src/ngtsc/imports'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; -import {getDeclaration, makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {makeTestBundleProgram} from '../helpers/utils'; -describe('NgccReferencesRegistry', () => { - it('should return a mapping from resolved reference identifiers to their declarations', () => { - const {program, options, host} = makeProgram([{ - name: 'index.ts', - contents: ` +runInEachFileSystem(() => { + describe('NgccReferencesRegistry', () => { + it('should return a mapping from resolved reference identifiers to their declarations', () => { + const _ = absoluteFrom; + const TEST_FILES: TestFile[] = [{ + name: _('/index.ts'), + contents: ` export class SomeClass {} export function someFunction() {} export const someVariable = 42; export const testArray = [SomeClass, someFunction, someVariable]; ` - }]); + }]; + loadTestFiles(TEST_FILES); + const {program} = makeTestBundleProgram(TEST_FILES[0].name); - const checker = program.getTypeChecker(); + const checker = program.getTypeChecker(); - const testArrayDeclaration = - getDeclaration(program, 'index.ts', 'testArray', ts.isVariableDeclaration); - const someClassDecl = getDeclaration(program, 'index.ts', 'SomeClass', ts.isClassDeclaration); - const someFunctionDecl = - getDeclaration(program, 'index.ts', 'someFunction', ts.isFunctionDeclaration); - const someVariableDecl = - getDeclaration(program, 'index.ts', 'someVariable', ts.isVariableDeclaration); - const testArrayExpression = testArrayDeclaration.initializer !; + const indexPath = _('/index.ts'); + const testArrayDeclaration = + getDeclaration(program, indexPath, 'testArray', ts.isVariableDeclaration); + const someClassDecl = getDeclaration(program, indexPath, 'SomeClass', ts.isClassDeclaration); + const someFunctionDecl = + getDeclaration(program, indexPath, 'someFunction', ts.isFunctionDeclaration); + const someVariableDecl = + getDeclaration(program, indexPath, 'someVariable', ts.isVariableDeclaration); + const testArrayExpression = testArrayDeclaration.initializer !; - const reflectionHost = new TypeScriptReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker); - const registry = new NgccReferencesRegistry(reflectionHost); + const reflectionHost = new TypeScriptReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker); + const registry = new NgccReferencesRegistry(reflectionHost); - const references = (evaluator.evaluate(testArrayExpression) as any[]) - .filter(ref => ref instanceof Reference) as Reference[]; - registry.add(null !, ...references); + const references = (evaluator.evaluate(testArrayExpression) as any[]).filter(isReference); + registry.add(null !, ...references); - const map = registry.getDeclarationMap(); - expect(map.size).toEqual(2); - expect(map.get(someClassDecl.name !) !.node).toBe(someClassDecl); - expect(map.get(someFunctionDecl.name !) !.node).toBe(someFunctionDecl); - expect(map.has(someVariableDecl.name as ts.Identifier)).toBe(false); + const map = registry.getDeclarationMap(); + expect(map.size).toEqual(2); + expect(map.get(someClassDecl.name !) !.node).toBe(someClassDecl); + expect(map.get(someFunctionDecl.name !) !.node).toBe(someFunctionDecl); + expect(map.has(someVariableDecl.name as ts.Identifier)).toBe(false); + }); }); + + function isReference(ref: any): ref is Reference { + return ref instanceof Reference; + } }); diff --git a/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts index f97ed131e2..dd60ee315f 100644 --- a/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts @@ -5,71 +5,77 @@ * 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 {absoluteFrom, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; -import {makeTestProgram} from '../helpers/utils'; +import {getRootFiles, makeTestBundleProgram} from '../helpers/utils'; -const TEST_PROGRAM = [ - { - name: 'entrypoint.js', - contents: ` - import {a} from './a'; - import {b} from './b'; - ` - }, - { - name: 'a.js', - contents: ` - import {c} from './c'; - export const a = 1; - ` - }, - { - name: 'b.js', - contents: ` - export const b = 42; - var factoryB = factory__PRE_R3__; - ` - }, - { - name: 'c.js', - contents: ` - export const c = 'So long, and thanks for all the fish!'; - var factoryC = factory__PRE_R3__; - var factoryD = factory__PRE_R3__; - ` - }, -]; +runInEachFileSystem(() => { + describe('SwitchMarkerAnalyzer', () => { + describe('analyzeProgram()', () => { + it('should check for switchable markers in all the files of the program', () => { + const _ = absoluteFrom; + const TEST_PROGRAM: TestFile[] = [ + { + name: _('/entrypoint.js'), + contents: ` + import {a} from './a'; + import {b} from './b'; + ` + }, + { + name: _('/a.js'), + contents: ` + import {c} from './c'; + export const a = 1; + ` + }, + { + name: _('/b.js'), + contents: ` + export const b = 42; + var factoryB = factory__PRE_R3__; + ` + }, + { + name: _('/c.js'), + contents: ` + export const c = 'So long, and thanks for all the fish!'; + var factoryC = factory__PRE_R3__; + var factoryD = factory__PRE_R3__; + ` + }, + ]; + loadTestFiles(TEST_PROGRAM); + const {program} = makeTestBundleProgram(getRootFiles(TEST_PROGRAM)[0]); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const analyzer = new SwitchMarkerAnalyzer(host); + const analysis = analyzer.analyzeProgram(program); -describe('SwitchMarkerAnalyzer', () => { - describe('analyzeProgram()', () => { - it('should check for switchable markers in all the files of the program', () => { - const program = makeTestProgram(...TEST_PROGRAM); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const analyzer = new SwitchMarkerAnalyzer(host); - const analysis = analyzer.analyzeProgram(program); + const entrypoint = getSourceFileOrError(program, _('/entrypoint.js')); + const a = getSourceFileOrError(program, _('/a.js')); + const b = getSourceFileOrError(program, _('/b.js')); + const c = getSourceFileOrError(program, _('/c.js')); - const entrypoint = program.getSourceFile('entrypoint.js') !; - const a = program.getSourceFile('a.js') !; - const b = program.getSourceFile('b.js') !; - const c = program.getSourceFile('c.js') !; + expect(analysis.size).toEqual(2); + expect(analysis.has(entrypoint)).toBe(false); + expect(analysis.has(a)).toBe(false); + expect(analysis.has(b)).toBe(true); + expect(analysis.get(b) !.sourceFile).toBe(b); + expect(analysis.get(b) !.declarations.map(decl => decl.getText())).toEqual([ + 'factoryB = factory__PRE_R3__' + ]); - expect(analysis.size).toEqual(2); - expect(analysis.has(entrypoint)).toBe(false); - expect(analysis.has(a)).toBe(false); - expect(analysis.has(b)).toBe(true); - expect(analysis.get(b) !.sourceFile).toBe(b); - expect(analysis.get(b) !.declarations.map(decl => decl.getText())).toEqual([ - 'factoryB = factory__PRE_R3__' - ]); - - expect(analysis.has(c)).toBe(true); - expect(analysis.get(c) !.sourceFile).toBe(c); - expect(analysis.get(c) !.declarations.map(decl => decl.getText())).toEqual([ - 'factoryC = factory__PRE_R3__', - 'factoryD = factory__PRE_R3__', - ]); + expect(analysis.has(c)).toBe(true); + expect(analysis.get(c) !.sourceFile).toBe(c); + expect(analysis.get(c) !.declarations.map(decl => decl.getText())).toEqual([ + 'factoryC = factory__PRE_R3__', + 'factoryD = factory__PRE_R3__', + ]); + }); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/dependencies/commonjs_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/commonjs_dependency_host_spec.ts index 951fec0ef6..6ca396c4cd 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/commonjs_dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/commonjs_dependency_host_spec.ts @@ -6,180 +6,216 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; - -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {CommonJsDependencyHost} from '../../src/dependencies/commonjs_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; -import {MockFileSystem} from '../helpers/mock_file_system'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { + describe('CommonJsDependencyHost', () => { + let _: typeof absoluteFrom; + let host: CommonJsDependencyHost; -describe('CommonJsDependencyHost', () => { - let host: CommonJsDependencyHost; - beforeEach(() => { - const fs = createMockFileSystem(); - host = new CommonJsDependencyHost(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(); + beforeEach(() => { + _ = absoluteFrom; + loadTestFiles([ + { + name: _('/no/imports/or/re-exports/index.js'), + contents: '// some text but no import-like statements' + }, + {name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/external/imports/index.js'), contents: commonJs(['lib_1', 'lib_1/sub_1'])}, + {name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/imports/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/re-exports/index.js'), + contents: commonJs(['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']) + }, + {name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/external/imports-missing/index.js'), contents: commonJs(['lib_1', 'missing'])}, + {name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/external/deep-import/index.js'), contents: commonJs(['lib_1/deep/import'])}, + {name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/deep-import/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/internal/outer/index.js'), contents: commonJs(['../inner'])}, + {name: _('/internal/outer/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/internal/inner/index.js'), contents: commonJs(['lib_1/sub_1'], ['X'])}, + { + name: _('/internal/circular_a/index.js'), + contents: commonJs(['../circular_b', 'lib_1/sub_1'], ['Y']) + }, + { + name: _('/internal/circular_b/index.js'), + contents: commonJs(['../circular_a', 'lib_1'], ['X']) + }, + {name: _('/internal/circular_a/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/internal/circular_a/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/re-directed/index.js'), contents: commonJs(['lib_1/sub_2'])}, + {name: _('/re-directed/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/re-directed/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/path-alias/index.js'), + contents: commonJs(['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']) + }, + {name: _('/path-alias/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/path-alias/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/node_modules/lib_1/index.d.ts'), contents: 'export declare class X {}'}, + { + name: _('/node_modules/lib_1/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/lib_1/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/node_modules/lib_1/deep/import/index.js'), + contents: 'export class DeepImport {}' + }, + {name: _('/node_modules/lib_1/sub_1/index.d.ts'), contents: 'export declare class Y {}'}, + { + name: _('/node_modules/lib_1/sub_1/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/lib_1/sub_1/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/node_modules/lib_1/sub_2.d.ts'), contents: `export * from './sub_2/sub_2';`}, + {name: _('/node_modules/lib_1/sub_2/sub_2.d.ts'), contents: `export declare class Z {}';`}, + { + name: _('/node_modules/lib_1/sub_2/package.json'), + contents: '{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}' + }, + {name: _('/node_modules/lib_1/sub_2/sub_2.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/dist/components/index.d.ts'), contents: `export declare class MyComponent {};`}, + { + name: _('/dist/components/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/dist/components/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/dist/shared/index.d.ts'), + contents: `import {X} from 'lib_1';\nexport declare class Service {}` + }, + { + name: _('/dist/shared/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/dist/shared/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/dist/lib/shared/test/index.d.ts'), contents: `export class TestHelper {}`}, + { + name: _('/dist/lib/shared/test/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/dist/lib/shared/test/index.metadata.json'), contents: 'MOCK METADATA'}, + ]); + const fs = getFileSystem(); + host = new CommonJsDependencyHost(fs, new ModuleResolver(fs)); }); - 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); - }); + 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 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 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 capture missing external imports', () => { - const {dependencies, missing, deepImports} = - host.findDependencies(_('/external/imports-missing/index.js')); + 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); + }); - expect(dependencies.size).toBe(1); - expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); - expect(missing.size).toBe(1); - expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); - expect(deepImports.size).toBe(0); - }); + it('should capture missing external imports', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/imports-missing/index.js')); - 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(1); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(missing.size).toBe(1); + expect(missing.has(relativeFrom('missing'))).toBe(true); + expect(deepImports.size).toBe(0); + }); - 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 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')); - it('should recurse into internal dependencies', () => { - const {dependencies, missing, deepImports} = - host.findDependencies(_('/internal/outer/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); + }); - 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 recurse into internal dependencies', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/internal/outer/index.js')); - 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); - }); + 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 support `paths` alias mappings when resolving modules', () => { - const fs = createMockFileSystem(); - host = new CommonJsDependencyHost(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); + 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 = getFileSystem(); + host = new CommonJsDependencyHost(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': commonJs(['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': - commonJs(['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': commonJs(['lib_1', 'missing']), - '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', - '/external/imports-missing/index.metadata.json': 'MOCK METADATA', - '/external/deep-import/index.js': commonJs(['lib_1/deep/import']), - '/external/deep-import/package.json': '{"esm2015": "./index.js"}', - '/external/deep-import/index.metadata.json': 'MOCK METADATA', - '/internal/outer/index.js': commonJs(['../inner']), - '/internal/outer/package.json': '{"esm2015": "./index.js"}', - '/internal/outer/index.metadata.json': 'MOCK METADATA', - '/internal/inner/index.js': commonJs(['lib_1/sub_1'], ['X']), - '/internal/circular_a/index.js': commonJs(['../circular_b', 'lib_1/sub_1'], ['Y']), - '/internal/circular_b/index.js': commonJs(['../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': commonJs(['lib_1/sub_2']), - '/re-directed/package.json': '{"esm2015": "./index.js"}', - '/re-directed/index.metadata.json': 'MOCK METADATA', - '/path-alias/index.js': - commonJs(['@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 commonJs(importPaths: string[], exportNames: string[] = []) { + const commonJsRequires = + importPaths + .map( + p => + `var ${p.replace('@angular/', '').replace(/\.?\.?\//g, '').replace(/@/,'')} = require('${p}');`) + .join('\n'); + const exportStatements = + exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n'); + return `${commonJsRequires} +${exportStatements}`; } }); - -function commonJs(importPaths: string[], exportNames: string[] = []) { - const commonJsRequires = - importPaths - .map( - p => - `var ${p.replace('@angular/', '').replace(/\.?\.?\//g, '').replace(/@/,'')} = require('${p}');`) - .join('\n'); - const exportStatements = - exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n'); - return `${commonJsRequires} -${exportStatements}`; -} 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 733e9ccccf..c8c31b3ff4 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -5,187 +5,207 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; 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'; -const _ = AbsoluteFsPath.from; +interface DepMap { + [path: string]: {resolved: string[], missing: string[]}; +} -describe('DependencyResolver', () => { - let host: EsmDependencyHost; - let resolver: DependencyResolver; - let fs: FileSystem; - let moduleResolver: ModuleResolver; - beforeEach(() => { - fs = new MockFileSystem(); - moduleResolver = new ModuleResolver(fs); - host = new EsmDependencyHost(fs, moduleResolver); - resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host}); - }); - describe('sortEntryPointsByDependency()', () => { - const first = { - path: _('/first'), - packageJson: {esm5: './index.js'}, - compiledByAngular: true - } as EntryPoint; - const second = { - path: _('/second'), - packageJson: {esm2015: './sub/index.js'}, - compiledByAngular: true - } as EntryPoint; - const third = { - path: _('/third'), - packageJson: {fesm5: './index.js'}, - compiledByAngular: true - } as EntryPoint; - const fourth = { - path: _('/fourth'), - packageJson: {fesm2015: './sub2/index.js'}, - compiledByAngular: true - } as EntryPoint; - const fifth = { - path: _('/fifth'), - packageJson: {module: './index.js'}, - compiledByAngular: true - } as EntryPoint; +runInEachFileSystem(() => { + describe('DependencyResolver', () => { + let _: typeof absoluteFrom; + let host: EsmDependencyHost; + let resolver: DependencyResolver; + let fs: FileSystem; + let moduleResolver: ModuleResolver; - const dependencies = { - [_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []}, - [_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []}, - [_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []}, - [_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []}, - [_('/fifth/index.js')]: {resolved: [], missing: []}, - }; - - it('should order the entry points by their dependency on each other', () => { - spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); - const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); - expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]); + beforeEach(() => { + _ = absoluteFrom; + fs = getFileSystem(); + moduleResolver = new ModuleResolver(fs); + host = new EsmDependencyHost(fs, moduleResolver); + resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host}); }); - it('should remove entry-points that have missing direct dependencies', () => { - spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ - [_('/first/index.js')]: {resolved: [], missing: ['/missing']}, - [_('/second/sub/index.js')]: {resolved: [], missing: []}, - })); - const result = resolver.sortEntryPointsByDependency([first, second]); - expect(result.entryPoints).toEqual([second]); - expect(result.invalidEntryPoints).toEqual([ - {entryPoint: first, missingDependencies: ['/missing']}, - ]); + describe('sortEntryPointsByDependency()', () => { + let first: EntryPoint; + let second: EntryPoint; + let third: EntryPoint; + let fourth: EntryPoint; + let fifth: EntryPoint; + let dependencies: DepMap; + + beforeEach(() => { + first = { + path: _('/first'), + packageJson: {esm5: './index.js'}, + compiledByAngular: true + } as EntryPoint; + second = { + path: _('/second'), + packageJson: {esm2015: './sub/index.js'}, + compiledByAngular: true + } as EntryPoint; + third = { + path: _('/third'), + packageJson: {fesm5: './index.js'}, + compiledByAngular: true + } as EntryPoint; + fourth = { + path: _('/fourth'), + packageJson: {fesm2015: './sub2/index.js'}, + compiledByAngular: true + } as EntryPoint; + fifth = { + path: _('/fifth'), + packageJson: {module: './index.js'}, + compiledByAngular: true + } as EntryPoint; + + dependencies = { + [_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []}, + [_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []}, + [_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []}, + [_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []}, + [_('/fifth/index.js')]: {resolved: [], missing: []}, + }; + }); + + it('should order the entry points by their dependency on each other', () => { + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); + expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]); + }); + + it('should remove entry-points that have missing direct dependencies', () => { + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ + [_('/first/index.js')]: {resolved: [], missing: ['/missing']}, + [_('/second/sub/index.js')]: {resolved: [], missing: []}, + })); + const result = resolver.sortEntryPointsByDependency([first, second]); + expect(result.entryPoints).toEqual([second]); + expect(result.invalidEntryPoints).toEqual([ + {entryPoint: first, missingDependencies: ['/missing']}, + ]); + }); + + it('should remove entry points that depended upon an invalid entry-point', () => { + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ + [_('/first/index.js')]: {resolved: [second.path, third.path], missing: []}, + [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, + [_('/third/index.js')]: {resolved: [], missing: []}, + })); + // Note that we will process `first` before `second`, which has the missing dependency. + const result = resolver.sortEntryPointsByDependency([first, second, third]); + expect(result.entryPoints).toEqual([third]); + expect(result.invalidEntryPoints).toEqual([ + {entryPoint: second, missingDependencies: ['/missing']}, + {entryPoint: first, missingDependencies: ['/missing']}, + ]); + }); + + it('should remove entry points that will depend upon an invalid entry-point', () => { + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ + [_('/first/index.js')]: {resolved: [second.path, third.path], missing: []}, + [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, + [_('/third/index.js')]: {resolved: [], missing: []}, + })); + // Note that we will process `first` after `second`, which has the missing dependency. + const result = resolver.sortEntryPointsByDependency([second, first, third]); + expect(result.entryPoints).toEqual([third]); + expect(result.invalidEntryPoints).toEqual([ + {entryPoint: second, missingDependencies: ['/missing']}, + {entryPoint: first, missingDependencies: [second.path]}, + ]); + }); + + 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 appropriate source code format in '/first' entry-point.`); + }); + + it('should error if there is no appropriate DependencyHost for the given formats', () => { + resolver = new DependencyResolver(fs, new MockLogger(), {esm2015: host}); + expect(() => resolver.sortEntryPointsByDependency([first])) + .toThrowError( + `Could not find a suitable format for computing dependencies of entry-point: '${first.path}'.`); + }); + + it('should capture any dependencies that were ignored', () => { + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); + expect(result.ignoredDependencies).toEqual([ + {entryPoint: first, dependencyPath: '/ignored-1'}, + {entryPoint: third, dependencyPath: '/ignored-2'}, + ]); + }); + + it('should only return dependencies of the target, if provided', () => { + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + const entryPoints = [fifth, first, fourth, second, third]; + let sorted: SortedEntryPointsInfo; + + sorted = resolver.sortEntryPointsByDependency(entryPoints, first); + expect(sorted.entryPoints).toEqual([fifth, fourth, third, second, first]); + sorted = resolver.sortEntryPointsByDependency(entryPoints, second); + expect(sorted.entryPoints).toEqual([fifth, fourth, third, second]); + sorted = resolver.sortEntryPointsByDependency(entryPoints, third); + expect(sorted.entryPoints).toEqual([fifth, fourth, third]); + sorted = resolver.sortEntryPointsByDependency(entryPoints, fourth); + expect(sorted.entryPoints).toEqual([fifth, fourth]); + sorted = resolver.sortEntryPointsByDependency(entryPoints, fifth); + 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(fs, 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(fs.resolve(first.path, 'index.js')); + expect(esm5Host.findDependencies) + .not.toHaveBeenCalledWith(fs.resolve(second.path, 'sub/index.js')); + expect(esm5Host.findDependencies).toHaveBeenCalledWith(fs.resolve(third.path, 'index.js')); + expect(esm5Host.findDependencies) + .not.toHaveBeenCalledWith(fs.resolve(fourth.path, 'sub2/index.js')); + expect(esm5Host.findDependencies).toHaveBeenCalledWith(fs.resolve(fifth.path, 'index.js')); + + expect(esm2015Host.findDependencies) + .not.toHaveBeenCalledWith(fs.resolve(first.path, 'index.js')); + expect(esm2015Host.findDependencies) + .toHaveBeenCalledWith(fs.resolve(second.path, 'sub/index.js')); + expect(esm2015Host.findDependencies) + .not.toHaveBeenCalledWith(fs.resolve(third.path, 'index.js')); + expect(esm2015Host.findDependencies) + .toHaveBeenCalledWith(fs.resolve(fourth.path, 'sub2/index.js')); + expect(esm2015Host.findDependencies) + .not.toHaveBeenCalledWith(fs.resolve(fifth.path, 'index.js')); + }); + + function createFakeComputeDependencies(deps: DepMap) { + return (entryPoint: string) => { + const dependencies = new Set(); + const missing = new Set(); + const deepImports = new Set(); + deps[entryPoint].resolved.forEach(dep => dependencies.add(dep)); + deps[entryPoint].missing.forEach(dep => missing.add(dep)); + return {dependencies, missing, deepImports}; + }; + } }); - - it('should remove entry points that depended upon an invalid entry-point', () => { - spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ - [_('/first/index.js')]: {resolved: [second.path, third.path], missing: []}, - [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, - [_('/third/index.js')]: {resolved: [], missing: []}, - })); - // Note that we will process `first` before `second`, which has the missing dependency. - const result = resolver.sortEntryPointsByDependency([first, second, third]); - expect(result.entryPoints).toEqual([third]); - expect(result.invalidEntryPoints).toEqual([ - {entryPoint: second, missingDependencies: ['/missing']}, - {entryPoint: first, missingDependencies: ['/missing']}, - ]); - }); - - it('should remove entry points that will depend upon an invalid entry-point', () => { - spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ - [_('/first/index.js')]: {resolved: [second.path, third.path], missing: []}, - [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, - [_('/third/index.js')]: {resolved: [], missing: []}, - })); - // Note that we will process `first` after `second`, which has the missing dependency. - const result = resolver.sortEntryPointsByDependency([second, first, third]); - expect(result.entryPoints).toEqual([third]); - expect(result.invalidEntryPoints).toEqual([ - {entryPoint: second, missingDependencies: ['/missing']}, - {entryPoint: first, missingDependencies: [second.path]}, - ]); - }); - - 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 appropriate source code format in '/first' entry-point.`); - }); - - it('should error if there is no appropriate DependencyHost for the given formats', () => { - resolver = new DependencyResolver(fs, new MockLogger(), {esm2015: host}); - expect(() => resolver.sortEntryPointsByDependency([first])) - .toThrowError( - `Could not find a suitable format for computing dependencies of entry-point: '${first.path}'.`); - }); - - it('should capture any dependencies that were ignored', () => { - spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); - const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); - expect(result.ignoredDependencies).toEqual([ - {entryPoint: first, dependencyPath: '/ignored-1'}, - {entryPoint: third, dependencyPath: '/ignored-2'}, - ]); - }); - - it('should only return dependencies of the target, if provided', () => { - spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); - const entryPoints = [fifth, first, fourth, second, third]; - let sorted: SortedEntryPointsInfo; - - sorted = resolver.sortEntryPointsByDependency(entryPoints, first); - expect(sorted.entryPoints).toEqual([fifth, fourth, third, second, first]); - sorted = resolver.sortEntryPointsByDependency(entryPoints, second); - expect(sorted.entryPoints).toEqual([fifth, fourth, third, second]); - sorted = resolver.sortEntryPointsByDependency(entryPoints, third); - expect(sorted.entryPoints).toEqual([fifth, fourth, third]); - sorted = resolver.sortEntryPointsByDependency(entryPoints, fourth); - expect(sorted.entryPoints).toEqual([fifth, fourth]); - sorted = resolver.sortEntryPointsByDependency(entryPoints, fifth); - 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(fs, 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.path}/index.js`); - expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${second.path}/sub/index.js`); - expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${third.path}/index.js`); - expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`); - expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${fifth.path}/index.js`); - - expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${first.path}/index.js`); - expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${second.path}/sub/index.js`); - expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${third.path}/index.js`); - expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`); - expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${fifth.path}/index.js`); - }); - - interface DepMap { - [path: string]: {resolved: string[], missing: string[]}; - } - - function createFakeComputeDependencies(deps: DepMap) { - return (entryPoint: string) => { - const dependencies = new Set(); - const missing = new Set(); - const deepImports = new Set(); - deps[entryPoint].resolved.forEach(dep => dependencies.add(dep)); - deps[entryPoint].missing.forEach(dep => missing.add(dep)); - return {dependencies, missing, deepImports}; - }; - } }); }); 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 6298ba3f2c..916d1411ad 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 @@ -7,222 +7,262 @@ */ import * as ts from 'typescript'; -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; -import {MockFileSystem} from '../helpers/mock_file_system'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { -describe('EsmDependencyHost', () => { - let host: EsmDependencyHost; - beforeEach(() => { - const fs = createMockFileSystem(); - host = new EsmDependencyHost(fs, new ModuleResolver(fs)); - }); - - describe('getDependencies()', () => { - it('should not generate a TS AST if the source does not contain any imports or re-exports', - () => { - 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); + describe('EsmDependencyHost', () => { + let _: typeof absoluteFrom; + let host: EsmDependencyHost; + beforeEach(() => { + _ = absoluteFrom; + setupMockFileSystem(); + const fs = getFileSystem(); + host = new EsmDependencyHost(fs, new ModuleResolver(fs)); }); - 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); + describe('getDependencies()', () => { + it('should not generate a TS AST if the source does not contain any imports or re-exports', + () => { + 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(relativeFrom('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 = getFileSystem(); + host = new EsmDependencyHost(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); + }); }); - 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(PathSegment.fromFsPath('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 EsmDependencyHost(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': `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`, - '/external/imports/package.json': '{"esm2015": "./index.js"}', - '/external/imports/index.metadata.json': 'MOCK METADATA', - '/external/re-exports/index.js': `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`, - '/external/re-exports/package.json': '{"esm2015": "./index.js"}', - '/external/re-exports/index.metadata.json': 'MOCK METADATA', - '/external/imports-missing/index.js': `import {X} from 'lib-1';\nimport {Y} from 'missing';`, - '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', - '/external/imports-missing/index.metadata.json': 'MOCK METADATA', - '/external/deep-import/index.js': `import {Y} from 'lib-1/deep/import';`, - '/external/deep-import/package.json': '{"esm2015": "./index.js"}', - '/external/deep-import/index.metadata.json': 'MOCK METADATA', - '/internal/outer/index.js': `import {X} from '../inner';`, - '/internal/outer/package.json': '{"esm2015": "./index.js"}', - '/internal/outer/index.metadata.json': 'MOCK METADATA', - '/internal/inner/index.js': `import {Y} from 'lib-1/sub-1'; export declare class X {}`, - '/internal/circular-a/index.js': - `import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`, - '/internal/circular-b/index.js': - `import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';`, - '/internal/circular-a/package.json': '{"esm2015": "./index.js"}', - '/internal/circular-a/index.metadata.json': 'MOCK METADATA', - '/re-directed/index.js': `import {Z} from 'lib-1/sub-2';`, - '/re-directed/package.json': '{"esm2015": "./index.js"}', - '/re-directed/index.metadata.json': 'MOCK METADATA', - '/path-alias/index.js': - `import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';`, - '/path-alias/package.json': '{"esm2015": "./index.js"}', - '/path-alias/index.metadata.json': 'MOCK METADATA', - '/node_modules/lib-1/index.js': 'export declare class X {}', - '/node_modules/lib-1/package.json': '{"esm2015": "./index.js"}', - '/node_modules/lib-1/index.metadata.json': 'MOCK METADATA', - '/node_modules/lib-1/deep/import/index.js': 'export declare class DeepImport {}', - '/node_modules/lib-1/sub-1/index.js': 'export declare class Y {}', - '/node_modules/lib-1/sub-1/package.json': '{"esm2015": "./index.js"}', - '/node_modules/lib-1/sub-1/index.metadata.json': 'MOCK METADATA', - '/node_modules/lib-1/sub-2.js': `export * from './sub-2/sub-2';`, - '/node_modules/lib-1/sub-2/sub-2.js': `export declare class Z {}';`, - '/node_modules/lib-1/sub-2/package.json': '{"esm2015": "./sub-2.js"}', - '/node_modules/lib-1/sub-2/sub-2.metadata.json': 'MOCK METADATA', - '/dist/components/index.js': `class MyComponent {};`, - '/dist/components/package.json': '{"esm2015": "./index.js"}', - '/dist/components/index.metadata.json': 'MOCK METADATA', - '/dist/shared/index.js': `import {X} from 'lib-1';\nexport class Service {}`, - '/dist/shared/package.json': '{"esm2015": "./index.js"}', - '/dist/shared/index.metadata.json': 'MOCK METADATA', - '/dist/lib/shared/test/index.js': `export class TestHelper {}`, - '/dist/lib/shared/test/package.json': '{"esm2015": "./index.js"}', - '/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', - }); - } - - describe('isStringImportOrReexport', () => { - it('should return true if the statement is an import', () => { - expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";'))) - .toBe(true); - expect(host.isStringImportOrReexport(createStatement('import * as X from "some/x";'))) - .toBe(true); - }); - - it('should return true if the statement is a re-export', () => { - expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";'))) - .toBe(true); - expect(host.isStringImportOrReexport(createStatement('export * from "some/x";'))).toBe(true); - }); - - it('should return false if the statement is not an import or a re-export', () => { - expect(host.isStringImportOrReexport(createStatement('class X {}'))).toBe(false); - expect(host.isStringImportOrReexport(createStatement('export function foo() {}'))) - .toBe(false); - expect(host.isStringImportOrReexport(createStatement('export const X = 10;'))).toBe(false); - }); - - function createStatement(source: string) { - return ts - .createSourceFile('source.js', source, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS) - .statements[0]; + function setupMockFileSystem(): void { + loadTestFiles([ + { + name: _('/no/imports/or/re-exports/index.js'), + contents: '// some text but no import-like statements' + }, + {name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/imports/index.js'), + contents: `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';` + }, + {name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/imports/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/re-exports/index.js'), + contents: `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';` + }, + {name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/imports-missing/index.js'), + contents: `import {X} from 'lib-1';\nimport {Y} from 'missing';` + }, + {name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/deep-import/index.js'), + contents: `import {Y} from 'lib-1/deep/import';` + }, + {name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/deep-import/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/internal/outer/index.js'), contents: `import {X} from '../inner';`}, + {name: _('/internal/outer/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/internal/inner/index.js'), + contents: `import {Y} from 'lib-1/sub-1'; export declare class X {}` + }, + { + name: _('/internal/circular-a/index.js'), + contents: + `import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';` + }, + { + name: _('/internal/circular-b/index.js'), + contents: + `import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';` + }, + {name: _('/internal/circular-a/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/internal/circular-a/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/re-directed/index.js'), contents: `import {Z} from 'lib-1/sub-2';`}, + {name: _('/re-directed/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/re-directed/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/path-alias/index.js'), + contents: + `import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';` + }, + {name: _('/path-alias/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/path-alias/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/node_modules/lib-1/index.js'), contents: 'export declare class X {}'}, + {name: _('/node_modules/lib-1/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/node_modules/lib-1/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/node_modules/lib-1/deep/import/index.js'), + contents: 'export declare class DeepImport {}' + }, + {name: _('/node_modules/lib-1/sub-1/index.js'), contents: 'export declare class Y {}'}, + {name: _('/node_modules/lib-1/sub-1/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/node_modules/lib-1/sub-1/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/node_modules/lib-1/sub-2.js'), contents: `export * from './sub-2/sub-2';`}, + {name: _('/node_modules/lib-1/sub-2/sub-2.js'), contents: `export declare class Z {}';`}, + {name: _('/node_modules/lib-1/sub-2/package.json'), contents: '{"esm2015": "./sub-2.js"}'}, + {name: _('/node_modules/lib-1/sub-2/sub-2.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/dist/components/index.js'), contents: `class MyComponent {};`}, + {name: _('/dist/components/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/dist/components/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/dist/shared/index.js'), + contents: `import {X} from 'lib-1';\nexport class Service {}` + }, + {name: _('/dist/shared/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/dist/shared/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/dist/lib/shared/test/index.js'), contents: `export class TestHelper {}`}, + {name: _('/dist/lib/shared/test/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/dist/lib/shared/test/index.metadata.json'), contents: 'MOCK METADATA'}, + ]); } - }); - describe('hasImportOrReexportStatements', () => { - it('should return true if there is an import statement', () => { - expect(host.hasImportOrReexportStatements('import {X} from "some/x";')).toBe(true); - expect(host.hasImportOrReexportStatements('import * as X from "some/x";')).toBe(true); - expect( - host.hasImportOrReexportStatements('blah blah\n\n import {X} from "some/x";\nblah blah')) - .toBe(true); - expect(host.hasImportOrReexportStatements('\t\timport {X} from "some/x";')).toBe(true); + describe('isStringImportOrReexport', () => { + it('should return true if the statement is an import', () => { + expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";'))) + .toBe(true); + expect(host.isStringImportOrReexport(createStatement('import * as X from "some/x";'))) + .toBe(true); + }); + + it('should return true if the statement is a re-export', () => { + expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";'))) + .toBe(true); + expect(host.isStringImportOrReexport(createStatement('export * from "some/x";'))) + .toBe(true); + }); + + it('should return false if the statement is not an import or a re-export', () => { + expect(host.isStringImportOrReexport(createStatement('class X {}'))).toBe(false); + expect(host.isStringImportOrReexport(createStatement('export function foo() {}'))) + .toBe(false); + expect(host.isStringImportOrReexport(createStatement('export const X = 10;'))).toBe(false); + }); + + function createStatement(source: string) { + return ts + .createSourceFile('source.js', source, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS) + .statements[0]; + } }); - it('should return true if there is a re-export statement', () => { - expect(host.hasImportOrReexportStatements('export {X} from "some/x";')).toBe(true); - expect( - host.hasImportOrReexportStatements('blah blah\n\n export {X} from "some/x";\nblah blah')) - .toBe(true); - expect(host.hasImportOrReexportStatements('\t\texport {X} from "some/x";')).toBe(true); - expect(host.hasImportOrReexportStatements( - 'blah blah\n\n export * from "@angular/core;\nblah blah')) - .toBe(true); - }); - it('should return false if there is no import nor re-export statement', () => { - expect(host.hasImportOrReexportStatements('blah blah')).toBe(false); - expect(host.hasImportOrReexportStatements('export function moo() {}')).toBe(false); - expect( - host.hasImportOrReexportStatements('Some text that happens to include the word import')) - .toBe(false); + + describe('hasImportOrReexportStatements', () => { + it('should return true if there is an import statement', () => { + expect(host.hasImportOrReexportStatements('import {X} from "some/x";')).toBe(true); + expect(host.hasImportOrReexportStatements('import * as X from "some/x";')).toBe(true); + expect(host.hasImportOrReexportStatements( + 'blah blah\n\n import {X} from "some/x";\nblah blah')) + .toBe(true); + expect(host.hasImportOrReexportStatements('\t\timport {X} from "some/x";')).toBe(true); + }); + it('should return true if there is a re-export statement', () => { + expect(host.hasImportOrReexportStatements('export {X} from "some/x";')).toBe(true); + expect(host.hasImportOrReexportStatements( + 'blah blah\n\n export {X} from "some/x";\nblah blah')) + .toBe(true); + expect(host.hasImportOrReexportStatements('\t\texport {X} from "some/x";')).toBe(true); + expect(host.hasImportOrReexportStatements( + 'blah blah\n\n export * from "@angular/core;\nblah blah')) + .toBe(true); + }); + it('should return false if there is no import nor re-export statement', () => { + expect(host.hasImportOrReexportStatements('blah blah')).toBe(false); + expect(host.hasImportOrReexportStatements('export function moo() {}')).toBe(false); + expect( + host.hasImportOrReexportStatements('Some text that happens to include the word import')) + .toBe(false); + }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts index 245a191f0a..452b2a75a4 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts @@ -5,202 +5,200 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {ModuleResolver, ResolvedDeepImport, ResolvedExternalModule, ResolvedRelativeModule} from '../../src/dependencies/module_resolver'; -import {MockFileSystem} from '../helpers/mock_file_system'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { + describe('ModuleResolver', () => { + let _: typeof absoluteFrom; -function createMockFileSystem() { - return new MockFileSystem({ - '/libs': { - 'local-package': { - 'package.json': 'PACKAGE.JSON for local-package', - 'index.js': `import {X} from './x';`, - 'x.js': `export class X {}`, - 'sub-folder': { - 'index.js': `import {X} from '../x';`, + beforeEach(() => { + _ = absoluteFrom; + loadTestFiles([ + {name: _('/libs/local-package/package.json'), contents: 'PACKAGE.JSON for local-package'}, + {name: _('/libs/local-package/index.js'), contents: `import {X} from './x';`}, + {name: _('/libs/local-package/x.js'), contents: `export class X {}`}, + {name: _('/libs/local-package/sub-folder/index.js'), contents: `import {X} from '../x';`}, + { + name: _('/libs/local-package/node_modules/package-1/sub-folder/index.js'), + contents: `export class Z {}` }, - 'node_modules': { - 'package-1': { - 'sub-folder': {'index.js': `export class Z {}`}, - 'package.json': 'PACKAGE.JSON for package-1', - }, + { + name: _('/libs/local-package/node_modules/package-1/package.json'), + contents: 'PACKAGE.JSON for package-1' }, - }, - 'node_modules': { - 'package-2': { - 'package.json': 'PACKAGE.JSON for package-2', - 'node_modules': { - 'package-3': { - 'package.json': 'PACKAGE.JSON for package-3', - }, - }, + { + name: _('/libs/node_modules/package-2/package.json'), + contents: 'PACKAGE.JSON for package-2' }, - }, - }, - '/dist': { - 'package-4': { - 'x.js': `export class X {}`, - 'package.json': 'PACKAGE.JSON for package-4', - 'sub-folder': {'index.js': `import {X} from '@shared/package-4/x';`}, - }, - 'sub-folder': { - 'package-4': { - 'package.json': 'PACKAGE.JSON for package-4', + { + name: _('/libs/node_modules/package-2/node_modules/package-3/package.json'), + contents: 'PACKAGE.JSON for package-3' }, - 'package-5': { - 'package.json': 'PACKAGE.JSON for package-5', - 'post-fix': { - 'package.json': 'PACKAGE.JSON for package-5/post-fix', - } + {name: _('/dist/package-4/x.js'), contents: `export class X {}`}, + {name: _('/dist/package-4/package.json'), contents: 'PACKAGE.JSON for package-4'}, + { + name: _('/dist/package-4/sub-folder/index.js'), + contents: `import {X} from '@shared/package-4/x';` }, - } - }, - '/node_modules': { - 'top-package': { - 'package.json': 'PACKAGE.JSON for top-package', - } - } - }); -} - - -describe('ModuleResolver', () => { - describe('resolveModule()', () => { - describe('with relative paths', () => { - it('should resolve sibling, child and aunt modules', () => { - const resolver = new ModuleResolver(createMockFileSystem()); - expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); - expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedRelativeModule(_('/libs/local-package/sub-folder/index.js'))); - expect(resolver.resolveModuleImport('../x', _('/libs/local-package/sub-folder/index.js'))) - .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); - }); - - it('should return `null` if the resolved module relative module does not exist', () => { - const resolver = new ModuleResolver(createMockFileSystem()); - expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null); - }); + { + name: _('/dist/sub-folder/package-4/package.json'), + contents: 'PACKAGE.JSON for package-4' + }, + { + name: _('/dist/sub-folder/package-5/package.json'), + contents: 'PACKAGE.JSON for package-5' + }, + { + name: _('/dist/sub-folder/package-5/post-fix/package.json'), + contents: 'PACKAGE.JSON for package-5/post-fix' + }, + { + name: _('/node_modules/top-package/package.json'), + contents: 'PACKAGE.JSON for top-package' + }, + ]); }); - describe('with non-mapped external paths', () => { - it('should resolve to the package.json of a local node_modules package', () => { - const resolver = new ModuleResolver(createMockFileSystem()); - expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); - expect( - resolver.resolveModuleImport('package-1', _('/libs/local-package/sub-folder/index.js'))) - .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); - expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js'))) - .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); - }); - - it('should resolve to the package.json of a higher node_modules package', () => { - const resolver = new ModuleResolver(createMockFileSystem()); - expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2'))); - expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/node_modules/top-package'))); - }); - - it('should return `null` if the package cannot be found', () => { - const resolver = new ModuleResolver(createMockFileSystem()); - expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js'))) - .toBe(null); - }); - - it('should return `null` if the package is not accessible because it is in a inner node_modules package', - () => { - const resolver = new ModuleResolver(createMockFileSystem()); - expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js'))) - .toBe(null); - }); - - it('should identify deep imports into an external module', () => { - const resolver = new ModuleResolver(createMockFileSystem()); - expect( - resolver.resolveModuleImport('package-1/sub-folder', _('/libs/local-package/index.js'))) - .toEqual( - new ResolvedDeepImport(_('/libs/local-package/node_modules/package-1/sub-folder'))); - }); - }); - - describe('with mapped path external modules', () => { - it('should resolve to the package.json of simple mapped packages', () => { - const resolver = new ModuleResolver( - createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); - - expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); - - expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5'))); - }); - - it('should select the best match by the length of prefix before the *', () => { - const resolver = new ModuleResolver(createMockFileSystem(), { - baseUrl: '/dist', - paths: { - '@lib/*': ['*'], - '@lib/sub-folder/*': ['*'], - } + describe('resolveModule()', () => { + describe('with relative paths', () => { + it('should resolve sibling, child and aunt modules', () => { + const resolver = new ModuleResolver(getFileSystem()); + expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); + expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/libs/local-package/sub-folder/index.js'))); + expect(resolver.resolveModuleImport('../x', _('/libs/local-package/sub-folder/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); }); - // We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map to - // `*` and so the final resolved path will not include the `sub-folder` segment. - expect(resolver.resolveModuleImport( - '@lib/sub-folder/package-4', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); + it('should return `null` if the resolved module relative module does not exist', () => { + const resolver = new ModuleResolver(getFileSystem()); + expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null); + }); }); - it('should follow the ordering of `paths` when matching mapped packages', () => { - let resolver: ModuleResolver; + describe('with non-mapped external paths', () => { + it('should resolve to the package.json of a local node_modules package', () => { + const resolver = new ModuleResolver(getFileSystem()); + expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); + expect(resolver.resolveModuleImport( + 'package-1', _('/libs/local-package/sub-folder/index.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); + expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); + }); - const fs = createMockFileSystem(); - resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); - expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); + it('should resolve to the package.json of a higher node_modules package', () => { + const resolver = new ModuleResolver(getFileSystem()); + expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2'))); + expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/node_modules/top-package'))); + }); - resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}}); - expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); + it('should return `null` if the package cannot be found', () => { + const resolver = new ModuleResolver(getFileSystem()); + expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js'))) + .toBe(null); + }); + + it('should return `null` if the package is not accessible because it is in a inner node_modules package', + () => { + const resolver = new ModuleResolver(getFileSystem()); + expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js'))) + .toBe(null); + }); + + it('should identify deep imports into an external module', () => { + const resolver = new ModuleResolver(getFileSystem()); + expect(resolver.resolveModuleImport( + 'package-1/sub-folder', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedDeepImport( + _('/libs/local-package/node_modules/package-1/sub-folder'))); + }); }); - it('should resolve packages when the path mappings have post-fixes', () => { - const resolver = new ModuleResolver( - createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); - expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); - }); + describe('with mapped path external modules', () => { + it('should resolve to the package.json of simple mapped packages', () => { + const resolver = new ModuleResolver( + getFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); - it('should match paths against complex path matchers', () => { - const resolver = new ModuleResolver( - createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); - expect(resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); - expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) - .toBe(null); - }); + expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); - it('should resolve path as "relative" if the mapped path is inside the current package', - () => { - const resolver = new ModuleResolver( - createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['*']}}); - expect(resolver.resolveModuleImport( - '@shared/package-4/x', _('/dist/package-4/sub-folder/index.js'))) - .toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js'))); - }); + expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5'))); + }); - it('should resolve paths where the wildcard matches more than one path segment', () => { - const resolver = new ModuleResolver( - createMockFileSystem(), - {baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); - expect( - resolver.resolveModuleImport( - '@shared/sub-folder/package-5/post-fix', _('/dist/package-4/sub-folder/index.js'))) - .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); + it('should select the best match by the length of prefix before the *', () => { + const resolver = new ModuleResolver(getFileSystem(), { + baseUrl: '/dist', + paths: { + '@lib/*': ['*'], + '@lib/sub-folder/*': ['*'], + } + }); + + // We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map + // to `*` and so the final resolved path will not include the `sub-folder` segment. + expect(resolver.resolveModuleImport( + '@lib/sub-folder/package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); + }); + + it('should follow the ordering of `paths` when matching mapped packages', () => { + let resolver: ModuleResolver; + + const fs = getFileSystem(); + resolver = + new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); + expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); + + resolver = + new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}}); + expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); + }); + + it('should resolve packages when the path mappings have post-fixes', () => { + const resolver = new ModuleResolver( + getFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); + expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); + }); + + it('should match paths against complex path matchers', () => { + const resolver = new ModuleResolver( + getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); + expect( + resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); + expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) + .toBe(null); + }); + + it('should resolve path as "relative" if the mapped path is inside the current package', + () => { + const resolver = new ModuleResolver( + getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['*']}}); + expect(resolver.resolveModuleImport( + '@shared/package-4/x', _('/dist/package-4/sub-folder/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js'))); + }); + + it('should resolve paths where the wildcard matches more than one path segment', () => { + const resolver = new ModuleResolver( + getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); + expect(resolver.resolveModuleImport( + '@shared/sub-folder/package-5/post-fix', + _('/dist/package-4/sub-folder/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); + }); }); }); }); 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 index 521af669b8..2dd04369e5 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts @@ -7,180 +7,231 @@ */ import * as ts from 'typescript'; -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; 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; +runInEachFileSystem(() => { + describe('UmdDependencyHost', () => { + let _: typeof absoluteFrom; + let host: UmdDependencyHost; -describe('UmdDependencyHost', () => { - let host: UmdDependencyHost; - beforeEach(() => { - const fs = createMockFileSystem(); - host = new UmdDependencyHost(fs, new ModuleResolver(fs)); + beforeEach(() => { + _ = absoluteFrom; + setupMockFileSystem(); + const fs = getFileSystem(); + 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(relativeFrom('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 = getFileSystem(); + 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 setupMockFileSystem(): void { + loadTestFiles([ + { + name: _('/no/imports/or/re-exports/index.js'), + contents: '// some text but no import-like statements' + }, + {name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/imports/index.js'), + contents: umd('imports_index', ['lib_1', 'lib_1/sub_1']) + }, + {name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/imports/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/re-exports/index.js'), + contents: umd('imports_index', ['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']) + }, + {name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/imports-missing/index.js'), + contents: umd('imports_missing', ['lib_1', 'missing']) + }, + {name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/external/deep-import/index.js'), + contents: umd('deep_import', ['lib_1/deep/import']) + }, + {name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/external/deep-import/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/internal/outer/index.js'), contents: umd('outer', ['../inner'])}, + {name: _('/internal/outer/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/internal/inner/index.js'), contents: umd('inner', ['lib_1/sub_1'], ['X'])}, + { + name: _('/internal/circular_a/index.js'), + contents: umd('circular_a', ['../circular_b', 'lib_1/sub_1'], ['Y']) + }, + { + name: _('/internal/circular_b/index.js'), + contents: umd('circular_b', ['../circular_a', 'lib_1'], ['X']) + }, + {name: _('/internal/circular_a/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/internal/circular_a/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/re-directed/index.js'), contents: umd('re_directed', ['lib_1/sub_2'])}, + {name: _('/re-directed/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/re-directed/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/path-alias/index.js'), + contents: + umd('path_alias', ['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']) + }, + {name: _('/path-alias/package.json'), contents: '{"esm2015": "./index.js"}'}, + {name: _('/path-alias/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/node_modules/lib_1/index.d.ts'), contents: 'export declare class X {}'}, + { + name: _('/node_modules/lib_1/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/lib_1/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/node_modules/lib_1/deep/import/index.js'), + contents: 'export class DeepImport {}' + }, + {name: _('/node_modules/lib_1/sub_1/index.d.ts'), contents: 'export declare class Y {}'}, + { + name: _('/node_modules/lib_1/sub_1/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/lib_1/sub_1/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/node_modules/lib_1/sub_2.d.ts'), contents: `export * from './sub_2/sub_2';`}, + {name: _('/node_modules/lib_1/sub_2/sub_2.d.ts'), contents: `export declare class Z {}';`}, + { + name: _('/node_modules/lib_1/sub_2/package.json'), + contents: '{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}' + }, + {name: _('/node_modules/lib_1/sub_2/sub_2.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/dist/components/index.d.ts'), contents: `export declare class MyComponent {};`}, + { + name: _('/dist/components/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/dist/components/index.metadata.json'), contents: 'MOCK METADATA'}, + { + name: _('/dist/shared/index.d.ts'), + contents: `import {X} from 'lib_1';\nexport declare class Service {}` + }, + { + name: _('/dist/shared/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/dist/shared/index.metadata.json'), contents: 'MOCK METADATA'}, + {name: _('/dist/lib/shared/test/index.d.ts'), contents: `export class TestHelper {}`}, + { + name: _('/dist/lib/shared/test/package.json'), + contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/dist/lib/shared/test/index.metadata.json'), contents: 'MOCK METADATA'}, + ]); + } }); - 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(PathSegment.fromFsPath('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 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) : @@ -189,4 +240,5 @@ function umd(moduleName: string, importPaths: string[], exportNames: string[] = ${exportStatements} }))); `; -} + } +}); diff --git a/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel b/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel index cc237359fe..8eeb12d009 100644 --- a/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel @@ -10,8 +10,8 @@ ts_library( ]), deps = [ "//packages/compiler-cli/ngcc", - "//packages/compiler-cli/src/ngtsc/path", - "//packages/compiler-cli/src/ngtsc/testing", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts b/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts deleted file mode 100644 index 3e8fd219b4..0000000000 --- a/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts +++ /dev/null @@ -1,174 +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 {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; -import {FileStats, FileSystem} from '../../src/file_system/file_system'; - -/** - * An in-memory file system that can be used in unit tests. - */ -export class MockFileSystem implements FileSystem { - files: Folder = {}; - constructor(...folders: Folder[]) { - folders.forEach(files => this.processFiles(this.files, files, true)); - } - - exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path) !== null; } - - readFile(path: AbsoluteFsPath): string { - const file = this.findFromPath(path); - if (isFile(file)) { - return file; - } else { - throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); - } - } - - writeFile(path: AbsoluteFsPath, data: string): void { - const [folderPath, basename] = this.splitIntoFolderAndFile(path); - const folder = this.findFromPath(folderPath); - if (!isFolder(folder)) { - throw new MockFileSystemError( - 'ENOENT', path, `Unable to write file "${path}". The containing folder does not exist.`); - } - folder[basename] = data; - } - - readdir(path: AbsoluteFsPath): PathSegment[] { - const folder = this.findFromPath(path); - if (folder === null) { - throw new MockFileSystemError( - 'ENOENT', path, `Unable to read directory "${path}". It does not exist.`); - } - if (isFile(folder)) { - throw new MockFileSystemError( - 'ENOTDIR', path, `Unable to read directory "${path}". It is a file.`); - } - return Object.keys(folder) as PathSegment[]; - } - - lstat(path: AbsoluteFsPath): FileStats { - const fileOrFolder = this.findFromPath(path); - if (fileOrFolder === null) { - throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); - } - return new MockFileStats(fileOrFolder); - } - - stat(path: AbsoluteFsPath): FileStats { - const fileOrFolder = this.findFromPath(path, {followSymLinks: true}); - if (fileOrFolder === null) { - throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); - } - return new MockFileStats(fileOrFolder); - } - - pwd(): AbsoluteFsPath { return AbsoluteFsPath.from('/'); } - - copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { - this.writeFile(to, this.readFile(from)); - } - - moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { - this.writeFile(to, this.readFile(from)); - const folder = this.findFromPath(AbsoluteFsPath.dirname(from)) as Folder; - const basename = PathSegment.basename(from); - delete folder[basename]; - } - - ensureDir(path: AbsoluteFsPath): void { this.ensureFolders(this.files, path.split('/')); } - - private processFiles(current: Folder, files: Folder, isRootPath = false): void { - Object.keys(files).forEach(path => { - const pathResolved = isRootPath ? AbsoluteFsPath.from(path) : path; - const segments = pathResolved.split('/'); - const lastSegment = segments.pop() !; - const containingFolder = this.ensureFolders(current, segments); - const entity = files[path]; - if (isFolder(entity)) { - const processedFolder = containingFolder[lastSegment] = {} as Folder; - this.processFiles(processedFolder, entity); - } else { - containingFolder[lastSegment] = entity; - } - }); - } - - private ensureFolders(current: Folder, segments: string[]): Folder { - for (const segment of segments) { - if (isFile(current[segment])) { - throw new Error(`Folder already exists as a file.`); - } - if (!current[segment]) { - current[segment] = {}; - } - current = current[segment] as Folder; - } - return current; - } - - private findFromPath(path: AbsoluteFsPath, options?: {followSymLinks: boolean}): Entity|null { - const followSymLinks = !!options && options.followSymLinks; - const segments = path.split('/'); - let current = this.files; - while (segments.length) { - const next: Entity = current[segments.shift() !]; - if (next === undefined) { - return null; - } - if (segments.length > 0 && (!isFolder(next))) { - return null; - } - if (isFile(next)) { - return next; - } - if (isSymLink(next)) { - return followSymLinks ? - this.findFromPath(AbsoluteFsPath.resolve(next.path, ...segments), {followSymLinks}) : - next; - } - current = next; - } - return current || null; - } - - private splitIntoFolderAndFile(path: AbsoluteFsPath): [AbsoluteFsPath, string] { - const segments = path.split('/'); - const file = segments.pop() !; - return [AbsoluteFsPath.fromUnchecked(segments.join('/')), file]; - } -} - -export type Entity = Folder | File | SymLink; -export interface Folder { [pathSegments: string]: Entity; } -export type File = string; -export class SymLink { - constructor(public path: AbsoluteFsPath) {} -} - -class MockFileStats implements FileStats { - constructor(private entity: Entity) {} - isFile(): boolean { return isFile(this.entity); } - isDirectory(): boolean { return isFolder(this.entity); } - isSymbolicLink(): boolean { return isSymLink(this.entity); } -} - -class MockFileSystemError extends Error { - constructor(public code: string, public path: string, message: string) { super(message); } -} - -function isFile(item: Entity | null): item is File { - return typeof item === 'string'; -} - -function isSymLink(item: Entity | null): item is SymLink { - return item instanceof SymLink; -} - -function isFolder(item: Entity | null): item is Folder { - return item !== null && !isFile(item) && !isSymLink(item); -} diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index 35bdeba4fc..e025a43ad1 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -5,19 +5,13 @@ * 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 {makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript'; -import {BundleProgram} from '../../src/packages/bundle_program'; +import {AbsoluteFsPath, NgtscCompilerHost, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {TestFile} from '../../../src/ngtsc/file_system/testing'; +import {BundleProgram, makeBundleProgram} from '../../src/packages/bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; -import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from '../../src/packages/patch_ts_expando_initializer'; -import {Folder} from './mock_file_system'; +import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host'; -export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript'; - -const _ = AbsoluteFsPath.fromUnchecked; /** * * @param format The format of the bundle. @@ -26,86 +20,31 @@ const _ = AbsoluteFsPath.fromUnchecked; */ export function makeTestEntryPointBundle( formatProperty: EntryPointJsonProperty, format: EntryPointFormat, isCore: boolean, - files: {name: string, contents: string, isRoot?: boolean}[], - dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]): EntryPointBundle { - const src = makeTestBundleProgram(files); - const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null; + srcRootNames: AbsoluteFsPath[], dtsRootNames?: AbsoluteFsPath[]): EntryPointBundle { + const src = makeTestBundleProgram(srcRootNames[0], isCore); + const dts = dtsRootNames ? makeTestDtsBundleProgram(dtsRootNames[0], isCore) : null; const isFlatCore = isCore && src.r3SymbolsFile === null; - return {formatProperty, format, rootDirs: [_('/')], src, dts, isCore, isFlatCore}; + return {formatProperty, format, rootDirs: [absoluteFrom('/')], src, dts, isCore, isFlatCore}; } -/** - * Create a bundle program for testing. - * @param files The source files of the bundle program. - */ -export function makeTestBundleProgram(files: {name: string, contents: string}[]): BundleProgram { - const {program, options, host} = makeTestProgramInternal(...files); - const path = _(files[0].name); - const file = program.getSourceFile(path) !; - const r3SymbolsInfo = files.find(file => file.name.indexOf('r3_symbols') !== -1) || null; - const r3SymbolsPath = r3SymbolsInfo && _(r3SymbolsInfo.name); - const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null; - return {program, options, host, path, file, r3SymbolsPath, r3SymbolsFile}; +export function makeTestBundleProgram( + path: AbsoluteFsPath, isCore: boolean = false): BundleProgram { + const fs = getFileSystem(); + const options = {allowJs: true, checkJs: false}; + const entryPointPath = fs.dirname(path); + const host = new NgccSourcesCompilerHost(fs, options, entryPointPath); + return makeBundleProgram(fs, isCore, path, 'r3_symbols.js', options, host); } -function makeTestProgramInternal( - ...files: {name: string, contents: string, isRoot?: boolean | undefined}[]): { - program: ts.Program, - host: ts.CompilerHost, - options: ts.CompilerOptions, -} { - const originalTsGetExpandoInitializer = patchTsGetExpandoInitializer(); - const program = - makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false}); - restoreGetExpandoInitializer(originalTsGetExpandoInitializer); - return program; +export function makeTestDtsBundleProgram( + path: AbsoluteFsPath, isCore: boolean = false): BundleProgram { + const fs = getFileSystem(); + const options = {}; + const host = new NgtscCompilerHost(fs, options); + return makeBundleProgram(fs, isCore, path, 'r3_symbols.d.ts', options, host); } -export function makeTestProgram( - ...files: {name: string, contents: string, isRoot?: boolean | undefined}[]): ts.Program { - return makeTestProgramInternal(...files).program; -} - -// TODO: unify this with the //packages/compiler-cli/test/ngtsc/fake_core package -export function getFakeCore() { - return { - name: 'node_modules/@angular/core/index.d.ts', - contents: ` - type FnWithArg = (arg?: any) => T; - - 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 declare const Input: any; - - 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 declare class InjectionToken { - constructor(name: string); - } - - export declare interface ModuleWithProviders {} - ` - }; -} - -export function getFakeTslib() { - return { - name: 'node_modules/tslib/index.d.ts', - contents: ` - 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); - ` - }; -} - -export function convertToDirectTsLibImport(filesystem: {name: string, contents: string}[]) { +export function convertToDirectTsLibImport(filesystem: TestFile[]) { return filesystem.map(file => { const contents = file.contents @@ -117,10 +56,6 @@ export function convertToDirectTsLibImport(filesystem: {name: string, contents: }); } -export function createFileSystemFromProgramFiles( - ...fileCollections: ({name: string, contents: string}[] | undefined)[]): Folder { - const folder: Folder = {}; - fileCollections.forEach( - files => files && files.forEach(file => folder[file.name] = file.contents)); - return folder; +export function getRootFiles(testFiles: TestFile[]): AbsoluteFsPath[] { + return testFiles.filter(f => f.isRoot !== false).map(f => absoluteFrom(f.name)); } diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index ca9b492323..05804114b2 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -5,21 +5,51 @@ * 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 {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {getIifeBody} from '../../src/host/esm5_host'; import {MockLogger} from '../helpers/mock_logger'; -import {getDeclaration, makeTestBundleProgram} from '../helpers/utils'; +import {getRootFiles, makeTestBundleProgram, makeTestDtsBundleProgram} from '../helpers/utils'; import {expectTypeValueReferencesForParameters} from './util'; -const SOME_DIRECTIVE_FILE = { - name: '/some_directive.cjs.js', - contents: ` +runInEachFileSystem(() => { + describe('CommonJsReflectionHost', () => { + + let _: typeof absoluteFrom; + + let SOME_DIRECTIVE_FILE: TestFile; + let TOPLEVEL_DECORATORS_FILE: TestFile; + let SIMPLE_ES2015_CLASS_FILE: TestFile; + let SIMPLE_CLASS_FILE: TestFile; + let FOO_FUNCTION_FILE: TestFile; + let INVALID_DECORATORS_FILE: TestFile; + let INVALID_DECORATOR_ARGS_FILE: TestFile; + let INVALID_PROP_DECORATORS_FILE: TestFile; + let INVALID_PROP_DECORATOR_ARGS_FILE: TestFile; + let INVALID_CTOR_DECORATORS_FILE: TestFile; + let INVALID_CTOR_DECORATOR_ARGS_FILE: TestFile; + let IMPORTS_FILES: TestFile[]; + let EXPORTS_FILES: TestFile[]; + let FUNCTION_BODY_FILE: TestFile; + let DECORATED_FILES: TestFile[]; + let TYPINGS_SRC_FILES: TestFile[]; + let TYPINGS_DTS_FILES: TestFile[]; + let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[]; + + beforeEach(() => { + _ = absoluteFrom; + + SOME_DIRECTIVE_FILE = { + name: _('/some_directive.cjs.js'), + contents: ` var core = require('@angular/core'); var INJECTED_TOKEN = new InjectionToken('injected'); @@ -51,11 +81,11 @@ var SomeDirective = (function() { }()); exports.SomeDirective = SomeDirective; ` -}; + }; -const TOPLEVEL_DECORATORS_FILE = { - name: '/toplevel_decorators.cjs.js', - contents: ` + TOPLEVEL_DECORATORS_FILE = { + name: _('/toplevel_decorators.cjs.js'), + contents: ` var core = require('@angular/core'); var INJECTED_TOKEN = new InjectionToken('injected'); @@ -80,18 +110,18 @@ SomeDirective.propDecorators = { }; exports.SomeDirective = SomeDirective; ` -}; + }; -const SIMPLE_ES2015_CLASS_FILE = { - name: '/simple_es2015_class.d.ts', - contents: ` + SIMPLE_ES2015_CLASS_FILE = { + name: _('/simple_es2015_class.d.ts'), + contents: ` export class EmptyClass {} `, -}; + }; -const SIMPLE_CLASS_FILE = { - name: '/simple_class.js', - contents: ` + SIMPLE_CLASS_FILE = { + name: _('/simple_class.js'), + contents: ` var EmptyClass = (function() { function EmptyClass() { } @@ -105,11 +135,11 @@ var NoDecoratorConstructorClass = (function() { exports.EmptyClass = EmptyClass; exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass; `, -}; + }; -const FOO_FUNCTION_FILE = { - name: '/foo_function.js', - contents: ` + FOO_FUNCTION_FILE = { + name: _('/foo_function.js'), + contents: ` var core = require('@angular/core'); function foo() {} foo.decorators = [ @@ -117,11 +147,11 @@ foo.decorators = [ ]; exports.foo = foo; `, -}; + }; -const INVALID_DECORATORS_FILE = { - name: '/invalid_decorators.js', - contents: ` + INVALID_DECORATORS_FILE = { + name: _('/invalid_decorators.js'), + contents: ` var core = require('@angular/core'); var NotArrayLiteral = (function() { function NotArrayLiteral() { @@ -162,11 +192,11 @@ var NotIdentifier = (function() { return NotIdentifier; }()); `, -}; + }; -const INVALID_DECORATOR_ARGS_FILE = { - name: '/invalid_decorator_args.js', - contents: ` + INVALID_DECORATOR_ARGS_FILE = { + name: _('/invalid_decorator_args.js'), + contents: ` var core = require('@angular/core'); var NoArgsProperty = (function() { function NoArgsProperty() { @@ -196,11 +226,11 @@ var NotArrayLiteral = (function() { return NotArrayLiteral; }()); `, -}; + }; -const INVALID_PROP_DECORATORS_FILE = { - name: '/invalid_prop_decorators.js', - contents: ` + INVALID_PROP_DECORATORS_FILE = { + name: _('/invalid_prop_decorators.js'), + contents: ` var core = require('@angular/core'); var NotObjectLiteral = (function() { function NotObjectLiteral() { @@ -247,11 +277,11 @@ var NotIdentifier = (function() { return NotIdentifier; }()); `, -}; + }; -const INVALID_PROP_DECORATOR_ARGS_FILE = { - name: '/invalid_prop_decorator_args.js', - contents: ` + INVALID_PROP_DECORATOR_ARGS_FILE = { + name: _('/invalid_prop_decorator_args.js'), + contents: ` var core = require('@angular/core'); var NoArgsProperty = (function() { function NoArgsProperty() { @@ -281,11 +311,11 @@ var NotArrayLiteral = (function() { return NotArrayLiteral; }()); `, -}; + }; -const INVALID_CTOR_DECORATORS_FILE = { - name: '/invalid_ctor_decorators.js', - contents: ` + INVALID_CTOR_DECORATORS_FILE = { + name: _('/invalid_ctor_decorators.js'), + contents: ` var core = require('@angular/core'); var NoParameters = (function() { function NoParameters() {} @@ -348,11 +378,11 @@ var NotIdentifier = (function() { return NotIdentifier; }()); `, -}; + }; -const INVALID_CTOR_DECORATOR_ARGS_FILE = { - name: '/invalid_ctor_decorator_args.js', - contents: ` + INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: _('/invalid_ctor_decorator_args.js'), + contents: ` var core = require('@angular/core'); var NoArgsProperty = (function() { function NoArgsProperty(arg1) { @@ -382,47 +412,64 @@ var NotArrayLiteral = (function() { return NotArrayLiteral; }()); `, -}; + }; -const IMPORTS_FILES = [ - { - name: '/file_a.js', - contents: ` + IMPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + var file_a = require('./file_a'); + var file_b = require('./file_b'); + var file_c = require('./file_c'); + ` + }, + { + name: _('/file_a.js'), + contents: ` var a = 'a'; exports.a = a; `, - }, - { - name: '/file_b.js', - contents: ` + }, + { + name: _('/file_b.js'), + contents: ` var file_a = require('./file_a'); var b = file_a.a; var c = 'c'; var d = c; `, - }, - { - name: '/file_c.js', - contents: ` + }, + { + name: _('/file_c.js'), + contents: ` var file_a = require('./file_a'); var c = file_a.a; `, - }, -]; + }, + ]; -const EXPORTS_FILES = [ - { - name: '/a_module.js', - contents: ` + EXPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + var a_module = require('./a_module'); + var b_module = require('./b_module'); + var xtra_module = require('./xtra_module'); + var wildcard_reexports = require('./wildcard_reexports'); + ` + }, + { + name: _('/a_module.js'), + contents: ` var a = 'a'; exports.a = a; `, - }, - { - name: '/b_module.js', - contents: ` + }, + { + name: _('/b_module.js'), + contents: ` var core = require('@angular/core'); -var a_module = require('/a_module'); +var a_module = require('./a_module'); var b = a_module.a; var e = 'e'; var SomeClass = (function() { @@ -439,31 +486,31 @@ exports.e = e; exports.DirectiveX = core.Directive; exports.SomeClass = SomeClass; `, - }, - { - name: '/xtra_module.js', - contents: ` + }, + { + name: _('/xtra_module.js'), + contents: ` var xtra1 = 'xtra1'; var xtra2 = 'xtra2'; exports.xtra1 = xtra1; exports.xtra2 = xtra2; `, - }, - { - name: '/wildcard_reexports.js', - contents: ` + }, + { + name: _('/wildcard_reexports.js'), + contents: ` function __export(m) { for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; } __export(require("./b_module")); __export(require("./xtra_module")); ` - }, -]; + }, + ]; -const FUNCTION_BODY_FILE = { - name: '/function_body.js', - contents: ` + FUNCTION_BODY_FILE = { + name: _('/function_body.js'), + contents: ` function foo(x) { return x; } @@ -496,12 +543,12 @@ function juu() { return x; } ` -}; + }; -const DECORATED_FILES = [ - { - name: '/primary.js', - contents: ` + DECORATED_FILES = [ + { + name: _('/primary.js'), + contents: ` var core = require('@angular/core'); var secondary = require('./secondary'); var A = (function() { @@ -528,10 +575,10 @@ exports.A = A; exports.x = x; exports.C = C; ` - }, - { - name: '/secondary.js', - contents: ` + }, + { + name: _('/secondary.js'), + contents: ` var core = require('@angular/core'); var D = (function() { function D() {} @@ -542,16 +589,19 @@ var D = (function() { }()); exports.D = D; ` - } -]; + } + ]; -const TYPINGS_SRC_FILES = [ - { - name: '/src/index.js', - contents: ` + TYPINGS_SRC_FILES = [ + { + name: _('/src/index.js'), + contents: ` var internal = require('./internal'); var class1 = require('./class1'); var class2 = require('./class2'); +var missing = require('./missing-class'); +var flatFile = require('./flat-file'); +var func1 = require('./func1'); function __export(m) { for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; } @@ -559,10 +609,10 @@ var InternalClass = internal.InternalClass; __export(class1); __export(class2); ` - }, - { - name: '/src/class1.js', - contents: ` + }, + { + name: _('/src/class1.js'), + contents: ` var Class1 = (function() { function Class1() {} return Class1; @@ -574,20 +624,20 @@ var MissingClass1 = (function() { exports.Class1 = Class1; exports.MissingClass1 = MissingClass1; ` - }, - { - name: '/src/class2.js', - contents: ` + }, + { + name: _('/src/class2.js'), + contents: ` var Class2 = (function() { function Class2() {} return Class2; }()); exports.Class2 = Class2; ` - }, - {name: '/src/func1.js', contents: 'function mooFn() {} export {mooFn}'}, { - name: '/src/internal.js', - contents: ` + }, + {name: _('/src/func1.js'), contents: 'function mooFn() {} export {mooFn}'}, { + name: _('/src/internal.js'), + contents: ` var InternalClass = (function() { function InternalClass() {} return InternalClass; @@ -599,20 +649,20 @@ var Class2 = (function() { exports.InternalClass =InternalClass; exports.Class2 = Class2; ` - }, - { - name: '/src/missing-class.js', - contents: ` + }, + { + name: _('/src/missing-class.js'), + contents: ` var MissingClass2 = (function() { function MissingClass2() {} return MissingClass2; }()); exports. MissingClass2 = MissingClass2; ` - }, - { - name: '/src/flat-file.js', - contents: ` + }, + { + name: _('/src/flat-file.js'), + contents: ` var Class1 = (function() { function Class1() {} return Class1; @@ -634,36 +684,48 @@ 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 {}`}, -]; + TYPINGS_DTS_FILES = [ + { + name: _('/typings/index.d.ts'), + contents: ` + import {InternalClass} from './internal'; + import {mooFn} from './func1'; + 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: ` + MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: _('/src/index.js'), + contents: ` + var functions = require('./functions'); + var methods = require('./methods'); + var aliased_class = require('./aliased_class'); + ` + }, + { + name: _('/src/functions.js'), + contents: ` var mod = require('./module'); var SomeService = (function() { function SomeService() {} @@ -703,10 +765,10 @@ exports.externalNgModule = externalNgModule; exports.SomeService = SomeService; exports.InternalModule = InternalModule; ` - }, - { - name: '/src/methods.js', - contents: ` + }, + { + name: _('/src/methods.js'), + contents: ` var mod = require('./module'); var SomeService = (function() { function SomeService() {} @@ -738,10 +800,10 @@ var InternalModule = (function() { exports.SomeService = SomeService; exports.InternalModule = InternalModule; ` - }, - { - name: '/src/aliased_class.js', - contents: ` + }, + { + name: _('/src/aliased_class.js'), + contents: ` var AliasedModule = (function() { function AliasedModule() {} AliasedModule_1 = AliasedModule; @@ -751,1113 +813,1241 @@ var AliasedModule = (function() { }()); exports.AliasedModule = AliasedModule; ` - }, - { - name: '/src/module.js', - contents: ` + }, + { + name: _('/src/module.js'), + contents: ` var ExternalModule = (function() { function ExternalModule() {} return ExternalModule; }()); exports.ExternalModule = ExternalModule; ` - }, -]; - - -describe('CommonJsReflectionHost', () => { - - describe('getDecoratorsOfDeclaration()', () => { - it('should find the decorators on a class', () => { - const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); - const host = new CommonJsReflectionHost(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 find the decorators on a class at the top level', () => { - const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 decorated members on a class at the top level', () => { - const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 find the decorated constructor parameters at the top level', () => { - const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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(CommonJsReflectionHost.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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const file = program.getSourceFile('/b_module.js') !; - 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() {\n function SomeClass() {}\n return SomeClass;\n}())`, - null - ], + describe('CommonJsReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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 handle wildcard re-exports of other modules', () => { - const {program, host: compilerHost} = makeTestBundleProgram(EXPORTS_FILES); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const file = program.getSourceFile('/wildcard_reexports.js') !; - 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>`, '/b_module'], - ['a', `a = 'a'`, '/b_module'], - ['b', `b = a_module.a`, '/b_module'], - ['c', `a = 'a'`, '/b_module'], - ['d', `b = a_module.a`, '/b_module'], - ['e', `e = 'e'`, '/b_module'], - ['DirectiveX', `Directive: FnWithArg<(clazz: any) => any>`, '/b_module'], - [ - 'SomeClass', - `SomeClass = (function() {\n function SomeClass() {}\n return SomeClass;\n}())`, - '/b_module' - ], - ['xtra1', `xtra1 = 'xtra1'`, '/xtra_module'], - ['xtra2', `xtra2 = 'xtra2'`, '/xtra_module'], + it('should find the decorators on a class at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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]\' }', ]); - }); - }); + }); - describe('getClassSymbol()', () => { - it('should return the class symbol for an ES2015 class', () => { - const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_ES2015_CLASS_FILE]); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const node = getDeclaration( - program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); - const classSymbol = host.getClassSymbol(node); + it('should return null if the symbol is not a class', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const decorators = host.getDecoratorsOfDeclaration(functionNode); + expect(decorators).toBe(null); + }); - expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); - }); + it('should return null if there are no decorators', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new CommonJsReflectionHost(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 return the class symbol for an ES5 class (outer variable declaration)', () => { - const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const node = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const classSymbol = host.getClassSymbol(node); + it('should ignore `decorators` if it is not an array literal', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); - expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); - }); + it('should ignore decorator elements that are not object literals', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; - it('should return the class symbol for an ES5 class (inner function declaration)', () => { - const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); - const host = new CommonJsReflectionHost(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(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); - expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(outerNode); - }); + it('should ignore decorator elements that have no `type` property', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; - 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const outerNode = getDeclaration( - program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); - expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); - }); + it('should ignore decorator elements whose `type` value is not an identifier', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; - it('should return undefined if node is not an ES5 class', () => { - const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const node = - getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); - const classSymbol = host.getClassSymbol(node); + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); - expect(classSymbol).toBeUndefined(); - }); - }); + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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) !; - describe('isClass()', () => { - let host: CommonJsReflectionHost; - let mockNode: ts.Node; - let getClassDeclarationSpy: jasmine.Spy; - let superGetClassDeclarationSpy: jasmine.Spy; + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); - beforeEach(() => { - const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); - host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - mockNode = {} as any; + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); - getClassDeclarationSpy = spyOn(CommonJsReflectionHost.prototype, 'getClassDeclaration'); - superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); - }); + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; - it('should return true if superclass returns true', () => { - superGetClassDeclarationSpy.and.returnValue(true); - getClassDeclarationSpy.and.callThrough(); + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - }); + it('should be an empty array if decorator\'s `args` has no property assignment', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; - it('should return true if it can find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(true); + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - }); + it('should be an empty array if `args` property value is not an array literal', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; - it('should return false if it cannot find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(false); + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); - expect(host.isClass(mockNode)).toBe(false); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - }); - }); + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); - describe('hasBaseClass()', () => { - function hasBaseClass(source: string) { - const file = { - name: '/synthesized_constructors.js', - contents: source, - }; + 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 {program, host: compilerHost} = makeTestBundleProgram([file]); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const classNode = getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); - return host.hasBaseClass(classNode); - } + 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 consider an IIFE with _super parameter as having a base class', () => { - const result = hasBaseClass(` + it('should find decorated members on a class at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new CommonJsReflectionHost(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', + () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = + new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = new CommonJsReflectionHost(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', + () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = + new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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 find the decorated constructor parameters at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new CommonJsReflectionHost(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', + () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = + new CommonJsReflectionHost(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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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(CommonJsReflectionHost.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', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = new CommonJsReflectionHost(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', + () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = + new CommonJsReflectionHost(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', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = new CommonJsReflectionHost(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', + () => { + loadTestFiles([FUNCTION_BODY_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FUNCTION_BODY_FILE.name); + const host = + new CommonJsReflectionHost(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', () => { + loadTestFiles(IMPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles(IMPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles(IMPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + const host = new CommonJsReflectionHost(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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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', () => { + loadFakeCore(getFileSystem()); + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new CommonJsReflectionHost(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 = + getSourceFileOrError(program, _('/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', () => { + loadFakeCore(getFileSystem()); + loadTestFiles(EXPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/b_module.js')); + 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() {\n function SomeClass() {}\n return SomeClass;\n}())`, + null + ], + ]); + }); + + it('should handle wildcard re-exports of other modules', () => { + loadFakeCore(getFileSystem()); + loadTestFiles(EXPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/wildcard_reexports.js')); + 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>`, _('/b_module')], + ['a', `a = 'a'`, _('/b_module')], + ['b', `b = a_module.a`, _('/b_module')], + ['c', `a = 'a'`, _('/b_module')], + ['d', `b = a_module.a`, _('/b_module')], + ['e', `e = 'e'`, _('/b_module')], + ['DirectiveX', `Directive: FnWithArg<(clazz: any) => any>`, _('/b_module')], + [ + 'SomeClass', + `SomeClass = (function() {\n function SomeClass() {}\n return SomeClass;\n}())`, + _('/b_module') + ], + ['xtra1', `xtra1 = 'xtra1'`, _('/xtra_module')], + ['xtra2', `xtra2 = 'xtra2'`, _('/xtra_module')], + ]); + }); + }); + + describe('getClassSymbol()', () => { + it('should return the class symbol for an ES2015 class', () => { + loadTestFiles([SIMPLE_ES2015_CLASS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name); + const host = new CommonJsReflectionHost(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)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new CommonJsReflectionHost(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)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new CommonJsReflectionHost(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', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + new CommonJsReflectionHost(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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new CommonJsReflectionHost(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: CommonJsReflectionHost; + let mockNode: ts.Node; + let getClassDeclarationSpy: jasmine.Spy; + let superGetClassDeclarationSpy: jasmine.Spy; + + beforeEach(() => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + mockNode = {} as any; + + getClassDeclarationSpy = spyOn(CommonJsReflectionHost.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, + }; + + loadTestFiles([file]); + const {program, host: compilerHost} = makeTestBundleProgram(file.name); + const host = new CommonJsReflectionHost(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); - }); + 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(` + 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); - }); + expect(result).toBe(true); + }); - it('should not consider an IIFE without parameter as having a base class', () => { - const result = hasBaseClass(` + 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); - }); - }); + expect(result).toBe(false); + }); + }); - describe('findClassSymbols()', () => { - it('should return an array of all classes in the given source file', () => { - const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; + describe('findClassSymbols()', () => { + it('should return an array of all classes in the given source file', () => { + loadTestFiles(DECORATED_FILES); + const {program, host: compilerHost} = + makeTestBundleProgram(getRootFiles(DECORATED_FILES)[0]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - expect(classSymbolsPrimary.length).toEqual(2); - expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B']); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + expect(classSymbolsPrimary.length).toEqual(2); + expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B']); - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - expect(classSymbolsSecondary.length).toEqual(1); - expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); - }); - }); + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + expect(classSymbolsSecondary.length).toEqual(1); + expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); + }); + }); - describe('getDecoratorsOfSymbol()', () => { - it('should return decorators of class symbol', () => { - const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES); - const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; + describe('getDecoratorsOfSymbol()', () => { + it('should return decorators of class symbol', () => { + loadTestFiles(DECORATED_FILES); + const {program, host: compilerHost} = + makeTestBundleProgram(getRootFiles(DECORATED_FILES)[0]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsPrimary.length).toEqual(2); - expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); - expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + const classDecoratorsPrimary = + classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsPrimary.length).toEqual(2); + expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); + expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - const classDecoratorsSecondary = - classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsSecondary.length).toEqual(1); - expect(classDecoratorsSecondary[0] !.map(d => d.name)).toEqual(['Directive']); - }); - }); + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + const classDecoratorsSecondary = + classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsSecondary.length).toEqual(1); + expect(classDecoratorsSecondary[0] !.map(d => d.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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + describe('getDtsDeclarationsOfClass()', () => { + it('should find the dts declaration that has the same relative path to the source file', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const class1 = + getDeclaration(program, _('/src/class1.js'), 'Class1', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); - const dtsDeclaration = host.getDtsDeclaration(class1); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); - }); + 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dtsProgram); + it('should find the dts declaration for exported functions', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const mooFn = + getDeclaration(program, _('/src/func1.js'), 'mooFn', ts.isFunctionDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + const dtsDeclaration = host.getDtsDeclaration(mooFn); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/func1.d.ts')); + }); - 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', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const missingClass = getDeclaration( + program, _('/src/class1.js'), 'MissingClass1', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); - 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); - expect(host.getDtsDeclaration(missingClass)).toBe(null); - }); + it('should return null if there is no matching dts file', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const missingClass = getDeclaration( + program, _('/src/missing-class.js'), 'MissingClass2', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); - 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); - 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const class1 = getDeclaration( + program, _('/src/flat-file.js'), 'Class1', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); - 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/class1.d.ts')); + }); - const dtsDeclaration = host.getDtsDeclaration(class1); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); - }); + it('should find aliased exports', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const class3 = + getDeclaration(program, _('/src/flat-file.js'), 'Class3', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); - 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + const dtsDeclaration = host.getDtsDeclaration(class3); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/class3.d.ts')); + }); - 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const internalClass = getDeclaration( + program, _('/src/internal.js'), 'InternalClass', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); - 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + const dtsDeclaration = host.getDtsDeclaration(internalClass); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/internal.d.ts')); + }); - 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestDtsBundleProgram(_('/typings/index.d.ts')); + const class2 = + getDeclaration(program, _('/src/class2.js'), 'Class2', ts.isVariableDeclaration); + const internalClass2 = + getDeclaration(program, _('/src/internal.js'), 'Class2', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); - 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 CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + const class2DtsDeclaration = host.getDtsDeclaration(class2); + expect(class2DtsDeclaration !.getSourceFile().fileName) + .toEqual(_('/typings/class2.d.ts')); - 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')); + }); + }); - 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', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/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'], + ]); + }); - 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 CommonJsReflectionHost(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', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/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: mod.ExternalModule }; }', + '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 CommonJsReflectionHost(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: mod.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 CommonJsReflectionHost(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'], - ]); + // https://github.com/angular/angular/issues/29078 + it('should resolve aliased module references to their original declaration', () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/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'], + ]); + }); + }); }); }); }); 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 44d6315a52..e97748a82d 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 @@ -8,17 +8,28 @@ import * as ts from 'typescript'; +import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {ClassMemberKind, Import, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; -import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils'; +import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils'; import {expectTypeValueReferencesForParameters} from './util'; -const FILES = [ - { - name: '/some_directive.js', - contents: ` +runInEachFileSystem(() => { + describe('Fesm2015ReflectionHost [import helper style]', () => { + let _: typeof absoluteFrom; + let FILES: {[label: string]: TestFile[]}; + + beforeEach(() => { + _ = absoluteFrom; + const NAMESPACED_IMPORT_FILES = [ + { + name: _('/some_directive.js'), + contents: ` import * as tslib_1 from 'tslib'; import { Directive, Inject, InjectionToken, Input } from '@angular/core'; const INJECTED_TOKEN = new InjectionToken('injected'); @@ -52,10 +63,10 @@ const FILES = [ ], SomeDirective); export { SomeDirective }; `, - }, - { - name: '/node_modules/@angular/core/some_directive.js', - contents: ` + }, + { + name: _('/node_modules/@angular/core/some_directive.js'), + contents: ` import * as tslib_1 from 'tslib'; import { Directive, Input } from './directives'; let SomeDirective = class SomeDirective { @@ -70,10 +81,10 @@ const FILES = [ ], SomeDirective); export { SomeDirective }; `, - }, - { - name: 'ngmodule.js', - contents: ` + }, + { + name: _('/ngmodule.js'), + contents: ` import * as tslib_1 from 'tslib'; import { NgModule } from './directives'; var HttpClientXsrfModule_1; @@ -96,311 +107,340 @@ const FILES = [ nonDecoratedVar = 43; export { HttpClientXsrfModule }; ` - }, -]; + }, + ]; -describe('Fesm2015ReflectionHost [import helper style]', () => { - [{files: FILES, label: 'namespaced'}, - {files: convertToDirectTsLibImport(FILES), label: 'direct import'}, - ].forEach(fileSystem => { - describe(`[${fileSystem.label}]`, () => { + const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES); - describe('getDecoratorsOfDeclaration()', () => { - it('should find the decorators on a class', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; + FILES = { + 'namespaced': NAMESPACED_IMPORT_FILES, + 'direct import': DIRECT_IMPORT_FILES, + }; + }); - 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]\' }', - ]); + ['namespaced', 'direct import'].forEach(label => { + describe(`[${label}]`, () => { + beforeEach(() => { + const fs = getFileSystem(); + loadFakeCore(fs); + loadTestFiles(FILES[label]); }); - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.callFake( - (identifier: ts.Identifier) => identifier.getText() === 'Directive' ? - {from: '@angular/core', name: 'Directive'} : - {}); - - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toEqual(1); - expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'}); - - const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); - expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy(); - }); - - it('should support decorators being used inside @angular/core', () => { - const program = makeTestProgram(fileSystem.files[1]); - const host = new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/node_modules/@angular/core/some_directive.js', '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: './directives'}); - expect(decorator.args !.map(arg => arg.getText())).toEqual([ - '{ selector: \'[someDirective]\' }', - ]); - }); - }); - - describe('getMembersOfClass()', () => { - it('should find decorated members on a class', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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 = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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 = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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.isMethodDeclaration(staticMethod.implementation !)).toEqual(true); - }); - - it('should find static properties on a class', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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 find static properties on a class that has an intermediate variable assignment', - () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/ngmodule.js', 'HttpClientXsrfModule', 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 use `getImportOfIdentifier()` to retrieve import info', () => { - const spy = - spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); - - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - - host.getMembersOfClass(classNode); - const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); - expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy(); - }); - - it('should support decorators being used inside @angular/core', () => { - const program = makeTestProgram(fileSystem.files[1]); - const host = new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/node_modules/@angular/core/some_directive.js', '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']); - }); - }); - - describe('getConstructorParameters', () => { - it('should find the decorated constructor parameters', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters).toBeDefined(); - expect(parameters !.map(parameter => parameter.name)).toEqual([ - '_viewContainer', '_template', 'injected' - ]); - expectTypeValueReferencesForParameters(parameters !, [ - 'ViewContainerRef', - 'TemplateRef', - 'String', - ]); - }); - - describe('(returned parameters `decorators`)', () => { - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const mockImportInfo = {} as Import; - const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeTestProgram(fileSystem.files[0]); + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![2].decorators !; + program, _('/some_directive.js'), '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 use `getImportOfIdentifier()` to retrieve import info', () => { + const spy = + spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.callFake( + (identifier: ts.Identifier) => identifier.getText() === 'Directive' ? + {from: '@angular/core', name: 'Directive'} : + {}); + + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + + const decorators = host.getDecoratorsOfDeclaration(classNode) !; expect(decorators.length).toEqual(1); - expect(decorators[0].import).toBe(mockImportInfo); + expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'}); - const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; - expect(typeIdentifier.text).toBe('Inject'); + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const {program} = + makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/node_modules/@angular/core/some_directive.js'), '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: './directives'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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.isMethodDeclaration(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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 find static properties on a class that has an intermediate variable assignment', + () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/ngmodule.js'), 'HttpClientXsrfModule', 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 use `getImportOfIdentifier()` to retrieve import info', () => { + const spy = + spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); + + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + + host.getMembersOfClass(classNode); + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const {program} = + makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/node_modules/@angular/core/some_directive.js'), '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']); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + 'String', + ]); + }); + + describe('(returned parameters `decorators`)', () => { + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + 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('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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.js'), 'ViewContainerRef', ts.isClassDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + + it('should return the declaration of an externally defined identifier', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const decoratorNode = classDecorators[0].node; + const identifierOfDirective = + ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ? + decoratorNode.expression : + null; + + const expectedDeclarationNode = getDeclaration( + 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); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + }); + + describe('getVariableValue', () => { + it('should find the "actual" declaration of an aliased variable identifier', () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const ngModuleRef = findVariableDeclaration( + getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1'); + + const value = host.getVariableValue(ngModuleRef !); + expect(value).not.toBe(null); + if (!value || !ts.isClassExpression(value)) { + throw new Error( + `Expected value to be a class expression: ${value && value.getText()}.`); + } + expect(value.name !.text).toBe('HttpClientXsrfModule'); + }); + + it('should return null if the variable has no assignment', () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const missingValue = findVariableDeclaration( + getSourceFileOrError(program, _('/ngmodule.js')), 'missingValue'); + const value = host.getVariableValue(missingValue !); + expect(value).toBe(null); + }); + + it('should return null if the variable is not assigned from a call to __decorate', () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const nonDecoratedVar = findVariableDeclaration( + getSourceFileOrError(program, _('/ngmodule.js')), 'nonDecoratedVar'); + const value = host.getVariableValue(nonDecoratedVar !); + expect(value).toBe(null); }); }); }); - - describe('getDeclarationOfIdentifier', () => { - it('should return the declaration of a locally defined identifier', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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.js', 'ViewContainerRef', ts.isClassDeclaration); - const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); - expect(actualDeclaration).not.toBe(null); - expect(actualDeclaration !.node).toBe(expectedDeclarationNode); - expect(actualDeclaration !.viaModule).toBe(null); - }); - - it('should return the declaration of an externally defined identifier', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; - const decoratorNode = classDecorators[0].node; - const identifierOfDirective = - ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ? - decoratorNode.expression : - null; - - const expectedDeclarationNode = getDeclaration( - 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); - expect(actualDeclaration !.viaModule).toBe('@angular/core'); - }); - }); - - describe('getVariableValue', () => { - it('should find the "actual" declaration of an aliased variable identifier', () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const ngModuleRef = findVariableDeclaration( - program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1'); - - const value = host.getVariableValue(ngModuleRef !); - expect(value).not.toBe(null); - if (!value || !ts.isClassExpression(value)) { - throw new Error( - `Expected value to be a class expression: ${value && value.getText()}.`); - } - expect(value.name !.text).toBe('HttpClientXsrfModule'); - }); - - it('should return null if the variable has no assignment', () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const missingValue = findVariableDeclaration( - program.getSourceFile(fileSystem.files[2].name) !, 'missingValue'); - const value = host.getVariableValue(missingValue !); - expect(value).toBe(null); - }); - - it('should return null if the variable is not assigned from a call to __decorate', () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const nonDecoratedVar = findVariableDeclaration( - program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar'); - const value = host.getVariableValue(nonDecoratedVar !); - expect(value).toBe(null); - }); - }); }); - }); - function findVariableDeclaration( - node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined { - if (!node) { - return; + function findVariableDeclaration( + node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined { + if (!node) { + return; + } + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && + node.name.text === variableName) { + return node; + } + return node.forEachChild(node => findVariableDeclaration(node, variableName)); } - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && - node.name.text === variableName) { - return node; - } - return node.forEachChild(node => findVariableDeclaration(node, variableName)); - } + }); }); 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 c4dcd6569b..8b95f3526f 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -8,16 +8,50 @@ import * as ts from 'typescript'; +import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; -import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; +import {getRootFiles, makeTestBundleProgram} from '../helpers/utils'; import {expectTypeValueReferencesForParameters} from './util'; -const SOME_DIRECTIVE_FILE = { - name: '/some_directive.js', - contents: ` +runInEachFileSystem(() => { + describe('Esm2015ReflectionHost', () => { + + let _: typeof absoluteFrom; + + let SOME_DIRECTIVE_FILE: TestFile; + let ACCESSORS_FILE: TestFile; + let SIMPLE_CLASS_FILE: TestFile; + let CLASS_EXPRESSION_FILE: TestFile; + let FOO_FUNCTION_FILE: TestFile; + let INVALID_DECORATORS_FILE: TestFile; + let INVALID_DECORATOR_ARGS_FILE: TestFile; + let INVALID_PROP_DECORATORS_FILE: TestFile; + let INVALID_PROP_DECORATOR_ARGS_FILE: TestFile; + let INVALID_CTOR_DECORATORS_FILE: TestFile; + let INVALID_CTOR_DECORATOR_ARGS_FILE: TestFile; + let IMPORTS_FILES: TestFile[]; + let EXPORTS_FILES: TestFile[]; + let FUNCTION_BODY_FILE: TestFile; + let MARKER_FILE: TestFile; + let DECORATED_FILES: TestFile[]; + let ARITY_CLASSES: TestFile[]; + let TYPINGS_SRC_FILES: TestFile[]; + let TYPINGS_DTS_FILES: TestFile[]; + let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[]; + let NAMESPACED_IMPORT_FILE: TestFile; + + beforeEach(() => { + _ = absoluteFrom; + + SOME_DIRECTIVE_FILE = { + name: _('/some_directive.js'), + contents: ` import { Directive, Inject, InjectionToken, Input, HostListener, HostBinding } from '@angular/core'; const INJECTED_TOKEN = new InjectionToken('injected'); @@ -53,11 +87,11 @@ const SOME_DIRECTIVE_FILE = { "onClick": [{ type: HostListener, args: ['click',] },], }; `, -}; + }; -const ACCESSORS_FILE = { - name: '/accessors.js', - contents: ` + ACCESSORS_FILE = { + name: _('/accessors.js'), + contents: ` import { Directive, Input, Output } from '@angular/core'; class SomeDirective { @@ -71,31 +105,31 @@ const ACCESSORS_FILE = { "setterAndGetter": [{ type: Input },], }; `, -}; + }; -const SIMPLE_CLASS_FILE = { - name: '/simple_class.js', - contents: ` + SIMPLE_CLASS_FILE = { + name: _('/simple_class.js'), + contents: ` class EmptyClass {} class NoDecoratorConstructorClass { constructor(foo) {} } `, -}; + }; -const CLASS_EXPRESSION_FILE = { - name: '/class_expression.js', - contents: ` + CLASS_EXPRESSION_FILE = { + name: _('/class_expression.js'), + contents: ` var AliasedClass_1; let EmptyClass = class EmptyClass {}; let AliasedClass = AliasedClass_1 = class AliasedClass {} let usageOfAliasedClass = AliasedClass_1; `, -}; + }; -const FOO_FUNCTION_FILE = { - name: '/foo_function.js', - contents: ` + FOO_FUNCTION_FILE = { + name: _('/foo_function.js'), + contents: ` import { Directive } from '@angular/core'; function foo() {} @@ -103,11 +137,11 @@ const FOO_FUNCTION_FILE = { { type: Directive, args: [{ selector: '[ignored]' },] } ]; `, -}; + }; -const INVALID_DECORATORS_FILE = { - name: '/invalid_decorators.js', - contents: ` + INVALID_DECORATORS_FILE = { + name: _('/invalid_decorators.js'), + contents: ` import {Directive} from '@angular/core'; class NotArrayLiteral { } @@ -136,11 +170,11 @@ const INVALID_DECORATORS_FILE = { { type: Directive }, ]; `, -}; + }; -const INVALID_DECORATOR_ARGS_FILE = { - name: '/invalid_decorator_args.js', - contents: ` + INVALID_DECORATOR_ARGS_FILE = { + name: _('/invalid_decorator_args.js'), + contents: ` import {Directive} from '@angular/core'; class NoArgsProperty { } @@ -161,11 +195,11 @@ const INVALID_DECORATOR_ARGS_FILE = { { type: Directive, args: () => [{ selector: '[ignored]' },] }, ]; `, -}; + }; -const INVALID_PROP_DECORATORS_FILE = { - name: '/invalid_prop_decorators.js', - contents: ` + INVALID_PROP_DECORATORS_FILE = { + name: _('/invalid_prop_decorators.js'), + contents: ` import {Input} from '@angular/core'; class NotObjectLiteral { } @@ -200,11 +234,11 @@ const INVALID_PROP_DECORATORS_FILE = { ] }; `, -}; + }; -const INVALID_PROP_DECORATOR_ARGS_FILE = { - name: '/invalid_prop_decorator_args.js', - contents: ` + INVALID_PROP_DECORATOR_ARGS_FILE = { + name: _('/invalid_prop_decorator_args.js'), + contents: ` import {Input} from '@angular/core'; class NoArgsProperty { } @@ -225,11 +259,11 @@ const INVALID_PROP_DECORATOR_ARGS_FILE = { "prop": [{ type: Input, args: () => [{ selector: '[ignored]' },] },], }; `, -}; + }; -const INVALID_CTOR_DECORATORS_FILE = { - name: '/invalid_ctor_decorators.js', - contents: ` + INVALID_CTOR_DECORATORS_FILE = { + name: _('/invalid_ctor_decorators.js'), + contents: ` import {Inject} from '@angular/core'; class NoParameters { constructor() { @@ -296,11 +330,11 @@ const INVALID_CTOR_DECORATORS_FILE = { }, ]; `, -}; + }; -const INVALID_CTOR_DECORATOR_ARGS_FILE = { - name: '/invalid_ctor_decorator_args.js', - contents: ` + INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: _('/invalid_ctor_decorator_args.js'), + contents: ` import {Inject} from '@angular/core'; class NoArgsProperty { constructor(arg1) { @@ -327,18 +361,26 @@ const INVALID_CTOR_DECORATOR_ARGS_FILE = { { type: 'ParamType', decorators: [{ type: Inject, args: () => [{ selector: '[ignored]' },] },] }, ]; `, -}; + }; -const IMPORTS_FILES = [ - { - name: '/a.js', - contents: ` + IMPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + import * as a from './a'; + import * as b from './b'; + import * as c from './c'; + ` + }, + { + name: _('/a.js'), + contents: ` export const a = 'a'; `, - }, - { - name: '/b.js', - contents: ` + }, + { + name: _('/b.js'), + contents: ` import {a} from './a.js'; import {a as foo} from './a.js'; @@ -346,19 +388,26 @@ const IMPORTS_FILES = [ const c = foo; const d = b; `, - }, -]; + }, + ]; -const EXPORTS_FILES = [ - { - name: '/a.js', - contents: ` + EXPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + import * as a from './a'; + import * as b from './b'; + ` + }, + { + name: _('/a.js'), + contents: ` export const a = 'a'; `, - }, - { - name: '/b.js', - contents: ` + }, + { + name: _('/b.js'), + contents: ` import {Directive} from '@angular/core'; import {a} from './a'; import {a as foo} from './a'; @@ -371,12 +420,12 @@ const EXPORTS_FILES = [ export const DirectiveX = Directive; export class SomeClass {} `, - }, -]; + }, + ]; -const FUNCTION_BODY_FILE = { - name: '/function_body.js', - contents: ` + FUNCTION_BODY_FILE = { + name: _('/function_body.js'), + contents: ` function foo(x) { return x; } @@ -404,11 +453,11 @@ const FUNCTION_BODY_FILE = { return x; } ` -}; + }; -const MARKER_FILE = { - name: '/marker.js', - contents: ` + MARKER_FILE = { + name: _('/marker.js'), + contents: ` let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { @@ -422,14 +471,14 @@ const MARKER_FILE = { return Promise.resolve(new R3NgModuleFactory(moduleType)); } ` -}; + }; -const DECORATED_FILES = [ - { - name: '/primary.js', - contents: ` + DECORATED_FILES = [ + { + name: _('/primary.js'), + contents: ` import {Directive} from '@angular/core'; - import {D} from '/secondary'; + import {D} from './secondary'; class A {} A.decorators = [ { type: Directive, args: [{ selector: '[a]' }] } @@ -443,10 +492,10 @@ const DECORATED_FILES = [ class C {} export { A, x, C }; ` - }, - { - name: '/secondary.js', - contents: ` + }, + { + name: _('/secondary.js'), + contents: ` import {Directive} from '@angular/core'; class D {} D.decorators = [ @@ -454,72 +503,95 @@ const DECORATED_FILES = [ ]; export {D}; ` - } -]; + } + ]; -const ARITY_CLASSES = [ - { - name: '/src/class.js', - contents: ` + ARITY_CLASSES = [ + { + name: _('/src/class.js'), + contents: ` export class NoTypeParam {} export class OneTypeParam {} export class TwoTypeParams {} `, - }, - { - name: '/typings/class.d.ts', - contents: ` + }, + { + name: _('/typings/class.d.ts'), + contents: ` export declare class NoTypeParam {} export declare class OneTypeParam {} export declare class TwoTypeParams {} `, - }, -]; + }, + ]; -const TYPINGS_SRC_FILES = [ - { - name: '/src/index.js', - contents: - `import {InternalClass} from './internal'; export * from './class1'; export * from './class2';` - }, - {name: '/src/class1.js', contents: 'export class Class1 {}\nexport class MissingClass1 {}'}, - {name: '/src/class2.js', contents: 'export class Class2 {}'}, - {name: '/src/func1.js', contents: 'export function mooFn() {}'}, - {name: '/src/internal.js', contents: 'export class InternalClass {}\nexport class Class2 {}'}, - {name: '/src/missing-class.js', contents: 'export class MissingClass2 {}'}, { - name: '/src/flat-file.js', - contents: - 'export class Class1 {}\nexport class MissingClass1 {}\nexport class MissingClass2 {}\class Class3 {}\nexport {Class3 as xClass3};', - } -]; + TYPINGS_SRC_FILES = [ + { + name: _('/src/index.js'), + contents: ` + import {InternalClass} from './internal'; + import * as func1 from './func1'; + import * as missing from './missing-class'; + import * as flatFile from './flat-file'; + export * from './class1'; + export * from './class2'; + ` + }, + { + name: _('/src/class1.js'), + contents: 'export class Class1 {}\nexport class MissingClass1 {}' + }, + {name: _('/src/class2.js'), contents: 'export class Class2 {}'}, + {name: _('/src/func1.js'), contents: 'export function mooFn() {}'}, { + name: _('/src/internal.js'), + contents: 'export class InternalClass {}\nexport class Class2 {}' + }, + {name: _('/src/missing-class.js'), contents: 'export class MissingClass2 {}'}, { + name: _('/src/flat-file.js'), + contents: + 'export class Class1 {}\nexport class MissingClass1 {}\nexport class MissingClass2 {}\class Class3 {}\nexport {Class3 as xClass3};', + } + ]; -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 {}`}, -]; + TYPINGS_DTS_FILES = [ + { + name: _('/typings/index.d.ts'), + contents: ` + import {InternalClass} from './internal'; + import {mooFn} from './func1'; + 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: ` + MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: _('/src/index.js'), + contents: ` + import * as functions from './functions'; + import * as methods from './methods'; + import * as aliased_class from './aliased_class'; + ` + }, + { + name: _('/src/functions.js'), + contents: ` import {ExternalModule} from './module'; import * as mod from './module'; export class SomeService {} @@ -537,10 +609,10 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ export function externalNgModule() { return { ngModule: ExternalModule }; } export function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } ` - }, - { - name: '/src/methods.js', - contents: ` + }, + { + name: _('/src/methods.js'), + contents: ` import {ExternalModule} from './module'; import * as mod from './module'; export class SomeService {} @@ -565,23 +637,23 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ instanceNamespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } } ` - }, - { - name: '/src/aliased_class.js', - contents: ` + }, + { + name: _('/src/aliased_class.js'), + contents: ` var AliasedModule_1; let AliasedModule = AliasedModule_1 = class AliasedModule { static forRoot() { return { ngModule: AliasedModule_1 }; } }; export { AliasedModule }; ` - }, - {name: '/src/module.js', contents: 'export class ExternalModule {}'}, -]; + }, + {name: _('/src/module.js'), contents: 'export class ExternalModule {}'}, + ]; -const NAMESPACED_IMPORT_FILE = { - name: '/some_directive.js', - contents: ` + NAMESPACED_IMPORT_FILE = { + name: _('/some_directive.js'), + contents: ` import * as core from '@angular/core'; class SomeDirective { @@ -590,1143 +662,1245 @@ const NAMESPACED_IMPORT_FILE = { { type: core.Directive, args: [{ selector: '[someDirective]' },] } ]; ` -}; - -describe('Esm2015ReflectionHost', () => { - - describe('getDecoratorsOfDeclaration()', () => { - it('should find the decorators on a class', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - 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 = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode); - expect(decorators).toBe(null); - }); - - it('should ignore `decorators` if it is not an array literal', () => { - const program = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedClassDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode); - expect(decorators).toEqual([]); - }); - - it('should ignore decorator elements that are not object literals', () => { - const program = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', isNamedClassDeclaration); - 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 mockImportInfo = { from: '@angular/core' } as Import; - const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', isNamedClassDeclaration); - 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 properties on a class', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - 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 = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - 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 handle equally named getter/setter pairs correctly', () => { - const program = makeTestProgram(ACCESSORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = - getDeclaration(program, ACCESSORS_FILE.name, 'SomeDirective', isNamedClassDeclaration); - const members = host.getMembersOfClass(classNode); - - const [combinedSetter, combinedGetter] = - members.filter(member => member.name === 'setterAndGetter'); - expect(combinedSetter.kind).toEqual(ClassMemberKind.Setter); - expect(combinedSetter.isStatic).toEqual(false); - expect(ts.isSetAccessor(combinedSetter.implementation !)).toEqual(true); - expect(combinedSetter.value).toBeNull(); - expect(combinedSetter.decorators !.map(d => d.name)).toEqual(['Input']); - expect(combinedGetter.kind).toEqual(ClassMemberKind.Getter); - expect(combinedGetter.isStatic).toEqual(false); - expect(ts.isGetAccessor(combinedGetter.implementation !)).toEqual(true); - expect(combinedGetter.value).toBeNull(); - expect(combinedGetter.decorators !.map(d => d.name)).toEqual([]); - }); - - it('should find static methods on a class', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - 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.isMethodDeclaration(staticMethod.implementation !)).toEqual(true); - }); - - it('should find static properties on a class', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - 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 = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', - isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', - isNamedClassDeclaration); - 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: 'Input'})); - }); - - it('should ignore prop decorator elements that have no `type` property', () => { - const program = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', isNamedClassDeclaration); - 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: 'Input'})); - }); - - it('should ignore prop decorator elements whose `type` value is not an identifier', () => { - const program = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', isNamedClassDeclaration); - 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: 'Input'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - let callCount = 0; - const spy = - spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { - callCount++; - return {name: `name${callCount}`, from: '@angular/core'}; - }); - - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - const members = host.getMembersOfClass(classNode); - - expect(spy).toHaveBeenCalled(); - expect(spy.calls.allArgs().map(arg => arg[0].getText())).toEqual([ - 'Input', - 'Input', - 'HostBinding', - 'Input', - 'HostListener', - ]); - - const member = members.find(member => member.name === 'input1') !; - expect(member.decorators !.length).toBe(1); - expect(member.decorators ![0].import).toEqual({name: 'name1', from: '@angular/core'}); - }); - - describe('(returned prop decorators `args`)', () => { - it('should be an empty array if prop decorator has no `args` property', () => { - const program = makeTestProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', - isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', - isNamedClassDeclaration); - 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 = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - 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 = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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() {}"'); - }); - - it('should return `null` if there is no constructor', () => { - const program = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - expect(parameters).toBe(null); - }); - - it('should return an array even if there are no decorators', () => { - const program = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters).toEqual([]); - }); - - it('should ignore decorators that are not imported from core', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotFromCore', isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode) !; - - expect(parameters.length).toBe(1); - expect(parameters[0]).toEqual(jasmine.objectContaining({ - name: 'arg1', - decorators: [], - })); - }); - - it('should ignore `ctorParameters` if it is not an arrow function', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrowFunction', isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode) !; - - expect(parameters.length).toBe(1); - expect(parameters[0]).toEqual(jasmine.objectContaining({ - name: 'arg1', - decorators: null, - })); - }); - - it('should ignore `ctorParameters` if it does not return an array literal', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode) !; - - expect(parameters.length).toBe(1); - expect(parameters[0]).toEqual(jasmine.objectContaining({ - name: 'arg1', - decorators: null, - })); - }); - - describe('synthesized constructors', () => { - function getConstructorParameters(constructor: string) { - const file = { - name: '/synthesized_constructors.js', - contents: ` - class BaseClass {} - class TestClass extends BaseClass { - ${constructor} - } - `, - }; - - const program = makeTestProgram(file); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); - return host.getConstructorParameters(classNode); - } - - it('recognizes super call as first statement', () => { - const parameters = getConstructorParameters(` - constructor() { - super(...arguments); - this.synthesizedProperty = null; - }`); - - expect(parameters).toBeNull(); - }); - - it('does not consider super call without spread element as synthesized', () => { - const parameters = getConstructorParameters(` - constructor() { - super(arguments); - }`); - - expect(parameters !.length).toBe(0); - }); - - it('does not consider constructors with parameters as synthesized', () => { - const parameters = getConstructorParameters(` - constructor(arg) { - super(...arguments); - }`); - - expect(parameters !.length).toBe(1); - }); - - it('does not consider manual super calls as synthesized', () => { - const parameters = getConstructorParameters(` - constructor() { - super(); - }`); - - expect(parameters !.length).toBe(0); - }); - - it('does not consider empty constructors as synthesized', () => { - const parameters = getConstructorParameters(` - constructor() { - }`); - - expect(parameters !.length).toBe(0); - }); - }); - - describe('(returned parameters `decorators`)', () => { - it('should ignore param decorator elements that are not object literals', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', - isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', isNamedClassDeclaration); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', isNamedClassDeclaration); - 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 mockImportInfo: Import = {name: 'mock', from: '@angular/core'}; - const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeTestProgram(SOME_DIRECTIVE_FILE); + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode) !; - const decorators = parameters[2].decorators !; + 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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toBe(null); + }); + + it('should ignore `decorators` if it is not an array literal', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); + + it('should ignore decorator elements that are not object literals', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', isNamedClassDeclaration); + 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 mockImportInfo = { from: '@angular/core' } as Import; + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + 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('Inject'); + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); }); }); - describe('(returned parameters `decorators.args`)', () => { - it('should be an empty array if param decorator has no `args` property', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + describe('getMembersOfClass()', () => { + it('should find decorated properties on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', - isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - expect(parameters !.length).toBe(1); - const decorators = parameters ![0].decorators !; + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + const members = host.getMembersOfClass(classNode); - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Inject'); - expect(decorators[0].args).toEqual([]); + 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 be an empty array if param decorator\'s `args` has no property assignment', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + it('should find non decorated properties on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + const members = host.getMembersOfClass(classNode); - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Inject'); - expect(decorators[0].args).toEqual([]); + 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 be an empty array if `args` property value is not an array literal', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + it('should handle equally named getter/setter pairs correctly', () => { + loadTestFiles([ACCESSORS_FILE]); + const {program} = makeTestBundleProgram(ACCESSORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, ACCESSORS_FILE.name, 'SomeDirective', isNamedClassDeclaration); + const members = host.getMembersOfClass(classNode); + + const [combinedSetter, combinedGetter] = + members.filter(member => member.name === 'setterAndGetter'); + expect(combinedSetter.kind).toEqual(ClassMemberKind.Setter); + expect(combinedSetter.isStatic).toEqual(false); + expect(ts.isSetAccessor(combinedSetter.implementation !)).toEqual(true); + expect(combinedSetter.value).toBeNull(); + expect(combinedSetter.decorators !.map(d => d.name)).toEqual(['Input']); + expect(combinedGetter.kind).toEqual(ClassMemberKind.Getter); + expect(combinedGetter.isStatic).toEqual(false); + expect(ts.isGetAccessor(combinedGetter.implementation !)).toEqual(true); + expect(combinedGetter.value).toBeNull(); + expect(combinedGetter.decorators !.map(d => d.name)).toEqual([]); + }); + + it('should find static methods on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + 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.isMethodDeclaration(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + 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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members).toEqual([]); + }); + + it('should not process decorated properties in `propDecorators` if it is not an object literal', + () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', isNamedClassDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; + 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('Inject'); - expect(decorators[0].args).toEqual([]); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Input'})); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', isNamedClassDeclaration); + 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: 'Input'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', isNamedClassDeclaration); + 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: 'Input'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + let callCount = 0; + const spy = + spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { + callCount++; + return {name: `name${callCount}`, from: '@angular/core'}; + }); + + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(spy).toHaveBeenCalled(); + expect(spy.calls.allArgs().map(arg => arg[0].getText())).toEqual([ + 'Input', + 'Input', + 'HostBinding', + 'Input', + 'HostListener', + ]); + + const member = members.find(member => member.name === 'input1') !; + expect(member.decorators !.length).toBe(1); + expect(member.decorators ![0].import).toEqual({name: 'name1', from: '@angular/core'}); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedClassDeclaration); + 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', + () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedClassDeclaration); + 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('getDefinitionOfFunction()', () => { - it('should return an object describing the function declaration passed as an argument', () => { - const program = makeTestProgram(FUNCTION_BODY_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + describe('getConstructorParameters()', () => { + it('should find the decorated constructor parameters', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + const parameters = host.getConstructorParameters(classNode) !; - 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); + expect(parameters).toBeDefined(); + expect(parameters.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters( + parameters, ['ViewContainerRef', 'TemplateRef', 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'); + it('should throw if the symbol is not a class', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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() {}"'); + }); - 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); + it('should return `null` if there is no constructor', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters).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); + it('should return an array even if there are no decorators', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', + isNamedClassDeclaration); + const parameters = host.getConstructorParameters(classNode) !; - const mooNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'moo', isNamedFunctionDeclaration) !; - const mooDef = host.getDefinitionOfFunction(mooNode) !; - expect(mooDef.node).toBe(mooNode); - expect(mooDef.body !.length).toEqual(3); - expect(mooDef.parameters).toEqual([]); + expect(parameters).toEqual(jasmine.any(Array)); + expect(parameters.length).toEqual(1); + expect(parameters[0].name).toEqual('foo'); + expect(parameters[0].decorators).toBe(null); + }); - const juuNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'juu', isNamedFunctionDeclaration) !; - const juuDef = host.getDefinitionOfFunction(juuNode) !; - expect(juuDef.node).toBe(juuNode); - expect(juuDef.body !.length).toEqual(2); - expect(juuDef.parameters).toEqual([]); - }); - }); + it('should return an empty array if there are no constructor parameters', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', isNamedClassDeclaration); + const parameters = host.getConstructorParameters(classNode); - describe('getImportOfIdentifier()', () => { - it('should find the import of an identifier', () => { - const program = makeTestProgram(...IMPORTS_FILES); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'b', isNamedVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + expect(parameters).toEqual([]); + }); - expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + it('should ignore decorators that are not imported from core', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotFromCore', isNamedClassDeclaration); + const parameters = host.getConstructorParameters(classNode) !; + + expect(parameters.length).toBe(1); + expect(parameters[0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: [], + })); + }); + + it('should ignore `ctorParameters` if it is not an arrow function', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrowFunction', + isNamedClassDeclaration); + const parameters = host.getConstructorParameters(classNode) !; + + expect(parameters.length).toBe(1); + expect(parameters[0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + it('should ignore `ctorParameters` if it does not return an array literal', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedClassDeclaration); + const parameters = host.getConstructorParameters(classNode) !; + + expect(parameters.length).toBe(1); + expect(parameters[0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + describe('synthesized constructors', () => { + function getConstructorParameters(constructor: string) { + const file = { + name: _('/synthesized_constructors.js'), + contents: ` + class BaseClass {} + class TestClass extends BaseClass { + ${constructor} + } + `, + }; + + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); + return host.getConstructorParameters(classNode); + } + + it('recognizes super call as first statement', () => { + const parameters = getConstructorParameters(` + constructor() { + super(...arguments); + this.synthesizedProperty = null; + }`); + + expect(parameters).toBeNull(); + }); + + it('does not consider super call without spread element as synthesized', () => { + const parameters = getConstructorParameters(` + constructor() { + super(arguments); + }`); + + expect(parameters !.length).toBe(0); + }); + + it('does not consider constructors with parameters as synthesized', () => { + const parameters = getConstructorParameters(` + constructor(arg) { + super(...arguments); + }`); + + expect(parameters !.length).toBe(1); + }); + + it('does not consider manual super calls as synthesized', () => { + const parameters = getConstructorParameters(` + constructor() { + super(); + }`); + + expect(parameters !.length).toBe(0); + }); + + it('does not consider empty constructors as synthesized', () => { + const parameters = getConstructorParameters(` + constructor() { + }`); + + expect(parameters !.length).toBe(0); + }); + }); + + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', + isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', isNamedClassDeclaration); + 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 mockImportInfo: Import = {name: 'mock', from: '@angular/core'}; + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedClassDeclaration); + 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', + () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedClassDeclaration); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedClassDeclaration); + 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 find the name by which the identifier was exported, not imported', () => { - const program = makeTestProgram(...IMPORTS_FILES); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'c', isNamedVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', + () => { + loadTestFiles([FUNCTION_BODY_FILE]); + const {program} = makeTestBundleProgram(FUNCTION_BODY_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + 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); + + const mooNode = getDeclaration( + program, FUNCTION_BODY_FILE.name, 'moo', isNamedFunctionDeclaration) !; + const mooDef = host.getDefinitionOfFunction(mooNode) !; + expect(mooDef.node).toBe(mooNode); + expect(mooDef.body !.length).toEqual(3); + expect(mooDef.parameters).toEqual([]); + + const juuNode = getDeclaration( + program, FUNCTION_BODY_FILE.name, 'juu', isNamedFunctionDeclaration) !; + const juuDef = host.getDefinitionOfFunction(juuNode) !; + expect(juuDef.node).toBe(juuNode); + expect(juuDef.body !.length).toEqual(2); + expect(juuDef.parameters).toEqual([]); + }); }); - it('should return null if the identifier was not imported', () => { - const program = makeTestProgram(...IMPORTS_FILES); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'd', isNamedVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + describe('getImportOfIdentifier()', () => { + it('should find the import of an identifier', () => { + loadTestFiles(IMPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const variableNode = getDeclaration(program, _('/b.js'), 'b', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - expect(importOfIdent).toBeNull(); - }); - }); + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); - describe('getDeclarationOfIdentifier()', () => { - it('should return the declaration of a locally defined identifier', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - const ctrDecorators = host.getConstructorParameters(classNode) !; - const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ - local: true, - expression: ts.Identifier, - defaultImportStatement: null, - }).expression; + it('should find the name by which the identifier was exported, not imported', () => { + loadTestFiles(IMPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const variableNode = getDeclaration(program, _('/b.js'), 'c', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - 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); + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should return null if the identifier was not imported', () => { + loadTestFiles(IMPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const variableNode = getDeclaration(program, _('/b.js'), 'd', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); }); - it('should return the declaration of an externally defined identifier', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); - const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; - const identifierOfDirective = ((classDecorators[0].node as ts.ObjectLiteralExpression) - .properties[0] as ts.PropertyAssignment) - .initializer as ts.Identifier; + describe('getDeclarationOfIdentifier()', () => { + it('should return the declaration of a locally defined identifier', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier, + defaultImportStatement: null, + }).expression; - const expectedDeclarationNode = getDeclaration( - 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); - expect(actualDeclaration !.viaModule).toBe('@angular/core'); + 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 declaration of an externally defined identifier', () => { + loadFakeCore(getFileSystem()); + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifierOfDirective = ((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + 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); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + + it('should return the source-file of an import namespace', () => { + loadFakeCore(getFileSystem()); + loadTestFiles([NAMESPACED_IMPORT_FILE]); + const {program} = makeTestBundleProgram(NAMESPACED_IMPORT_FILE.name); + 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 = + getSourceFileOrError(program, _('/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', () => { + loadTestFiles([CLASS_EXPRESSION_FILE]); + const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classDeclaration = getDeclaration( + program, CLASS_EXPRESSION_FILE.name, 'AliasedClass', ts.isVariableDeclaration); + const usageOfAliasedClass = getDeclaration( + program, CLASS_EXPRESSION_FILE.name, 'usageOfAliasedClass', ts.isVariableDeclaration); + const aliasedClassIdentifier = usageOfAliasedClass.initializer as ts.Identifier; + expect(aliasedClassIdentifier.text).toBe('AliasedClass_1'); + expect(host.getDeclarationOfIdentifier(aliasedClassIdentifier) !.node) + .toBe(classDeclaration); + }); }); - 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; + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + loadFakeCore(getFileSystem()); + loadTestFiles(EXPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/b.js')); + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.keys())).toEqual([ + 'Directive', + 'a', + 'b', + 'c', + 'd', + 'e', + 'DirectiveX', + 'SomeClass', + ]); - 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); + const values = Array.from(exportDeclarations !.values()) + .map(declaration => [declaration.node.getText(), declaration.viaModule]); + expect(values).toEqual([ + [`Directive: FnWithArg<(clazz: any) => any>`, null], + [`a = 'a'`, null], + [`b = a`, null], + [`c = foo`, null], + [`d = b`, null], + [`e = 'e'`, null], + [`DirectiveX = Directive`, null], + ['export class SomeClass {}', 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()); - const classDeclaration = getDeclaration( - program, CLASS_EXPRESSION_FILE.name, 'AliasedClass', ts.isVariableDeclaration); - const usageOfAliasedClass = getDeclaration( - program, CLASS_EXPRESSION_FILE.name, 'usageOfAliasedClass', ts.isVariableDeclaration); - const aliasedClassIdentifier = usageOfAliasedClass.initializer as ts.Identifier; - expect(aliasedClassIdentifier.text).toBe('AliasedClass_1'); - expect(host.getDeclarationOfIdentifier(aliasedClassIdentifier) !.node).toBe(classDeclaration); - }); - }); + describe('isClass()', () => { + it('should return true if a given node is a TS class declaration', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + expect(host.isClass(node)).toBe(true); + }); - describe('getExportsOfModule()', () => { - it('should return a map of all the exports from a given module', () => { - const program = makeTestProgram(...EXPORTS_FILES); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const file = program.getSourceFile(EXPORTS_FILES[1].name) !; - const exportDeclarations = host.getExportsOfModule(file); - expect(exportDeclarations).not.toBe(null); - expect(Array.from(exportDeclarations !.keys())).toEqual([ - 'Directive', - 'a', - 'b', - 'c', - 'd', - 'e', - 'DirectiveX', - 'SomeClass', - ]); + it('should return true if a given node is a class expression assigned into a variable', + () => { + loadTestFiles([CLASS_EXPRESSION_FILE]); + const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = getDeclaration( + program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + expect(host.isClass(node)).toBe(true); + }); - const values = Array.from(exportDeclarations !.values()) - .map(declaration => [declaration.node.getText(), declaration.viaModule]); - expect(values).toEqual([ - [`Directive: FnWithArg<(clazz: any) => any>`, null], - [`a = 'a'`, null], - [`b = a`, null], - [`c = foo`, null], - [`d = b`, null], - [`e = 'e'`, null], - [`DirectiveX = Directive`, null], - ['export class SomeClass {}', null], - ]); - }); - }); + it('should return true if a given node is a class expression assigned into two variables', + () => { + loadTestFiles([CLASS_EXPRESSION_FILE]); + const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = getDeclaration( + program, CLASS_EXPRESSION_FILE.name, 'AliasedClass', ts.isVariableDeclaration); + expect(host.isClass(node)).toBe(true); + }); - describe('isClass()', () => { - it('should return true if a given node is a TS class declaration', () => { - const program = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); - expect(host.isClass(node)).toBe(true); + it('should return false if a given node is a TS function declaration', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(host.isClass(node)).toBe(false); + }); }); - it('should return true if a given node is a class expression assigned into a variable', () => { - const program = makeTestProgram(CLASS_EXPRESSION_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = getDeclaration( - program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', ts.isVariableDeclaration); - expect(host.isClass(node)).toBe(true); - }); + describe('hasBaseClass()', () => { + it('should not consider a class without extends clause as having a base class', () => { + const file = { + name: _('/base_class.js'), + contents: `class TestClass {}`, + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); + expect(host.hasBaseClass(classNode)).toBe(false); + }); - it('should return true if a given node is a class expression assigned into two variables', - () => { - const program = makeTestProgram(CLASS_EXPRESSION_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = getDeclaration( - program, CLASS_EXPRESSION_FILE.name, 'AliasedClass', ts.isVariableDeclaration); - expect(host.isClass(node)).toBe(true); - }); - - it('should return false if a given node is a TS function declaration', () => { - const program = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = - getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); - expect(host.isClass(node)).toBe(false); - }); - }); - - describe('hasBaseClass()', () => { - it('should not consider a class without extends clause as having a base class', () => { - const file = { - name: '/base_class.js', - contents: `class TestClass {}`, - }; - const program = makeTestProgram(file); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); - expect(host.hasBaseClass(classNode)).toBe(false); - }); - - it('should consider a class with extends clause as having a base class', () => { - const file = { - name: '/base_class.js', - contents: ` + it('should consider a class with extends clause as having a base class', () => { + const file = { + name: _('/base_class.js'), + contents: ` class BaseClass {} class TestClass extends BaseClass {}`, - }; - const program = makeTestProgram(file); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); - expect(host.hasBaseClass(classNode)).toBe(true); - }); + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration); + expect(host.hasBaseClass(classNode)).toBe(true); + }); - it('should consider an aliased class with extends clause as having a base class', () => { - const file = { - name: '/base_class.js', - contents: ` + it('should consider an aliased class with extends clause as having a base class', () => { + const file = { + name: _('/base_class.js'), + contents: ` let TestClass_1; class BaseClass {} let TestClass = TestClass_1 = class TestClass extends BaseClass {}`, - }; - const program = makeTestProgram(file); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); - expect(host.hasBaseClass(classNode)).toBe(true); - }); - }); - - describe('getGenericArityOfClass()', () => { - it('should properly count type parameters', () => { - const program = makeTestProgram(ARITY_CLASSES[0]); - const dtsProgram = makeTestProgram(ARITY_CLASSES[1]); - const dts = makeTestBundleProgram([ARITY_CLASSES[1]]); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); - const noTypeParamClass = - getDeclaration(program, '/src/class.js', 'NoTypeParam', isNamedClassDeclaration); - expect(host.getGenericArityOfClass(noTypeParamClass)).toBe(0); - const oneTypeParamClass = - getDeclaration(program, '/src/class.js', 'OneTypeParam', isNamedClassDeclaration); - expect(host.getGenericArityOfClass(oneTypeParamClass)).toBe(1); - const twoTypeParamsClass = - getDeclaration(program, '/src/class.js', 'TwoTypeParams', isNamedClassDeclaration); - expect(host.getGenericArityOfClass(twoTypeParamsClass)).toBe(2); - }); - }); - - describe('getSwitchableDeclarations()', () => { - it('should return a collection of all the switchable variable declarations in the given module', - () => { - const program = makeTestProgram(MARKER_FILE); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const file = program.getSourceFile(MARKER_FILE.name) !; - const declarations = host.getSwitchableDeclarations(file); - expect(declarations.map(d => [d.name.getText(), d.initializer !.getText()])).toEqual([ - ['compileNgModuleFactory', 'compileNgModuleFactory__PRE_R3__'] - ]); - }); - }); - - describe('findClassSymbols()', () => { - it('should return an array of all classes in the given source file', () => { - const program = makeTestProgram(...DECORATED_FILES); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; - - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - expect(classSymbolsPrimary.length).toEqual(3); - expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B', 'C']); - - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - expect(classSymbolsSecondary.length).toEqual(1); - expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); - }); - }); - - describe('getDecoratorsOfSymbol()', () => { - it('should return decorators of class symbol', () => { - const program = makeTestProgram(...DECORATED_FILES); - const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; - - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsPrimary.length).toEqual(3); - expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); - expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); - expect(classDecoratorsPrimary[2]).toBe(null); - - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - const classDecoratorsSecondary = - classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsSecondary.length).toEqual(1); - expect(classDecoratorsSecondary[0] !.map(d => d.name)).toEqual(['Directive']); - }); - }); - - describe('getDtsDeclarationsOfClass()', () => { - it('should find the dts declaration that has the same relative path to the source file', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class1 = - getDeclaration(srcProgram, '/src/class1.js', 'Class1', isNamedClassDeclaration); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); - - const dtsDeclaration = host.getDtsDeclaration(class1); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + expect(host.hasBaseClass(classNode)).toBe(true); + }); }); - it('should find the dts declaration for exported functions', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dtsProgram = makeTestBundleProgram(TYPINGS_DTS_FILES); - const mooFn = - getDeclaration(srcProgram, '/src/func1.js', 'mooFn', isNamedFunctionDeclaration); - const host = new Esm2015ReflectionHost( - new MockLogger(), false, srcProgram.getTypeChecker(), dtsProgram); - - const dtsDeclaration = host.getDtsDeclaration(mooFn); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/func1.d.ts'); + describe('getGenericArityOfClass()', () => { + it('should properly count type parameters', () => { + loadTestFiles(ARITY_CLASSES); + const {program} = makeTestBundleProgram(ARITY_CLASSES[0].name); + const dts = makeTestBundleProgram(ARITY_CLASSES[1].name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); + const noTypeParamClass = + getDeclaration(program, _('/src/class.js'), 'NoTypeParam', isNamedClassDeclaration); + expect(host.getGenericArityOfClass(noTypeParamClass)).toBe(0); + const oneTypeParamClass = + getDeclaration(program, _('/src/class.js'), 'OneTypeParam', isNamedClassDeclaration); + expect(host.getGenericArityOfClass(oneTypeParamClass)).toBe(1); + const twoTypeParamsClass = + getDeclaration(program, _('/src/class.js'), 'TwoTypeParams', isNamedClassDeclaration); + expect(host.getGenericArityOfClass(twoTypeParamsClass)).toBe(2); + }); }); - it('should return null if there is no matching class in the matching dts file', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const missingClass = - getDeclaration(srcProgram, '/src/class1.js', 'MissingClass1', isNamedClassDeclaration); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); - - expect(host.getDtsDeclaration(missingClass)).toBe(null); + describe('getSwitchableDeclarations()', () => { + it('should return a collection of all the switchable variable declarations in the given module', + () => { + loadTestFiles([MARKER_FILE]); + const {program} = makeTestBundleProgram(MARKER_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, MARKER_FILE.name); + const declarations = host.getSwitchableDeclarations(file); + expect(declarations.map(d => [d.name.getText(), d.initializer !.getText()])).toEqual([ + ['compileNgModuleFactory', 'compileNgModuleFactory__PRE_R3__'] + ]); + }); }); - it('should return null if there is no matching dts file', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const missingClass = getDeclaration( - srcProgram, '/src/missing-class.js', 'MissingClass2', isNamedClassDeclaration); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); + describe('findClassSymbols()', () => { + it('should return an array of all classes in the given source file', () => { + loadTestFiles(DECORATED_FILES); + const {program} = makeTestBundleProgram(getRootFiles(DECORATED_FILES)[0]); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - expect(host.getDtsDeclaration(missingClass)).toBe(null); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + expect(classSymbolsPrimary.length).toEqual(3); + expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B', 'C']); + + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + expect(classSymbolsSecondary.length).toEqual(1); + expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); + }); }); - it('should find the dts file that contains a matching class declaration, even if the source files do not match', - () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class1 = - getDeclaration(srcProgram, '/src/flat-file.js', 'Class1', isNamedClassDeclaration); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); + describe('getDecoratorsOfSymbol()', () => { + it('should return decorators of class symbol', () => { + loadTestFiles(DECORATED_FILES); + const {program} = makeTestBundleProgram(getRootFiles(DECORATED_FILES)[0]); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - const dtsDeclaration = host.getDtsDeclaration(class1); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); - }); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsPrimary.length).toEqual(3); + expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); + expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); + expect(classDecoratorsPrimary[2]).toBe(null); - it('should find aliased exports', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class3 = - getDeclaration(srcProgram, '/src/flat-file.js', 'Class3', isNamedClassDeclaration); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); - - const dtsDeclaration = host.getDtsDeclaration(class3); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts'); + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + const classDecoratorsSecondary = + classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsSecondary.length).toEqual(1); + expect(classDecoratorsSecondary[0] !.map(d => d.name)).toEqual(['Directive']); + }); }); - it('should find the dts file that contains a matching class declaration, even if the class is not publicly exported', - () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const internalClass = getDeclaration( - srcProgram, '/src/internal.js', 'InternalClass', isNamedClassDeclaration); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); + describe('getDtsDeclarationsOfClass()', () => { + it('should find the dts declaration that has the same relative path to the source file', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class1 = + getDeclaration(program, _('/src/class1.js'), 'Class1', isNamedClassDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); - const dtsDeclaration = host.getDtsDeclaration(internalClass); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts'); - }); + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/class1.d.ts')); + }); - it('should prefer the publicly exported class if there are multiple classes with the same name', - () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class2 = - getDeclaration(srcProgram, '/src/class2.js', 'Class2', isNamedClassDeclaration); - const internalClass2 = - getDeclaration(srcProgram, '/src/internal.js', 'Class2', isNamedClassDeclaration); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); + it('should find the dts declaration for exported functions', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const mooFn = + getDeclaration(program, _('/src/func1.js'), 'mooFn', isNamedFunctionDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); - const class2DtsDeclaration = host.getDtsDeclaration(class2); - expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts'); + const dtsDeclaration = host.getDtsDeclaration(mooFn); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/func1.d.ts')); + }); - const internalClass2DtsDeclaration = host.getDtsDeclaration(internalClass2); - expect(internalClass2DtsDeclaration !.getSourceFile().fileName) - .toEqual('/typings/class2.d.ts'); - }); - }); + it('should return null if there is no matching class in the matching dts file', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const missingClass = + getDeclaration(program, _('/src/class1.js'), 'MissingClass1', isNamedClassDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); - describe('getModuleWithProvidersFunctions()', () => { - it('should find every exported function that returns an object that looks like a ModuleWithProviders object', - () => { - const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker()); - const file = srcProgram.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'], - ['namespacedExternalNgModule', 'ExternalModule'], - ]); - }); + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); - it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', - () => { - const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); - const host = - new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker()); - const file = srcProgram.getSourceFile('/src/methods.js') !; - const fn = host.getModuleWithProvidersFunctions(file); - expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])) - .toEqual([ - ['ngModuleIdentifier', 'InternalModule'], - ['ngModuleWithEmptyProviders', 'InternalModule'], - ['ngModuleWithProviders', 'InternalModule'], - ['externalNgModule', 'ExternalModule'], - ['namespacedExternalNgModule', 'ExternalModule'], - ]); - }); + it('should return null if there is no matching dts file', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const missingClass = getDeclaration( + program, _('/src/missing-class.js'), 'MissingClass2', isNamedClassDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); - // https://github.com/angular/angular/issues/29078 - it('should resolve aliased module references to their original declaration', () => { - const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); - const host = new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker()); - const file = srcProgram.getSourceFile('/src/aliased_class.js') !; - const fn = host.getModuleWithProvidersFunctions(file); - expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])).toEqual([ - ['forRoot', 'AliasedModule'], - ]); + 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class1 = + getDeclaration(program, _('/src/flat-file.js'), 'Class1', isNamedClassDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/class1.d.ts')); + }); + + it('should find aliased exports', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class3 = + getDeclaration(program, _('/src/flat-file.js'), 'Class3', isNamedClassDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const internalClass = getDeclaration( + program, _('/src/internal.js'), 'InternalClass', isNamedClassDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class2 = + getDeclaration(program, _('/src/class2.js'), 'Class2', isNamedClassDeclaration); + const internalClass2 = + getDeclaration(program, _('/src/internal.js'), 'Class2', isNamedClassDeclaration); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), 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', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program} = makeTestBundleProgram(getRootFiles(MODULE_WITH_PROVIDERS_PROGRAM)[0]); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/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'], + ['namespacedExternalNgModule', 'ExternalModule'], + ]); + }); + + it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program} = makeTestBundleProgram(getRootFiles(MODULE_WITH_PROVIDERS_PROGRAM)[0]); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/src/methods.js')); + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])) + .toEqual([ + ['ngModuleIdentifier', 'InternalModule'], + ['ngModuleWithEmptyProviders', 'InternalModule'], + ['ngModuleWithProviders', 'InternalModule'], + ['externalNgModule', 'ExternalModule'], + ['namespacedExternalNgModule', 'ExternalModule'], + ]); + }); + + // https://github.com/angular/angular/issues/29078 + it('should resolve aliased module references to their original declaration', () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program} = makeTestBundleProgram(getRootFiles(MODULE_WITH_PROVIDERS_PROGRAM)[0]); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/src/aliased_class.js')); + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])) + .toEqual([ + ['forRoot', 'AliasedModule'], + ]); + }); }); }); }); 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 a6c5161d73..26fc3c441c 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 @@ -5,20 +5,38 @@ * 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 {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {ClassMemberKind, Import, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {MockLogger} from '../helpers/mock_logger'; -import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils'; +import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils'; import {expectTypeValueReferencesForParameters} from './util'; -const FILES = [ - { - name: '/some_directive.js', - contents: ` +runInEachFileSystem(() => { + describe('Esm5ReflectionHost [import helper style]', () => { + let _: typeof absoluteFrom; + let FILES: {[label: string]: TestFile[]}; + + beforeEach(() => { + _ = absoluteFrom; + const NAMESPACED_IMPORT_FILES = [ + { + name: _('/index.js'), + contents: ` + import * as some_directive from './some_directive'; + import * as some_directive2 from '/node_modules/@angular/core/some_directive'; + import * as ngmodule from './ngmodule'; + ` + }, + { + name: _('/some_directive.js'), + contents: ` import * as tslib_1 from 'tslib'; import { Directive, Inject, InjectionToken, Input } from '@angular/core'; var INJECTED_TOKEN = new InjectionToken('injected'); @@ -59,10 +77,10 @@ const FILES = [ }()); export { SomeDirective }; `, - }, - { - name: '/node_modules/@angular/core/some_directive.js', - contents: ` + }, + { + name: _('/node_modules/@angular/core/some_directive.js'), + contents: ` import * as tslib_1 from 'tslib'; import { Directive, Input } from './directives'; var SomeDirective = /** @class */ (function () { @@ -80,10 +98,10 @@ const FILES = [ }()); export { SomeDirective }; `, - }, - { - name: '/ngmodule.js', - contents: ` + }, + { + name: _('/ngmodule.js'), + contents: ` import * as tslib_1 from 'tslib'; import { NgModule } from '@angular/core'; var HttpClientXsrfModule = /** @class */ (function () { @@ -110,366 +128,380 @@ export { SomeDirective }; nonDecoratedVar = 43; export { HttpClientXsrfModule }; ` - }, -]; + }, + ]; -describe('Esm5ReflectionHost [import helper style]', () => { - [{files: FILES, label: 'namespaced'}, - {files: convertToDirectTsLibImport(FILES), label: 'direct import'}, - ].forEach(fileSystem => { - describe(`[${fileSystem.label}]`, () => { + const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES); - describe('getDecoratorsOfDeclaration()', () => { - it('should find the decorators on a class', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const decorators = host.getDecoratorsOfDeclaration(classNode) !; + FILES = { + 'namespaced': NAMESPACED_IMPORT_FILES, + 'direct import': DIRECT_IMPORT_FILES, + }; + }); - 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]\' }', - ]); + ['namespaced', 'direct import'].forEach(label => { + describe(`[${label}]`, () => { + beforeEach(() => { + const fs = getFileSystem(); + loadFakeCore(fs); + loadTestFiles(FILES[label]); }); - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') - .and.callFake( - (identifier: ts.Identifier) => identifier.getText() === 'Directive' ? - {from: '@angular/core', name: 'Directive'} : - {}); - - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - - const decorators = host.getDecoratorsOfDeclaration(classNode) !; - - expect(decorators.length).toEqual(1); - expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'}); - - const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); - expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy(); - }); - - it('should support decorators being used inside @angular/core', () => { - const program = makeTestProgram(fileSystem.files[1]); - const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/node_modules/@angular/core/some_directive.js', '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: './directives'}); - expect(decorator.args !.map(arg => arg.getText())).toEqual([ - '{ selector: \'[someDirective]\' }', - ]); - }); - }); - - describe('getMembersOfClass()', () => { - it('should find decorated members on a class', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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 = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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 = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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 = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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 use `getImportOfIdentifier()` to retrieve import info', () => { - const spy = - spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); - - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - - host.getMembersOfClass(classNode); - const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); - expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy(); - }); - - it('should support decorators being used inside @angular/core', () => { - const program = makeTestProgram(fileSystem.files[1]); - const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/node_modules/@angular/core/some_directive.js', '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']); - }); - }); - - describe('getConstructorParameters', () => { - it('should find the decorated constructor parameters', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const parameters = host.getConstructorParameters(classNode); - - expect(parameters).toBeDefined(); - expect(parameters !.map(parameter => parameter.name)).toEqual([ - '_viewContainer', '_template', 'injected' - ]); - expectTypeValueReferencesForParameters(parameters !, [ - 'ViewContainerRef', - 'TemplateRef', - 'String', - ]); - }); - - describe('(returned parameters `decorators`)', () => { - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const mockImportInfo = {} as Import; - const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeTestProgram(fileSystem.files[0]); + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![2].decorators !; + program, _('/some_directive.js'), '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 use `getImportOfIdentifier()` to retrieve import info', () => { + const spy = + spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.callFake( + (identifier: ts.Identifier) => identifier.getText() === 'Directive' ? + {from: '@angular/core', name: 'Directive'} : + {}); + + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + + const decorators = host.getDecoratorsOfDeclaration(classNode) !; expect(decorators.length).toEqual(1); - expect(decorators[0].import).toBe(mockImportInfo); + expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'}); - const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; - expect(typeIdentifier.text).toBe('Inject'); + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const {program} = + makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/node_modules/@angular/core/some_directive.js'), '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: './directives'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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 use `getImportOfIdentifier()` to retrieve import info', () => { + const spy = + spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); + + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + + host.getMembersOfClass(classNode); + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const {program} = + makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/node_modules/@angular/core/some_directive.js'), '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']); + }); + }); + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + 'String', + ]); + }); + + describe('(returned parameters `decorators`)', () => { + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = + new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + 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('findClassSymbols()', () => { + it('should return an array of all classes in the given source file', () => { + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + + const ngModuleFile = getSourceFileOrError(program, _('/ngmodule.js')); + const ngModuleClasses = host.findClassSymbols(ngModuleFile); + expect(ngModuleClasses.length).toEqual(1); + expect(ngModuleClasses[0].name).toBe('HttpClientXsrfModule'); + + const someDirectiveFile = getSourceFileOrError(program, _('/some_directive.js')); + const someDirectiveClasses = host.findClassSymbols(someDirectiveFile); + expect(someDirectiveClasses.length).toEqual(3); + expect(someDirectiveClasses[0].name).toBe('ViewContainerRef'); + expect(someDirectiveClasses[1].name).toBe('TemplateRef'); + expect(someDirectiveClasses[2].name).toBe('SomeDirective'); + }); + }); + + describe('getDecoratorsOfSymbol()', () => { + it('should return decorators of class symbol', () => { + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + + const ngModuleFile = getSourceFileOrError(program, _('/ngmodule.js')); + const ngModuleClasses = host.findClassSymbols(ngModuleFile); + const ngModuleDecorators = ngModuleClasses.map(s => host.getDecoratorsOfSymbol(s)); + + expect(ngModuleClasses.length).toEqual(1); + expect(ngModuleDecorators[0] !.map(d => d.name)).toEqual(['NgModule']); + + const someDirectiveFile = getSourceFileOrError(program, _('/some_directive.js')); + const someDirectiveClasses = host.findClassSymbols(someDirectiveFile); + const someDirectiveDecorators = + someDirectiveClasses.map(s => host.getDecoratorsOfSymbol(s)); + + expect(someDirectiveDecorators.length).toEqual(3); + expect(someDirectiveDecorators[0]).toBe(null); + expect(someDirectiveDecorators[1]).toBe(null); + expect(someDirectiveDecorators[2] !.map(d => d.name)).toEqual(['Directive']); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), '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.js'), '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 declaration of an externally defined identifier', () => { + const {program} = makeTestBundleProgram(_('/some_directive.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const decoratorNode = classDecorators[0].node; + + const identifierOfDirective = + ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ? + decoratorNode.expression : + null; + + const expectedDeclarationNode = getDeclaration( + 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); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + + it('should find the "actual" declaration of an aliased variable identifier', () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const ngModuleRef = findIdentifier( + getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1', + isNgModulePropertyAssignment); + + const declaration = host.getDeclarationOfIdentifier(ngModuleRef !); + expect(declaration).not.toBe(null); + expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()'); + }); + }); + describe('getVariableValue', () => { + it('should find the "actual" declaration of an aliased variable identifier', () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const ngModuleRef = findVariableDeclaration( + getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1'); + + const value = host.getVariableValue(ngModuleRef !); + expect(value).not.toBe(null); + if (!value || !ts.isFunctionDeclaration(value.parent)) { + throw new Error( + `Expected result to be a function declaration: ${value && value.getText()}.`); + } + expect(value.getText()).toBe('HttpClientXsrfModule'); + }); + + it('should return undefined if the variable has no assignment', () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const missingValue = findVariableDeclaration( + getSourceFileOrError(program, _('/ngmodule.js')), 'missingValue'); + const value = host.getVariableValue(missingValue !); + expect(value).toBe(null); + }); + + it('should return null if the variable is not assigned from a call to __decorate', () => { + const {program} = makeTestBundleProgram(_('/ngmodule.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const nonDecoratedVar = findVariableDeclaration( + getSourceFileOrError(program, _('/ngmodule.js')), 'nonDecoratedVar'); + const value = host.getVariableValue(nonDecoratedVar !); + expect(value).toBe(null); }); }); }); - describe('findClassSymbols()', () => { - it('should return an array of all classes in the given source file', () => { - const program = makeTestProgram(...fileSystem.files); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - - const ngModuleFile = program.getSourceFile('/ngmodule.js') !; - const ngModuleClasses = host.findClassSymbols(ngModuleFile); - expect(ngModuleClasses.length).toEqual(1); - expect(ngModuleClasses[0].name).toBe('HttpClientXsrfModule'); - - const someDirectiveFile = program.getSourceFile('/some_directive.js') !; - const someDirectiveClasses = host.findClassSymbols(someDirectiveFile); - expect(someDirectiveClasses.length).toEqual(3); - expect(someDirectiveClasses[0].name).toBe('ViewContainerRef'); - expect(someDirectiveClasses[1].name).toBe('TemplateRef'); - expect(someDirectiveClasses[2].name).toBe('SomeDirective'); - }); - }); - - describe('getDecoratorsOfSymbol()', () => { - it('should return decorators of class symbol', () => { - const program = makeTestProgram(...fileSystem.files); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - - const ngModuleFile = program.getSourceFile('/ngmodule.js') !; - const ngModuleClasses = host.findClassSymbols(ngModuleFile); - const ngModuleDecorators = ngModuleClasses.map(s => host.getDecoratorsOfSymbol(s)); - - expect(ngModuleClasses.length).toEqual(1); - expect(ngModuleDecorators[0] !.map(d => d.name)).toEqual(['NgModule']); - - const someDirectiveFile = program.getSourceFile('/some_directive.js') !; - const someDirectiveClasses = host.findClassSymbols(someDirectiveFile); - const someDirectiveDecorators = - someDirectiveClasses.map(s => host.getDecoratorsOfSymbol(s)); - - expect(someDirectiveDecorators.length).toEqual(3); - expect(someDirectiveDecorators[0]).toBe(null); - expect(someDirectiveDecorators[1]).toBe(null); - expect(someDirectiveDecorators[2] !.map(d => d.name)).toEqual(['Directive']); - }); - }); - - describe('getDeclarationOfIdentifier', () => { - it('should return the declaration of a locally defined identifier', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', '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.js', '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 declaration of an externally defined identifier', () => { - const program = makeTestProgram(fileSystem.files[0]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); - const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; - const decoratorNode = classDecorators[0].node; - - const identifierOfDirective = - ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ? - decoratorNode.expression : - null; - - const expectedDeclarationNode = getDeclaration( - 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); - expect(actualDeclaration !.viaModule).toBe('@angular/core'); - }); - - it('should find the "actual" declaration of an aliased variable identifier', () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const ngModuleRef = findIdentifier( - program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1', - isNgModulePropertyAssignment); - - const declaration = host.getDeclarationOfIdentifier(ngModuleRef !); - expect(declaration).not.toBe(null); - expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()'); - }); - }); - }); - - describe('getVariableValue', () => { - it('should find the "actual" declaration of an aliased variable identifier', () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const ngModuleRef = findVariableDeclaration( - program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1'); - - const value = host.getVariableValue(ngModuleRef !); - expect(value).not.toBe(null); - if (!value || !ts.isFunctionDeclaration(value.parent)) { - throw new Error( - `Expected result to be a function declaration: ${value && value.getText()}.`); + function findVariableDeclaration( + node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined { + if (!node) { + return; } - expect(value.getText()).toBe('HttpClientXsrfModule'); - }); - - it('should return undefined if the variable has no assignment', () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const missingValue = findVariableDeclaration( - program.getSourceFile(fileSystem.files[2].name) !, 'missingValue'); - const value = host.getVariableValue(missingValue !); - expect(value).toBe(null); - }); - - it('should return null if the variable is not assigned from a call to __decorate', () => { - const program = makeTestProgram(fileSystem.files[2]); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const nonDecoratedVar = findVariableDeclaration( - program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar'); - const value = host.getVariableValue(nonDecoratedVar !); - expect(value).toBe(null); - }); + if (isNamedVariableDeclaration(node) && node.name.text === variableName) { + return node; + } + return node.forEachChild(node => findVariableDeclaration(node, variableName)); + } }); + + function findIdentifier( + node: ts.Node | undefined, identifierName: string, + requireFn: (node: ts.Identifier) => boolean): ts.Identifier|undefined { + if (!node) { + return undefined; + } + if (ts.isIdentifier(node) && node.text === identifierName && requireFn(node)) { + return node; + } + return node.forEachChild(node => findIdentifier(node, identifierName, requireFn)); + } + + function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean { + return ts.isPropertyAssignment(identifier.parent) && + identifier.parent.name.getText() === 'ngModule'; + } }); - - function findVariableDeclaration( - node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined { - if (!node) { - return; - } - if (isNamedVariableDeclaration(node) && node.name.text === variableName) { - return node; - } - return node.forEachChild(node => findVariableDeclaration(node, variableName)); - } }); - -function findIdentifier( - node: ts.Node | undefined, identifierName: string, - requireFn: (node: ts.Identifier) => boolean): ts.Identifier|undefined { - if (!node) { - return undefined; - } - if (ts.isIdentifier(node) && node.text === identifierName && requireFn(node)) { - return node; - } - return node.forEachChild(node => findIdentifier(node, identifierName, requireFn)); -} - -function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean { - return ts.isPropertyAssignment(identifier.parent) && - identifier.parent.name.getText() === 'ngModule'; -} 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 05a1adb824..b09b4b1cda 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -8,17 +8,50 @@ import * as ts from 'typescript'; +import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {ClassMemberKind, CtorParameter, Decorator, Import, TsHelperFn, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; import {MockLogger} from '../helpers/mock_logger'; -import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; +import {getRootFiles, makeTestBundleProgram} from '../helpers/utils'; import {expectTypeValueReferencesForParameters} from './util'; -const SOME_DIRECTIVE_FILE = { - name: '/some_directive.js', - contents: ` +runInEachFileSystem(() => { + describe('Esm5ReflectionHost', () => { + + let _: typeof absoluteFrom; + + let SOME_DIRECTIVE_FILE: TestFile; + let ACCESSORS_FILE: TestFile; + let SIMPLE_ES2015_CLASS_FILE: TestFile; + let SIMPLE_CLASS_FILE: TestFile; + let TOPLEVEL_DECORATORS_FILE: TestFile; + let FOO_FUNCTION_FILE: TestFile; + let INVALID_DECORATORS_FILE: TestFile; + let INVALID_DECORATOR_ARGS_FILE: TestFile; + let INVALID_PROP_DECORATORS_FILE: TestFile; + let INVALID_PROP_DECORATOR_ARGS_FILE: TestFile; + let INVALID_CTOR_DECORATORS_FILE: TestFile; + let INVALID_CTOR_DECORATOR_ARGS_FILE: TestFile; + let IMPORTS_FILES: TestFile[]; + let EXPORTS_FILES: TestFile[]; + let FUNCTION_BODY_FILE: TestFile; + let DECORATED_FILES: TestFile[]; + let UNWANTED_PROTOTYPE_EXPORT_FILE: TestFile; + let TYPINGS_SRC_FILES: TestFile[]; + let TYPINGS_DTS_FILES: TestFile[]; + let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[]; + let NAMESPACED_IMPORT_FILE: TestFile; + + beforeEach(() => { + _ = absoluteFrom; + SOME_DIRECTIVE_FILE = { + name: _('/some_directive.js'), + contents: ` import { Directive, Inject, InjectionToken, Input } from '@angular/core'; var INJECTED_TOKEN = new InjectionToken('injected'); @@ -49,11 +82,11 @@ const SOME_DIRECTIVE_FILE = { return SomeDirective; }()); `, -}; + }; -const TOPLEVEL_DECORATORS_FILE = { - name: '/toplevel_decorators.js', - contents: ` + TOPLEVEL_DECORATORS_FILE = { + name: _('/toplevel_decorators.js'), + contents: ` import { Directive, Inject, InjectionToken, Input } from '@angular/core'; var INJECTED_TOKEN = new InjectionToken('injected'); @@ -77,11 +110,11 @@ const TOPLEVEL_DECORATORS_FILE = { "input2": [{ type: Input },], }; `, -}; + }; -const ACCESSORS_FILE = { - name: '/accessors.js', - contents: ` + ACCESSORS_FILE = { + name: _('/accessors.js'), + contents: ` import { Directive, Input, Output } from '@angular/core'; var SomeDirective = (function() { @@ -124,18 +157,18 @@ const ACCESSORS_FILE = { return SomeDirective; }()); `, -}; + }; -const SIMPLE_ES2015_CLASS_FILE = { - name: '/simple_es2015_class.d.ts', - contents: ` + SIMPLE_ES2015_CLASS_FILE = { + name: _('/simple_es2015_class.d.ts'), + contents: ` export class EmptyClass {} `, -}; + }; -const SIMPLE_CLASS_FILE = { - name: '/simple_class.js', - contents: ` + SIMPLE_CLASS_FILE = { + name: _('/simple_class.js'), + contents: ` var EmptyClass = (function() { function EmptyClass() { } @@ -147,11 +180,11 @@ const SIMPLE_CLASS_FILE = { return NoDecoratorConstructorClass; }()); `, -}; + }; -const FOO_FUNCTION_FILE = { - name: '/foo_function.js', - contents: ` + FOO_FUNCTION_FILE = { + name: _('/foo_function.js'), + contents: ` import { Directive } from '@angular/core'; function foo() {} @@ -159,11 +192,11 @@ const FOO_FUNCTION_FILE = { { type: Directive, args: [{ selector: '[ignored]' },] } ]; `, -}; + }; -const INVALID_DECORATORS_FILE = { - name: '/invalid_decorators.js', - contents: ` + INVALID_DECORATORS_FILE = { + name: _('/invalid_decorators.js'), + contents: ` import { Directive } from '@angular/core'; var NotArrayLiteral = (function() { function NotArrayLiteral() { @@ -204,11 +237,11 @@ const INVALID_DECORATORS_FILE = { return NotIdentifier; }()); `, -}; + }; -const INVALID_DECORATOR_ARGS_FILE = { - name: '/invalid_decorator_args.js', - contents: ` + INVALID_DECORATOR_ARGS_FILE = { + name: _('/invalid_decorator_args.js'), + contents: ` import { Directive } from '@angular/core'; var NoArgsProperty = (function() { function NoArgsProperty() { @@ -238,11 +271,11 @@ const INVALID_DECORATOR_ARGS_FILE = { return NotArrayLiteral; }()); `, -}; + }; -const INVALID_PROP_DECORATORS_FILE = { - name: '/invalid_prop_decorators.js', - contents: ` + INVALID_PROP_DECORATORS_FILE = { + name: _('/invalid_prop_decorators.js'), + contents: ` import { Input } from '@angular/core'; var NotObjectLiteral = (function() { function NotObjectLiteral() { @@ -289,11 +322,11 @@ const INVALID_PROP_DECORATORS_FILE = { return NotIdentifier; }()); `, -}; + }; -const INVALID_PROP_DECORATOR_ARGS_FILE = { - name: '/invalid_prop_decorator_args.js', - contents: ` + INVALID_PROP_DECORATOR_ARGS_FILE = { + name: _('/invalid_prop_decorator_args.js'), + contents: ` import { Input } from '@angular/core'; var NoArgsProperty = (function() { function NoArgsProperty() { @@ -323,11 +356,11 @@ const INVALID_PROP_DECORATOR_ARGS_FILE = { return NotArrayLiteral; }()); `, -}; + }; -const INVALID_CTOR_DECORATORS_FILE = { - name: '/invalid_ctor_decorators.js', - contents: ` + INVALID_CTOR_DECORATORS_FILE = { + name: _('/invalid_ctor_decorators.js'), + contents: ` import { Inject } from '@angular/core'; var NoParametersDecorator = {}; var NoParameters = (function() { @@ -391,11 +424,11 @@ const INVALID_CTOR_DECORATORS_FILE = { return NotIdentifier; }()); `, -}; + }; -const INVALID_CTOR_DECORATOR_ARGS_FILE = { - name: '/invalid_ctor_decorator_args.js', - contents: ` + INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: _('/invalid_ctor_decorator_args.js'), + contents: ` import { Inject } from '@angular/core'; var NoArgsProperty = (function() { function NoArgsProperty(arg1) { @@ -425,18 +458,26 @@ const INVALID_CTOR_DECORATOR_ARGS_FILE = { return NotArrayLiteral; }()); `, -}; + }; -const IMPORTS_FILES = [ - { - name: '/a.js', - contents: ` + IMPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + import * as a from './a'; + import * as b from './b'; + import * as c from './c'; + ` + }, + { + name: _('/a.js'), + contents: ` export var a = 'a'; `, - }, - { - name: '/b.js', - contents: ` + }, + { + name: _('/b.js'), + contents: ` import {a} from './a.js'; import {a as foo} from './a.js'; @@ -444,19 +485,26 @@ const IMPORTS_FILES = [ var c = foo; var d = b; `, - }, -]; + }, + ]; -const EXPORTS_FILES = [ - { - name: '/a.js', - contents: ` + EXPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + import * as a from './a'; + import * as b from './b'; + ` + }, + { + name: _('/a.js'), + contents: ` export var a = 'a'; `, - }, - { - name: '/b.js', - contents: ` + }, + { + name: _('/b.js'), + contents: ` import {Directive} from '@angular/core'; import {a} from './a'; import {a as foo} from './a'; @@ -472,12 +520,12 @@ const EXPORTS_FILES = [ return SomeClass; }()); `, - }, -]; + }, + ]; -const FUNCTION_BODY_FILE = { - name: '/function_body.js', - contents: ` + FUNCTION_BODY_FILE = { + name: _('/function_body.js'), + contents: ` function foo(x) { return x; } @@ -510,14 +558,14 @@ const FUNCTION_BODY_FILE = { return x; } ` -}; + }; -const DECORATED_FILES = [ - { - name: '/primary.js', - contents: ` + DECORATED_FILES = [ + { + name: _('/primary.js'), + contents: ` import {Directive} from '@angular/core'; - import { D } from '/secondary'; + import { D } from './secondary'; var A = (function() { function A() {} A.decorators = [ @@ -540,10 +588,10 @@ const DECORATED_FILES = [ }); export { A, x, C }; ` - }, - { - name: '/secondary.js', - contents: ` + }, + { + name: _('/secondary.js'), + contents: ` import {Directive} from '@angular/core'; var D = (function() { function D() {} @@ -554,27 +602,33 @@ const DECORATED_FILES = [ }()); export { D }; ` - } -]; + } + ]; -const UNWANTED_PROTOTYPE_EXPORT_FILE = { - name: '/library.d.ts', - contents: ` + UNWANTED_PROTOTYPE_EXPORT_FILE = { + name: _('/library.d.ts'), + contents: ` export declare class SomeParam { someInstanceMethod(): void; static someStaticProp: any; }` -}; + }; -const TYPINGS_SRC_FILES = [ - { - name: '/src/index.js', - contents: - `import {InternalClass} from './internal'; export * from './class1'; export * from './class2';` - }, - { - name: '/src/class1.js', - contents: ` + TYPINGS_SRC_FILES = [ + { + name: _('/src/index.js'), + contents: ` + import {InternalClass} from './internal'; + import * as func1 from './func1'; + import * as missing from './missing-class'; + import * as flatFile from './flat-file'; + export * from './class1'; + export * from './class2'; + ` + }, + { + name: _('/src/class1.js'), + contents: ` var Class1 = (function() { function Class1() {} return Class1; @@ -585,20 +639,20 @@ const TYPINGS_SRC_FILES = [ }()); export {Class1, MissingClass1}; ` - }, - { - name: '/src/class2.js', - contents: ` + }, + { + name: _('/src/class2.js'), + contents: ` var Class2 = (function() { function Class2() {} return Class2; }()); export {Class2}; ` - }, - {name: '/src/func1.js', contents: 'function mooFn() {} export {mooFn}'}, { - name: '/src/internal.js', - contents: ` + }, + {name: _('/src/func1.js'), contents: 'function mooFn() {} export {mooFn}'}, { + name: _('/src/internal.js'), + contents: ` var InternalClass = (function() { function InternalClass() {} return InternalClass; @@ -609,20 +663,20 @@ const TYPINGS_SRC_FILES = [ }()); export {InternalClass, Class2}; ` - }, - { - name: '/src/missing-class.js', - contents: ` + }, + { + name: _('/src/missing-class.js'), + contents: ` var MissingClass2 = (function() { function MissingClass2() {} return MissingClass2; }()); export {MissingClass2}; ` - }, - { - name: '/src/flat-file.js', - contents: ` + }, + { + name: _('/src/flat-file.js'), + contents: ` var Class1 = (function() { function Class1() {} return Class1; @@ -641,36 +695,48 @@ const TYPINGS_SRC_FILES = [ }()); export {Class1, Class3 as xClass3, MissingClass1, 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 {}`}, -]; + TYPINGS_DTS_FILES = [ + { + name: _('/typings/index.d.ts'), + contents: ` + import {InternalClass} from './internal'; + import {mooFn} from './func1'; + 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: ` + MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: _('/src/index.js'), + contents: ` + import * as functions from './functions'; + import * as methods from './methods'; + import * as aliased_class from './aliased_class'; + ` + }, + { + name: _('/src/functions.js'), + contents: ` import {ExternalModule} from './module'; import * as mod from './module'; @@ -697,10 +763,10 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ export function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } export {SomeService, InternalModule}; ` - }, - { - name: '/src/methods.js', - contents: ` + }, + { + name: _('/src/methods.js'), + contents: ` import {ExternalModule} from './module'; import * as mod from './module'; var SomeService = (function() { @@ -733,10 +799,10 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ }()); export {SomeService, InternalModule}; ` - }, - { - name: '/src/aliased_class.js', - contents: ` + }, + { + name: _('/src/aliased_class.js'), + contents: ` var AliasedModule = (function() { function AliasedModule() {} AliasedModule_1 = AliasedModule; @@ -746,13 +812,13 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ }()); export { AliasedModule }; ` - }, - {name: '/src/module.js', contents: 'export class ExternalModule {}'}, -]; + }, + {name: _('/src/module.js'), contents: 'export class ExternalModule {}'}, + ]; -const NAMESPACED_IMPORT_FILE = { - name: '/some_directive.js', - contents: ` + NAMESPACED_IMPORT_FILE = { + name: _('/some_directive.js'), + contents: ` import * as core from '@angular/core'; var SomeDirective = (function() { @@ -764,644 +830,688 @@ const NAMESPACED_IMPORT_FILE = { return SomeDirective; }()); ` -}; - -describe('Esm5ReflectionHost', () => { - - describe('getDecoratorsOfDeclaration()', () => { - it('should find the decorators on a class', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 find the decorators on a class at the top level', () => { - const program = makeTestProgram(TOPLEVEL_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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 = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 mockImportInfo = { name: 'mock', from: '@angular/core' } as Import; - const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_DECORATOR_ARGS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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(input2.decorators !.map(d => d.name)).toEqual(['Input']); - }); - - it('should find decorated members on a class at the top level', () => { - const program = makeTestProgram(TOPLEVEL_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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(input2.decorators !.map(d => d.name)).toEqual(['Input']); - }); - - it('should find Object.defineProperty members on a class', () => { - const program = makeTestProgram(ACCESSORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = - getDeclaration(program, ACCESSORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); - const members = host.getMembersOfClass(classNode); - - const setter = members.find(member => member.name === 'setter') !; - expect(setter.kind).toEqual(ClassMemberKind.Setter); - expect(setter.isStatic).toEqual(false); - expect(setter.value).toBeNull(); - expect(setter.decorators !.map(d => d.name)).toEqual(['Input']); - expect(ts.isFunctionExpression(setter.implementation !)).toEqual(true); - expect((setter.implementation as ts.FunctionExpression).body.statements[0].getText()) - .toEqual('this.value = value;'); - - const getter = members.find(member => member.name === 'getter') !; - expect(getter.kind).toEqual(ClassMemberKind.Getter); - expect(getter.isStatic).toEqual(false); - expect(getter.value).toBeNull(); - expect(getter.decorators !.map(d => d.name)).toEqual(['Output']); - expect(ts.isFunctionExpression(getter.implementation !)).toEqual(true); - expect((getter.implementation as ts.FunctionExpression).body.statements[0].getText()) - .toEqual('return null;'); - - const [combinedSetter, combinedGetter] = - members.filter(member => member.name === 'setterAndGetter'); - expect(combinedSetter.kind).toEqual(ClassMemberKind.Setter); - expect(combinedSetter.isStatic).toEqual(false); - expect(combinedSetter.decorators !.map(d => d.name)).toEqual(['Input']); - expect(combinedGetter.kind).toEqual(ClassMemberKind.Getter); - expect(combinedGetter.isStatic).toEqual(false); - expect(combinedGetter.decorators !.map(d => d.name)).toEqual([]); - - const staticSetter = members.find(member => member.name === 'staticSetter') !; - expect(staticSetter.kind).toEqual(ClassMemberKind.Setter); - expect(staticSetter.isStatic).toEqual(true); - expect(staticSetter.value).toBeNull(); - expect(staticSetter.decorators !.map(d => d.name)).toEqual([]); - - const none = members.find(member => member.name === 'none'); - expect(none).toBeUndefined(); - - const incomplete = members.find(member => member.name === 'incomplete'); - expect(incomplete).toBeUndefined(); - }); - - it('should find non decorated properties on a class', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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(staticMethod.value).toBeNull(); - expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true); - }); - - it('should find static properties on a class', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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: 'Input'})); - }); - - it('should ignore prop decorator elements that have no `type` property', () => { - const program = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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: 'Input'})); - }); - - it('should ignore prop decorator elements whose `type` value is not an identifier', () => { - const program = makeTestProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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: 'Input'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - let callCount = 0; - const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { - callCount++; - return {name: `name${callCount}`, from: `@angular/core`}; - }); - - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); - const members = host.getMembersOfClass(classNode); - - expect(spy).toHaveBeenCalled(); - spy.calls.allArgs().forEach(arg => expect(arg[0].getText()).toEqual('Input')); - - const index = members.findIndex(member => member.name === 'input1'); - expect(members[index].decorators !.length).toBe(1); - expect(members[index].decorators ![0].import).toEqual({name: 'name1', from: '@angular/core'}); - }); - - describe('(returned prop decorators `args`)', () => { - it('should be an empty array if prop decorator has no `args` property', () => { - const program = makeTestProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_PROP_DECORATOR_ARGS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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([]); - }); - }); - - it('should ignore the prototype pseudo-static property on class imported from typings files', - () => { - const program = makeTestProgram(UNWANTED_PROTOTYPE_EXPORT_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, UNWANTED_PROTOTYPE_EXPORT_FILE.name, 'SomeParam', isNamedClassDeclaration); - const members = host.getMembersOfClass(classNode); - expect(members.find(m => m.name === 'prototype')).toBeUndefined(); - }); - }); - - describe('getConstructorParameters()', () => { - it('should find the decorated constructor parameters', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 find the decorated constructor parameters at the top level', () => { - const program = makeTestProgram(TOPLEVEL_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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 = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 = makeTestProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 mockImportInfo = { name: 'mock', from: '@angulare/core' } as Import; - const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeTestProgram(SOME_DIRECTIVE_FILE); + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![2].decorators !; + 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 find the decorators on a class at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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 mockImportInfo = { name: 'mock', from: '@angular/core' } as Import; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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('Inject'); + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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('synthesized constructors', () => { - function getConstructorParameters(constructor: string) { - const file = { - name: '/synthesized_constructors.js', - contents: ` + describe('getMembersOfClass()', () => { + it('should find decorated members on a class at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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(input2.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find decorated members on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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(input2.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find Object.defineProperty members on a class', () => { + loadTestFiles([ACCESSORS_FILE]); + const {program} = makeTestBundleProgram(ACCESSORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, ACCESSORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const setter = members.find(member => member.name === 'setter') !; + expect(setter.kind).toEqual(ClassMemberKind.Setter); + expect(setter.isStatic).toEqual(false); + expect(setter.value).toBeNull(); + expect(setter.decorators !.map(d => d.name)).toEqual(['Input']); + expect(ts.isFunctionExpression(setter.implementation !)).toEqual(true); + expect((setter.implementation as ts.FunctionExpression).body.statements[0].getText()) + .toEqual('this.value = value;'); + + const getter = members.find(member => member.name === 'getter') !; + expect(getter.kind).toEqual(ClassMemberKind.Getter); + expect(getter.isStatic).toEqual(false); + expect(getter.value).toBeNull(); + expect(getter.decorators !.map(d => d.name)).toEqual(['Output']); + expect(ts.isFunctionExpression(getter.implementation !)).toEqual(true); + expect((getter.implementation as ts.FunctionExpression).body.statements[0].getText()) + .toEqual('return null;'); + + const [combinedSetter, combinedGetter] = + members.filter(member => member.name === 'setterAndGetter'); + expect(combinedSetter.kind).toEqual(ClassMemberKind.Setter); + expect(combinedSetter.isStatic).toEqual(false); + expect(combinedSetter.decorators !.map(d => d.name)).toEqual(['Input']); + expect(combinedGetter.kind).toEqual(ClassMemberKind.Getter); + expect(combinedGetter.isStatic).toEqual(false); + expect(combinedGetter.decorators !.map(d => d.name)).toEqual([]); + + const staticSetter = members.find(member => member.name === 'staticSetter') !; + expect(staticSetter.kind).toEqual(ClassMemberKind.Setter); + expect(staticSetter.isStatic).toEqual(true); + expect(staticSetter.value).toBeNull(); + expect(staticSetter.decorators !.map(d => d.name)).toEqual([]); + + const none = members.find(member => member.name === 'none'); + expect(none).toBeUndefined(); + + const incomplete = members.find(member => member.name === 'incomplete'); + expect(incomplete).toBeUndefined(); + }); + + it('should find non decorated properties on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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(staticMethod.value).toBeNull(); + expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', + () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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: 'Input'})); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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: 'Input'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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: 'Input'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + let callCount = 0; + const spy = + spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { + callCount++; + return {name: `name${callCount}`, from: `@angular/core`}; + }); + + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(spy).toHaveBeenCalled(); + spy.calls.allArgs().forEach(arg => expect(arg[0].getText()).toEqual('Input')); + + const index = members.findIndex(member => member.name === 'input1'); + expect(members[index].decorators !.length).toBe(1); + expect(members[index].decorators ![0].import) + .toEqual({name: 'name1', from: '@angular/core'}); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', + () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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([]); + }); + }); + + it('should ignore the prototype pseudo-static property on class imported from typings files', + () => { + loadTestFiles([UNWANTED_PROTOTYPE_EXPORT_FILE]); + const {program} = makeTestBundleProgram(UNWANTED_PROTOTYPE_EXPORT_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, UNWANTED_PROTOTYPE_EXPORT_FILE.name, 'SomeParam', isNamedClassDeclaration); + const members = host.getMembersOfClass(classNode); + expect(members.find(m => m.name === 'prototype')).toBeUndefined(); + }); + }); + + describe('getConstructorParameters()', () => { + it('should find the decorated constructor parameters', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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 find the decorated constructor parameters at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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 mockImportInfo = { name: 'mock', from: '@angulare/core' } as Import; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + 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('synthesized constructors', () => { + function getConstructorParameters(constructor: string) { + const file = { + name: _('/synthesized_constructors.js'), + contents: ` var TestClass = /** @class */ (function (_super) { __extends(TestClass, _super); ${constructor} return TestClass; }(null)); `, - }; + }; - const program = makeTestProgram(file); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const classNode = - getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); - return host.getConstructorParameters(classNode); - } + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + return host.getConstructorParameters(classNode); + } - it('recognizes _this assignment from super call', () => { - const parameters = getConstructorParameters(` + it('recognizes _this assignment from super call', () => { + const parameters = getConstructorParameters(` function TestClass() { var _this = _super !== null && _super.apply(this, arguments) || this; _this.synthesizedProperty = null; return _this; }`); - expect(parameters).toBeNull(); - }); + expect(parameters).toBeNull(); + }); - it('recognizes super call as return statement', () => { - const parameters = getConstructorParameters(` + it('recognizes super call as return statement', () => { + const parameters = getConstructorParameters(` function TestClass() { return _super !== null && _super.apply(this, arguments) || this; }`); - expect(parameters).toBeNull(); - }); + expect(parameters).toBeNull(); + }); - it('handles the case where a unique name was generated for _super or _this', () => { - const parameters = getConstructorParameters(` + it('handles the case where a unique name was generated for _super or _this', () => { + const parameters = getConstructorParameters(` function TestClass() { var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this; _this_1._this = null; @@ -1409,665 +1519,710 @@ describe('Esm5ReflectionHost', () => { return _this_1; }`); - expect(parameters).toBeNull(); - }); + expect(parameters).toBeNull(); + }); - it('does not consider constructors with parameters as synthesized', () => { - const parameters = getConstructorParameters(` + it('does not consider constructors with parameters as synthesized', () => { + const parameters = getConstructorParameters(` function TestClass(arg) { return _super !== null && _super.apply(this, arguments) || this; }`); - expect(parameters !.length).toBe(1); - }); + expect(parameters !.length).toBe(1); + }); - it('does not consider manual super calls as synthesized', () => { - const parameters = getConstructorParameters(` + it('does not consider manual super calls as synthesized', () => { + const parameters = getConstructorParameters(` function TestClass() { return _super.call(this) || this; }`); - expect(parameters !.length).toBe(0); - }); + expect(parameters !.length).toBe(0); + }); - it('does not consider empty constructors as synthesized', () => { - const parameters = getConstructorParameters(` + it('does not consider empty constructors as synthesized', () => { + const parameters = getConstructorParameters(` function TestClass() { }`); - expect(parameters !.length).toBe(0); + expect(parameters !.length).toBe(0); + }); + }); + + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', + () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program} = makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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('(returned parameters `decorators.args`)', () => { - it('should be an empty array if param decorator has no `args` property', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', + () => { + loadTestFiles([FUNCTION_BODY_FILE]); + const {program} = makeTestBundleProgram(FUNCTION_BODY_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + + 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); + }); + + it('should recognize TypeScript __spread helper function declaration', () => { + const file: TestFile = { + name: _('/declaration.d.ts'), + contents: `export declare function __spread(...args: any[]): any[];`, + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + + const node = getDeclaration(program, file.name, '__spread', isNamedFunctionDeclaration) !; + + const definition = host.getDefinitionOfFunction(node) !; + expect(definition.node).toBe(node); + expect(definition.body).toBeNull(); + expect(definition.helper).toBe(TsHelperFn.Spread); + expect(definition.parameters.length).toEqual(0); + }); + + it('should recognize TypeScript __spread helper function implementation', () => { + const file: TestFile = { + name: _('/implementation.js'), + contents: ` + var __spread = (this && this.__spread) || function () { + for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); + return ar; + };`, + }; + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + + const node = getDeclaration(program, file.name, '__spread', ts.isVariableDeclaration) !; + + const definition = host.getDefinitionOfFunction(node) !; + expect(definition.node).toBe(node); + expect(definition.body).toBeNull(); + expect(definition.helper).toBe(TsHelperFn.Spread); + expect(definition.parameters.length).toEqual(0); + }); + }); + + describe('getImportOfIdentifier()', () => { + it('should find the import of an identifier', () => { + loadTestFiles(IMPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const variableNode = getDeclaration(program, _('/b.js'), 'b', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should find the name by which the identifier was exported, not imported', () => { + loadTestFiles(IMPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const variableNode = getDeclaration(program, _('/b.js'), 'c', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should return null if the identifier was not imported', () => { + loadTestFiles(IMPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const variableNode = getDeclaration(program, _('/b.js'), 'd', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); + }); + + describe('getDeclarationOfIdentifier()', () => { + it('should return the declaration of a locally defined identifier', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); 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 !; + 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; - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Inject'); - expect(decorators[0].args).toEqual([]); + 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 be an empty array if param decorator\'s `args` has no property assignment', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + it('should return the declaration of an externally defined identifier', () => { + loadFakeCore(getFileSystem()); + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - isNamedVariableDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; + 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.Identifier; - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Inject'); - expect(decorators[0].args).toEqual([]); + const expectedDeclarationNode = getDeclaration( + 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); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); }); - it('should be an empty array if `args` property value is not an array literal', () => { - const program = makeTestProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + it('should return the source-file of an import namespace', () => { + loadFakeCore(getFileSystem()); + loadTestFiles([NAMESPACED_IMPORT_FILE]); + const {program} = makeTestBundleProgram(NAMESPACED_IMPORT_FILE.name); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const classNode = getDeclaration( - program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', - isNamedVariableDeclaration); - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![0].decorators !; + 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; - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Inject'); - expect(decorators[0].args).toEqual([]); + const expectedDeclarationNode = + getSourceFileOrError(program, _('/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); }); - }); - }); - describe('getDefinitionOfFunction()', () => { - it('should return an object describing the function declaration passed as an argument', () => { - const program = makeTestProgram(FUNCTION_BODY_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + it('should return the correct declaration for an inner function identifier inside an ES5 IIFE', + () => { + const superGetDeclarationOfIdentifierSpy = + spyOn(Esm2015ReflectionHost.prototype, 'getDeclarationOfIdentifier') + .and.callThrough(); + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 outerDeclaration = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerDeclaration = (((outerDeclaration.initializer as ts.ParenthesizedExpression) + .expression as ts.CallExpression) + .expression as ts.FunctionExpression) + .body.statements[0] as ts.FunctionDeclaration; - 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 outerIdentifier = outerDeclaration.name as ts.Identifier; + const innerIdentifier = innerDeclaration.name as ts.Identifier; - 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); + expect(host.getDeclarationOfIdentifier(outerIdentifier) !.node).toBe(outerDeclaration); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledWith(outerIdentifier); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledTimes(1); - 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); + superGetDeclarationOfIdentifierSpy.calls.reset(); + + expect(host.getDeclarationOfIdentifier(innerIdentifier) !.node).toBe(outerDeclaration); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledWith(outerIdentifier); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledTimes(1); + }); }); - it('should recognize TypeScript __spread helper function declaration', () => { - const file = { - name: 'declaration.d.ts', - contents: `export declare function __spread(...args: any[]): any[];`, - }; - const program = makeTestProgram(file); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + loadFakeCore(getFileSystem()); + loadTestFiles(EXPORTS_FILES); + const {program} = makeTestBundleProgram(_('/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/b.js')); + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.keys())).toEqual([ + 'Directive', + 'a', + 'b', + 'c', + 'd', + 'e', + 'DirectiveX', + 'SomeClass', + ]); - const node = getDeclaration(program, file.name, '__spread', isNamedFunctionDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Spread); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __spread helper function implementation', () => { - const file = { - name: 'implementation.js', - contents: ` - var __spread = (this && this.__spread) || function () { - for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); - return ar; - };`, - }; - const program = makeTestProgram(file); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - - const node = getDeclaration(program, file.name, '__spread', ts.isVariableDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Spread); - expect(definition.parameters.length).toEqual(0); - }); - }); - - describe('getImportOfIdentifier()', () => { - it('should find the import of an identifier', () => { - const program = makeTestProgram(...IMPORTS_FILES); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'b', isNamedVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - - expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); - }); - - it('should find the name by which the identifier was exported, not imported', () => { - const program = makeTestProgram(...IMPORTS_FILES); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'c', isNamedVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - - expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); - }); - - it('should return null if the identifier was not imported', () => { - const program = makeTestProgram(...IMPORTS_FILES); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const variableNode = - getDeclaration(program, IMPORTS_FILES[1].name, 'd', isNamedVariableDeclaration); - const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); - - expect(importOfIdent).toBeNull(); - }); - }); - - describe('getDeclarationOfIdentifier()', () => { - it('should return the declaration of a locally defined identifier', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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 declaration of an externally defined identifier', () => { - const program = makeTestProgram(SOME_DIRECTIVE_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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.Identifier; - - const expectedDeclarationNode = getDeclaration( - 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); - 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 = - spyOn(Esm2015ReflectionHost.prototype, 'getDeclarationOfIdentifier').and.callThrough(); - const program = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - - const outerDeclaration = getDeclaration( - program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerDeclaration = (((outerDeclaration.initializer as ts.ParenthesizedExpression) - .expression as ts.CallExpression) - .expression as ts.FunctionExpression) - .body.statements[0] as ts.FunctionDeclaration; - - const outerIdentifier = outerDeclaration.name as ts.Identifier; - const innerIdentifier = innerDeclaration.name as ts.Identifier; - - expect(host.getDeclarationOfIdentifier(outerIdentifier) !.node).toBe(outerDeclaration); - expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledWith(outerIdentifier); - expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledTimes(1); - - superGetDeclarationOfIdentifierSpy.calls.reset(); - - expect(host.getDeclarationOfIdentifier(innerIdentifier) !.node).toBe(outerDeclaration); - expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledWith(outerIdentifier); - expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('getExportsOfModule()', () => { - it('should return a map of all the exports from a given module', () => { - const program = makeTestProgram(...EXPORTS_FILES); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const file = program.getSourceFile(EXPORTS_FILES[1].name) !; - const exportDeclarations = host.getExportsOfModule(file); - expect(exportDeclarations).not.toBe(null); - expect(Array.from(exportDeclarations !.keys())).toEqual([ - 'Directive', - 'a', - 'b', - 'c', - 'd', - 'e', - 'DirectiveX', - 'SomeClass', - ]); - - const values = Array.from(exportDeclarations !.values()) - .map(declaration => [declaration.node.getText(), declaration.viaModule]); - expect(values).toEqual([ - [`Directive: FnWithArg<(clazz: any) => any>`, null], - [`a = 'a'`, null], - [`b = a`, null], - [`c = foo`, null], - [`d = b`, null], - [`e = 'e'`, null], - [`DirectiveX = Directive`, null], - [ - `SomeClass = (function() { + const values = Array.from(exportDeclarations !.values()) + .map(declaration => [declaration.node.getText(), declaration.viaModule]); + expect(values).toEqual([ + [`Directive: FnWithArg<(clazz: any) => any>`, null], + [`a = 'a'`, null], + [`b = a`, null], + [`c = foo`, null], + [`d = b`, null], + [`e = 'e'`, null], + [`DirectiveX = Directive`, null], + [ + `SomeClass = (function() { function SomeClass() {} return SomeClass; }())`, - null - ], - ]); - }); - }); - - describe('getClassSymbol()', () => { - it('should return the class symbol for an ES2015 class', () => { - const program = makeTestProgram(SIMPLE_ES2015_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = getDeclaration( - program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); - const classSymbol = host.getClassSymbol(node); - - expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + null + ], + ]); + }); }); - it('should return the class symbol for an ES5 class (outer variable declaration)', () => { - const program = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const classSymbol = host.getClassSymbol(node); + describe('getClassSymbol()', () => { + it('should return the class symbol for an ES2015 class', () => { + loadTestFiles([SIMPLE_ES2015_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const classSymbol = host.getClassSymbol(node); - expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for an ES5 class (outer variable declaration)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + 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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); }); - it('should return the class symbol for an ES5 class (inner function declaration)', () => { - const program = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const outerNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; - const classSymbol = host.getClassSymbol(innerNode); + describe('isClass()', () => { + let host: Esm5ReflectionHost; + let mockNode: ts.Node; + let getClassDeclarationSpy: jasmine.Spy; + let superGetClassDeclarationSpy: jasmine.Spy; - expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(outerNode); + beforeEach(() => { + host = new Esm5ReflectionHost(new MockLogger(), false, null as any); + mockNode = {} as any; + + getClassDeclarationSpy = spyOn(Esm5ReflectionHost.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); + }); }); - it('should return the same class symbol (of the outer declaration) for outer and inner declarations', - () => { - const program = makeTestProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const outerNode = getDeclaration( - program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + describe('hasBaseClass()', () => { + function hasBaseClass(source: string) { + const file = { + name: _('/synthesized_constructors.js'), + contents: source, + }; - expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); - }); + loadTestFiles([file]); + const {program} = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = + getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + return host.hasBaseClass(classNode); + } - it('should return undefined if node is not an ES5 class', () => { - const program = makeTestProgram(FOO_FUNCTION_FILE); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = - getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); - const classSymbol = host.getClassSymbol(node); - - expect(classSymbol).toBeUndefined(); - }); - }); - - describe('isClass()', () => { - let host: Esm5ReflectionHost; - let mockNode: ts.Node; - let getClassDeclarationSpy: jasmine.Spy; - let superGetClassDeclarationSpy: jasmine.Spy; - - beforeEach(() => { - host = new Esm5ReflectionHost(new MockLogger(), false, null as any); - mockNode = {} as any; - - getClassDeclarationSpy = spyOn(Esm5ReflectionHost.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 = makeTestProgram(file); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - 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(` + 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); - }); + 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(` + 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); - }); + expect(result).toBe(true); + }); - it('should not consider an IIFE without parameter as having a base class', () => { - const result = hasBaseClass(` + 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('findClassSymbols()', () => { - it('should return an array of all classes in the given source file', () => { - const program = makeTestProgram(...DECORATED_FILES); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; - - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - expect(classSymbolsPrimary.length).toEqual(2); - expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B']); - - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - expect(classSymbolsSecondary.length).toEqual(1); - expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); - }); - }); - - describe('getDecoratorsOfSymbol()', () => { - it('should return decorators of class symbol', () => { - const program = makeTestProgram(...DECORATED_FILES); - const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; - - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsPrimary.length).toEqual(2); - expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); - expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); - - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - const classDecoratorsSecondary = - classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsSecondary.length).toEqual(1); - expect(classDecoratorsSecondary[0] !.map(d => d.name)).toEqual(['Directive']); - }); - }); - - describe('getDtsDeclarationsOfClass()', () => { - it('should find the dts declaration that has the same relative path to the source file', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class1 = - getDeclaration(srcProgram, '/src/class1.js', 'Class1', ts.isVariableDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); - - const dtsDeclaration = host.getDtsDeclaration(class1); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + expect(result).toBe(false); + }); }); - it('should find the dts declaration for exported functions', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dtsProgram = makeTestBundleProgram(TYPINGS_DTS_FILES); - const mooFn = getDeclaration(srcProgram, '/src/func1.js', 'mooFn', ts.isFunctionDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dtsProgram); + describe('findClassSymbols()', () => { + it('should return an array of all classes in the given source file', () => { + loadTestFiles(DECORATED_FILES); + const {program} = makeTestBundleProgram(getRootFiles(DECORATED_FILES)[0]); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - const dtsDeclaration = host.getDtsDeclaration(mooFn); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/func1.d.ts'); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + expect(classSymbolsPrimary.length).toEqual(2); + expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B']); + + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + expect(classSymbolsSecondary.length).toEqual(1); + expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); + }); }); - it('should return null if there is no matching class in the matching dts file', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const missingClass = - getDeclaration(srcProgram, '/src/class1.js', 'MissingClass1', ts.isVariableDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); + describe('getDecoratorsOfSymbol()', () => { + it('should return decorators of class symbol', () => { + loadTestFiles(DECORATED_FILES); + const {program} = makeTestBundleProgram(getRootFiles(DECORATED_FILES)[0]); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - expect(host.getDtsDeclaration(missingClass)).toBe(null); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsPrimary.length).toEqual(2); + expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); + expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); + + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + const classDecoratorsSecondary = + classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsSecondary.length).toEqual(1); + expect(classDecoratorsSecondary[0] !.map(d => d.name)).toEqual(['Directive']); + }); }); - it('should return null if there is no matching dts file', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const missingClass = getDeclaration( - srcProgram, '/src/missing-class.js', 'MissingClass2', ts.isVariableDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); + describe('getDtsDeclarationsOfClass()', () => { + it('should find the dts declaration that has the same relative path to the source file', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class1 = + getDeclaration(program, _('/src/class1.js'), 'Class1', ts.isVariableDeclaration); + const host = + new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); - expect(host.getDtsDeclaration(missingClass)).toBe(null); + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/class1.d.ts')); + }); + + it('should find the dts declaration for exported functions', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const mooFn = + getDeclaration(program, _('/src/func1.js'), 'mooFn', ts.isFunctionDeclaration); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); + + 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', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const missingClass = + getDeclaration(program, _('/src/class1.js'), 'MissingClass1', ts.isVariableDeclaration); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); + + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); + + it('should return null if there is no matching dts file', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const missingClass = getDeclaration( + program, _('/src/missing-class.js'), 'MissingClass2', ts.isVariableDeclaration); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class1 = + getDeclaration(program, _('/src/flat-file.js'), 'Class1', ts.isVariableDeclaration); + const host = + new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual(_('/typings/class1.d.ts')); + }); + + it('should find aliased exports', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class3 = + getDeclaration(program, _('/src/flat-file.js'), 'Class3', ts.isVariableDeclaration); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const internalClass = getDeclaration( + program, _('/src/internal.js'), 'InternalClass', ts.isVariableDeclaration); + const host = + new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), 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', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program} = makeTestBundleProgram(getRootFiles(TYPINGS_SRC_FILES)[0]); + const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]); + const class2 = + getDeclaration(program, _('/src/class2.js'), 'Class2', ts.isVariableDeclaration); + const internalClass2 = + getDeclaration(program, _('/src/internal.js'), 'Class2', ts.isVariableDeclaration); + const host = + new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker(), 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')); + }); }); - it('should find the dts file that contains a matching class declaration, even if the source files do not match', - () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class1 = - getDeclaration(srcProgram, '/src/flat-file.js', 'Class1', ts.isVariableDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), dts); + describe('getModuleWithProvidersFunctions', () => { + it('should find every exported function that returns an object that looks like a ModuleWithProviders object', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program} = makeTestBundleProgram(_('/src/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/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'], + ['namespacedExternalNgModule', 'ExternalModule'], + ]); + }); - const dtsDeclaration = host.getDtsDeclaration(class1); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); - }); + it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program} = makeTestBundleProgram(_('/src/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/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: ExternalModule }; }', + 'ExternalModule', + ], + [ + 'function() { return { ngModule: mod.ExternalModule }; }', + 'ExternalModule', + ], + ]); + }); - it('should find aliased exports', () => { - const srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class3 = - getDeclaration(srcProgram, '/src/flat-file.js', 'Class3', ts.isVariableDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), 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 srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const internalClass = getDeclaration( - srcProgram, '/src/internal.js', 'InternalClass', ts.isVariableDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), 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 srcProgram = makeTestProgram(...TYPINGS_SRC_FILES); - const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); - const class2 = - getDeclaration(srcProgram, '/src/class2.js', 'Class2', ts.isVariableDeclaration); - const internalClass2 = - getDeclaration(srcProgram, '/src/internal.js', 'Class2', ts.isVariableDeclaration); - const host = - new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker(), 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 srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); - const host = new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker()); - const file = srcProgram.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'], - ['namespacedExternalNgModule', 'ExternalModule'], - ]); - }); - - it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', - () => { - const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); - const host = new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker()); - const file = srcProgram.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: ExternalModule }; }', - 'ExternalModule', - ], - [ - 'function() { return { ngModule: mod.ExternalModule }; }', - 'ExternalModule', - ], - ]); - }); - - // https://github.com/angular/angular/issues/29078 - it('should resolve aliased module references to their original declaration', () => { - const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); - const host = new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker()); - const file = srcProgram.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'], - ]); + // https://github.com/angular/angular/issues/29078 + it('should resolve aliased module references to their original declaration', () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program} = makeTestBundleProgram(_('/src/index.js')); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const file = getSourceFileOrError(program, _('/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'], + ]); + }); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index deb5057a0e..2edceabf97 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -8,18 +8,48 @@ import * as ts from 'typescript'; +import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; 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 {getRootFiles, makeTestBundleProgram} from '../helpers/utils'; import {expectTypeValueReferencesForParameters} from './util'; -const SOME_DIRECTIVE_FILE = { - name: '/some_directive.umd.js', - contents: ` +runInEachFileSystem(() => { + describe('UmdReflectionHost', () => { + let _: typeof absoluteFrom; + + let SOME_DIRECTIVE_FILE: TestFile; + let TOPLEVEL_DECORATORS_FILE: TestFile; + let SIMPLE_ES2015_CLASS_FILE: TestFile; + let SIMPLE_CLASS_FILE: TestFile; + let FOO_FUNCTION_FILE: TestFile; + let INVALID_DECORATORS_FILE: TestFile; + let INVALID_DECORATOR_ARGS_FILE: TestFile; + let INVALID_PROP_DECORATORS_FILE: TestFile; + let INVALID_PROP_DECORATOR_ARGS_FILE: TestFile; + let INVALID_CTOR_DECORATORS_FILE: TestFile; + let INVALID_CTOR_DECORATOR_ARGS_FILE: TestFile; + let IMPORTS_FILES: TestFile[]; + let EXPORTS_FILES: TestFile[]; + let FUNCTION_BODY_FILE: TestFile; + let DECORATED_FILES: TestFile[]; + let TYPINGS_SRC_FILES: TestFile[]; + let TYPINGS_DTS_FILES: TestFile[]; + let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[]; + + beforeEach(() => { + _ = absoluteFrom; + + 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) : @@ -55,18 +85,18 @@ const SOME_DIRECTIVE_FILE = { }()); exports.SomeDirective = SomeDirective; })));`, -}; + }; -const TOPLEVEL_DECORATORS_FILE = { - name: '/toplevel_decorators.umd.js', - contents: ` + TOPLEVEL_DECORATORS_FILE = { + name: _('/toplevel_decorators.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 INJECTED_TOKEN = new core.InjectionToken('injected'); var ViewContainerRef = {}; var TemplateRef = {}; @@ -88,18 +118,19 @@ const TOPLEVEL_DECORATORS_FILE = { }; exports.SomeDirective = SomeDirective; })));`, -}; + }; -const SIMPLE_ES2015_CLASS_FILE = { - name: '/simple_es2015_class.d.ts', - contents: ` + + SIMPLE_ES2015_CLASS_FILE = { + name: _('/simple_es2015_class.d.ts'), + contents: ` export class EmptyClass {} `, -}; + }; -const SIMPLE_CLASS_FILE = { - name: '/simple_class.js', - contents: ` + 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) : @@ -118,11 +149,11 @@ const SIMPLE_CLASS_FILE = { exports.EmptyClass = EmptyClass; exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass; })));`, -}; + }; -const FOO_FUNCTION_FILE = { - name: '/foo_function.js', - contents: ` + 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) : @@ -134,11 +165,11 @@ const FOO_FUNCTION_FILE = { ]; exports.foo = foo; })));`, -}; + }; -const INVALID_DECORATORS_FILE = { - name: '/invalid_decorators.js', - contents: ` + 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) : @@ -183,11 +214,11 @@ const INVALID_DECORATORS_FILE = { return NotIdentifier; }()); })));`, -}; + }; -const INVALID_DECORATOR_ARGS_FILE = { - name: '/invalid_decorator_args.js', - contents: ` + 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) : @@ -221,11 +252,11 @@ const INVALID_DECORATOR_ARGS_FILE = { return NotArrayLiteral; }()); })));`, -}; + }; -const INVALID_PROP_DECORATORS_FILE = { - name: '/invalid_prop_decorators.js', - contents: ` + 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) : @@ -276,11 +307,11 @@ const INVALID_PROP_DECORATORS_FILE = { return NotIdentifier; }()); })));`, -}; + }; -const INVALID_PROP_DECORATOR_ARGS_FILE = { - name: '/invalid_prop_decorator_args.js', - contents: ` + 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) : @@ -314,11 +345,11 @@ const INVALID_PROP_DECORATOR_ARGS_FILE = { return NotArrayLiteral; }()); })));`, -}; + }; -const INVALID_CTOR_DECORATORS_FILE = { - name: '/invalid_ctor_decorators.js', - contents: ` + 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) : @@ -385,11 +416,11 @@ const INVALID_CTOR_DECORATORS_FILE = { return NotIdentifier; }()); })));`, -}; + }; -const INVALID_CTOR_DECORATOR_ARGS_FILE = { - name: '/invalid_ctor_decorator_args.js', - contents: ` + 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) : @@ -423,12 +454,22 @@ const INVALID_CTOR_DECORATOR_ARGS_FILE = { return NotArrayLiteral; }()); })));`, -}; + }; -const IMPORTS_FILES = [ - { - name: '/file_a.js', - contents: ` + IMPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./file_a'), require('./file_b'), require('./file_c')) : + typeof define === 'function' && define.amd ? define('index', ['exports', './file_a', './file_b', './file_c'], factory) : + (factory(global.index, global.file_a, global.file_b, global.file_c)); + }(this, (function (exports, file_a, file_b, file_c) { 'use strict'; + })));`, + }, + { + 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) : @@ -437,10 +478,10 @@ const IMPORTS_FILES = [ var a = 'a'; exports.a = a; })));`, - }, - { - name: '/file_b.js', - contents: ` + }, + { + 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) : @@ -450,10 +491,10 @@ const IMPORTS_FILES = [ var c = 'c'; var d = c; })));`, - }, - { - name: '/file_c.js', - contents: ` + }, + { + 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) : @@ -461,13 +502,24 @@ const IMPORTS_FILES = [ }(this, function (exports, file_a) { 'use strict'; var c = file_a.a; }));`, - }, -]; + }, + ]; -const EXPORTS_FILES = [ - { - name: '/a_module.js', - contents: ` + EXPORTS_FILES = [ + { + name: _('/index.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./a_module'), require('./b_module')) : + typeof define === 'function' && define.amd ? define('index', ['exports', './a_module', './b_module'], factory) : + (factory(global.index, global.a_module, global.b_module)); + }(this, (function (exports, a_module, b_module) { 'use strict'; + }))); + ` + }, + { + 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) : @@ -476,10 +528,10 @@ const EXPORTS_FILES = [ var a = 'a'; exports.a = a; })));`, - }, - { - name: '/b_module.js', - contents: ` + }, + { + 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) : @@ -501,12 +553,12 @@ const EXPORTS_FILES = [ exports.DirectiveX = core.Directive; exports.SomeClass = SomeClass; })));`, - }, -]; + }, + ]; -const FUNCTION_BODY_FILE = { - name: '/function_body.js', - contents: ` + 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) : @@ -544,12 +596,12 @@ const FUNCTION_BODY_FILE = { return x; } })));` -}; + }; -const DECORATED_FILES = [ - { - name: '/primary.js', - contents: ` + 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) : @@ -579,10 +631,10 @@ const DECORATED_FILES = [ exports.x = x; exports.C = C; })));` - }, - { - name: '/secondary.js', - contents: ` + }, + { + 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) : @@ -598,18 +650,18 @@ const DECORATED_FILES = [ exports.D = D; }))); ` - } -]; + } + ]; -const TYPINGS_SRC_FILES = [ - { - name: '/src/index.js', - contents: ` + 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'; + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./internal'), require('./class1'), require('./class2'), require('./missing-class'), require('./flat-file'), require('./func1')) : + typeof define === 'function' && define.amd ? define('index', ['exports', './internal', './class1', './class2', './missing-class', './flat-file', './func1'], factory) : + (factory(global.index,global.internal,global.class1,global.class2,global.missing_class,global.flat_file,global.func1)); + }(this, (function (exports,internal,class1,class2,missing_class,flat_file,func1) { 'use strict'; function __export(m) { for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; } @@ -618,10 +670,10 @@ const TYPINGS_SRC_FILES = [ __export(class2); }))); ` - }, - { - name: '/src/class1.js', - contents: ` + }, + { + 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) : @@ -639,10 +691,10 @@ const TYPINGS_SRC_FILES = [ exports.MissingClass1 = MissingClass1; }))); ` - }, - { - name: '/src/class2.js', - contents: ` + }, + { + 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) : @@ -655,10 +707,10 @@ const TYPINGS_SRC_FILES = [ exports.Class2 = Class2; }))); ` - }, - {name: '/src/func1.js', contents: 'function mooFn() {} export {mooFn}'}, { - name: '/src/internal.js', - contents: ` + }, + {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) : @@ -676,10 +728,10 @@ const TYPINGS_SRC_FILES = [ exports.Class2 = Class2; }))); ` - }, - { - name: '/src/missing-class.js', - contents: ` + }, + { + 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) : @@ -692,10 +744,10 @@ const TYPINGS_SRC_FILES = [ exports. MissingClass2 = MissingClass2; }))); ` - }, - { - name: '/src/flat-file.js', - contents: ` + }, + { + 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) : @@ -723,36 +775,51 @@ const TYPINGS_SRC_FILES = [ 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 {}`}, -]; + TYPINGS_DTS_FILES = [ + { + name: _('/typings/index.d.ts'), + contents: ` + import {InternalClass} from './internal'; + import {mooFn} from './func1'; + 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: ` + MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: _('/src/index.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./functions'), require('./methods'), require('./aliased_class')) : + typeof define === 'function' && define.amd ? define('index', ['exports', './functions', './methods', './aliased_class'], factory) : + (factory(global.index,global.functions,global.methods,global.aliased_class)); + }(this, (function (exports,functions,methods,aliased_class) { 'use strict'; + })))); + `, + }, + { + 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) : @@ -797,10 +864,10 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ exports.InternalModule = InternalModule; }))); ` - }, - { - name: '/src/methods.js', - contents: ` + }, + { + 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) : @@ -837,10 +904,10 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ exports.InternalModule = InternalModule; }))); ` - }, - { - name: '/src/aliased_class.js', - contents: ` + }, + { + 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) : @@ -856,10 +923,10 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ exports.AliasedModule = AliasedModule; }))); ` - }, - { - name: '/src/module.js', - contents: ` + }, + { + 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) : @@ -872,308 +939,337 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ 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 find the decorators on a class at the top level', () => { - const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); - const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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]); + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', - isNamedVariableDeclaration); + 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 find the decorators on a class at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + 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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + 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', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); + 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].name).toBe('Directive'); - expect(decorators[0].args).toEqual([]); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); }); - it('should be an empty array if decorator\'s `args` has no property assignment', () => { - const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + it('should ignore decorator elements that have no `type` property', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', - isNamedVariableDeclaration); + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); const decorators = host.getDecoratorsOfDeclaration(classNode) !; expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Directive'); - expect(decorators[0].args).toEqual([]); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); }); - it('should be an empty array if `args` property value is not an array literal', () => { - const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + it('should ignore decorator elements whose `type` value is not an identifier', () => { + loadTestFiles([INVALID_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(INVALID_DECORATORS_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( - program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', - isNamedVariableDeclaration); + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); const decorators = host.getDecoratorsOfDeclaration(classNode) !; expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Directive'); - expect(decorators[0].args).toEqual([]); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + 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'); + }); + + it('should find decorated members on a class at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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']); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + 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', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + 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', () => { + loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_DECORATOR_ARGS_FILE.name); + 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); + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + 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 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']); - }); + 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 decorated members on a class at the top level', () => { - const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); - const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); - const members = host.getMembersOfClass(classNode); + it('should find non decorated properties on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + 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 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'`); + }); - 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 static methods on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); - 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 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); + }); - 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 properties on a class', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); - 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 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'`); + }); - 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 throw if the symbol is not a class', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + 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 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); + it('should return an empty array if there are no prop decorators', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', 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'`); - }); + expect(members).toEqual([]); + }); - 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 not process decorated properties in `propDecorators` if it is not an object literal', + () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + 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); - 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.map(member => member.name)).not.toContain('prop'); + }); - expect(members).toEqual([]); - }); + it('should ignore prop decorator elements that are not object literals', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + 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 !; - 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(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); - expect(members.map(member => member.name)).not.toContain('prop'); - }); + it('should ignore prop decorator elements that have no `type` property', () => { + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); + 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 !; - 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'})); + 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]); + loadTestFiles([INVALID_PROP_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_PROP_DECORATORS_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); @@ -1186,7 +1282,8 @@ describe('UmdReflectionHost', () => { }); it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); 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); @@ -1204,8 +1301,9 @@ describe('UmdReflectionHost', () => { describe('(returned prop decorators `args`)', () => { it('should be an empty array if prop decorator has no `args` property', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); const {program, host: compilerHost} = - makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', @@ -1220,8 +1318,9 @@ describe('UmdReflectionHost', () => { }); it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); const {program, host: compilerHost} = - makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', @@ -1236,8 +1335,9 @@ describe('UmdReflectionHost', () => { }); it('should be an empty array if `args` property value is not an array literal', () => { + loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); const {program, host: compilerHost} = - makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + makeTestBundleProgram(INVALID_PROP_DECORATOR_ARGS_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', @@ -1251,708 +1351,784 @@ describe('UmdReflectionHost', () => { 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 find the decorated constructor parameters at the top level', () => { - const {program, host: compilerHost} = makeTestBundleProgram([TOPLEVEL_DECORATORS_FILE]); - const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - const classNode = getDeclaration( - program, TOPLEVEL_DECORATORS_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]); + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', + 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 find the decorated constructor parameters at the top level', () => { + loadTestFiles([TOPLEVEL_DECORATORS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, TOPLEVEL_DECORATORS_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', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + 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', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + 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 !.length).toBe(2); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + 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, })); - 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 !; + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + 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(decorators.length).toBe(1); - expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + 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', () => { + loadTestFiles([INVALID_CTOR_DECORATORS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATORS_FILE.name); + 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', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + 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'); + }); }); - 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 !; + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + 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]).toEqual(jasmine.objectContaining({name: 'Inject'})); - }); + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); - 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); + it('should be an empty array if param decorator\'s `args` has no property assignment', + () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + 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 !; - const parameters = host.getConstructorParameters(classNode); - const decorators = parameters ![2].decorators !; + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); - expect(decorators.length).toEqual(1); - expect(decorators[0].import).toBe(mockImportInfo); + it('should be an empty array if `args` property value is not an array literal', () => { + loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(INVALID_CTOR_DECORATOR_ARGS_FILE.name); + 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 !; - const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; - expect(typeIdentifier.text).toBe('Inject'); + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); }); }); - 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 !; + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', + () => { + loadTestFiles([FUNCTION_BODY_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FUNCTION_BODY_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Inject'); - expect(decorators[0].args).toEqual([]); + 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', () => { + loadTestFiles(IMPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + 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', () => { + loadTestFiles(IMPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + 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', () => { + loadTestFiles(IMPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + 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'}); + }); }); - 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 !; + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + 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; - expect(decorators.length).toBe(1); - expect(decorators[0].name).toBe('Inject'); - expect(decorators[0].args).toEqual([]); + 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', () => { + loadFakeCore(getFileSystem()); + loadTestFiles([SOME_DIRECTIVE_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); + 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 = + getSourceFileOrError(program, _('/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'); + }); }); - 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() { + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + loadFakeCore(getFileSystem()); + loadTestFiles(EXPORTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/index.js')); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/b_module.js')); + 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 - ], - ]); - }); + 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. - }); + // 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); + describe('getClassSymbol()', () => { + it('should return the class symbol for an ES2015 class', () => { + loadTestFiles([SIMPLE_ES2015_CLASS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name); + 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); - }); + 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); + it('should return the class symbol for an ES5 class (outer variable declaration)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + 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); - }); + 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); + it('should return the class symbol for an ES5 class (inner function declaration)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + 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); - }); + 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) !; + it('should return the same class symbol (of the outer declaration) for outer and inner declarations', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + 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)); - }); + 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); + it('should return undefined if node is not an ES5 class', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + 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(); - }); - }); + expect(classSymbol).toBeUndefined(); + }); + }); - describe('isClass()', () => { - let host: UmdReflectionHost; - let mockNode: ts.Node; - let getClassDeclarationSpy: jasmine.Spy; - let superGetClassDeclarationSpy: jasmine.Spy; + 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; + beforeEach(() => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + mockNode = {} as any; - getClassDeclarationSpy = spyOn(UmdReflectionHost.prototype, 'getClassDeclaration'); - superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); - }); + 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(); + 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); - }); + 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); + 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); - }); + 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); + 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); - }); - }); + expect(host.isClass(mockNode)).toBe(false); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + }); - describe('hasBaseClass()', () => { - function hasBaseClass(source: string) { - const file = { - name: '/synthesized_constructors.js', - contents: source, - }; + 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); - } + loadTestFiles([file]); + const {program, host: compilerHost} = makeTestBundleProgram(file.name); + 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(` + 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); - }); + 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(` + 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); - }); + expect(result).toBe(true); + }); - it('should not consider an IIFE without parameter as having a base class', () => { - const result = hasBaseClass(` + 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); - }); - }); + expect(result).toBe(false); + }); + }); - describe('findClassSymbols()', () => { - it('should return an array of all classes in the given source file', () => { - const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES); - const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; + describe('findClassSymbols()', () => { + it('should return an array of all classes in the given source file', () => { + loadTestFiles(DECORATED_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES[0].name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - expect(classSymbolsPrimary.length).toEqual(2); - expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B']); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + expect(classSymbolsPrimary.length).toEqual(2); + expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B']); - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - expect(classSymbolsSecondary.length).toEqual(1); - expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); - }); - }); + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + expect(classSymbolsSecondary.length).toEqual(1); + expect(classSymbolsSecondary.map(c => c.name)).toEqual(['D']); + }); + }); - describe('getDecoratorsOfSymbol()', () => { - it('should return decorators of class symbol', () => { - const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES); - const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; - const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !; + describe('getDecoratorsOfSymbol()', () => { + it('should return decorators of class symbol', () => { + loadTestFiles(DECORATED_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES[0].name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const primaryFile = getSourceFileOrError(program, DECORATED_FILES[0].name); + const secondaryFile = getSourceFileOrError(program, DECORATED_FILES[1].name); - const classSymbolsPrimary = host.findClassSymbols(primaryFile); - const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsPrimary.length).toEqual(2); - expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); - expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); + const classSymbolsPrimary = host.findClassSymbols(primaryFile); + const classDecoratorsPrimary = + classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsPrimary.length).toEqual(2); + expect(classDecoratorsPrimary[0] !.map(d => d.name)).toEqual(['Directive']); + expect(classDecoratorsPrimary[1] !.map(d => d.name)).toEqual(['Directive']); - const classSymbolsSecondary = host.findClassSymbols(secondaryFile); - const classDecoratorsSecondary = - classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsSecondary.length).toEqual(1); - expect(classDecoratorsSecondary[0] !.map(d => d.name)).toEqual(['Directive']); - }); - }); + const classSymbolsSecondary = host.findClassSymbols(secondaryFile); + const classDecoratorsSecondary = + classSymbolsSecondary.map(s => host.getDecoratorsOfSymbol(s)); + expect(classDecoratorsSecondary.length).toEqual(1); + expect(classDecoratorsSecondary[0] !.map(d => d.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); + describe('getDtsDeclarationsOfClass()', () => { + it('should find the dts declaration that has the same relative path to the source file', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + 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'); - }); + 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); + it('should find the dts declaration for exported functions', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + const mooFn = + getDeclaration(program, _('/src/func1.js'), 'mooFn', ts.isFunctionDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); - const dtsDeclaration = host.getDtsDeclaration(mooFn); - expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/func1.d.ts'); - }); + 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); + it('should return null if there is no matching class in the matching dts file', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + 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); - }); + 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); + it('should return null if there is no matching dts file', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + 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); - }); + 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); + it('should find the dts file that contains a matching class declaration, even if the source files do not match', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + 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'); - }); + 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); + it('should find aliased exports', () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + 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'); - }); + 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); + it('should find the dts file that contains a matching class declaration, even if the class is not publicly exported', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + 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'); - }); + 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); + it('should prefer the publicly exported class if there are multiple classes with the same name', + () => { + loadTestFiles(TYPINGS_SRC_FILES); + loadTestFiles(TYPINGS_DTS_FILES); + const {program, host: compilerHost} = makeTestBundleProgram(_('/src/index.js')); + const dts = makeTestBundleProgram(_('/typings/index.d.ts')); + 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 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'); - }); - }); + 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'], + describe('getModuleWithProvidersFunctions', () => { + it('should find every exported function that returns an object that looks like a ModuleWithProviders object', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program, host: compilerHost} = + makeTestBundleProgram(getRootFiles(MODULE_WITH_PROVIDERS_PROGRAM)[0]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/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', + () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program, host: compilerHost} = + makeTestBundleProgram(getRootFiles(MODULE_WITH_PROVIDERS_PROGRAM)[0]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/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', + ], ]); - }); + }); - 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'], - ]); + // https://github.com/angular/angular/issues/29078 + it('should resolve aliased module references to their original declaration', () => { + loadTestFiles(MODULE_WITH_PROVIDERS_PROGRAM); + const {program, host: compilerHost} = + makeTestBundleProgram(getRootFiles(MODULE_WITH_PROVIDERS_PROGRAM)[0]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = getSourceFileOrError(program, _('/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'], + ]); + }); + }); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/host/util.ts b/packages/compiler-cli/ngcc/test/host/util.ts index eb59c9038d..09bb27282b 100644 --- a/packages/compiler-cli/ngcc/test/host/util.ts +++ b/packages/compiler-cli/ngcc/test/host/util.ts @@ -6,9 +6,7 @@ * 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 {CtorParameter} from '../../../src/ngtsc/reflection'; /** diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 2fc2d4506a..e26a5cc197 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -5,422 +5,397 @@ * 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 {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; -import {existsSync, readFileSync, readdirSync, statSync, symlinkSync} from 'fs'; -import * as mockFs from 'mock-fs'; -import * as path from 'path'; - -import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; +import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system'; +import {Folder, MockFileSystem, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers'; import {mainNgcc} from '../../src/main'; import {markAsProcessed} from '../../src/packages/build_marker'; import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; import {MockLogger} from '../helpers/mock_logger'; -const _ = AbsoluteFsPath.from; +const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true}); -describe('ngcc main()', () => { - beforeEach(createMockFileSystem); - afterEach(restoreRealFileSystem); +runInEachFileSystem(() => { + describe('ngcc main()', () => { + let _: typeof absoluteFrom; + let fs: FileSystem; - it('should run ngcc without errors for esm2015', () => { - expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']})) - .not.toThrow(); - }); + beforeEach(() => { + _ = absoluteFrom; + fs = getFileSystem(); + initMockFileSystem(fs, testFiles); + }); - it('should run ngcc without errors for esm5', () => { - expect(() => mainNgcc({ + it('should run ngcc without errors for esm2015', () => { + expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']})) + .not.toThrow(); + }); + + it('should run ngcc without errors for esm5', () => { + expect(() => mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['esm5'], + logger: new MockLogger(), + })) + .not.toThrow(); + }); + + it('should run ngcc without errors when "main" property is not present', () => { + mainNgcc({ + basePath: '/dist', + propertiesToConsider: ['main', 'es2015'], + logger: new MockLogger(), + }); + + expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + }); + + 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', + esm2015: '0.0.0-PLACEHOLDER', + fesm5: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }; + + mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing'}); + expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__) + .toEqual(STANDARD_MARKERS); + // * `common/http` is a dependency of `common/http/testing`, so is compiled. + expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__) + .toEqual(STANDARD_MARKERS); + // * `core` is a dependency of `common/http`, so is compiled. + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS); + // * `common` is a private (only in .js not .d.ts) dependency so is compiled. + expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS); + // * `common/testing` is not a dependency so is not compiled. + expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toBeUndefined(); + }); + + it('should mark a non-Angular package target as processed', () => { + mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'test-package'}); + + // `test-package` has no Angular but is marked as processed. + expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + }); + + // * `core` is a dependency of `test-package`, but it is not processed, since test-package + // was not processed. + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined(); + }); + }); + + describe('early skipping of target entry-point', () => { + describe('[compileAllFormats === true]', () => { + it('should skip all processing if all the properties are marked as processed', () => { + const logger = new MockLogger(); + markPropertiesAsProcessed('@angular/common/http/testing', SUPPORTED_FORMAT_PROPERTIES); + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: '@angular/common/http/testing', logger, + }); + expect(logger.logs.debug).toContain([ + 'The target entry-point has already been processed' + ]); + }); + + it('should process the target if any `propertyToConsider` is not marked as processed', + () => { + const logger = new MockLogger(); + markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']); + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: '@angular/common/http/testing', + propertiesToConsider: ['fesm2015', 'esm5', 'esm2015'], logger, + }); + expect(logger.logs.debug).not.toContain([ + 'The target entry-point has already been processed' + ]); + }); + }); + + describe('[compileAllFormats === false]', () => { + it('should process the target if the first matching `propertyToConsider` is not marked as processed', + () => { + const logger = new MockLogger(); + markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']); + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: '@angular/common/http/testing', + propertiesToConsider: ['esm5', 'esm2015'], + compileAllFormats: false, logger, + }); + + expect(logger.logs.debug).not.toContain([ + 'The target entry-point has already been processed' + ]); + }); + + it('should skip all processing if the first matching `propertyToConsider` is marked as processed', + () => { + const logger = new MockLogger(); + markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']); + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: '@angular/common/http/testing', + // Simulate a property that does not exist on the package.json and will be ignored. + propertiesToConsider: ['missing', 'esm2015', 'esm5'], + compileAllFormats: false, logger, + }); + + expect(logger.logs.debug).toContain([ + 'The target entry-point has already been processed' + ]); + }); + }); + }); + + + function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) { + const basePath = _('/node_modules'); + const targetPackageJsonPath = join(basePath, packagePath, 'package.json'); + const targetPackage = loadPackage(packagePath); + markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings'); + properties.forEach( + property => markAsProcessed(fs, targetPackage, targetPackageJsonPath, property)); + } + + + describe('with propertiesToConsider', () => { + it('should only compile the entry-point formats given in the `propertiesToConsider` list', + () => { + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['main', 'esm5', 'module', 'fesm5'], + logger: new MockLogger(), + + }); + + // 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', + }); + }); + }); + + describe('with compileAllFormats set to false', () => { + it('should only compile the first matching format', () => { + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['module', 'fesm5', 'esm5'], + compileAllFormats: false, + logger: new MockLogger(), + + }); + // * 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. + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + fesm5: '0.0.0-PLACEHOLDER', + module: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ + fesm5: '0.0.0-PLACEHOLDER', + module: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({ + fesm5: '0.0.0-PLACEHOLDER', + module: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ + fesm5: '0.0.0-PLACEHOLDER', + module: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + }); + + it('should cope with compiling the same entry-point multiple times with different formats', + () => { + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['module'], + compileAllFormats: false, + logger: new MockLogger(), + + }); + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + module: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + // If ngcc tries to write out the typings files again, this will throw an exception. + mainNgcc({ basePath: '/node_modules', propertiesToConsider: ['esm5'], + compileAllFormats: false, logger: new MockLogger(), - })) - .not.toThrow(); - }); - - it('should run ngcc without errors when "main" property is not present', () => { - mainNgcc({ - basePath: '/dist', - propertiesToConsider: ['main', 'es2015'], - logger: new MockLogger(), + }); + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + esm5: '0.0.0-PLACEHOLDER', + module: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + }); }); - expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({ - es2015: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - }); - - 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', - esm2015: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', - fesm2015: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }; - - mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing'}); - expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__) - .toEqual(STANDARD_MARKERS); - // * `common/http` is a dependency of `common/http/testing`, so is compiled. - expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__) - .toEqual(STANDARD_MARKERS); - // * `core` is a dependency of `common/http`, so is compiled. - expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS); - // * `common` is a private (only in .js not .d.ts) dependency so is compiled. - expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS); - // * `common/testing` is not a dependency so is not compiled. - expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toBeUndefined(); - }); - - it('should mark a non-Angular package target as processed', () => { - mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'test-package'}); - - // `test-package` has no Angular but is marked as processed. - expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toEqual({ - es2015: '0.0.0-PLACEHOLDER', - }); - - // * `core` is a dependency of `test-package`, but it is not processed, since test-package - // was not processed. - expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined(); - }); - }); - - describe('early skipping of target entry-point', () => { - describe('[compileAllFormats === true]', () => { - it('should skip all processing if all the properties are marked as processed', () => { - const logger = new MockLogger(); - markPropertiesAsProcessed('@angular/common/http/testing', SUPPORTED_FORMAT_PROPERTIES); + describe('with createNewEntryPointFormats', () => { + it('should create new files rather than overwriting the originals', () => { + const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/; mainNgcc({ basePath: '/node_modules', - targetEntryPointPath: '@angular/common/http/testing', logger, + createNewEntryPointFormats: true, + propertiesToConsider: ['esm5'], + logger: new MockLogger(), + }); - expect(logger.logs.debug).toContain(['The target entry-point has already been processed']); + + // Updates the package.json + expect(loadPackage('@angular/common').esm5).toEqual('./esm5/common.js'); + expect((loadPackage('@angular/common') as any).esm5_ivy_ngcc) + .toEqual('__ivy_ngcc__/esm5/common.js'); + + // Doesn't touch original files + expect(fs.readFile(_(`/node_modules/@angular/common/esm5/src/common_module.js`))) + .not.toMatch(ANGULAR_CORE_IMPORT_REGEX); + // Or create a backup of the original + expect( + fs.exists(_(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`))) + .toBe(false); + + // Creates new files + expect( + fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`))) + .toMatch(ANGULAR_CORE_IMPORT_REGEX); + + // Copies over files (unchanged) that did not need compiling + expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`))); + expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`))) + .toEqual(fs.readFile(_(`/node_modules/@angular/common/esm5/src/version.js`))); + + // Overwrites .d.ts files (as usual) + expect(fs.readFile(_(`/node_modules/@angular/common/common.d.ts`))) + .toMatch(ANGULAR_CORE_IMPORT_REGEX); + expect(fs.exists(_(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`))).toBe(true); + }); + }); + + describe('logger', () => { + it('should log info message to the console by default', () => { + const consoleInfoSpy = spyOn(console, 'info'); + mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']}); + expect(consoleInfoSpy) + .toHaveBeenCalledWith('Compiling @angular/common/http : esm2015 as esm2015'); }); - it('should process the target if any `propertyToConsider` is not marked as processed', () => { + it('should use a custom logger if provided', () => { const logger = new MockLogger(); - markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']); mainNgcc({ basePath: '/node_modules', - targetEntryPointPath: '@angular/common/http/testing', - propertiesToConsider: ['fesm2015', 'esm5', 'esm2015'], logger, + propertiesToConsider: ['esm2015'], logger, }); - expect(logger.logs.debug).not.toContain([ - 'The target entry-point has already been processed' - ]); + expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']); }); }); - describe('[compileAllFormats === false]', () => { - it('should process the target if the first matching `propertyToConsider` is not marked as processed', - () => { - const logger = new MockLogger(); - markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']); - mainNgcc({ - basePath: '/node_modules', - targetEntryPointPath: '@angular/common/http/testing', - propertiesToConsider: ['esm5', 'esm2015'], - compileAllFormats: false, logger, - }); - - expect(logger.logs.debug).not.toContain([ - 'The target entry-point has already been processed' - ]); - }); - - it('should skip all processing if the first matching `propertyToConsider` is marked as processed', - () => { - const logger = new MockLogger(); - markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']); - mainNgcc({ - basePath: '/node_modules', - targetEntryPointPath: '@angular/common/http/testing', - // Simulate a property that does not exist on the package.json and will be ignored. - propertiesToConsider: ['missing', 'esm2015', 'esm5'], - compileAllFormats: false, logger, - }); - - expect(logger.logs.debug).toContain([ - 'The target entry-point has already been processed' - ]); - }); - }); - }); - - - function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) { - const basePath = _('/node_modules'); - const targetPackageJsonPath = AbsoluteFsPath.join(basePath, packagePath, 'package.json'); - const targetPackage = loadPackage(packagePath); - const fs = new NodeJSFileSystem(); - markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings'); - properties.forEach( - property => markAsProcessed(fs, targetPackage, targetPackageJsonPath, property)); - } - - - describe('with propertiesToConsider', () => { - it('should only compile the entry-point formats given in the `propertiesToConsider` list', - () => { - mainNgcc({ - basePath: '/node_modules', - propertiesToConsider: ['main', 'esm5', 'module', 'fesm5'], - logger: new MockLogger(), - - }); - - // 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', - }); - }); - }); - - describe('with compileAllFormats set to false', () => { - it('should only compile the first matching format', () => { - mainNgcc({ - basePath: '/node_modules', - propertiesToConsider: ['module', 'fesm5', 'esm5'], - compileAllFormats: false, - logger: new MockLogger(), - - }); - // * 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. - expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', + describe('with pathMappings', () => { + it('should find and compile packages accessible via the pathMappings', () => { + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['es2015'], + pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'}, + }); + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); }); }); - it('should cope with compiling the same entry-point multiple times with different formats', - () => { - mainNgcc({ - basePath: '/node_modules', - propertiesToConsider: ['module'], - compileAllFormats: false, - logger: new MockLogger(), + function loadPackage( + packageName: string, basePath: AbsoluteFsPath = _('/node_modules')): EntryPointPackageJson { + return JSON.parse(fs.readFile(fs.resolve(basePath, packageName, 'package.json'))); + } - }); - expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - module: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - // If ngcc tries to write out the typings files again, this will throw an exception. - mainNgcc({ - basePath: '/node_modules', - propertiesToConsider: ['esm5'], - compileAllFormats: false, - logger: new MockLogger(), - }); - expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - esm5: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - }); - }); + function initMockFileSystem(fs: FileSystem, testFiles: Folder) { + if (fs instanceof MockFileSystem) { + fs.init(testFiles); + } - describe('with createNewEntryPointFormats', () => { - it('should create new files rather than overwriting the originals', () => { - const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/; - mainNgcc({ - basePath: '/node_modules', - createNewEntryPointFormats: true, - propertiesToConsider: ['esm5'], - logger: new MockLogger(), + // a random test package that no metadata.json file so not compiled by Angular. + loadTestFiles([ + { + name: _('/node_modules/test-package/package.json'), + contents: '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}' + }, + { + name: _('/node_modules/test-package/index.js'), + contents: + 'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};' + }, + { + name: _('/node_modules/test-package/index.d.ts'), + contents: + 'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;' + }, + ]); - }); - - // Updates the package.json - expect(loadPackage('@angular/common').esm5).toEqual('./esm5/common.js'); - expect((loadPackage('@angular/common') as any).esm5_ivy_ngcc) - .toEqual('__ivy_ngcc__/esm5/common.js'); - - // Doesn't touch original files - expect(readFileSync(`/node_modules/@angular/common/esm5/src/common_module.js`, 'utf8')) - .not.toMatch(ANGULAR_CORE_IMPORT_REGEX); - // Or create a backup of the original - expect(existsSync(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`)) - .toBe(false); - - // Creates new files - expect(readFileSync( - `/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`, 'utf8')) - .toMatch(ANGULAR_CORE_IMPORT_REGEX); - - // Copies over files (unchanged) that did not need compiling - expect(existsSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)); - expect(readFileSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`, 'utf8')) - .toEqual(readFileSync(`/node_modules/@angular/common/esm5/src/version.js`, 'utf8')); - - // Overwrites .d.ts files (as usual) - expect(readFileSync(`/node_modules/@angular/common/common.d.ts`, 'utf8')) - .toMatch(ANGULAR_CORE_IMPORT_REGEX); - expect(existsSync(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`)).toBe(true); - }); - }); - - describe('logger', () => { - it('should log info message to the console by default', () => { - const consoleInfoSpy = spyOn(console, 'info'); - mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']}); - expect(consoleInfoSpy) - .toHaveBeenCalledWith('Compiling @angular/common/http : esm2015 as esm2015'); - }); - - it('should use a custom logger if provided', () => { - const logger = new MockLogger(); - mainNgcc({ - basePath: '/node_modules', - propertiesToConsider: ['esm2015'], logger, - }); - expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']); - }); - }); - - describe('with pathMappings', () => { - it('should find and compile packages accessible via the pathMappings', () => { - mainNgcc({ - basePath: '/node_modules', - propertiesToConsider: ['es2015'], - pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'}, - }); - expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - es2015: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({ - es2015: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - }); - }); -}); - - -function createMockFileSystem() { - const typeScriptPath = path.join(process.env.RUNFILES !, 'typescript'); - if (!existsSync(typeScriptPath)) { - symlinkSync(resolveNpmTreeArtifact('typescript'), typeScriptPath, 'junction'); - } - - mockFs({ - '/node_modules/@angular': loadAngularPackages(), - '/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs')), - '/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib')), - '/node_modules/test-package': { - 'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}', - // no metadata.json file so not compiled by Angular. - 'index.js': - 'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};', - 'index.d.ts': - 'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;', - }, - '/dist/local-package': { - 'package.json': - '{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}', - 'index.metadata.json': 'DUMMY DATA', - 'index.js': ` - import {Component} from '@angular/core'; - export class AppComponent {}; - AppComponent.decorators = [ - { type: Component, args: [{selector: 'app', template: '

Hello

'}] } - ];`, - 'index.d.ts': ` - export declare class AppComponent {};`, - }, - }); -} - -function restoreRealFileSystem() { - mockFs.restore(); -} - - -/** Load the built Angular packages into an in-memory structure. */ -function loadAngularPackages(): Directory { - const packagesDirectory: Directory = {}; - - getAngularPackagesFromRunfiles().forEach( - ({name, pkgPath}) => { packagesDirectory[name] = loadDirectory(pkgPath); }); - - return packagesDirectory; -} - -/** - * Load real files from the filesystem into an "in-memory" structure, - * which can be used with `mock-fs`. - * @param directoryPath the path to the directory we want to load. - */ -function loadDirectory(directoryPath: string): Directory { - const directory: Directory = {}; - - readdirSync(directoryPath).forEach(item => { - const itemPath = AbsoluteFsPath.resolve(directoryPath, item); - if (statSync(itemPath).isDirectory()) { - directory[item] = loadDirectory(itemPath); - } else { - directory[item] = readFileSync(itemPath, 'utf-8'); + // An Angular package that has been built locally and stored in the `dist` directory. + loadTestFiles([ + { + name: _('/dist/local-package/package.json'), + contents: '{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}' + }, + {name: _('/dist/local-package/index.metadata.json'), contents: 'DUMMY DATA'}, + { + name: _('/dist/local-package/index.js'), + contents: + `import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '

Hello

'}] }\n];` + }, + { + name: _('/dist/local-package/index.d.ts'), + contents: `export declare class AppComponent {};` + }, + ]); } }); - - return directory; -} - -interface Directory { - [pathSegment: string]: string|Directory; -} - -function loadPackage(packageName: string, basePath = '/node_modules'): EntryPointPackageJson { - return JSON.parse(readFileSync(`${basePath}/${packageName}/package.json`, 'utf8')); -} +}); diff --git a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts index 8e12ce8d37..1e66ac1306 100644 --- a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts @@ -5,168 +5,159 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker'; -import {MockFileSystem} from '../helpers/mock_file_system'; -function createMockFileSystem() { - return new MockFileSystem({ - '/node_modules/@angular/common': { - 'package.json': `{ - "fesm2015": "./fesm2015/common.js", - "fesm5": "./fesm5/common.js", - "typings": "./common.d.ts" - }`, - 'fesm2015': { - 'common.js': 'DUMMY CONTENT', - 'http.js': 'DUMMY CONTENT', - 'http/testing.js': 'DUMMY CONTENT', - 'testing.js': 'DUMMY CONTENT', - }, - 'http': { - 'package.json': `{ - "fesm2015": "../fesm2015/http.js", - "fesm5": "../fesm5/http.js", - "typings": "./http.d.ts" - }`, - 'testing': { - 'package.json': `{ - "fesm2015": "../../fesm2015/http/testing.js", - "fesm5": "../../fesm5/http/testing.js", - "typings": "../http/testing.d.ts" - }`, +runInEachFileSystem(() => { + describe('Marker files', () => { + let _: typeof absoluteFrom; + beforeEach(() => { + _ = absoluteFrom; + loadTestFiles([ + { + name: _('/node_modules/@angular/common/package.json'), + contents: + `{"fesm2015": "./fesm2015/common.js", "fesm5": "./fesm5/common.js", "typings": "./common.d.ts"}` }, - }, - 'other': { - 'package.json': '{ }', - }, - 'testing': { - 'package.json': `{ - "fesm2015": "../fesm2015/testing.js", - "fesm5": "../fesm5/testing.js", - "typings": "../testing.d.ts" - }`, - }, - 'node_modules': { - 'tslib': { - 'package.json': '{ }', - 'node_modules': { - 'other-lib': { - 'package.json': '{ }', - }, - }, + {name: _('/node_modules/@angular/common/fesm2015/common.js'), contents: 'DUMMY CONTENT'}, + {name: _('/node_modules/@angular/common/fesm2015/http.js'), contents: 'DUMMY CONTENT'}, + { + name: _('/node_modules/@angular/common/fesm2015/http/testing.js'), + contents: 'DUMMY CONTENT' }, - }, - }, - '/node_modules/@angular/no-typings': { - 'package.json': `{ - "fesm2015": "./fesm2015/index.js" - }`, - 'fesm2015': { - 'index.js': 'DUMMY CONTENT', - 'index.d.ts': 'DUMMY CONTENT', - }, - }, - '/node_modules/@angular/other': { - 'not-package.json': '{ "fesm2015": "./fesm2015/other.js" }', - 'package.jsonot': '{ "fesm5": "./fesm5/other.js" }', - }, - '/node_modules/@angular/other2': { - 'node_modules_not': { - 'lib1': { - 'package.json': '{ }', + {name: _('/node_modules/@angular/common/fesm2015/testing.js'), contents: 'DUMMY CONTENT'}, + { + name: _('/node_modules/@angular/common/http/package.json'), + contents: + `{"fesm2015": "../fesm2015/http.js", "fesm5": "../fesm5/http.js", "typings": "./http.d.ts"}` }, - }, - 'not_node_modules': { - 'lib2': { - 'package.json': '{ }', + { + name: _('/node_modules/@angular/common/http/testing/package.json'), + contents: + `{"fesm2015": "../../fesm2015/http/testing.js", "fesm5": "../../fesm5/http/testing.js", "typings": "../http/testing.d.ts" }` }, - }, - }, - }); -} - -describe('Marker files', () => { - const COMMON_PACKAGE_PATH = AbsoluteFsPath.from('/node_modules/@angular/common/package.json'); - - describe('markAsProcessed', () => { - it('should write a property in the package.json containing the version placeholder', () => { - const fs = createMockFileSystem(); - - let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); - expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); - expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); - - markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); - pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); - expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); - expect(pkg.__processed_by_ivy_ngcc__.esm5).toBeUndefined(); - - markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'esm5'); - pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); - expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); - expect(pkg.__processed_by_ivy_ngcc__.esm5).toEqual('0.0.0-PLACEHOLDER'); + {name: _('/node_modules/@angular/common/other/package.json'), contents: '{ }'}, + { + name: _('/node_modules/@angular/common/testing/package.json'), + contents: + `{"fesm2015": "../fesm2015/testing.js", "fesm5": "../fesm5/testing.js", "typings": "../testing.d.ts"}` + }, + {name: _('/node_modules/@angular/common/node_modules/tslib/package.json'), contents: '{ }'}, + { + name: _( + '/node_modules/@angular/common/node_modules/tslib/node_modules/other-lib/package.json'), + contents: '{ }' + }, + { + name: _('/node_modules/@angular/no-typings/package.json'), + contents: `{ "fesm2015": "./fesm2015/index.js" }` + }, + {name: _('/node_modules/@angular/no-typings/fesm2015/index.js'), contents: 'DUMMY CONTENT'}, + { + name: _('/node_modules/@angular/no-typings/fesm2015/index.d.ts'), + contents: 'DUMMY CONTENT' + }, + { + name: _('/node_modules/@angular/other/not-package.json'), + contents: '{ "fesm2015": "./fesm2015/other.js" }' + }, + { + name: _('/node_modules/@angular/other/package.jsonot'), + contents: '{ "fesm5": "./fesm5/other.js" }' + }, + { + name: _('/node_modules/@angular/other2/node_modules_not/lib1/package.json'), + contents: '{ }' + }, + { + name: _('/node_modules/@angular/other2/not_node_modules/lib2/package.json'), + contents: '{ }' + }, + ]); }); - it('should update the packageJson object in-place', () => { - const fs = createMockFileSystem(); - let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); - expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); - markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); - expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); - }); - }); + describe('markAsProcessed', () => { + it('should write a property in the package.json containing the version placeholder', () => { + const COMMON_PACKAGE_PATH = _('/node_modules/@angular/common/package.json'); + const fs = getFileSystem(); + let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); + expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); + expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); - describe('hasBeenProcessed', () => { - it('should return true if the marker exists for the given format property', () => { - expect(hasBeenProcessed( - {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}}, - 'fesm2015')) - .toBe(true); + markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); + pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); + expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); + expect(pkg.__processed_by_ivy_ngcc__.esm5).toBeUndefined(); + + markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'esm5'); + pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); + expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); + expect(pkg.__processed_by_ivy_ngcc__.esm5).toEqual('0.0.0-PLACEHOLDER'); + }); + + it('should update the packageJson object in-place', () => { + const COMMON_PACKAGE_PATH = _('/node_modules/@angular/common/package.json'); + const fs = getFileSystem(); + let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); + expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); + markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); + expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); + }); }); - it('should return false if the marker does not exist for the given format property', () => { - expect(hasBeenProcessed( - {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}}, - 'module')) - .toBe(false); - }); - it('should return false if no markers exist', - () => { expect(hasBeenProcessed({name: 'test'}, 'module')).toBe(false); }); - it('should throw an Error if the format has been compiled with a different version.', () => { - expect( - () => hasBeenProcessed( - {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'fesm2015')) - .toThrowError( - 'The ngcc compiler has changed since the last ngcc build.\n' + - 'Please completely remove `node_modules` and try again.'); - }); - it('should throw an Error if any format has been compiled with a different version.', () => { - expect( - () => hasBeenProcessed( - {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'module')) - .toThrowError( - 'The ngcc compiler has changed since the last ngcc build.\n' + - 'Please completely remove `node_modules` and try again.'); - expect( - () => hasBeenProcessed( - { - name: 'test', - __processed_by_ivy_ngcc__: {'module': '0.0.0-PLACEHOLDER', 'fesm2015': '8.0.0'} - }, - 'module')) - .toThrowError( - 'The ngcc compiler has changed since the last ngcc build.\n' + - 'Please completely remove `node_modules` and try again.'); - expect( - () => hasBeenProcessed( - { - name: 'test', - __processed_by_ivy_ngcc__: {'module': '0.0.0-PLACEHOLDER', 'fesm2015': '8.0.0'} - }, - 'fesm2015')) - .toThrowError( - 'The ngcc compiler has changed since the last ngcc build.\n' + - 'Please completely remove `node_modules` and try again.'); + + describe('hasBeenProcessed', () => { + it('should return true if the marker exists for the given format property', () => { + expect(hasBeenProcessed( + {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}}, + 'fesm2015')) + .toBe(true); + }); + it('should return false if the marker does not exist for the given format property', () => { + expect(hasBeenProcessed( + {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}}, + 'module')) + .toBe(false); + }); + it('should return false if no markers exist', + () => { expect(hasBeenProcessed({name: 'test'}, 'module')).toBe(false); }); + it('should throw an Error if the format has been compiled with a different version.', () => { + expect( + () => hasBeenProcessed( + {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'fesm2015')) + .toThrowError( + 'The ngcc compiler has changed since the last ngcc build.\n' + + 'Please completely remove `node_modules` and try again.'); + }); + it('should throw an Error if any format has been compiled with a different version.', () => { + expect( + () => hasBeenProcessed( + {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'module')) + .toThrowError( + 'The ngcc compiler has changed since the last ngcc build.\n' + + 'Please completely remove `node_modules` and try again.'); + expect( + () => hasBeenProcessed( + { + name: 'test', + __processed_by_ivy_ngcc__: {'module': '0.0.0-PLACEHOLDER', 'fesm2015': '8.0.0'} + }, + 'module')) + .toThrowError( + 'The ngcc compiler has changed since the last ngcc build.\n' + + 'Please completely remove `node_modules` and try again.'); + expect( + () => hasBeenProcessed( + { + name: 'test', + __processed_by_ivy_ngcc__: {'module': '0.0.0-PLACEHOLDER', 'fesm2015': '8.0.0'} + }, + 'fesm2015')) + .toThrowError( + 'The ngcc compiler has changed since the last ngcc build.\n' + + 'Please completely remove `node_modules` and try again.'); + }); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts index 4049ac9c25..8e4714c654 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts @@ -5,118 +5,161 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; -import {MockFileSystem} from '../helpers/mock_file_system'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { + describe('entry point bundle', () => { -function createMockFileSystem() { - return new MockFileSystem({ - '/node_modules/test': { - 'package.json': - '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', - 'index.d.ts': 'export * from "./public_api";', - 'index.js': 'export * from "./public_api";', - 'index.metadata.json': '...', - 'public_api.d.ts': ` + function setupMockFileSystem(): void { + const _ = absoluteFrom; + loadTestFiles([ + { + name: _('/node_modules/test/package.json'), + contents: + '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/test/index.d.ts'), contents: 'export * from "./public_api";'}, + {name: _('/node_modules/test/index.js'), contents: 'export * from "./public_api";'}, + {name: _('/node_modules/test/index.metadata.json'), contents: '...'}, + { + name: _('/node_modules/test/public_api.d.ts'), + contents: ` export * from "test/secondary"; export * from "./nested"; export declare class TestClass {}; - `, - 'public_api.js': ` + ` + }, + { + name: _('/node_modules/test/public_api.js'), + contents: ` export * from "test/secondary"; export * from "./nested"; export const TestClass = function() {}; - `, - 'root.d.ts': ` + ` + }, + { + name: _('/node_modules/test/root.d.ts'), + contents: ` import * from 'other'; export declare class RootClass {}; - `, - 'root.js': ` + ` + }, + { + name: _('/node_modules/test/root.js'), + contents: ` import * from 'other'; export const RootClass = function() {}; - `, - 'nested': { - 'index.d.ts': 'export * from "../root";', - 'index.js': 'export * from "../root";', - }, - 'es2015': { - 'index.js': 'export * from "./public_api";', - 'public_api.js': 'export class TestClass {};', - 'root.js': ` + ` + }, + {name: _('/node_modules/test/nested/index.d.ts'), contents: 'export * from "../root";'}, + {name: _('/node_modules/test/nested/index.js'), contents: 'export * from "../root";'}, + {name: _('/node_modules/test/es2015/index.js'), contents: 'export * from "./public_api";'}, + { + name: _('/node_modules/test/es2015/public_api.js'), + contents: 'export class TestClass {};' + }, + { + name: _('/node_modules/test/es2015/root.js'), + contents: ` import * from 'other'; export class RootClass {}; - `, - 'nested': { - 'index.js': 'export * from "../root";', + ` }, - }, - 'secondary': { - 'package.json': - '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', - 'index.d.ts': 'export * from "./public_api";', - 'index.js': 'export * from "./public_api";', - 'index.metadata.json': '...', - 'public_api.d.ts': 'export declare class SecondaryClass {};', - 'public_api.js': 'export class SecondaryClass {};', - 'es2015': { - 'index.js': 'export * from "./public_api";', - 'public_api.js': 'export class SecondaryClass {};', + { + name: _('/node_modules/test/es2015/nested/index.js'), + contents: 'export * from "../root";' }, - }, - }, - '/node_modules/other': { - 'package.json': - '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', - 'index.d.ts': 'export * from "./public_api";', - 'index.js': 'export * from "./public_api";', - 'index.metadata.json': '...', - 'public_api.d.ts': 'export declare class OtherClass {};', - 'public_api.js': 'export class OtherClass {};', - 'es2015': { - 'index.js': 'export * from "./public_api";', - 'public_api.js': 'export class OtherClass {};', - }, - }, - }); -} + { + name: _('/node_modules/test/secondary/package.json'), + contents: + '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}' + }, + { + name: _('/node_modules/test/secondary/index.d.ts'), + contents: 'export * from "./public_api";' + }, + { + name: _('/node_modules/test/secondary/index.js'), + contents: 'export * from "./public_api";' + }, + {name: _('/node_modules/test/secondary/index.metadata.json'), contents: '...'}, + { + name: _('/node_modules/test/secondary/public_api.d.ts'), + contents: 'export declare class SecondaryClass {};' + }, + { + name: _('/node_modules/test/secondary/public_api.js'), + contents: 'export class SecondaryClass {};' + }, + { + name: _('/node_modules/test/secondary/es2015/index.js'), + contents: 'export * from "./public_api";' + }, + { + name: _('/node_modules/test/secondary/es2015/public_api.js'), + contents: 'export class SecondaryClass {};' + }, + { + name: _('/node_modules/other/package.json'), + contents: + '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/other/index.d.ts'), contents: 'export * from "./public_api";'}, + {name: _('/node_modules/other/index.js'), contents: 'export * from "./public_api";'}, + {name: _('/node_modules/other/index.metadata.json'), contents: '...'}, + { + name: _('/node_modules/other/public_api.d.ts'), + contents: 'export declare class OtherClass {};' + }, + {name: _('/node_modules/other/public_api.js'), contents: 'export class OtherClass {};'}, + {name: _('/node_modules/other/es2015/index.js'), contents: 'export * from "./public_api";'}, + { + name: _('/node_modules/other/es2015/public_api.js'), + contents: 'export class OtherClass {};' + }, + ]); + } -describe('entry point bundle', () => { - // https://github.com/angular/angular/issues/29939 - it('should resolve JavaScript sources instead of declaration files if they are adjacent', () => { - const fs = createMockFileSystem(); - const esm5bundle = makeEntryPointBundle( - fs, '/node_modules/test', './index.js', './index.d.ts', false, 'esm5', 'esm5', true) !; + // https://github.com/angular/angular/issues/29939 + it('should resolve JavaScript sources instead of declaration files if they are adjacent', + () => { + setupMockFileSystem(); + const fs = getFileSystem(); + const esm5bundle = makeEntryPointBundle( + fs, '/node_modules/test', './index.js', './index.d.ts', false, 'esm5', 'esm5', true) !; - expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName)) - .toEqual(jasmine.arrayWithExactContents([ - // Modules from the entry-point itself should be source files - '/node_modules/test/index.js', - '/node_modules/test/public_api.js', - '/node_modules/test/nested/index.js', - '/node_modules/test/root.js', + expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName)) + .toEqual(jasmine.arrayWithExactContents([ + // Modules from the entry-point itself should be source files + '/node_modules/test/index.js', + '/node_modules/test/public_api.js', + '/node_modules/test/nested/index.js', + '/node_modules/test/root.js', - // Modules from a secondary entry-point should be declaration files - '/node_modules/test/secondary/public_api.d.ts', - '/node_modules/test/secondary/index.d.ts', + // Modules from a secondary entry-point should be declaration files + '/node_modules/test/secondary/public_api.d.ts', + '/node_modules/test/secondary/index.d.ts', - // Modules resolved from "other" should be declaration files - '/node_modules/other/public_api.d.ts', - '/node_modules/other/index.d.ts', - ].map(p => _(p).toString()))); + // Modules resolved from "other" should be declaration files + '/node_modules/other/public_api.d.ts', + '/node_modules/other/index.d.ts', + ].map(p => absoluteFrom(p).toString()))); - expect(esm5bundle.dts !.program.getSourceFiles().map(sf => sf.fileName)) - .toEqual(jasmine.arrayWithExactContents([ - // All modules in the dts program should be declaration files - '/node_modules/test/index.d.ts', - '/node_modules/test/public_api.d.ts', - '/node_modules/test/nested/index.d.ts', - '/node_modules/test/root.d.ts', - '/node_modules/test/secondary/public_api.d.ts', - '/node_modules/test/secondary/index.d.ts', - '/node_modules/other/public_api.d.ts', - '/node_modules/other/index.d.ts', - ].map(p => _(p).toString()))); + expect(esm5bundle.dts !.program.getSourceFiles().map(sf => sf.fileName)) + .toEqual(jasmine.arrayWithExactContents([ + // All modules in the dts program should be declaration files + '/node_modules/test/index.d.ts', + '/node_modules/test/public_api.d.ts', + '/node_modules/test/nested/index.d.ts', + '/node_modules/test/root.d.ts', + '/node_modules/test/secondary/public_api.d.ts', + '/node_modules/test/secondary/index.d.ts', + '/node_modules/other/public_api.d.ts', + '/node_modules/other/index.d.ts', + ].map(p => absoluteFrom(p).toString()))); + }); }); }); 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 bb524f9bfc..527c630ea8 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 @@ -5,200 +5,206 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {DependencyResolver} from '../../src/dependencies/dependency_resolver'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPointFinder} from '../../src/packages/entry_point_finder'; -import {MockFileSystem, SymLink} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { -describe('findEntryPoints()', () => { - let resolver: DependencyResolver; - let finder: EntryPointFinder; - beforeEach(() => { - const fs = createMockFileSystem(); - resolver = new DependencyResolver( - fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))}); - spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { - return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; + describe('findEntryPoints()', () => { + let resolver: DependencyResolver; + let finder: EntryPointFinder; + let _: typeof absoluteFrom; + + beforeEach(() => { + const fs = getFileSystem(); + _ = absoluteFrom; + setupMockFileSystem(); + resolver = new DependencyResolver( + fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))}); + spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { + return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; + }); + finder = new EntryPointFinder(fs, new MockLogger(), resolver); }); - finder = new EntryPointFinder(fs, new MockLogger(), resolver); - }); - it('should find sub-entry-points within a package', () => { - const {entryPoints} = finder.findEntryPoints(_('/sub_entry_points')); - const entryPointPaths = entryPoints.map(x => [x.package, x.path]); - expect(entryPointPaths).toEqual([ - [_('/sub_entry_points/common'), _('/sub_entry_points/common')], - [_('/sub_entry_points/common'), _('/sub_entry_points/common/http')], - [_('/sub_entry_points/common'), _('/sub_entry_points/common/http/testing')], - [_('/sub_entry_points/common'), _('/sub_entry_points/common/testing')], - ]); - }); - - it('should find packages inside a namespace', () => { - const {entryPoints} = finder.findEntryPoints(_('/namespaced')); - const entryPointPaths = entryPoints.map(x => [x.package, x.path]); - expect(entryPointPaths).toEqual([ - [_('/namespaced/@angular/common'), _('/namespaced/@angular/common')], - [_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http')], - [_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http/testing')], - [_('/namespaced/@angular/common'), _('/namespaced/@angular/common/testing')], - ]); - }); - - it('should find entry-points via `pathMappings', () => { - const {entryPoints} = finder.findEntryPoints( - _('/pathMappings/node_modules'), undefined, - {baseUrl: _('/pathMappings'), paths: {'my-lib': ['dist/my-lib']}}); - const entryPointPaths = entryPoints.map(x => [x.package, x.path]); - expect(entryPointPaths).toEqual([ - [_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib')], - [_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib/sub-lib')], - [ - _('/pathMappings/node_modules/@angular/common'), - _('/pathMappings/node_modules/@angular/common') - ], - ]); - }); - - it('should return an empty array if there are no packages', () => { - const {entryPoints} = finder.findEntryPoints(_('/no_packages')); - expect(entryPoints).toEqual([]); - }); - - it('should return an empty array if there are no valid entry-points', () => { - const {entryPoints} = finder.findEntryPoints(_('/no_valid_entry_points')); - expect(entryPoints).toEqual([]); - }); - - it('should ignore folders starting with .', () => { - const {entryPoints} = finder.findEntryPoints(_('/dotted_folders')); - expect(entryPoints).toEqual([]); - }); - - it('should ignore folders that are symlinked', () => { - const {entryPoints} = finder.findEntryPoints(_('/symlinked_folders')); - expect(entryPoints).toEqual([]); - }); - - it('should handle nested node_modules folders', () => { - const {entryPoints} = finder.findEntryPoints(_('/nested_node_modules')); - const entryPointPaths = entryPoints.map(x => [x.package, x.path]); - expect(entryPointPaths).toEqual([ - [_('/nested_node_modules/outer'), _('/nested_node_modules/outer')], - // Note that the inner entry point does not get included as part of the outer package - [ - _('/nested_node_modules/outer/node_modules/inner'), - _('/nested_node_modules/outer/node_modules/inner'), - ], - ]); - }); - - function createMockFileSystem() { - return new MockFileSystem({ - '/sub_entry_points': { - 'common': { - 'package.json': createPackageJson('common'), - 'common.metadata.json': 'metadata info', - 'http': { - 'package.json': createPackageJson('http'), - 'http.metadata.json': 'metadata info', - 'testing': { - 'package.json': createPackageJson('testing'), - 'testing.metadata.json': 'metadata info', - }, - }, - 'testing': { - 'package.json': createPackageJson('testing'), - 'testing.metadata.json': 'metadata info', - }, - }, - }, - '/pathMappings': { - 'dist': { - 'my-lib': { - 'package.json': createPackageJson('my-lib'), - 'my-lib.metadata.json': 'metadata info', - 'sub-lib': { - 'package.json': createPackageJson('sub-lib'), - 'sub-lib.metadata.json': 'metadata info', - }, - }, - }, - 'node_modules': { - '@angular': { - 'common': { - 'package.json': createPackageJson('common'), - 'common.metadata.json': 'metadata info', - }, - }, - } - }, - '/namespaced': { - '@angular': { - 'common': { - 'package.json': createPackageJson('common'), - 'common.metadata.json': 'metadata info', - 'http': { - 'package.json': createPackageJson('http'), - 'http.metadata.json': 'metadata info', - 'testing': { - 'package.json': createPackageJson('testing'), - 'testing.metadata.json': 'metadata info', - }, - }, - 'testing': { - 'package.json': createPackageJson('testing'), - 'testing.metadata.json': 'metadata info', - }, - }, - }, - }, - '/no_packages': {'should_not_be_found': {}}, - '/no_valid_entry_points': { - 'some_package': { - 'package.json': '{}', - }, - }, - '/dotted_folders': { - '.common': { - 'package.json': createPackageJson('common'), - 'common.metadata.json': 'metadata info', - }, - }, - '/symlinked_folders': { - 'common': new SymLink(_('/sub_entry_points/common')), - }, - '/nested_node_modules': { - 'outer': { - 'package.json': createPackageJson('outer'), - 'outer.metadata.json': 'metadata info', - 'node_modules': { - 'inner': { - 'package.json': createPackageJson('inner'), - 'inner.metadata.json': 'metadata info', - }, - }, - }, - }, + it('should find sub-entry-points within a package', () => { + const {entryPoints} = finder.findEntryPoints(_('/sub_entry_points')); + const entryPointPaths = entryPoints.map(x => [x.package, x.path]); + expect(entryPointPaths).toEqual([ + [_('/sub_entry_points/common'), _('/sub_entry_points/common')], + [_('/sub_entry_points/common'), _('/sub_entry_points/common/http')], + [_('/sub_entry_points/common'), _('/sub_entry_points/common/http/testing')], + [_('/sub_entry_points/common'), _('/sub_entry_points/common/testing')], + ]); }); + + it('should find packages inside a namespace', () => { + const {entryPoints} = finder.findEntryPoints(_('/namespaced')); + const entryPointPaths = entryPoints.map(x => [x.package, x.path]); + expect(entryPointPaths).toEqual([ + [_('/namespaced/@angular/common'), _('/namespaced/@angular/common')], + [_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http')], + [_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http/testing')], + [_('/namespaced/@angular/common'), _('/namespaced/@angular/common/testing')], + ]); + }); + + it('should find entry-points via `pathMappings', () => { + const {entryPoints} = finder.findEntryPoints( + _('/pathMappings/node_modules'), undefined, + {baseUrl: _('/pathMappings'), paths: {'my-lib': ['dist/my-lib']}}); + const entryPointPaths = entryPoints.map(x => [x.package, x.path]); + expect(entryPointPaths).toEqual([ + [_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib')], + [_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib/sub-lib')], + [ + _('/pathMappings/node_modules/@angular/common'), + _('/pathMappings/node_modules/@angular/common') + ], + ]); + }); + + it('should return an empty array if there are no packages', () => { + const {entryPoints} = finder.findEntryPoints(_('/no_packages')); + expect(entryPoints).toEqual([]); + }); + + it('should return an empty array if there are no valid entry-points', () => { + const {entryPoints} = finder.findEntryPoints(_('/no_valid_entry_points')); + expect(entryPoints).toEqual([]); + }); + + it('should ignore folders starting with .', () => { + const {entryPoints} = finder.findEntryPoints(_('/dotted_folders')); + expect(entryPoints).toEqual([]); + }); + + it('should ignore folders that are symlinked', () => { + const {entryPoints} = finder.findEntryPoints(_('/symlinked_folders')); + expect(entryPoints).toEqual([]); + }); + + it('should handle nested node_modules folders', () => { + const {entryPoints} = finder.findEntryPoints(_('/nested_node_modules')); + const entryPointPaths = entryPoints.map(x => [x.package, x.path]); + expect(entryPointPaths).toEqual([ + [_('/nested_node_modules/outer'), _('/nested_node_modules/outer')], + // Note that the inner entry point does not get included as part of the outer package + [ + _('/nested_node_modules/outer/node_modules/inner'), + _('/nested_node_modules/outer/node_modules/inner'), + ], + ]); + }); + + function setupMockFileSystem(): void { + loadTestFiles([ + {name: _('/sub_entry_points/common/package.json'), contents: createPackageJson('common')}, + {name: _('/sub_entry_points/common/common.metadata.json'), contents: 'metadata info'}, + { + name: _('/sub_entry_points/common/http/package.json'), + contents: createPackageJson('http') + }, + {name: _('/sub_entry_points/common/http/http.metadata.json'), contents: 'metadata info'}, + { + name: _('/sub_entry_points/common/http/testing/package.json'), + contents: createPackageJson('testing') + }, + { + name: _('/sub_entry_points/common/http/testing/testing.metadata.json'), + contents: 'metadata info' + }, + { + name: _('/sub_entry_points/common/testing/package.json'), + contents: createPackageJson('testing') + }, + { + name: _('/sub_entry_points/common/testing/testing.metadata.json'), + contents: 'metadata info' + }, + {name: _('/pathMappings/dist/my-lib/package.json'), contents: createPackageJson('my-lib')}, + {name: _('/pathMappings/dist/my-lib/my-lib.metadata.json'), contents: 'metadata info'}, + { + name: _('/pathMappings/dist/my-lib/sub-lib/package.json'), + contents: createPackageJson('sub-lib') + }, + { + name: _('/pathMappings/dist/my-lib/sub-lib/sub-lib.metadata.json'), + contents: 'metadata info' + }, + { + name: _('/pathMappings/node_modules/@angular/common/package.json'), + contents: createPackageJson('common') + }, + { + name: _('/pathMappings/node_modules/@angular/common/common.metadata.json'), + contents: 'metadata info' + }, + { + name: _('/namespaced/@angular/common/package.json'), + contents: createPackageJson('common') + }, + {name: _('/namespaced/@angular/common/common.metadata.json'), contents: 'metadata info'}, + { + name: _('/namespaced/@angular/common/http/package.json'), + contents: createPackageJson('http') + }, + {name: _('/namespaced/@angular/common/http/http.metadata.json'), contents: 'metadata info'}, + { + name: _('/namespaced/@angular/common/http/testing/package.json'), + contents: createPackageJson('testing') + }, + { + name: _('/namespaced/@angular/common/http/testing/testing.metadata.json'), + contents: 'metadata info' + }, + { + name: _('/namespaced/@angular/common/testing/package.json'), + contents: createPackageJson('testing') + }, + { + name: _('/namespaced/@angular/common/testing/testing.metadata.json'), + contents: 'metadata info' + }, + {name: _('/no_valid_entry_points/some_package/package.json'), contents: '{}'}, + {name: _('/dotted_folders/.common/package.json'), contents: createPackageJson('common')}, + {name: _('/dotted_folders/.common/common.metadata.json'), contents: 'metadata info'}, + {name: _('/nested_node_modules/outer/package.json'), contents: createPackageJson('outer')}, + {name: _('/nested_node_modules/outer/outer.metadata.json'), contents: 'metadata info'}, + { + name: _('/nested_node_modules/outer/node_modules/inner/package.json'), + contents: createPackageJson('inner') + }, + { + name: _('/nested_node_modules/outer/node_modules/inner/inner.metadata.json'), + contents: 'metadata info' + }, + ]); + const fs = getFileSystem(); + + fs.ensureDir(_('/no_packages/should_not_be_found')); + + fs.ensureDir(_('/symlinked_folders')); + fs.symlink(_('/sub_entry_points/common'), _('/symlinked_folders/common')); + } + }); + + function createPackageJson(packageName: string): string { + const packageJson: any = { + typings: `./${packageName}.d.ts`, + fesm2015: `./fesm2015/${packageName}.js`, + esm2015: `./esm2015/${packageName}.js`, + fesm5: `./fesm2015/${packageName}.js`, + esm5: `./esm2015/${packageName}.js`, + main: `./bundles/${packageName}.umd.js`, + }; + return JSON.stringify(packageJson); } }); - -function createPackageJson(packageName: string): string { - const packageJson: any = { - typings: `./${packageName}.d.ts`, - fesm2015: `./fesm2015/${packageName}.js`, - esm2015: `./esm2015/${packageName}.js`, - fesm5: `./fesm2015/${packageName}.js`, - esm5: `./esm2015/${packageName}.js`, - main: `./bundles/${packageName}.umd.js`, - }; - return JSON.stringify(packageJson); -} diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index c921fb38c2..12f8dd0ab1 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -6,163 +6,186 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {FileSystem} from '../../src/file_system/file_system'; +import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {getEntryPointInfo} from '../../src/packages/entry_point'; -import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { + describe('getEntryPointInfo()', () => { + let SOME_PACKAGE: AbsoluteFsPath; + let _: typeof absoluteFrom; + let fs: FileSystem; -describe('getEntryPointInfo()', () => { - const SOME_PACKAGE = _('/some_package'); + beforeEach(() => { + setupMockFileSystem(); + SOME_PACKAGE = absoluteFrom('/some_package'); + _ = absoluteFrom; + fs = getFileSystem(); + }); - it('should return an object containing absolute paths to the formats of the specified entry-point', - () => { - const fs = createMockFileSystem(); - const entryPoint = getEntryPointInfo( - fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point')); - expect(entryPoint).toEqual({ - name: 'some-package/valid_entry_point', - package: SOME_PACKAGE, - path: _('/some_package/valid_entry_point'), - typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`), - packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'), - compiledByAngular: true, + it('should return an object containing absolute paths to the formats of the specified entry-point', + () => { + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point')); + expect(entryPoint).toEqual({ + name: 'some-package/valid_entry_point', + package: SOME_PACKAGE, + path: _('/some_package/valid_entry_point'), + typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`), + packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'), + compiledByAngular: true, + }); }); - }); - it('should return null if there is no package.json at the entry-point path', () => { - const fs = createMockFileSystem(); - const entryPoint = getEntryPointInfo( - fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json')); - expect(entryPoint).toBe(null); - }); + it('should return null if there is no package.json at the entry-point path', () => { + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json')); + expect(entryPoint).toBe(null); + }); - it('should return null if there is no typings or types field in the package.json', () => { - const fs = createMockFileSystem(); - const entryPoint = - getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings')); - expect(entryPoint).toBe(null); - }); + it('should return null if there is no typings or types field in the package.json', () => { + const entryPoint = + getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings')); + expect(entryPoint).toBe(null); + }); - it('should return an object with `compiledByAngular` set to false if there is no metadata.json file next to the typing file', - () => { - const fs = createMockFileSystem(); - const entryPoint = getEntryPointInfo( - fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata')); - expect(entryPoint).toEqual({ - name: 'some-package/missing_metadata', - package: SOME_PACKAGE, - path: _('/some_package/missing_metadata'), - typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`), - packageJson: loadPackageJson(fs, '/some_package/missing_metadata'), - compiledByAngular: false, + it('should return an object with `compiledByAngular` set to false if there is no metadata.json file next to the typing file', + () => { + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata')); + expect(entryPoint).toEqual({ + name: 'some-package/missing_metadata', + package: SOME_PACKAGE, + path: _('/some_package/missing_metadata'), + typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`), + packageJson: loadPackageJson(fs, '/some_package/missing_metadata'), + compiledByAngular: false, + }); }); - }); - it('should work if the typings field is named `types', () => { - const fs = createMockFileSystem(); - const entryPoint = getEntryPointInfo( - fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings')); - expect(entryPoint).toEqual({ - name: 'some-package/types_rather_than_typings', - package: SOME_PACKAGE, - path: _('/some_package/types_rather_than_typings'), - typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`), - packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'), - compiledByAngular: true, + it('should work if the typings field is named `types', () => { + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings')); + expect(entryPoint).toEqual({ + name: 'some-package/types_rather_than_typings', + package: SOME_PACKAGE, + path: _('/some_package/types_rather_than_typings'), + typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`), + packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'), + compiledByAngular: true, + }); + }); + + it('should work with Angular Material style package.json', () => { + const entryPoint = + getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style')); + expect(entryPoint).toEqual({ + name: 'some_package/material_style', + package: SOME_PACKAGE, + path: _('/some_package/material_style'), + typings: _(`/some_package/material_style/material_style.d.ts`), + packageJson: loadPackageJson(fs, '/some_package/material_style'), + compiledByAngular: true, + }); + }); + + it('should return null if the package.json is not valid JSON', () => { + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols')); + expect(entryPoint).toBe(null); }); }); - it('should work with Angular Material style package.json', () => { - const fs = createMockFileSystem(); - const entryPoint = - getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style')); - expect(entryPoint).toEqual({ - name: 'some_package/material_style', - package: SOME_PACKAGE, - path: _('/some_package/material_style'), - typings: _(`/some_package/material_style/material_style.d.ts`), - packageJson: loadPackageJson(fs, '/some_package/material_style'), - compiledByAngular: true, - }); - }); - - it('should return null if the package.json is not valid JSON', () => { - const fs = createMockFileSystem(); - const entryPoint = getEntryPointInfo( - fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols')); - expect(entryPoint).toBe(null); - }); -}); - -function createMockFileSystem() { - return new MockFileSystem({ - '/some_package': { - 'valid_entry_point': { - 'package.json': createPackageJson('valid_entry_point'), - 'valid_entry_point.metadata.json': 'some meta data', + function setupMockFileSystem(): void { + const _ = absoluteFrom; + loadTestFiles([ + { + name: _('/some_package/valid_entry_point/package.json'), + contents: createPackageJson('valid_entry_point') }, - 'missing_package_json': { - // no package.json! - 'missing_package_json.metadata.json': 'some meta data', + { + name: _('/some_package/valid_entry_point/valid_entry_point.metadata.json'), + contents: 'some meta data' }, - 'missing_typings': { - 'package.json': createPackageJson('missing_typings', {excludes: ['typings']}), - 'missing_typings.metadata.json': 'some meta data', + // no package.json! + { + name: _('/some_package/missing_package_json/missing_package_json.metadata.json'), + contents: 'some meta data' }, - 'types_rather_than_typings': { - 'package.json': createPackageJson('types_rather_than_typings', {}, 'types'), - 'types_rather_than_typings.metadata.json': 'some meta data', + { + name: _('/some_package/missing_typings/package.json'), + contents: createPackageJson('missing_typings', {excludes: ['typings']}) }, - 'missing_esm2015': { - 'package.json': createPackageJson('missing_fesm2015', {excludes: ['esm2015', 'fesm2015']}), - 'missing_esm2015.metadata.json': 'some meta data', + { + name: _('/some_package/missing_typings/missing_typings.metadata.json'), + contents: 'some meta data' }, - 'missing_metadata': { - 'package.json': createPackageJson('missing_metadata'), - // no metadata.json! + { + name: _('/some_package/types_rather_than_typings/package.json'), + contents: createPackageJson('types_rather_than_typings', {}, 'types') }, - 'material_style': { - 'package.json': `{ + { + name: _('/some_package/types_rather_than_typings/types_rather_than_typings.metadata.json'), + contents: 'some meta data' + }, + { + name: _('/some_package/missing_esm2015/package.json'), + contents: createPackageJson('missing_fesm2015', {excludes: ['esm2015', 'fesm2015']}) + }, + { + name: _('/some_package/missing_esm2015/missing_esm2015.metadata.json'), + contents: 'some meta data' + }, + // no metadata.json! + { + name: _('/some_package/missing_metadata/package.json'), + contents: createPackageJson('missing_metadata') + }, + { + name: _('/some_package/material_style/package.json'), + contents: `{ "name": "some_package/material_style", "typings": "./material_style.d.ts", "main": "./bundles/material_style.umd.js", "module": "./esm5/material_style.es5.js", "es2015": "./esm2015/material_style.js" - }`, - 'material_style.metadata.json': 'some meta data', + }` }, - 'unexpected_symbols': { - // package.json might not be a valid JSON - // for example, @schematics/angular contains a package.json blueprint - // with unexpected symbols - 'package.json': - '{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}', + { + name: _('/some_package/material_style/material_style.metadata.json'), + contents: 'some meta data' }, - } - }); -} - -function createPackageJson( - packageName: string, {excludes}: {excludes?: string[]} = {}, - typingsProp: string = 'typings'): string { - const packageJson: any = { - name: `some-package/${packageName}`, - [typingsProp]: `./${packageName}.d.ts`, - fesm2015: `./fesm2015/${packageName}.js`, - esm2015: `./esm2015/${packageName}.js`, - fesm5: `./fesm2015/${packageName}.js`, - esm5: `./esm2015/${packageName}.js`, - main: `./bundles/${packageName}.umd.js`, - }; - if (excludes) { - excludes.forEach(exclude => delete packageJson[exclude]); + // package.json might not be a valid JSON + // for example, @schematics/angular contains a package.json blueprint + // with unexpected symbols + { + name: _('/some_package/unexpected_symbols/package.json'), + contents: '{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}' + }, + ]); } - return JSON.stringify(packageJson); -} + + function createPackageJson( + packageName: string, {excludes}: {excludes?: string[]} = {}, + typingsProp: string = 'typings'): string { + const packageJson: any = { + name: `some-package/${packageName}`, + [typingsProp]: `./${packageName}.d.ts`, + fesm2015: `./fesm2015/${packageName}.js`, + esm2015: `./esm2015/${packageName}.js`, + fesm5: `./fesm2015/${packageName}.js`, + esm5: `./esm2015/${packageName}.js`, + main: `./bundles/${packageName}.umd.js`, + }; + if (excludes) { + excludes.forEach(exclude => delete packageJson[exclude]); + } + return JSON.stringify(packageJson); + } +}); export function loadPackageJson(fs: FileSystem, packagePath: string) { - return JSON.parse(fs.readFile(_(packagePath + '/package.json'))); + return JSON.parse(fs.readFile(fs.resolve(packagePath + '/package.json'))); } diff --git a/packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts b/packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts index 7ea5a4789a..196cf395cd 100644 --- a/packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts @@ -8,44 +8,30 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {NoopImportRewriter} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, getFileSystem, getSourceFileOrError, absoluteFrom, absoluteFromSourceFile} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; +import {getDeclaration} from '../../../src/ngtsc/testing'; 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 {CommonJsReflectionHost} from '../../src/host/commonjs_host'; import {CommonJsRenderingFormatter} from '../../src/rendering/commonjs_rendering_formatter'; -import {makeTestEntryPointBundle, getDeclaration, createFileSystemFromProgramFiles} from '../helpers/utils'; -import {MockFileSystem} from '../helpers/mock_file_system'; +import {makeTestEntryPointBundle} from '../helpers/utils'; import {MockLogger} from '../helpers/mock_logger'; -const _ = AbsoluteFsPath.fromUnchecked; +runInEachFileSystem(() => { + describe('CommonJsRenderingFormatter', () => { + let _: typeof absoluteFrom; + let PROGRAM: TestFile; + let PROGRAM_DECORATE_HELPER: TestFile; -function setup(file: {name: AbsoluteFsPath, contents: string}) { - const fs = new MockFileSystem(createFileSystemFromProgramFiles([file])); - const logger = new MockLogger(); - const bundle = makeTestEntryPointBundle('module', 'commonjs', false, [file]); - const typeChecker = bundle.src.program.getTypeChecker(); - const host = new CommonJsReflectionHost(logger, false, bundle.src.program, bundle.src.host); - const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = - new DecorationAnalyzer( - fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, - referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) - .analyzeProgram(); - const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new CommonJsRenderingFormatter(host, false); - const importManager = new ImportManager(new NoopImportRewriter(), 'i'); - return { - host, - program: bundle.src.program, - sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager - }; -} - -const PROGRAM = { - name: _('/some/file.js'), - contents: ` + beforeEach(() => { + _ = absoluteFrom; + PROGRAM = { + name: _('/some/file.js'), + contents: ` /* A copyright notice */ require('some-side-effect'); var core = require('@angular/core'); @@ -105,11 +91,11 @@ exports.B = B; exports.C = C; exports.NoIife = NoIife; exports.BadIife = BadIife;` -}; + }; -const PROGRAM_DECORATE_HELPER = { - name: _('/some/file.js'), - contents: ` + PROGRAM_DECORATE_HELPER = { + name: _('/some/file.js'), + contents: ` var tslib_1 = require("tslib"); /* A copyright notice */ var core = require('@angular/core'); @@ -156,43 +142,70 @@ var D = /** @class */ (function () { }()); exports.D = D; // Some other content` -}; + }; + }); -describe('CommonJsRenderingFormatter', () => { + function setup(file: {name: AbsoluteFsPath, contents: string}) { + loadTestFiles([file]); + const fs = getFileSystem(); + const logger = new MockLogger(); + const bundle = makeTestEntryPointBundle('module', 'commonjs', false, [file.name]); + const typeChecker = bundle.src.program.getTypeChecker(); + const host = new CommonJsReflectionHost(logger, false, bundle.src.program, bundle.src.host); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = + new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, + referencesRegistry, [absoluteFrom('/')], false) + .analyzeProgram(); + const switchMarkerAnalyses = + new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); + const renderer = new CommonJsRenderingFormatter(host, false); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + return { + host, + program: bundle.src.program, + sourceFile: bundle.src.file, + renderer, + decorationAnalyses, + switchMarkerAnalyses, + importManager + }; + } - describe('addImports', () => { - it('should insert the given imports after existing imports of the source file', () => { - const {renderer, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addImports( - output, - [ - {specifier: '@angular/core', qualifier: 'i0'}, - {specifier: '@angular/common', qualifier: 'i1'} - ], - sourceFile); - expect(output.toString()).toContain(`/* A copyright notice */ + describe('addImports', () => { + it('should insert the given imports after existing imports of the source file', () => { + const {renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + sourceFile); + expect(output.toString()).toContain(`/* A copyright notice */ require('some-side-effect'); var core = require('@angular/core'); var i0 = require('@angular/core'); var i1 = require('@angular/common');`); + }); }); - }); - 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); - 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(` + 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); + 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 exports.A = A; exports.B = B; @@ -203,238 +216,237 @@ exports.ComponentA1 = i0.ComponentA1; exports.ComponentA2 = i0.ComponentA2; exports.ComponentB = i1.ComponentB; exports.TopLevelComponent = TopLevelComponent;`); + }); + + 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 as ComponentA1}`); + expect(outputString).not.toContain(`{eComponentB as ComponentB}`); + expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); + }); }); - 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 as ComponentA1}`); - expect(outputString).not.toContain(`{eComponentB as ComponentB}`); - expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); - }); - }); - - 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(` + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + expect(output.toString()).toContain(` var core = require('@angular/core'); var x = 3; var A = (function() {`); - }); + }); - it('should insert constants after inserted imports', () => { - 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); - renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); - expect(output.toString()).toContain(` + it('should insert constants after inserted imports', () => { + const {renderer, program} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); + expect(output.toString()).toContain(` var core = require('@angular/core'); var i0 = require('@angular/core'); var x = 3; var A = (function() {`); + }); }); - }); - 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('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + 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(` + 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); + 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 noIifeDeclaration = getDeclaration( + program, absoluteFromSourceFile(sourceFile), '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]' }] }`); + const badIifeDeclaration = getDeclaration( + program, absoluteFromSourceFile(sourceFile), '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')}`); + }); }); - 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]' }] }`); - }); + 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 (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()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); - expect(output.toString()).not.toContain(`C.decorators`); - }); + 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]' }] }`); + }); - }); - 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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).toContain(`Directive({ 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()) + .toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); + expect(output.toString()).not.toContain(`C.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 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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); - }); + 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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); - expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); - expect(output.toString()).toContain(`function C() {\n }\n return 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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`function C() {\n }\n return C;`); + }); + }); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts index 5f27b1a847..ef3f893b38 100644 --- a/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts @@ -7,21 +7,19 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; -import {fromObject} from 'convert-source-map'; +import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; 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; +import {DtsRenderer} from '../../src/rendering/dts_renderer'; +import {MockLogger} from '../helpers/mock_logger'; +import {makeTestEntryPointBundle, getRootFiles} from '../helpers/utils'; class TestRenderingFormatter implements RenderingFormatter { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { @@ -50,13 +48,19 @@ class TestRenderingFormatter implements RenderingFormatter { } function createTestRenderer( - packageName: string, files: {name: string, contents: string}[], - dtsFiles?: {name: string, contents: string}[], - mappingFiles?: {name: string, contents: string}[]) { + packageName: string, files: TestFile[], dtsFiles?: TestFile[], mappingFiles?: TestFile[]) { const logger = new MockLogger(); - const fs = new MockFileSystem(createFileSystemFromProgramFiles(files, dtsFiles, mappingFiles)); + loadTestFiles(files); + if (dtsFiles) { + loadTestFiles(dtsFiles); + } + if (mappingFiles) { + loadTestFiles(mappingFiles); + } + const fs = getFileSystem(); const isCore = packageName === '@angular/core'; - const bundle = makeTestEntryPointBundle('es2015', 'esm2015', isCore, files, dtsFiles); + const bundle = makeTestEntryPointBundle( + 'es2015', 'esm2015', isCore, getRootFiles(files), dtsFiles && getRootFiles(dtsFiles)); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); @@ -87,95 +91,75 @@ function createTestRenderer( bundle}; } +runInEachFileSystem(() => { + describe('DtsRenderer', () => { + let _: typeof absoluteFrom; + let INPUT_PROGRAM: TestFile; + let INPUT_DTS_PROGRAM: TestFile; -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` - }; + beforeEach(() => { + _ = absoluteFrom; + 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` + }; + 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] - }); + 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 RENDERED_CONTENTS = ` -// ADD IMPORTS + const typingsFile = result.find(f => f.path === _('/typings/file.d.ts')) !; + expect(typingsFile.contents) + .toContain( + 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵɵDirectiveDefWithMeta'); + }); -// ADD EXPORTS + 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); -// ADD CONSTANTS + const typingsFile = result.find(f => f.path === _('/typings/file.d.ts')) !; + expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); + }); -// ADD DEFINITIONS + it('should render exports into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); -// REMOVE DECORATORS -` + INPUT_PROGRAM.contents; + // Add a mock export to trigger export rendering + privateDeclarationsAnalyses.push( + {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); - const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ - 'version': 3, - 'sources': ['/src/file.ts'], - 'names': [], - 'mappings': ';;;;;;;;;;AAAA', - 'file': 'file.js', - 'sourcesContent': [INPUT_PROGRAM.contents] - }); + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); - 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(`\n// ADD EXPORTS\n`); + }); - 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 ModuleWithProviders type params', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - 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 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`); + 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_rendering_formatter_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts index 27df790454..7be8524748 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts @@ -8,32 +8,32 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {NoopImportRewriter} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, absoluteFrom, absoluteFromSourceFile, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {getDeclaration} from '../../../src/ngtsc/testing'; import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {loadTestFiles} from '../../../test/helpers'; 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 {Esm5RenderingFormatter} from '../../src/rendering/esm5_rendering_formatter'; -import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; -import {MockFileSystem} from '../helpers/mock_file_system'; +import {makeTestEntryPointBundle} from '../helpers/utils'; import {MockLogger} from '../helpers/mock_logger'; -const _ = AbsoluteFsPath.fromUnchecked; - function setup(file: {name: AbsoluteFsPath, contents: string}) { - const fs = new MockFileSystem(); + loadTestFiles([file]); + const fs = getFileSystem(); const logger = new MockLogger(); - const bundle = makeTestEntryPointBundle('module', 'esm5', false, [file]); + const bundle = makeTestEntryPointBundle('module', 'esm5', false, [file.name]); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm5ReflectionHost(logger, false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = - new DecorationAnalyzer( - fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, - referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) - .analyzeProgram(); + const decorationAnalyses = new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, + typeChecker, host, referencesRegistry, [absoluteFrom('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new Esm5RenderingFormatter(host, false); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); @@ -44,9 +44,18 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { }; } -const PROGRAM = { - name: _('/some/file.js'), - contents: ` +runInEachFileSystem(() => { + describe('Esm5RenderingFormatter', () => { + + let _: typeof absoluteFrom; + let PROGRAM: TestFile; + let PROGRAM_DECORATE_HELPER: TestFile; + + beforeEach(() => { + _ = absoluteFrom; + PROGRAM = { + name: _('/some/file.js'), + contents: ` /* A copyright notice */ import 'some-side-effect'; import {Directive} from '@angular/core'; @@ -102,11 +111,11 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { } // Some other content export {A, B, C, NoIife, BadIife};` -}; + }; -const PROGRAM_DECORATE_HELPER = { - name: _('/some/file.js'), - contents: ` + PROGRAM_DECORATE_HELPER = { + name: _('/some/file.js'), + contents: ` import * as tslib_1 from "tslib"; /* A copyright notice */ import { Directive } from '@angular/core'; @@ -153,275 +162,272 @@ var D = /** @class */ (function () { }()); export { D }; // Some other content` -}; + }; + }); -describe('Esm5RenderingFormatter', () => { - - describe('addImports', () => { - it('should insert the given imports after existing imports of the source file', () => { - const {renderer, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addImports( - output, - [ - {specifier: '@angular/core', qualifier: 'i0'}, - {specifier: '@angular/common', qualifier: 'i1'} - ], - sourceFile); - expect(output.toString()).toContain(`/* A copyright notice */ + describe('addImports', () => { + it('should insert the given imports after existing imports of the source file', () => { + const {renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + sourceFile); + expect(output.toString()).toContain(`/* A copyright notice */ import 'some-side-effect'; import {Directive} from '@angular/core'; import * as i0 from '@angular/core'; 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 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'}, - ], - importManager, sourceFile); - expect(output.toString()).toContain(` + 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); + 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'; export {ComponentA2} from './a'; export {ComponentB} from './foo/b'; export {TopLevelComponent};`); + }); + + 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 as ComponentA1}`); + expect(outputString).not.toContain(`{eComponentB as ComponentB}`); + expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); + }); }); - 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 as ComponentA1}`); - expect(outputString).not.toContain(`{eComponentB as ComponentB}`); - expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); - }); - }); - - 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(` + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + expect(output.toString()).toContain(` import {Directive} from '@angular/core'; var x = 3; var A = (function() {`); - }); + }); - it('should insert constants after inserted imports', () => { - 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); - renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); - expect(output.toString()).toContain(` + it('should insert constants after inserted imports', () => { + const {renderer, program} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + const output = new MagicString(PROGRAM.contents); + 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'; var x = 3; var A = (function() {`); + }); }); - }); - 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('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + 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(` + 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); + 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 noIifeDeclaration = getDeclaration( + program, absoluteFromSourceFile(sourceFile), '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: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + const badIifeDeclaration = getDeclaration( + program, absoluteFromSourceFile(sourceFile), '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')}`); + }); }); - 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: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - }); + 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: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: 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: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - 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`); - }); + 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: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); - }); - 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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).toContain(`Directive({ 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: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + 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`); + }); + }); - 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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); - }); + 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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); - expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); - expect(output.toString()).toContain(`function C() {\n }\n return 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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`function C() {\n }\n return C;`); + }); + }); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts index 1b0defc741..d6ad64afb6 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts @@ -8,7 +8,9 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {NoopImportRewriter} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {loadTestFiles} from '../../../test/helpers'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; @@ -16,25 +18,25 @@ import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../../src/constants'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {EsmRenderingFormatter} from '../../src/rendering/esm_rendering_formatter'; -import {makeTestEntryPointBundle} from '../helpers/utils'; -import {MockFileSystem} from '../helpers/mock_file_system'; +import {makeTestEntryPointBundle, getRootFiles} from '../helpers/utils'; import {MockLogger} from '../helpers/mock_logger'; import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; -const _ = AbsoluteFsPath.fromUnchecked; - -function setup( - files: {name: string, contents: string}[], - dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]) { - const fs = new MockFileSystem(); +function setup(files: TestFile[], dtsFiles?: TestFile[]) { + loadTestFiles(files); + if (dtsFiles) { + loadTestFiles(dtsFiles); + } + const fs = getFileSystem(); const logger = new MockLogger(); - const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, files, dtsFiles) !; + const bundle = makeTestEntryPointBundle( + 'es2015', 'esm2015', false, getRootFiles(files), dtsFiles && getRootFiles(dtsFiles)) !; const typeChecker = bundle.src.program.getTypeChecker(); 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) + typeChecker, host, referencesRegistry, [absoluteFrom('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new EsmRenderingFormatter(host, false); @@ -47,9 +49,18 @@ function setup( }; } -const PROGRAM = { - name: _('/some/file.js'), - contents: ` +runInEachFileSystem(() => { + describe('EsmRenderingFormatter', () => { + + let _: typeof absoluteFrom; + let PROGRAM: TestFile; + + beforeEach(() => { + _ = absoluteFrom; + + PROGRAM = { + name: _('/some/file.js'), + contents: ` /* A copyright notice */ import 'some-side-effect'; import {Directive} from '@angular/core'; @@ -81,209 +92,209 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { return Promise.resolve(new R3NgModuleFactory(moduleType)); } // Some other content` -}; + }; + }); -describe('EsmRenderingFormatter', () => { - - describe('addImports', () => { - it('should insert the given imports after existing imports of the source file', () => { - const {renderer, sourceFile} = setup([PROGRAM]); - const output = new MagicString(PROGRAM.contents); - renderer.addImports( - output, - [ - {specifier: '@angular/core', qualifier: 'i0'}, - {specifier: '@angular/common', qualifier: 'i1'} - ], - sourceFile); - expect(output.toString()).toContain(`/* A copyright notice */ + describe('addImports', () => { + it('should insert the given imports after existing imports of the source file', () => { + const {renderer, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + sourceFile); + expect(output.toString()).toContain(`/* A copyright notice */ import 'some-side-effect'; import {Directive} from '@angular/core'; import * as i0 from '@angular/core'; 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 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'}, - ], - importManager, sourceFile); - expect(output.toString()).toContain(` + 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); + 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'; export {ComponentA2} from './a'; export {ComponentB} from './foo/b'; export {TopLevelComponent};`); + }); + + 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 as ComponentA1}`); + expect(outputString).not.toContain(`{eComponentB as ComponentB}`); + expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); + }); }); - 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 as ComponentA1}`); - expect(outputString).not.toContain(`{eComponentB as ComponentB}`); - expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); - }); - }); - - 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, 'const x = 3;', file); - expect(output.toString()).toContain(` + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup([PROGRAM]); + const file = getSourceFileOrError(program, _('/some/file.js')); + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'const x = 3;', file); + expect(output.toString()).toContain(` import {Directive} from '@angular/core'; const x = 3; export class A {}`); - }); + }); - it('should insert constants after inserted imports', () => { - 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, 'const x = 3;', file); - renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); - expect(output.toString()).toContain(` + it('should insert constants after inserted imports', () => { + const {renderer, program} = setup([PROGRAM]); + const file = getSourceFileOrError(program, _('/some/file.js')); + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'const 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; export class A {`); + }); }); - }); - describe('rewriteSwitchableDeclarations', () => { - it('should switch marked declaration initializers', () => { - 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`); - } - const output = new MagicString(PROGRAM.contents); - renderer.rewriteSwitchableDeclarations( - output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); - expect(output.toString()) - .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); - expect(output.toString()) - .toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); - expect(output.toString()) - .toContain(`let 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('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]); + const file = getSourceFileOrError(program, _('/some/file.js')); + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); + expect(output.toString()) + .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); + expect(output.toString()) + .toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`let 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 after the class declaration', () => { - 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(` + describe('addDefinitions', () => { + it('should insert the definitions directly after the class declaration', () => { + 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(` export class A {} SOME DEFINITION TEXT A.decorators = [ `); + }); + }); - }); + + describe('removeDecorators', () => { + 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 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: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); - describe('removeDecorators', () => { - 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 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: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - }); + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {decorationAnalyses, sourceFile, renderer} = 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: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); - it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', - () => { - const {decorationAnalyses, sourceFile, renderer} = 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: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - }); - - - 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 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); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - expect(output.toString()).not.toContain(`C.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 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); + expect(output.toString()) + .toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + expect(output.toString()).not.toContain(`C.decorators = [`); + }); + }); }); - }); - describe('[__decorate declarations]', () => { + describe('[__decorate declarations]', () => { + let PROGRAM_DECORATE_HELPER: TestFile; - const PROGRAM_DECORATE_HELPER = { - name: '/some/file.js', - contents: ` + beforeEach(() => { + PROGRAM_DECORATE_HELPER = { + name: _('/some/file.js'), + contents: ` import * as tslib_1 from "tslib"; var D_1; /* A copyright notice */ @@ -317,67 +328,72 @@ D = D_1 = tslib_1.__decorate([ ], 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 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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); + 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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`Directive({ 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_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(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`let C = class C {\n};\nexport { 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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).toContain(`Directive({ 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_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(`Directive({ selector: '[a]' }),`); - expect(output.toString()).toContain(`OtherA()`); - expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); - expect(output.toString()).toContain(`OtherB()`); - expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); - expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); - expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`); - }); - }); - - describe('addModuleWithProvidersParams', () => { - const MODULE_WITH_PROVIDERS_PROGRAM = [ - { - name: '/src/index.js', - contents: ` + describe('addModuleWithProvidersParams', () => { + let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[]; + let MODULE_WITH_PROVIDERS_DTS_PROGRAM: TestFile[]; + beforeEach(() => { + MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: _('/src/index.js'), + contents: ` import {ExternalModule} from './module'; import {LibraryModule} from 'some-library'; export class SomeClass {} @@ -397,26 +413,29 @@ export { D }; 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 function withProviders7() { return {ngModule: SomeModule, providers: []}; } + export function withProviders8() { return {ngModule: SomeModule}; } + export {ExternalModule} from './module'; + ` + }, + { + 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: ` + }, + { + name: _('/node_modules/some-library/index.d.ts'), + contents: 'export declare class LibraryModule {}' + }, + ]; + + 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 {} @@ -437,38 +456,42 @@ export { D }; 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 declare function withProviders8(): MyModuleWithProviders; + export {ExternalModule} from './module'; + ` + }, + { + 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 {}' - }, - ]; + }, + { + 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); + 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 referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = getSourceFileOrError(bundle.dts !.program, _('/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); + 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(` + expect(output.toString()).toContain(` static withProviders1(): ModuleWithProviders; static withProviders2(): ModuleWithProviders; static withProviders3(): ModuleWithProviders; @@ -477,7 +500,7 @@ export { D }; static withProviders6(): ModuleWithProviders; static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); - expect(output.toString()).toContain(` + expect(output.toString()).toContain(` export declare function withProviders1(): ModuleWithProviders; export declare function withProviders2(): ModuleWithProviders; export declare function withProviders3(): ModuleWithProviders; @@ -486,26 +509,28 @@ export { D }; 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); + 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 referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = + getSourceFileOrError(bundle.dts !.program, _('/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(` + 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 78e31282ad..6ea93a4ac0 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -7,8 +7,10 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; -import {fromObject, generateMapFileComment} from 'convert-source-map'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {fromObject, generateMapFileComment, SourceMapConverter} from 'convert-source-map'; +import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; @@ -16,13 +18,10 @@ import {ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_ import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -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 {MockFileSystem} from '../helpers/mock_file_system'; +import {makeTestEntryPointBundle, getRootFiles} from '../helpers/utils'; class TestRenderingFormatter implements RenderingFormatter { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { @@ -51,13 +50,19 @@ class TestRenderingFormatter implements RenderingFormatter { } function createTestRenderer( - packageName: string, files: {name: string, contents: string}[], - dtsFiles?: {name: string, contents: string}[], - mappingFiles?: {name: string, contents: string}[]) { + packageName: string, files: TestFile[], dtsFiles?: TestFile[], mappingFiles?: TestFile[]) { const logger = new MockLogger(); - const fs = new MockFileSystem(createFileSystemFromProgramFiles(files, dtsFiles, mappingFiles)); + loadTestFiles(files); + if (dtsFiles) { + loadTestFiles(dtsFiles); + } + if (mappingFiles) { + loadTestFiles(mappingFiles); + } + const fs = getFileSystem(); const isCore = packageName === '@angular/core'; - const bundle = makeTestEntryPointBundle('es2015', 'esm2015', isCore, files, dtsFiles); + const bundle = makeTestEntryPointBundle( + 'es2015', 'esm2015', isCore, getRootFiles(files), dtsFiles && getRootFiles(dtsFiles)); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); @@ -87,32 +92,43 @@ function createTestRenderer( bundle}; } +runInEachFileSystem(() => { + describe('Renderer', () => { + let _: typeof absoluteFrom; + let INPUT_PROGRAM: TestFile; + let COMPONENT_PROGRAM: TestFile; + let INPUT_PROGRAM_MAP: SourceMapConverter; + let RENDERED_CONTENTS: string; + let OUTPUT_PROGRAM_MAP: SourceMapConverter; + let MERGED_OUTPUT_PROGRAM_MAP: SourceMapConverter; -describe('Renderer', () => { - 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` - }; + beforeEach(() => { + _ = absoluteFrom; - const COMPONENT_PROGRAM = { - name: '/src/component.js', - contents: - `import { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n` - }; + 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_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] - }); + COMPONENT_PROGRAM = { + name: _('/src/component.js'), + contents: + `import { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n` + }; - const RENDERED_CONTENTS = ` + 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] + }); + + RENDERED_CONTENTS = ` // ADD IMPORTS // ADD EXPORTS @@ -124,47 +140,49 @@ describe('Renderer', () => { // REMOVE DECORATORS ` + INPUT_PROGRAM.contents; - const OUTPUT_PROGRAM_MAP = fromObject({ - 'version': 3, - 'file': 'file.js', - 'sources': ['/src/file.js'], - 'sourcesContent': [INPUT_PROGRAM.contents], - 'names': [], - 'mappings': ';;;;;;;;;;AAAA;;;;;;;;;' - }); + OUTPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'file': 'file.js', + 'sources': [_('/src/file.js')], + 'sourcesContent': [INPUT_PROGRAM.contents], + 'names': [], + 'mappings': ';;;;;;;;;;AAAA;;;;;;;;;' + }); - const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ - 'version': 3, - 'sources': ['/src/file.ts'], - 'names': [], - 'mappings': ';;;;;;;;;;AAAA', - 'file': 'file.js', - 'sourcesContent': [INPUT_PROGRAM.contents] - }); + MERGED_OUTPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'sources': [_('/src/file.ts')], + 'names': [], + 'mappings': ';;;;;;;;;;AAAA', + 'file': 'file.js', + 'sourcesContent': [INPUT_PROGRAM.contents] + }); + }); - 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} = - createTestRenderer('test-package', [INPUT_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); - expect(result[0].path).toEqual('/src/file.js'); - expect(result[0].contents) - .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); - expect(result[1].path).toEqual('/src/file.js.map'); - expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON()); - }); + 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} = + createTestRenderer('test-package', [INPUT_PROGRAM]); + const result = renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + expect(result[0].path).toEqual(_('/src/file.js')); + expect(result[0].contents) + .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); + expect(result[1].path).toEqual(_('/src/file.js.map')); + expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON()); + }); - it('should render as JavaScript', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - 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) { + it('should render as JavaScript', () => { + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + 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) { ɵngcc0.ɵɵtext(0); } if (rf & 2) { ɵngcc0.ɵɵtextInterpolate(ctx.person.name); @@ -173,194 +191,199 @@ describe('Renderer', () => { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }], null, null);`); - }); + }); - describe('calling RenderingFormatter methods', () => { - it('should call addImports with the source code and info about the core Angular library.', - () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); - const result = renderer.renderProgram( - 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'} - ]); - }); + describe('calling RenderingFormatter methods', () => { + it('should call addImports with the source code and info about the core Angular library.', + () => { + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const result = renderer.renderProgram( + 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'} + ]); + }); - it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.', - () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); - const result = renderer.renderProgram( - 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'), - decorators: [jasmine.objectContaining({name: _('Directive')})] - })); - expect(addDefinitionsSpy.calls.first().args[2]) - .toEqual( - `A.ngDirectiveDef = ɵngcc0.ɵɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } }); + it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.', + () => { + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const result = renderer.renderProgram( + 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', + decorators: [jasmine.objectContaining({name: 'Directive'})] + })); + expect(addDefinitionsSpy.calls.first().args[2]) + .toEqual( + `A.ngDirectiveDef = ɵngcc0.ɵɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } }); /*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{ type: Directive, args: [{ selector: '[a]' }] }], null, { foo: [] });`); - }); + }); - it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', - () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); - const removeDecoratorsSpy = testFormatter.removeDecorators as jasmine.Spy; - expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); + it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', + () => { + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const result = renderer.renderProgram( + 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 - // Each map value is an array of TS nodes that are the decorators to remove - const map = removeDecoratorsSpy.calls.first().args[1] as Map; - const keys = Array.from(map.keys()); - expect(keys.length).toEqual(1); - expect(keys[0].getText()) - .toEqual(`[\n { type: Directive, args: [{ selector: '[a]' }] }\n]`); - const values = Array.from(map.values()); - expect(values.length).toEqual(1); - expect(values[0].length).toEqual(1); - expect(values[0][0].getText()) - .toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`); - }); + // Each map key is the TS node of the decorator container + // Each map value is an array of TS nodes that are the decorators to remove + const map = removeDecoratorsSpy.calls.first().args[1] as Map; + const keys = Array.from(map.keys()); + expect(keys.length).toEqual(1); + expect(keys[0].getText()) + .toEqual(`[\n { type: Directive, args: [{ selector: '[a]' }] }\n]`); + const values = Array.from(map.values()); + expect(values.length).toEqual(1); + expect(values[0].length).toEqual(1); + expect(values[0][0].getText()) + .toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`); + }); - it('should render classes without decorators if handler matches', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - testFormatter} = createTestRenderer('test-package', [{ - name: '/src/file.js', - contents: ` - import { Directive, ViewChild } from '@angular/core'; - - export class UndecoratedBase { test = null; } - - UndecoratedBase.propDecorators = { - test: [{ - type: ViewChild, - args: ["test", {static: true}] - }], - }; - ` - }]); + it('should render classes without decorators if handler matches', () => { + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + testFormatter} = createTestRenderer('test-package', [{ + name: _('/src/file.js'), + contents: ` + import { Directive, ViewChild } from '@angular/core'; - renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + export class UndecoratedBase { test = null; } - const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; - expect(addDefinitionsSpy.calls.first().args[2]) - .toEqual( - `UndecoratedBase.ngBaseDef = ɵngcc0.ɵɵdefineBase({ viewQuery: function (rf, ctx) { if (rf & 1) { + UndecoratedBase.propDecorators = { + test: [{ + type: ViewChild, + args: ["test", {static: true}] + }], + }; + ` + }]); + + renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; + expect(addDefinitionsSpy.calls.first().args[2]) + .toEqual( + `UndecoratedBase.ngBaseDef = ɵngcc0.ɵɵdefineBase({ viewQuery: function (rf, ctx) { if (rf & 1) { ɵngcc0.ɵɵstaticViewQuery(_c0, true, null); } if (rf & 2) { var _t; ɵngcc0.ɵɵqueryRefresh(_t = ɵngcc0.ɵɵloadViewQuery()) && (ctx.test = _t.first); } } });`); + }); + + it('should call renderImports after other abstract methods', () => { + // This allows the other methods to add additional imports if necessary + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + 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); + expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); + }); }); - it('should call renderImports after other abstract methods', () => { - // This allows the other methods to add additional imports if necessary - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - 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); - expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); - expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); - expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); - }); - }); + 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} = + createTestRenderer( + 'test-package', [{ + ...INPUT_PROGRAM, + contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() + }]); + const result = renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + expect(result[0].path).toEqual(_('/src/file.js')); + expect(result[0].contents) + .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); + expect(result[1]).toBeUndefined(); + }); - 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} = - createTestRenderer( - 'test-package', [{ - ...INPUT_PROGRAM, - contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() - }]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); - expect(result[0].path).toEqual('/src/file.js'); - expect(result[0].contents) - .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); - expect(result[1]).toBeUndefined(); - }); - - it('should merge any external source map from the original file and write the output to an external source map', - () => { - const sourceFiles = [{ - ...INPUT_PROGRAM, - contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' - }]; - const mappingFiles = - [{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}]; - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = - createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); - expect(result[0].path).toEqual('/src/file.js'); - expect(result[0].contents) - .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); - expect(result[1].path).toEqual('/src/file.js.map'); - expect(JSON.parse(result[1].contents)).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject()); - }); - }); - - describe('@angular/core support', () => { - it('should render relative imports in ESM bundles', () => { - const CORE_FILE = { - name: '/src/core.js', - contents: - `import { NgModule } from './ng_module';\nexport class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n` - }; - const R3_SYMBOLS_FILE = { - // r3_symbols in the file name indicates that this is the path to rewrite core imports to - name: '/src/r3_symbols.js', - contents: `export const NgModule = () => null;` - }; - // The package name of `@angular/core` indicates that we are compiling the core library. - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - testFormatter} = createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); - renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); - const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; - expect(addDefinitionsSpy.calls.first().args[2]) - .toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`); - const addImportsSpy = testFormatter.addImports as jasmine.Spy; - expect(addImportsSpy.calls.first().args[1]).toEqual([ - {specifier: './r3_symbols', qualifier: 'ɵngcc0'} - ]); + it('should merge any external source map from the original file and write the output to an external source map', + () => { + const sourceFiles: TestFile[] = [{ + ...INPUT_PROGRAM, + contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' + }]; + const mappingFiles: TestFile[] = + [{name: _(INPUT_PROGRAM.name + '.map'), contents: INPUT_PROGRAM_MAP.toJSON()}]; + const {decorationAnalyses, renderer, switchMarkerAnalyses, + privateDeclarationsAnalyses} = + createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); + const result = renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + expect(result[0].path).toEqual(_('/src/file.js')); + expect(result[0].contents) + .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); + expect(result[1].path).toEqual(_('/src/file.js.map')); + expect(JSON.parse(result[1].contents)).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject()); + }); }); - it('should render no imports in FESM bundles', () => { - const CORE_FILE = { - name: '/src/core.js', - contents: `export const NgModule = () => null; + describe('@angular/core support', () => { + it('should render relative imports in ESM bundles', () => { + const CORE_FILE: TestFile = { + name: _('/src/core.js'), + contents: + `import { NgModule } from './ng_module';\nexport class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n` + }; + const R3_SYMBOLS_FILE: TestFile = { + // r3_symbols in the file name indicates that this is the path to rewrite core imports + // to + name: _('/src/r3_symbols.js'), + contents: `export const NgModule = () => null;` + }; + // The package name of `@angular/core` indicates that we are compiling the core library. + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); + renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; + expect(addDefinitionsSpy.calls.first().args[2]) + .toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`); + const addImportsSpy = testFormatter.addImports as jasmine.Spy; + expect(addImportsSpy.calls.first().args[1]).toEqual([ + {specifier: './r3_symbols', qualifier: 'ɵngcc0'} + ]); + }); + + it('should render no imports in FESM bundles', () => { + const CORE_FILE: TestFile = { + name: _('/src/core.js'), + contents: `export const NgModule = () => null; export class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n` - }; + }; - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]); - renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); - const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; - expect(addDefinitionsSpy.calls.first().args[2]) - .toContain(`/*@__PURE__*/ setClassMetadata(`); - const addImportsSpy = testFormatter.addImports as jasmine.Spy; - expect(addImportsSpy.calls.first().args[1]).toEqual([]); + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]); + renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; + expect(addDefinitionsSpy.calls.first().args[2]) + .toContain(`/*@__PURE__*/ setClassMetadata(`); + const addImportsSpy = testFormatter.addImports as jasmine.Spy; + expect(addImportsSpy.calls.first().args[1]).toEqual([]); + }); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts index 4341c0b3b1..42f4c818b1 100644 --- a/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts @@ -8,30 +8,31 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {NoopImportRewriter} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, absoluteFromSourceFile, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; +import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; +import {getDeclaration} from '../../../src/ngtsc/testing'; 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 {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'; +import {makeTestEntryPointBundle} from '../helpers/utils'; -const _ = AbsoluteFsPath.fromUnchecked; - -function setup(file: {name: string, contents: string}) { - const fs = new MockFileSystem(createFileSystemFromProgramFiles([file])); +function setup(file: TestFile) { + loadTestFiles([file]); + const fs = getFileSystem(); const logger = new MockLogger(); - const bundle = makeTestEntryPointBundle('esm5', 'esm5', false, [file]); + const bundle = makeTestEntryPointBundle('esm5', 'esm5', false, [file.name]); 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) + referencesRegistry, [absoluteFrom('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); const renderer = new UmdRenderingFormatter(host, false); @@ -45,9 +46,19 @@ function setup(file: {name: string, contents: string}) { }; } -const PROGRAM = { - name: _('/some/file.js'), - contents: ` +runInEachFileSystem(() => { + describe('UmdRenderingFormatter', () => { + + let _: typeof absoluteFrom; + let PROGRAM: TestFile; + let PROGRAM_DECORATE_HELPER: TestFile; + + beforeEach(() => { + _ = absoluteFrom; + + 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')) : @@ -111,12 +122,12 @@ exports.C = C; exports.NoIife = NoIife; exports.BadIife = BadIife; })));`, -}; + }; -const PROGRAM_DECORATE_HELPER = { - name: '/some/file.js', - contents: ` + 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')) : @@ -167,91 +178,92 @@ typeof define === 'function' && define.amd ? define('file', ['exports','/tslib', exports.D = D; // Some other content })));` -}; - -describe('UmdRenderingFormatter', () => { - - 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) :`); + describe('addImports', () => { + it('should append the given imports into the CommonJS factory call', () => { + const {renderer, program} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/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 = getSourceFileOrError(program, _('/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 = getSourceFileOrError(program, _('/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 = getSourceFileOrError(program, _('/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';`); + }); }); - 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));`); - }); + 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); - 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(` + expect(output.toString()).toContain(` exports.A = A; exports.B = B; exports.C = C; @@ -263,228 +275,229 @@ exports.ComponentB = i1.ComponentB; exports.TopLevelComponent = TopLevelComponent; })));`); - expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA1'); - expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA2'); - expect(generateNamedImportSpy).toHaveBeenCalledWith('./foo/b', 'ComponentB'); + 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`); + }); }); - 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(` + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + 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. + }); }); - 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('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); + const file = getSourceFileOrError(program, _('/some/file.js')); + 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(` + 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); + 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 noIifeDeclaration = getDeclaration( + program, absoluteFromSourceFile(sourceFile), '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]' }] }`); + const badIifeDeclaration = getDeclaration( + program, absoluteFromSourceFile(sourceFile), '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 (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 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 (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`); - }); + 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]' }] }`); + }); - }); - 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 (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`); + }); + }); - 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]' })`); - }); + 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;`); - }); + 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;`); + }); + }); }); }); diff --git a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts index 7e4ba0619a..b86f10fe95 100644 --- a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts @@ -5,80 +5,79 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer'; -import {MockFileSystem} from '../helpers/mock_file_system'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { + describe('InPlaceFileWriter', () => { -function createMockFileSystem() { - return new MockFileSystem({ - '/package/path': { - 'top-level.js': 'ORIGINAL TOP LEVEL', - 'folder-1': { - 'file-1.js': 'ORIGINAL FILE 1', - 'file-2.js': 'ORIGINAL FILE 2', - }, - 'folder-2': { - 'file-3.js': 'ORIGINAL FILE 3', - 'file-4.js': 'ORIGINAL FILE 4', - }, - 'already-backed-up.js.__ivy_ngcc_bak': 'BACKED UP', - } - }); -} + let _: typeof absoluteFrom; -describe('InPlaceFileWriter', () => { - it('should write all the FileInfo to the disk', () => { - const fs = createMockFileSystem(); - const fileWriter = new InPlaceFileWriter(fs); - fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ - {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, - {path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'}, - {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, - {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, - ]); - expect(fs.readFile(_('/package/path/top-level.js'))).toEqual('MODIFIED TOP LEVEL'); - expect(fs.readFile(_('/package/path/folder-1/file-1.js'))).toEqual('MODIFIED FILE 1'); - expect(fs.readFile(_('/package/path/folder-1/file-2.js'))).toEqual('ORIGINAL FILE 2'); - expect(fs.readFile(_('/package/path/folder-2/file-3.js'))).toEqual('ORIGINAL FILE 3'); - expect(fs.readFile(_('/package/path/folder-2/file-4.js'))).toEqual('MODIFIED FILE 4'); - expect(fs.readFile(_('/package/path/folder-3/file-5.js'))).toEqual('NEW FILE 5'); - }); + beforeEach(() => { + _ = absoluteFrom; + loadTestFiles([ + {name: _('/package/path/top-level.js'), contents: 'ORIGINAL TOP LEVEL'}, + {name: _('/package/path/folder-1/file-1.js'), contents: 'ORIGINAL FILE 1'}, + {name: _('/package/path/folder-1/file-2.js'), contents: 'ORIGINAL FILE 2'}, + {name: _('/package/path/folder-2/file-3.js'), contents: 'ORIGINAL FILE 3'}, + {name: _('/package/path/folder-2/file-4.js'), contents: 'ORIGINAL FILE 4'}, + {name: _('/package/path/already-backed-up.js.__ivy_ngcc_bak'), contents: 'BACKED UP'}, + ]); + }); - it('should create backups of all files that previously existed', () => { - const fs = createMockFileSystem(); - const fileWriter = new InPlaceFileWriter(fs); - fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ - {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, - {path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'}, - {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, - {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, - ]); - expect(fs.readFile(_('/package/path/top-level.js.__ivy_ngcc_bak'))) - .toEqual('ORIGINAL TOP LEVEL'); - expect(fs.readFile(_('/package/path/folder-1/file-1.js.__ivy_ngcc_bak'))) - .toEqual('ORIGINAL FILE 1'); - expect(fs.exists(_('/package/path/folder-1/file-2.js.__ivy_ngcc_bak'))).toBe(false); - expect(fs.exists(_('/package/path/folder-2/file-3.js.__ivy_ngcc_bak'))).toBe(false); - expect(fs.readFile(_('/package/path/folder-2/file-4.js.__ivy_ngcc_bak'))) - .toEqual('ORIGINAL FILE 4'); - expect(fs.exists(_('/package/path/folder-3/file-5.js.__ivy_ngcc_bak'))).toBe(false); - }); + it('should write all the FileInfo to the disk', () => { + const fs = getFileSystem(); + const fileWriter = new InPlaceFileWriter(fs); + fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ + {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, + {path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'}, + {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, + {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, + ]); + expect(fs.readFile(_('/package/path/top-level.js'))).toEqual('MODIFIED TOP LEVEL'); + expect(fs.readFile(_('/package/path/folder-1/file-1.js'))).toEqual('MODIFIED FILE 1'); + expect(fs.readFile(_('/package/path/folder-1/file-2.js'))).toEqual('ORIGINAL FILE 2'); + expect(fs.readFile(_('/package/path/folder-2/file-3.js'))).toEqual('ORIGINAL FILE 3'); + expect(fs.readFile(_('/package/path/folder-2/file-4.js'))).toEqual('MODIFIED FILE 4'); + expect(fs.readFile(_('/package/path/folder-3/file-5.js'))).toEqual('NEW FILE 5'); + }); - it('should error if the backup file already exists', () => { - const fs = createMockFileSystem(); - const fileWriter = new InPlaceFileWriter(fs); - const absoluteBackupPath = _('/package/path/already-backed-up.js'); - expect( - () => fileWriter.writeBundle( - {} as EntryPoint, {} as EntryPointBundle, - [ - {path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}, - ])) - .toThrowError( - `Tried to overwrite ${absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`); + it('should create backups of all files that previously existed', () => { + const fs = getFileSystem(); + const fileWriter = new InPlaceFileWriter(fs); + fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ + {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, + {path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'}, + {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, + {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, + ]); + expect(fs.readFile(_('/package/path/top-level.js.__ivy_ngcc_bak'))) + .toEqual('ORIGINAL TOP LEVEL'); + expect(fs.readFile(_('/package/path/folder-1/file-1.js.__ivy_ngcc_bak'))) + .toEqual('ORIGINAL FILE 1'); + expect(fs.exists(_('/package/path/folder-1/file-2.js.__ivy_ngcc_bak'))).toBe(false); + expect(fs.exists(_('/package/path/folder-2/file-3.js.__ivy_ngcc_bak'))).toBe(false); + expect(fs.readFile(_('/package/path/folder-2/file-4.js.__ivy_ngcc_bak'))) + .toEqual('ORIGINAL FILE 4'); + expect(fs.exists(_('/package/path/folder-3/file-5.js.__ivy_ngcc_bak'))).toBe(false); + }); + + it('should error if the backup file already exists', () => { + const fs = getFileSystem(); + const fileWriter = new InPlaceFileWriter(fs); + const absoluteBackupPath = _('/package/path/already-backed-up.js'); + expect( + () => fileWriter.writeBundle( + {} as EntryPoint, {} as EntryPointBundle, + [ + {path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}, + ])) + .toThrowError( + `Tried to overwrite ${absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`); + }); }); }); diff --git a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts index 051c85f084..1ece970a1f 100644 --- a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts @@ -5,345 +5,354 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {FileSystem} from '../../src/file_system/file_system'; +import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo} from '../../src/packages/entry_point'; import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; import {FileWriter} from '../../src/writing/file_writer'; import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_file_writer'; -import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; import {loadPackageJson} from '../packages/entry_point_spec'; -const _ = AbsoluteFsPath.from; +runInEachFileSystem(() => { + describe('NewEntryPointFileWriter', () => { -function createMockFileSystem() { - return new MockFileSystem({ - '/node_modules/test': { - 'package.json': - '{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', - 'index.d.ts': 'export declare class FooTop {}', - 'index.d.ts.map': 'ORIGINAL MAPPING DATA', - 'index.metadata.json': '...', - 'esm5.js': 'export function FooTop() {}', - 'esm5.js.map': 'ORIGINAL MAPPING DATA', - 'es2015': { - 'index.js': 'export {FooTop} from "./foo";', - 'foo.js': 'export class FooTop {}', - }, - 'a': { - 'package.json': - '{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', - 'index.d.ts': 'export declare class FooA {}', - 'index.metadata.json': '...', - 'esm5.js': 'export function FooA() {}', - 'es2015': { - 'index.js': 'export {FooA} from "./foo";', - 'foo.js': 'export class FooA {}', - }, - }, - 'b': { - // This entry-point points to files outside its folder - 'package.json': - '{"module": "../lib/esm5.js", "es2015": "../lib/es2015/index.js", "typings": "../typings/index.d.ts"}', - }, - 'lib': { - 'esm5.js': 'export function FooB() {}', - 'es2015': { - 'index.js': 'export {FooB} from "./foo"; import * from "other";', - 'foo.js': 'import {FooA} from "test/a"; import "events"; export class FooB {}', - }, - }, - 'typings': { - 'index.d.ts': 'export declare class FooB {}', - 'index.metadata.json': '...', - } - }, - '/node_modules/other': { - 'package.json': '{"module": "./esm5.js", "typings": "./index.d.ts"}', - 'index.d.ts': 'export declare class OtherClass {}', - 'esm5.js': 'export class OtherClass {}', - }, - '/node_modules/events': { - 'package.json': '{"main": "./events.js"}', - 'events.js': 'export class OtherClass {}', - }, - }); -} + let _: typeof absoluteFrom; + let fs: FileSystem; + let fileWriter: FileWriter; + let entryPoint: EntryPoint; + let esm5bundle: EntryPointBundle; + let esm2015bundle: EntryPointBundle; -describe('NewEntryPointFileWriter', () => { - let fs: FileSystem; - let fileWriter: FileWriter; - let entryPoint: EntryPoint; - let esm5bundle: EntryPointBundle; - let esm2015bundle: EntryPointBundle; - - describe('writeBundle() [primary entry-point]', () => { beforeEach(() => { - fs = createMockFileSystem(); - fileWriter = new NewEntryPointFileWriter(fs); - entryPoint = getEntryPointInfo( - fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !; - esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); - esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); + _ = absoluteFrom; + loadTestFiles([ + + { + name: _('/node_modules/test/package.json'), + contents: + '{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/test/index.d.ts'), contents: 'export declare class FooTop {}'}, + {name: _('/node_modules/test/index.d.ts.map'), contents: 'ORIGINAL MAPPING DATA'}, + {name: _('/node_modules/test/index.metadata.json'), contents: '...'}, + {name: _('/node_modules/test/esm5.js'), contents: 'export function FooTop() {}'}, + {name: _('/node_modules/test/esm5.js.map'), contents: 'ORIGINAL MAPPING DATA'}, + {name: _('/node_modules/test/es2015/index.js'), contents: 'export {FooTop} from "./foo";'}, + {name: _('/node_modules/test/es2015/foo.js'), contents: 'export class FooTop {}'}, + { + name: _('/node_modules/test/a/package.json'), + contents: + `{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}` + }, + {name: _('/node_modules/test/a/index.d.ts'), contents: 'export declare class FooA {}'}, + {name: _('/node_modules/test/a/index.metadata.json'), contents: '...'}, + {name: _('/node_modules/test/a/esm5.js'), contents: 'export function FooA() {}'}, + {name: _('/node_modules/test/a/es2015/index.js'), contents: 'export {FooA} from "./foo";'}, + {name: _('/node_modules/test/a/es2015/foo.js'), contents: 'export class FooA {}'}, + { + name: _('/node_modules/test/b/package.json'), + // This entry-point points to files outside its folder + contents: + `{"module": "../lib/esm5.js", "es2015": "../lib/es2015/index.js", "typings": "../typings/index.d.ts"}` + }, + {name: _('/node_modules/test/lib/esm5.js'), contents: 'export function FooB() {}'}, + { + name: _('/node_modules/test/lib/es2015/index.js'), + contents: 'export {FooB} from "./foo"; import * from "other";' + }, + { + name: _('/node_modules/test/lib/es2015/foo.js'), + contents: 'import {FooA} from "test/a"; import "events"; export class FooB {}' + }, + { + name: _('/node_modules/test/typings/index.d.ts'), + contents: 'export declare class FooB {}' + }, + {name: _('/node_modules/test/typings/index.metadata.json'), contents: '...'}, + { + name: _('/node_modules/other/package.json'), + contents: '{"module": "./esm5.js", "typings": "./index.d.ts"}' + }, + {name: _('/node_modules/other/index.d.ts'), contents: 'export declare class OtherClass {}'}, + {name: _('/node_modules/other/esm5.js'), contents: 'export class OtherClass {}'}, + {name: _('/node_modules/events/package.json'), contents: '{"main": "./events.js"}'}, + {name: _('/node_modules/events/events.js'), contents: 'export class OtherClass {}'}, + ]); }); - it('should write the modified files to a new folder', () => { - fileWriter.writeBundle(entryPoint, esm5bundle, [ - { - path: _('/node_modules/test/esm5.js'), - contents: 'export function FooTop() {} // MODIFIED' - }, - {path: _('/node_modules/test/esm5.js.map'), contents: 'MODIFIED MAPPING DATA'}, - ]); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/esm5.js'))) - .toEqual('export function FooTop() {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/esm5.js'))).toEqual('export function FooTop() {}'); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/esm5.js.map'))) - .toEqual('MODIFIED MAPPING DATA'); - expect(fs.readFile(_('/node_modules/test/esm5.js.map'))).toEqual('ORIGINAL MAPPING DATA'); + describe('writeBundle() [primary entry-point]', () => { + beforeEach(() => { + fs = getFileSystem(); + fileWriter = new NewEntryPointFileWriter(fs); + entryPoint = getEntryPointInfo( + fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !; + esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); + }); + + it('should write the modified files to a new folder', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + { + path: _('/node_modules/test/esm5.js'), + contents: 'export function FooTop() {} // MODIFIED' + }, + {path: _('/node_modules/test/esm5.js.map'), contents: 'MODIFIED MAPPING DATA'}, + ]); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/esm5.js'))) + .toEqual('export function FooTop() {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/esm5.js'))).toEqual('export function FooTop() {}'); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/esm5.js.map'))) + .toEqual('MODIFIED MAPPING DATA'); + expect(fs.readFile(_('/node_modules/test/esm5.js.map'))).toEqual('ORIGINAL MAPPING DATA'); + }); + + it('should also copy unmodified files in the program', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/es2015/foo.js'), + contents: 'export class FooTop {} // MODIFIED' + }, + ]); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/foo.js'))) + .toEqual('export class FooTop {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/es2015/foo.js'))) + .toEqual('export class FooTop {}'); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/index.js'))) + .toEqual('export {FooTop} from "./foo";'); + expect(fs.readFile(_('/node_modules/test/es2015/index.js'))) + .toEqual('export {FooTop} from "./foo";'); + }); + + it('should update the package.json properties', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + { + path: _('/node_modules/test/esm5.js'), + contents: 'export function FooTop() {} // MODIFIED' + }, + ]); + expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '__ivy_ngcc__/esm5.js', + })); + + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/es2015/foo.js'), + contents: 'export class FooTop {} // MODIFIED' + }, + ]); + expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '__ivy_ngcc__/esm5.js', + es2015_ivy_ngcc: '__ivy_ngcc__/es2015/index.js', + })); + }); + + it('should overwrite and backup typings files', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/index.d.ts'), + contents: 'export declare class FooTop {} // MODIFIED' + }, + {path: _('/node_modules/test/index.d.ts.map'), contents: 'MODIFIED MAPPING DATA'}, + ]); + expect(fs.readFile(_('/node_modules/test/index.d.ts'))) + .toEqual('export declare class FooTop {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/index.d.ts.__ivy_ngcc_bak'))) + .toEqual('export declare class FooTop {}'); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts'))).toBe(false); + + expect(fs.readFile(_('/node_modules/test/index.d.ts.map'))) + .toEqual('MODIFIED MAPPING DATA'); + expect(fs.readFile(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak'))) + .toEqual('ORIGINAL MAPPING DATA'); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts.map'))).toBe(false); + }); }); - it('should also copy unmodified files in the program', () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/es2015/foo.js'), - contents: 'export class FooTop {} // MODIFIED' - }, - ]); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/foo.js'))) - .toEqual('export class FooTop {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/es2015/foo.js'))).toEqual('export class FooTop {}'); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/index.js'))) - .toEqual('export {FooTop} from "./foo";'); - expect(fs.readFile(_('/node_modules/test/es2015/index.js'))) - .toEqual('export {FooTop} from "./foo";'); + describe('writeBundle() [secondary entry-point]', () => { + beforeEach(() => { + fs = getFileSystem(); + fileWriter = new NewEntryPointFileWriter(fs); + entryPoint = getEntryPointInfo( + fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !; + esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); + }); + + it('should write the modified file to a new folder', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + { + path: _('/node_modules/test/a/esm5.js'), + contents: 'export function FooA() {} // MODIFIED' + }, + ]); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/esm5.js'))) + .toEqual('export function FooA() {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/a/esm5.js'))).toEqual('export function FooA() {}'); + }); + + it('should also copy unmodified files in the program', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/a/es2015/foo.js'), + contents: 'export class FooA {} // MODIFIED' + }, + ]); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js'))) + .toEqual('export class FooA {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/a/es2015/foo.js'))) + .toEqual('export class FooA {}'); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/index.js'))) + .toEqual('export {FooA} from "./foo";'); + expect(fs.readFile(_('/node_modules/test/a/es2015/index.js'))) + .toEqual('export {FooA} from "./foo";'); + }); + + it('should update the package.json properties', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + { + path: _('/node_modules/test/a/esm5.js'), + contents: 'export function FooA() {} // MODIFIED' + }, + ]); + expect(loadPackageJson(fs, '/node_modules/test/a')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', + })); + + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/a/es2015/foo.js'), + contents: 'export class FooA {} // MODIFIED' + }, + ]); + expect(loadPackageJson(fs, '/node_modules/test/a')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', + es2015_ivy_ngcc: '../__ivy_ngcc__/a/es2015/index.js', + })); + }); + + it('should overwrite and backup typings files', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/a/index.d.ts'), + contents: 'export declare class FooA {} // MODIFIED' + }, + ]); + expect(fs.readFile(_('/node_modules/test/a/index.d.ts'))) + .toEqual('export declare class FooA {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/a/index.d.ts.__ivy_ngcc_bak'))) + .toEqual('export declare class FooA {}'); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/a/index.d.ts'))).toBe(false); + }); }); - it('should update the package.json properties', () => { - fileWriter.writeBundle(entryPoint, esm5bundle, [ - { - path: _('/node_modules/test/esm5.js'), - contents: 'export function FooTop() {} // MODIFIED' - }, - ]); - expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ - module_ivy_ngcc: '__ivy_ngcc__/esm5.js', - })); + describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => { + beforeEach(() => { + fs = getFileSystem(); + fileWriter = new NewEntryPointFileWriter(fs); + entryPoint = getEntryPointInfo( + fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !; + esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); + }); - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/es2015/foo.js'), - contents: 'export class FooTop {} // MODIFIED' - }, - ]); - expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ - module_ivy_ngcc: '__ivy_ngcc__/esm5.js', - es2015_ivy_ngcc: '__ivy_ngcc__/es2015/index.js', - })); - }); + it('should write the modified file to a new folder', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + { + path: _('/node_modules/test/lib/esm5.js'), + contents: 'export function FooB() {} // MODIFIED' + }, + ]); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/esm5.js'))) + .toEqual('export function FooB() {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/lib/esm5.js'))) + .toEqual('export function FooB() {}'); + }); - it('should overwrite and backup typings files', () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/index.d.ts'), - contents: 'export declare class FooTop {} // MODIFIED' - }, - {path: _('/node_modules/test/index.d.ts.map'), contents: 'MODIFIED MAPPING DATA'}, - ]); - expect(fs.readFile(_('/node_modules/test/index.d.ts'))) - .toEqual('export declare class FooTop {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/index.d.ts.__ivy_ngcc_bak'))) - .toEqual('export declare class FooTop {}'); - expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts'))).toBe(false); + it('should also copy unmodified files in the program', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/lib/es2015/foo.js'), + contents: 'export class FooB {} // MODIFIED' + }, + ]); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/foo.js'))) + .toEqual('export class FooB {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/lib/es2015/foo.js'))) + .toEqual('import {FooA} from "test/a"; import "events"; export class FooB {}'); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/index.js'))) + .toEqual('export {FooB} from "./foo"; import * from "other";'); + expect(fs.readFile(_('/node_modules/test/lib/es2015/index.js'))) + .toEqual('export {FooB} from "./foo"; import * from "other";'); + }); - expect(fs.readFile(_('/node_modules/test/index.d.ts.map'))).toEqual('MODIFIED MAPPING DATA'); - expect(fs.readFile(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak'))) - .toEqual('ORIGINAL MAPPING DATA'); - expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts.map'))).toBe(false); + it('should not copy typings files within the package (i.e. from a different entry-point)', + () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/lib/es2015/foo.js'), + contents: 'export class FooB {} // MODIFIED' + }, + ]); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/a/index.d.ts'))).toEqual(false); + }); + + it('should not copy files outside of the package', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/lib/es2015/foo.js'), + contents: 'export class FooB {} // MODIFIED' + }, + ]); + expect(fs.exists(_('/node_modules/test/other/index.d.ts'))).toEqual(false); + expect(fs.exists(_('/node_modules/test/events/events.js'))).toEqual(false); + }); + + it('should update the package.json properties', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + { + path: _('/node_modules/test/lib/esm5.js'), + contents: 'export function FooB() {} // MODIFIED' + }, + ]); + expect(loadPackageJson(fs, '/node_modules/test/b')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', + })); + + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/lib/es2015/foo.js'), + contents: 'export class FooB {} // MODIFIED' + }, + ]); + expect(loadPackageJson(fs, '/node_modules/test/b')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', + es2015_ivy_ngcc: '../__ivy_ngcc__/lib/es2015/index.js', + })); + }); + + it('should overwrite and backup typings files', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: _('/node_modules/test/typings/index.d.ts'), + contents: 'export declare class FooB {} // MODIFIED' + }, + ]); + expect(fs.readFile(_('/node_modules/test/typings/index.d.ts'))) + .toEqual('export declare class FooB {} // MODIFIED'); + expect(fs.readFile(_('/node_modules/test/typings/index.d.ts.__ivy_ngcc_bak'))) + .toEqual('export declare class FooB {}'); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/typings/index.d.ts'))).toBe(false); + }); }); }); - describe('writeBundle() [secondary entry-point]', () => { - beforeEach(() => { - fs = createMockFileSystem(); - fileWriter = new NewEntryPointFileWriter(fs); - entryPoint = getEntryPointInfo( - fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !; - esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); - esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); - }); - - it('should write the modified file to a new folder', () => { - fileWriter.writeBundle(entryPoint, esm5bundle, [ - { - path: _('/node_modules/test/a/esm5.js'), - contents: 'export function FooA() {} // MODIFIED' - }, - ]); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/esm5.js'))) - .toEqual('export function FooA() {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/a/esm5.js'))).toEqual('export function FooA() {}'); - }); - - it('should also copy unmodified files in the program', () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/a/es2015/foo.js'), - contents: 'export class FooA {} // MODIFIED' - }, - ]); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js'))) - .toEqual('export class FooA {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/a/es2015/foo.js'))).toEqual('export class FooA {}'); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/index.js'))) - .toEqual('export {FooA} from "./foo";'); - expect(fs.readFile(_('/node_modules/test/a/es2015/index.js'))) - .toEqual('export {FooA} from "./foo";'); - }); - - it('should update the package.json properties', () => { - fileWriter.writeBundle(entryPoint, esm5bundle, [ - { - path: _('/node_modules/test/a/esm5.js'), - contents: 'export function FooA() {} // MODIFIED' - }, - ]); - expect(loadPackageJson(fs, '/node_modules/test/a')).toEqual(jasmine.objectContaining({ - module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', - })); - - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/a/es2015/foo.js'), - contents: 'export class FooA {} // MODIFIED' - }, - ]); - expect(loadPackageJson(fs, '/node_modules/test/a')).toEqual(jasmine.objectContaining({ - module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', - es2015_ivy_ngcc: '../__ivy_ngcc__/a/es2015/index.js', - })); - }); - - it('should overwrite and backup typings files', () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/a/index.d.ts'), - contents: 'export declare class FooA {} // MODIFIED' - }, - ]); - expect(fs.readFile(_('/node_modules/test/a/index.d.ts'))) - .toEqual('export declare class FooA {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/a/index.d.ts.__ivy_ngcc_bak'))) - .toEqual('export declare class FooA {}'); - expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/a/index.d.ts'))).toBe(false); - }); - }); - - describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => { - beforeEach(() => { - fs = createMockFileSystem(); - fileWriter = new NewEntryPointFileWriter(fs); - entryPoint = getEntryPointInfo( - fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !; - esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); - esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); - }); - - it('should write the modified file to a new folder', () => { - fileWriter.writeBundle(entryPoint, esm5bundle, [ - { - path: _('/node_modules/test/lib/esm5.js'), - contents: 'export function FooB() {} // MODIFIED' - }, - ]); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/esm5.js'))) - .toEqual('export function FooB() {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/lib/esm5.js'))).toEqual('export function FooB() {}'); - }); - - it('should also copy unmodified files in the program', () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/lib/es2015/foo.js'), - contents: 'export class FooB {} // MODIFIED' - }, - ]); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/foo.js'))) - .toEqual('export class FooB {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/lib/es2015/foo.js'))) - .toEqual('import {FooA} from "test/a"; import "events"; export class FooB {}'); - expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/index.js'))) - .toEqual('export {FooB} from "./foo"; import * from "other";'); - expect(fs.readFile(_('/node_modules/test/lib/es2015/index.js'))) - .toEqual('export {FooB} from "./foo"; import * from "other";'); - }); - - it('should not copy typings files within the package (i.e. from a different entry-point)', - () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/lib/es2015/foo.js'), - contents: 'export class FooB {} // MODIFIED' - }, - ]); - expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/a/index.d.ts'))).toEqual(false); - }); - - it('should not copy files outside of the package', () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/lib/es2015/foo.js'), - contents: 'export class FooB {} // MODIFIED' - }, - ]); - expect(fs.exists(_('/node_modules/test/other/index.d.ts'))).toEqual(false); - expect(fs.exists(_('/node_modules/test/events/events.js'))).toEqual(false); - }); - - it('should update the package.json properties', () => { - fileWriter.writeBundle(entryPoint, esm5bundle, [ - { - path: _('/node_modules/test/lib/esm5.js'), - contents: 'export function FooB() {} // MODIFIED' - }, - ]); - expect(loadPackageJson(fs, '/node_modules/test/b')).toEqual(jasmine.objectContaining({ - module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', - })); - - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/lib/es2015/foo.js'), - contents: 'export class FooB {} // MODIFIED' - }, - ]); - expect(loadPackageJson(fs, '/node_modules/test/b')).toEqual(jasmine.objectContaining({ - module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', - es2015_ivy_ngcc: '../__ivy_ngcc__/lib/es2015/index.js', - })); - }); - - it('should overwrite and backup typings files', () => { - fileWriter.writeBundle(entryPoint, esm2015bundle, [ - { - path: _('/node_modules/test/typings/index.d.ts'), - contents: 'export declare class FooB {} // MODIFIED' - }, - ]); - expect(fs.readFile(_('/node_modules/test/typings/index.d.ts'))) - .toEqual('export declare class FooB {} // MODIFIED'); - expect(fs.readFile(_('/node_modules/test/typings/index.d.ts.__ivy_ngcc_bak'))) - .toEqual('export declare class FooB {}'); - expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/typings/index.d.ts'))).toBe(false); - }); - }); + function makeTestBundle( + fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, + format: EntryPointFormat): EntryPointBundle { + return makeEntryPointBundle( + fs, entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false, + formatProperty, format, true) !; + } }); - -function makeTestBundle( - fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, - format: EntryPointFormat): EntryPointBundle { - return makeEntryPointBundle( - fs, entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false, - formatProperty, format, true) !; -} diff --git a/packages/compiler-cli/src/extract_i18n.ts b/packages/compiler-cli/src/extract_i18n.ts index 7cc9bb4359..ad4cd93cb2 100644 --- a/packages/compiler-cli/src/extract_i18n.ts +++ b/packages/compiler-cli/src/extract_i18n.ts @@ -7,7 +7,6 @@ * found in the LICENSE file at https://angular.io/license */ - /** * Extract i18n messages from source code */ @@ -16,11 +15,12 @@ import 'reflect-metadata'; import * as api from './transformers/api'; import {ParsedConfiguration} from './perform_compile'; import {main, readCommandLineAndConfiguration} from './main'; +import {setFileSystem, NodeJSFileSystem} from './ngtsc/file_system'; export function mainXi18n( args: string[], consoleError: (msg: string) => void = console.error): number { const config = readXi18nCommandLineAndConfiguration(args); - return main(args, consoleError, config); + return main(args, consoleError, config, undefined, undefined, undefined); } function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfiguration { @@ -42,5 +42,7 @@ function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfigurati // Entry point if (require.main === module) { const args = process.argv.slice(2); + // We are running the real compiler so run against the real file-system + setFileSystem(new NodeJSFileSystem()); process.exitCode = mainXi18n(args); } diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index c682a7b83e..13ca9d5c22 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -17,8 +17,9 @@ import {replaceTsWithNgInErrors} from './ngtsc/diagnostics'; import * as api from './transformers/api'; import {GENERATED_FILES} from './transformers/util'; -import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult, filterErrorsAndWarnings} from './perform_compile'; +import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, filterErrorsAndWarnings} from './perform_compile'; import {performWatchCompilation, createPerformWatchHost} from './perform_watch'; +import {NodeJSFileSystem, setFileSystem} from './ngtsc/file_system'; export function main( args: string[], consoleError: (s: string) => void = console.error, @@ -227,5 +228,7 @@ export function watchMode( // CLI entry point if (require.main === module) { const args = process.argv.slice(2); + // We are running the real compiler so run against the real file-system + setFileSystem(new NodeJSFileSystem()); process.exitCode = main(args); } diff --git a/packages/compiler-cli/src/ngtools_api2.ts b/packages/compiler-cli/src/ngtools_api2.ts index ec3a8dfed9..445eb9f5bf 100644 --- a/packages/compiler-cli/src/ngtools_api2.ts +++ b/packages/compiler-cli/src/ngtools_api2.ts @@ -25,7 +25,6 @@ import {ParseSourceSpan} from '@angular/compiler'; import * as ts from 'typescript'; import {formatDiagnostics as formatDiagnosticsOrig} from './perform_compile'; -import {Program as ProgramOrig} from './transformers/api'; import {createCompilerHost as createCompilerOrig} from './transformers/compiler_host'; import {createProgram as createProgramOrig} from './transformers/program'; diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index cbeec1e959..ad7acdbe12 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/metadata", diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index c5626ef2ad..b59a226853 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -7,11 +7,11 @@ */ import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler'; -import * as path from 'path'; import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; +import {absoluteFrom, relative} from '../../file_system'; import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {IndexingContext} from '../../indexer'; import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata'; @@ -156,7 +156,7 @@ export class ComponentDecoratorHandler implements // Go through the root directories for this project, and select the one with the smallest // relative path representation. const relativeContextFilePath = this.rootDirs.reduce((previous, rootDir) => { - const candidate = path.posix.relative(rootDir, containingFile); + const candidate = relative(absoluteFrom(rootDir), absoluteFrom(containingFile)); if (previous === undefined || candidate.length < previous.length) { return candidate; } else { @@ -205,7 +205,7 @@ export class ComponentDecoratorHandler implements /* escapedString */ false, options); } else { // Expect an inline template to be present. - const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath); + const inlineTemplate = this._extractInlineTemplate(component, containingFile); if (inlineTemplate === null) { throw new FatalDiagnosticError( ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, @@ -583,8 +583,7 @@ export class ComponentDecoratorHandler implements } } - private _extractInlineTemplate( - component: Map, relativeContextFilePath: string): { + private _extractInlineTemplate(component: Map, containingFile: string): { templateStr: string, templateUrl: string, templateRange: LexerRange|undefined, @@ -606,7 +605,7 @@ export class ComponentDecoratorHandler implements // strip templateRange = getTemplateRange(templateExpr); templateStr = templateExpr.getSourceFile().text; - templateUrl = relativeContextFilePath; + templateUrl = containingFile; escapedString = true; } else { const resolvedTemplate = this.evaluator.evaluate(templateExpr); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/references_registry.ts b/packages/compiler-cli/src/ngtsc/annotations/src/references_registry.ts index 52731b2d6b..340be53737 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/references_registry.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/references_registry.ts @@ -8,7 +8,6 @@ import * as ts from 'typescript'; import {Reference} from '../../imports'; -import {Declaration} from '../../reflection'; /** * Implement this interface if you want DecoratorHandlers to register diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel index 44df168958..ac632ecd04 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel @@ -14,15 +14,15 @@ ts_library( "//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", - "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/translator", - "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index 9491bfd6fb..f265235f92 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -7,12 +7,14 @@ */ import {CycleAnalyzer, ImportGraph} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; +import {absoluteFrom} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; import {ResourceLoader} from '../src/api'; import {ComponentDecoratorHandler} from '../src/component'; @@ -22,62 +24,64 @@ export class NoopResourceLoader implements ResourceLoader { load(): string { throw new Error('Not implemented'); } preload(): Promise|undefined { throw new Error('Not implemented'); } } +runInEachFileSystem(() => { + describe('ComponentDecoratorHandler', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); -describe('ComponentDecoratorHandler', () => { - it('should produce a diagnostic when @Component has non-literal argument', () => { - const {program, options, host} = makeProgram([ - { - name: 'node_modules/@angular/core/index.d.ts', - contents: 'export const Component: any;', - }, - { - name: 'entry.ts', - contents: ` + it('should produce a diagnostic when @Component has non-literal argument', () => { + const {program, options, host} = makeProgram([ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Component: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {Component} from '@angular/core'; const TEST = ''; @Component(TEST) class TestCmp {} ` - }, - ]); - const checker = program.getTypeChecker(); - const reflectionHost = new TypeScriptReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker); - const moduleResolver = new ModuleResolver(program, options, host); - const importGraph = new ImportGraph(moduleResolver); - const cycleAnalyzer = new CycleAnalyzer(importGraph); - const metaRegistry = new LocalMetadataRegistry(); - const dtsReader = new DtsMetadataReader(checker, reflectionHost); - const scopeRegistry = new LocalModuleScopeRegistry( - metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), - null); - const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); - const refEmitter = new ReferenceEmitter([]); + }, + ]); + const checker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker); + const moduleResolver = new ModuleResolver(program, options, host); + const importGraph = new ImportGraph(moduleResolver); + const cycleAnalyzer = new CycleAnalyzer(importGraph); + const metaRegistry = new LocalMetadataRegistry(); + const dtsReader = new DtsMetadataReader(checker, reflectionHost); + const scopeRegistry = new LocalModuleScopeRegistry( + metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), + new ReferenceEmitter([]), null); + const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); + const refEmitter = new ReferenceEmitter([]); - const handler = new ComponentDecoratorHandler( - reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, false, - new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer, refEmitter, - NOOP_DEFAULT_IMPORT_RECORDER); - const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', isNamedClassDeclaration); - const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); - if (detected === undefined) { - return fail('Failed to recognize @Component'); - } - try { - handler.analyze(TestCmp, detected.metadata); - return fail('Analysis should have failed'); - } catch (err) { - if (!(err instanceof FatalDiagnosticError)) { - return fail('Error should be a FatalDiagnosticError'); + const handler = new ComponentDecoratorHandler( + reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, false, + new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer, refEmitter, + NOOP_DEFAULT_IMPORT_RECORDER); + const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration); + const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); + if (detected === undefined) { + return fail('Failed to recognize @Component'); } - const diag = err.toDiagnostic(); - expect(diag.code).toEqual(ivyCode(ErrorCode.DECORATOR_ARG_NOT_LITERAL)); - expect(diag.file.fileName.endsWith('entry.ts')).toBe(true); - expect(diag.start).toBe(detected.metadata.args ![0].getStart()); - } + try { + handler.analyze(TestCmp, detected.metadata); + return fail('Analysis should have failed'); + } catch (err) { + if (!(err instanceof FatalDiagnosticError)) { + return fail('Error should be a FatalDiagnosticError'); + } + const diag = err.toDiagnostic(); + expect(diag.code).toEqual(ivyCode(ErrorCode.DECORATOR_ARG_NOT_LITERAL)); + expect(diag.file.fileName.endsWith('entry.ts')).toBe(true); + expect(diag.start).toBe(detected.metadata.args ![0].getStart()); + } + }); }); -}); -function ivyCode(code: ErrorCode): number { - return Number('-99' + code.valueOf()); -} + function ivyCode(code: ErrorCode): number { return Number('-99' + code.valueOf()); } +}); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts index 3dafd6bc7a..50d4efc594 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts @@ -5,25 +5,30 @@ * 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 {absoluteFrom} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; import {DirectiveDecoratorHandler} from '../src/directive'; +runInEachFileSystem(() => { + describe('DirectiveDecoratorHandler', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); -describe('DirectiveDecoratorHandler', () => { - it('should use the `ReflectionHost` to detect class inheritance', () => { - const {program} = makeProgram([ - { - name: 'node_modules/@angular/core/index.d.ts', - contents: 'export const Directive: any;', - }, - { - name: 'entry.ts', - contents: ` + it('should use the `ReflectionHost` to detect class inheritance', () => { + const {program} = makeProgram([ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Directive: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {Directive} from '@angular/core'; @Directive({selector: 'test-dir-1'}) @@ -32,51 +37,53 @@ describe('DirectiveDecoratorHandler', () => { @Directive({selector: 'test-dir-2'}) export class TestDir2 {} `, - }, - ]); + }, + ]); - const checker = program.getTypeChecker(); - const reflectionHost = new TestReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker); - const metaReader = new LocalMetadataRegistry(); - const dtsReader = new DtsMetadataReader(checker, reflectionHost); - const scopeRegistry = new LocalModuleScopeRegistry( - metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), - null); - const handler = new DirectiveDecoratorHandler( - reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false); + const checker = program.getTypeChecker(); + const reflectionHost = new TestReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker); + const metaReader = new LocalMetadataRegistry(); + const dtsReader = new DtsMetadataReader(checker, reflectionHost); + const scopeRegistry = new LocalModuleScopeRegistry( + metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), + null); + const handler = new DirectiveDecoratorHandler( + reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false); - const analyzeDirective = (dirName: string) => { - const DirNode = getDeclaration(program, 'entry.ts', dirName, isNamedClassDeclaration); + const analyzeDirective = (dirName: string) => { + const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration); - const detected = handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode)); - if (detected === undefined) { - throw new Error(`Failed to recognize @Directive (${dirName}).`); - } + const detected = + handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode)); + if (detected === undefined) { + throw new Error(`Failed to recognize @Directive (${dirName}).`); + } - const {analysis} = handler.analyze(DirNode, detected.metadata); - if (analysis === undefined) { - throw new Error(`Failed to analyze @Directive (${dirName}).`); - } + const {analysis} = handler.analyze(DirNode, detected.metadata); + if (analysis === undefined) { + throw new Error(`Failed to analyze @Directive (${dirName}).`); + } - return analysis; - }; + return analysis; + }; - // By default, `TestReflectionHost#hasBaseClass()` returns `false`. - const analysis1 = analyzeDirective('TestDir1'); - expect(analysis1.meta.usesInheritance).toBe(false); + // By default, `TestReflectionHost#hasBaseClass()` returns `false`. + const analysis1 = analyzeDirective('TestDir1'); + expect(analysis1.meta.usesInheritance).toBe(false); - // Tweak `TestReflectionHost#hasBaseClass()` to return true. - reflectionHost.hasBaseClassReturnValue = true; + // Tweak `TestReflectionHost#hasBaseClass()` to return true. + reflectionHost.hasBaseClassReturnValue = true; - const analysis2 = analyzeDirective('TestDir2'); - expect(analysis2.meta.usesInheritance).toBe(true); + const analysis2 = analyzeDirective('TestDir2'); + expect(analysis2.meta.usesInheritance).toBe(true); + }); }); + + // Helpers + class TestReflectionHost extends TypeScriptReflectionHost { + hasBaseClassReturnValue = false; + + hasBaseClass(clazz: ClassDeclaration): boolean { return this.hasBaseClassReturnValue; } + } }); - -// Helpers -class TestReflectionHost extends TypeScriptReflectionHost { - hasBaseClassReturnValue = false; - - hasBaseClass(clazz: ClassDeclaration): boolean { return this.hasBaseClassReturnValue; } -} diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts index ec24285097..a034fa37a4 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts @@ -5,53 +5,44 @@ * 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 {absoluteFrom, getSourceFileOrError} from '../../file_system'; +import {TestFile, runInEachFileSystem} from '../../file_system/testing'; import {NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports'; import {TypeScriptReflectionHost} from '../../reflection'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; import {ImportManager, translateStatement} from '../../translator'; import {generateSetClassMetadataCall} from '../src/metadata'; -const CORE = { - name: 'node_modules/@angular/core/index.d.ts', - contents: ` - export declare function Input(...args: any[]): any; - export declare function Inject(...args: any[]): any; - export declare function Component(...args: any[]): any; - export declare class Injector {} - ` -}; - -describe('ngtsc setClassMetadata converter', () => { - it('should convert decorated class metadata', () => { - const res = compileAndPrint(` +runInEachFileSystem(() => { + describe('ngtsc setClassMetadata converter', () => { + it('should convert decorated class metadata', () => { + const res = compileAndPrint(` import {Component} from '@angular/core'; - + @Component('metadata') class Target {} `); - expect(res).toEqual( - `/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`); - }); + expect(res).toEqual( + `/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`); + }); - it('should convert decorated class constructor parameter metadata', () => { - const res = compileAndPrint(` + it('should convert decorated class constructor parameter metadata', () => { + const res = compileAndPrint(` import {Component, Inject, Injector} from '@angular/core'; const FOO = 'foo'; - + @Component('metadata') class Target { constructor(@Inject(FOO) foo: any, bar: Injector) {} } `); - expect(res).toContain( - `function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`); - }); + expect(res).toContain( + `function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`); + }); - it('should convert decorated field metadata', () => { - const res = compileAndPrint(` + it('should convert decorated field metadata', () => { + const res = compileAndPrint(` import {Component, Input} from '@angular/core'; - + @Component('metadata') class Target { @Input() foo: string; @@ -60,35 +51,47 @@ describe('ngtsc setClassMetadata converter', () => { notDecorated: string; } `); - expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`); - }); + expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`); + }); - it('should not convert non-angular decorators to metadata', () => { - const res = compileAndPrint(` + it('should not convert non-angular decorators to metadata', () => { + const res = compileAndPrint(` declare function NotAComponent(...args: any[]): any; - + @NotAComponent('metadata') class Target {} `); - expect(res).toBe(''); + expect(res).toBe(''); + }); }); -}); -function compileAndPrint(contents: string): string { - const {program} = makeProgram([ - CORE, { - name: 'index.ts', - contents, + function compileAndPrint(contents: string): string { + const _ = absoluteFrom; + const CORE: TestFile = { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: ` + export declare function Input(...args: any[]): any; + export declare function Inject(...args: any[]): any; + export declare function Component(...args: any[]): any; + export declare class Injector {} + ` + }; + + const {program} = makeProgram([ + CORE, { + name: _('/index.ts'), + contents, + } + ]); + const host = new TypeScriptReflectionHost(program.getTypeChecker()); + const target = getDeclaration(program, _('/index.ts'), 'Target', ts.isClassDeclaration); + const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false); + if (call === null) { + return ''; } - ]); - const host = new TypeScriptReflectionHost(program.getTypeChecker()); - const target = getDeclaration(program, 'index.ts', 'Target', ts.isClassDeclaration); - const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false); - if (call === null) { - return ''; + const sf = getSourceFileOrError(program, _('/index.ts')); + const im = new ImportManager(new NoopImportRewriter(), 'i'); + const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER); + const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf); + return res.replace(/\s+/g, ' '); } - const sf = program.getSourceFile('index.ts') !; - const im = new ImportManager(new NoopImportRewriter(), 'i'); - const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER); - const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf); - return res.replace(/\s+/g, ' '); -} +}); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts index 6662c645ba..c310425e26 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts @@ -5,34 +5,36 @@ * 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 {WrappedNodeExpr} from '@angular/compiler'; import {R3Reference} from '@angular/compiler/src/compiler'; import * as ts from 'typescript'; - +import {absoluteFrom} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; import {NgModuleDecoratorHandler} from '../src/ng_module'; import {NoopReferencesRegistry} from '../src/references_registry'; -describe('NgModuleDecoratorHandler', () => { - it('should resolve forwardRef', () => { - const {program} = makeProgram([ - { - name: 'node_modules/@angular/core/index.d.ts', - contents: ` +runInEachFileSystem(() => { + describe('NgModuleDecoratorHandler', () => { + it('should resolve forwardRef', () => { + const _ = absoluteFrom; + const {program} = makeProgram([ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: ` export const Component: any; export const NgModule: any; export declare function forwardRef(fn: () => any): any; `, - }, - { - name: 'entry.ts', - contents: ` + }, + { + name: _('/entry.ts'), + contents: ` import {Component, forwardRef, NgModule} from '@angular/core'; @Component({ @@ -50,37 +52,38 @@ describe('NgModuleDecoratorHandler', () => { }) export class TestModule {} ` - }, - ]); - const checker = program.getTypeChecker(); - const reflectionHost = new TypeScriptReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker); - const referencesRegistry = new NoopReferencesRegistry(); - const metaRegistry = new LocalMetadataRegistry(); - const dtsReader = new DtsMetadataReader(checker, reflectionHost); - const scopeRegistry = new LocalModuleScopeRegistry( - metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), - null); - const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); + }, + ]); + const checker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker); + const referencesRegistry = new NoopReferencesRegistry(); + const metaRegistry = new LocalMetadataRegistry(); + const dtsReader = new DtsMetadataReader(checker, reflectionHost); + const scopeRegistry = new LocalModuleScopeRegistry( + metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), + new ReferenceEmitter([]), null); + const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); - const handler = new NgModuleDecoratorHandler( - reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null, - refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); - const TestModule = getDeclaration(program, 'entry.ts', 'TestModule', isNamedClassDeclaration); - const detected = - handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule)); - if (detected === undefined) { - return fail('Failed to recognize @NgModule'); - } - const moduleDef = handler.analyze(TestModule, detected.metadata).analysis !.ngModuleDef; + const handler = new NgModuleDecoratorHandler( + reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null, + refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); + const TestModule = + getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); + const detected = + handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule)); + if (detected === undefined) { + return fail('Failed to recognize @NgModule'); + } + const moduleDef = handler.analyze(TestModule, detected.metadata).analysis !.ngModuleDef; - expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']); - expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']); - expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']); + expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']); + expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']); + expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']); - function getReferenceIdentifierTexts(references: R3Reference[]) { - return references.map(ref => (ref.value as WrappedNodeExpr).node.text); - } + function getReferenceIdentifierTexts(references: R3Reference[]) { + return references.map(ref => (ref.value as WrappedNodeExpr).node.text); + } + }); }); - }); diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel index 37728045e2..e95443472f 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel @@ -11,6 +11,8 @@ ts_library( deps = [ "//packages:types", "//packages/compiler-cli/src/ngtsc/cycles", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/testing", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts index 310deaf23f..3f11d7c418 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts @@ -5,62 +5,66 @@ * 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 {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {ModuleResolver} from '../../imports'; import {CycleAnalyzer} from '../src/analyzer'; import {ImportGraph} from '../src/imports'; - import {makeProgramFromGraph} from './util'; -describe('cycle analyzer', () => { - it('should not detect a cycle when there isn\'t one', () => { - const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); - const b = program.getSourceFile('b.ts') !; - const c = program.getSourceFile('c.ts') !; - expect(analyzer.wouldCreateCycle(b, c)).toBe(false); - expect(analyzer.wouldCreateCycle(c, b)).toBe(false); +runInEachFileSystem(() => { + describe('cycle analyzer', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); + + it('should not detect a cycle when there isn\'t one', () => { + const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); + const b = getSourceFileOrError(program, (_('/b.ts'))); + const c = getSourceFileOrError(program, (_('/c.ts'))); + expect(analyzer.wouldCreateCycle(b, c)).toBe(false); + expect(analyzer.wouldCreateCycle(c, b)).toBe(false); + }); + + it('should detect a simple cycle between two files', () => { + const {program, analyzer} = makeAnalyzer('a:b;b'); + const a = getSourceFileOrError(program, (_('/a.ts'))); + const b = getSourceFileOrError(program, (_('/b.ts'))); + expect(analyzer.wouldCreateCycle(a, b)).toBe(false); + expect(analyzer.wouldCreateCycle(b, a)).toBe(true); + }); + + it('should detect a cycle with a re-export in the chain', () => { + const {program, analyzer} = makeAnalyzer('a:*b;b:c;c'); + const a = getSourceFileOrError(program, (_('/a.ts'))); + const c = getSourceFileOrError(program, (_('/c.ts'))); + expect(analyzer.wouldCreateCycle(a, c)).toBe(false); + expect(analyzer.wouldCreateCycle(c, a)).toBe(true); + }); + + it('should detect a cycle in a more complex program', () => { + const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g'); + const b = getSourceFileOrError(program, (_('/b.ts'))); + const g = getSourceFileOrError(program, (_('/g.ts'))); + expect(analyzer.wouldCreateCycle(b, g)).toBe(false); + expect(analyzer.wouldCreateCycle(g, b)).toBe(true); + }); + + it('should detect a cycle caused by a synthetic edge', () => { + const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); + const b = getSourceFileOrError(program, (_('/b.ts'))); + const c = getSourceFileOrError(program, (_('/c.ts'))); + expect(analyzer.wouldCreateCycle(b, c)).toBe(false); + analyzer.recordSyntheticImport(c, b); + expect(analyzer.wouldCreateCycle(b, c)).toBe(true); + }); }); - it('should detect a simple cycle between two files', () => { - const {program, analyzer} = makeAnalyzer('a:b;b'); - const a = program.getSourceFile('a.ts') !; - const b = program.getSourceFile('b.ts') !; - expect(analyzer.wouldCreateCycle(a, b)).toBe(false); - expect(analyzer.wouldCreateCycle(b, a)).toBe(true); - }); - - it('should detect a cycle with a re-export in the chain', () => { - const {program, analyzer} = makeAnalyzer('a:*b;b:c;c'); - const a = program.getSourceFile('a.ts') !; - const c = program.getSourceFile('c.ts') !; - expect(analyzer.wouldCreateCycle(a, c)).toBe(false); - expect(analyzer.wouldCreateCycle(c, a)).toBe(true); - }); - - it('should detect a cycle in a more complex program', () => { - const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g'); - const b = program.getSourceFile('b.ts') !; - const g = program.getSourceFile('g.ts') !; - expect(analyzer.wouldCreateCycle(b, g)).toBe(false); - expect(analyzer.wouldCreateCycle(g, b)).toBe(true); - }); - - it('should detect a cycle caused by a synthetic edge', () => { - const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); - const b = program.getSourceFile('b.ts') !; - const c = program.getSourceFile('c.ts') !; - expect(analyzer.wouldCreateCycle(b, c)).toBe(false); - analyzer.recordSyntheticImport(c, b); - expect(analyzer.wouldCreateCycle(b, c)).toBe(true); - }); + function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} { + const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph); + return { + program, + analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))), + }; + } }); - -function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} { - const {program, options, host} = makeProgramFromGraph(graph); - return { - program, - analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))), - }; -} diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts b/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts index 4dba9dd800..3a0fe67815 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts @@ -5,59 +5,67 @@ * 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 {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {ModuleResolver} from '../../imports'; import {ImportGraph} from '../src/imports'; - import {makeProgramFromGraph} from './util'; -describe('import graph', () => { - it('should record imports of a simple program', () => { - const {program, graph} = makeImportGraph('a:b;b:c;c'); - const a = program.getSourceFile('a.ts') !; - const b = program.getSourceFile('b.ts') !; - const c = program.getSourceFile('c.ts') !; - expect(importsToString(graph.importsOf(a))).toBe('b'); - expect(importsToString(graph.importsOf(b))).toBe('c'); +runInEachFileSystem(() => { + describe('import graph', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); + + it('should record imports of a simple program', () => { + const {program, graph} = makeImportGraph('a:b;b:c;c'); + const a = getSourceFileOrError(program, (_('/a.ts'))); + const b = getSourceFileOrError(program, (_('/b.ts'))); + const c = getSourceFileOrError(program, (_('/c.ts'))); + expect(importsToString(graph.importsOf(a))).toBe('b'); + expect(importsToString(graph.importsOf(b))).toBe('c'); + }); + + it('should calculate transitive imports of a simple program', () => { + const {program, graph} = makeImportGraph('a:b;b:c;c'); + const a = getSourceFileOrError(program, (_('/a.ts'))); + const b = getSourceFileOrError(program, (_('/b.ts'))); + const c = getSourceFileOrError(program, (_('/c.ts'))); + expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c'); + }); + + it('should calculate transitive imports in a more complex program (with a cycle)', () => { + const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g'); + const c = getSourceFileOrError(program, (_('/c.ts'))); + expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h'); + }); + + it('should reflect the addition of a synthetic import', () => { + const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b'); + const b = getSourceFileOrError(program, (_('/b.ts'))); + const c = getSourceFileOrError(program, (_('/c.ts'))); + const d = getSourceFileOrError(program, (_('/d.ts'))); + expect(importsToString(graph.importsOf(b))).toEqual(''); + expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d'); + graph.addSyntheticImport(b, c); + expect(importsToString(graph.importsOf(b))).toEqual('c'); + expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d'); + }); }); - it('should calculate transitive imports of a simple program', () => { - const {program, graph} = makeImportGraph('a:b;b:c;c'); - const a = program.getSourceFile('a.ts') !; - const b = program.getSourceFile('b.ts') !; - const c = program.getSourceFile('c.ts') !; - expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c'); - }); + function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} { + const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph); + return { + program, + graph: new ImportGraph(new ModuleResolver(program, options, host)), + }; + } - it('should calculate transitive imports in a more complex program (with a cycle)', () => { - const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g'); - const c = program.getSourceFile('c.ts') !; - expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h'); - }); - - it('should reflect the addition of a synthetic import', () => { - const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b'); - const b = program.getSourceFile('b.ts') !; - const c = program.getSourceFile('c.ts') !; - const d = program.getSourceFile('d.ts') !; - expect(importsToString(graph.importsOf(b))).toEqual(''); - expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d'); - graph.addSyntheticImport(b, c); - expect(importsToString(graph.importsOf(b))).toEqual('c'); - expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d'); - }); + function importsToString(imports: Set): string { + const fs = getFileSystem(); + return Array.from(imports) + .map(sf => fs.basename(sf.fileName).replace('.ts', '')) + .sort() + .join(','); + } }); - -function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} { - const {program, options, host} = makeProgramFromGraph(graph); - return { - program, - graph: new ImportGraph(new ModuleResolver(program, options, host)), - }; -} - -function importsToString(imports: Set): string { - return Array.from(imports).map(sf => sf.fileName.substr(1).replace('.ts', '')).sort().join(','); -} diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/util.ts b/packages/compiler-cli/src/ngtsc/cycles/test/util.ts index 9c0dcc73c4..8aa3d135bf 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/util.ts @@ -5,10 +5,10 @@ * 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 {makeProgram} from '../../testing/in_memory_typescript'; +import {FileSystem} from '../../file_system'; +import {TestFile} from '../../file_system/testing'; +import {makeProgram} from '../../testing'; /** * Construct a TS program consisting solely of an import graph, from a string-based representation @@ -31,12 +31,12 @@ import {makeProgram} from '../../testing/in_memory_typescript'; * * represents a program where a.ts exports from b.ts and imports from c.ts. */ -export function makeProgramFromGraph(graph: string): { +export function makeProgramFromGraph(fs: FileSystem, graph: string): { program: ts.Program, host: ts.CompilerHost, options: ts.CompilerOptions, } { - const files = graph.split(';').map(fileSegment => { + const files: TestFile[] = graph.split(';').map(fileSegment => { const [name, importList] = fileSegment.split(':'); const contents = (importList ? importList.split(',') : []) .map(i => { @@ -50,7 +50,7 @@ export function makeProgramFromGraph(graph: string): { .join('\n') + `export const ${name} = '${name}';\n`; return { - name: `${name}.ts`, + name: fs.resolve(`/${name}.ts`), contents, }; }); diff --git a/packages/compiler-cli/src/ngtsc/entry_point/BUILD.bazel b/packages/compiler-cli/src/ngtsc/entry_point/BUILD.bazel index 125021b949..1ac6df5c26 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/entry_point/BUILD.bazel @@ -10,7 +10,7 @@ ts_library( module_name = "@angular/compiler-cli/src/ngtsc/entry_point", deps = [ "//packages/compiler-cli/src/ngtsc/diagnostics", - "//packages/compiler-cli/src/ngtsc/path", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/shims", "//packages/compiler-cli/src/ngtsc/util", "@npm//@types/node", diff --git a/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts b/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts index ef1eaa614a..fcf3b760d2 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts @@ -8,9 +8,9 @@ /// -import * as path from 'path'; import * as ts from 'typescript'; +import {AbsoluteFsPath, dirname, join} from '../../file_system'; import {ShimGenerator} from '../../shims'; import {relativePathBetween} from '../../util/src/path'; @@ -18,11 +18,10 @@ export class FlatIndexGenerator implements ShimGenerator { readonly flatIndexPath: string; constructor( - readonly entryPoint: string, relativeFlatIndexPath: string, + readonly entryPoint: AbsoluteFsPath, relativeFlatIndexPath: string, readonly moduleName: string|null) { - this.flatIndexPath = path.posix.join(path.posix.dirname(entryPoint), relativeFlatIndexPath) - .replace(/\.js$/, '') + - '.ts'; + this.flatIndexPath = + join(dirname(entryPoint), relativeFlatIndexPath).replace(/\.js$/, '') + '.ts'; } recognize(fileName: string): boolean { return fileName === this.flatIndexPath; } diff --git a/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts b/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts index 64ecb4634e..a639f5bf09 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts @@ -6,15 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath} from '../../path/src/types'; +import {AbsoluteFsPath, getFileSystem} from '../../file_system'; import {isNonDeclarationTsPath} from '../../util/src/typescript'; -export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray): string|null { +export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray): AbsoluteFsPath| + null { // There are two ways for a file to be recognized as the flat module index: // 1) if it's the only file!!!!!! // 2) (deprecated) if it's named 'index.ts' and has the shortest path of all such files. const tsFiles = rootFiles.filter(file => isNonDeclarationTsPath(file)); - let resolvedEntryPoint: string|null = null; + let resolvedEntryPoint: AbsoluteFsPath|null = null; if (tsFiles.length === 1) { // There's only one file - this is the flat module index. @@ -26,7 +27,7 @@ export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray // // This behavior is DEPRECATED and only exists to support existing usages. for (const tsFile of tsFiles) { - if (tsFile.endsWith('/index.ts') && + if (getFileSystem().basename(tsFile) === 'index.ts' && (resolvedEntryPoint === null || tsFile.length <= resolvedEntryPoint.length)) { resolvedEntryPoint = tsFile; } diff --git a/packages/compiler-cli/src/ngtsc/entry_point/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/entry_point/test/BUILD.bazel index 02748f80fe..03c4b93867 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/entry_point/test/BUILD.bazel @@ -11,7 +11,8 @@ ts_library( deps = [ "//packages:types", "//packages/compiler-cli/src/ngtsc/entry_point", - "//packages/compiler-cli/src/ngtsc/path", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts b/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts index 88dfbca699..7099cb8cb6 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts @@ -6,24 +6,25 @@ * 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 {AbsoluteFsPath} from '../../path/src/types'; +import {absoluteFrom} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {findFlatIndexEntryPoint} from '../src/logic'; -describe('entry_point logic', () => { +runInEachFileSystem(() => { + describe('entry_point logic', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); - describe('findFlatIndexEntryPoint', () => { + describe('findFlatIndexEntryPoint', () => { - it('should use the only source file if only a single one is specified', () => { - expect(findFlatIndexEntryPoint([AbsoluteFsPath.fromUnchecked('/src/index.ts')])) - .toBe('/src/index.ts'); - }); + it('should use the only source file if only a single one is specified', + () => { expect(findFlatIndexEntryPoint([_('/src/index.ts')])).toBe(_('/src/index.ts')); }); - it('should use the shortest source file ending with "index.ts" for multiple files', () => { - expect(findFlatIndexEntryPoint([ - AbsoluteFsPath.fromUnchecked('/src/deep/index.ts'), - AbsoluteFsPath.fromUnchecked('/src/index.ts'), AbsoluteFsPath.fromUnchecked('/index.ts') - ])).toBe('/index.ts'); + it('should use the shortest source file ending with "index.ts" for multiple files', () => { + expect(findFlatIndexEntryPoint([ + _('/src/deep/index.ts'), _('/src/index.ts'), _('/index.ts') + ])).toBe(_('/index.ts')); + }); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/path/BUILD.bazel b/packages/compiler-cli/src/ngtsc/file_system/BUILD.bazel similarity index 91% rename from packages/compiler-cli/src/ngtsc/path/BUILD.bazel rename to packages/compiler-cli/src/ngtsc/file_system/BUILD.bazel index ebdbdfe262..206189e03c 100644 --- a/packages/compiler-cli/src/ngtsc/path/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/file_system/BUILD.bazel @@ -3,7 +3,7 @@ package(default_visibility = ["//visibility:public"]) load("//tools:defaults.bzl", "ts_library") ts_library( - name = "path", + name = "file_system", srcs = ["index.ts"] + glob([ "src/*.ts", ]), diff --git a/packages/compiler-cli/src/ngtsc/file_system/README.md b/packages/compiler-cli/src/ngtsc/file_system/README.md new file mode 100644 index 0000000000..06a29acf08 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/README.md @@ -0,0 +1,42 @@ +# Virtual file-system layer + +To improve cross platform support, all file access (and path manipulation) +is now done through a well known interface (`FileSystem`). + +For testing a number of `MockFileSystem` implementations are supplied. +These provide an in-memory file-system which emulates operating systems +like OS/X, Unix and Windows. + +The current file system is always available via the helper method, +`getFileSystem()`. This is also used by a number of helper +methods to avoid having to pass `FileSystem` objects around all the time. +The result of this is that one must be careful to ensure that the file-system +has been initialized before using any of these helper methods. +To prevent this happening accidentally the current file system always starts out +as an instance of `InvalidFileSystem`, which will throw an error if any of its +methods are called. + +You can set the current file-system by calling `setFileSystem()`. +During testing you can call the helper function `initMockFileSystem(os)` +which takes a string name of the OS to emulate, and will also monkey-patch +aspects of the TypeScript library to ensure that TS is also using the +current file-system. + +Finally there is the `NgtscCompilerHost` to be used for any TypeScript +compilation, which uses a given file-system. + +All tests that interact with the file-system should be tested against each +of the mock file-systems. A series of helpers have been provided to support +such tests: + +* `runInEachFileSystem()` - wrap your tests in this helper to run all the +wrapped tests in each of the mock file-systems, it calls `initMockFileSystem()` +for each OS to emulate. +* `loadTestFiles()` - use this to add files and their contents +to the mock file system for testing. +* `loadStandardTestFiles()` - use this to load a mirror image of files on +disk into the in-memory mock file-system. +* `loadFakeCore()` - use this to load a fake version of `@angular/core` +into the mock file-system. + +All ngcc and ngtsc source and tests now use this virtual file-system setup. diff --git a/packages/compiler-cli/src/ngtsc/file_system/index.ts b/packages/compiler-cli/src/ngtsc/file_system/index.ts new file mode 100644 index 0000000000..e0cd876adb --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export {NgtscCompilerHost} from './src/compiler_host'; +export {absoluteFrom, absoluteFromSourceFile, basename, dirname, getFileSystem, isRoot, join, relative, relativeFrom, resolve, setFileSystem} from './src/helpers'; +export {LogicalFileSystem, LogicalProjectPath} from './src/logical'; +export {NodeJSFileSystem} from './src/node_js_file_system'; +export {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './src/types'; +export {getSourceFileOrError} from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts b/packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts new file mode 100644 index 0000000000..df5c072ce2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts @@ -0,0 +1,71 @@ +/** + * @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 os from 'os'; +import * as ts from 'typescript'; + +import {absoluteFrom} from './helpers'; +import {FileSystem} from './types'; + +export class NgtscCompilerHost implements ts.CompilerHost { + constructor(protected fs: FileSystem, protected options: ts.CompilerOptions = {}) {} + + getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { + const text = this.readFile(fileName); + return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion, true) : + undefined; + } + + getDefaultLibFileName(options: ts.CompilerOptions): string { + return this.fs.join(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options)); + } + + getDefaultLibLocation(): string { return this.fs.getDefaultLibLocation(); } + + writeFile( + fileName: string, data: string, writeByteOrderMark: boolean, + onError: ((message: string) => void)|undefined, + sourceFiles?: ReadonlyArray): void { + const path = absoluteFrom(fileName); + this.fs.ensureDir(this.fs.dirname(path)); + this.fs.writeFile(path, data); + } + + getCurrentDirectory(): string { return this.fs.pwd(); } + + getCanonicalFileName(fileName: string): string { + return this.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); + } + + useCaseSensitiveFileNames(): boolean { return this.fs.isCaseSensitive(); } + + getNewLine(): string { + switch (this.options.newLine) { + case ts.NewLineKind.CarriageReturnLineFeed: + return '\r\n'; + case ts.NewLineKind.LineFeed: + return '\n'; + default: + return os.EOL; + } + } + + fileExists(fileName: string): boolean { + const absPath = this.fs.resolve(fileName); + return this.fs.exists(absPath); + } + + readFile(fileName: string): string|undefined { + const absPath = this.fs.resolve(fileName); + if (!this.fileExists(absPath)) { + return undefined; + } + return this.fs.readFile(absPath); + } +} diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts b/packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts new file mode 100644 index 0000000000..9cf87fedb6 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts @@ -0,0 +1,88 @@ +/** + * @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 {InvalidFileSystem} from './invalid_file_system'; +import {AbsoluteFsPath, FileSystem, PathSegment, PathString} from './types'; +import {normalizeSeparators} from './util'; + +let fs: FileSystem = new InvalidFileSystem(); +export function getFileSystem(): FileSystem { + return fs; +} +export function setFileSystem(fileSystem: FileSystem) { + fs = fileSystem; +} + +/** + * Convert the path `path` to an `AbsoluteFsPath`, throwing an error if it's not an absolute path. + */ +export function absoluteFrom(path: string): AbsoluteFsPath { + if (!fs.isRooted(path)) { + throw new Error(`Internal Error: absoluteFrom(${path}): path is not absolute`); + } + return fs.resolve(path); +} + +/** + * Extract an `AbsoluteFsPath` from a `ts.SourceFile`. + */ +export function absoluteFromSourceFile(sf: ts.SourceFile): AbsoluteFsPath { + return fs.resolve(sf.fileName); +} + +/** +* Convert the path `path` to a `PathSegment`, throwing an error if it's not a relative path. +*/ +export function relativeFrom(path: string): PathSegment { + const normalized = normalizeSeparators(path); + if (fs.isRooted(normalized)) { + throw new Error(`Internal Error: relativeFrom(${path}): path is not relative`); + } + return normalized as PathSegment; +} + +/** + * Static access to `dirname`. + */ +export function dirname(file: T): T { + return fs.dirname(file); +} + +/** + * Static access to `join`. + */ +export function join(basePath: T, ...paths: string[]): T { + return fs.join(basePath, ...paths); +} + +/** + * Static access to `resolve`s. + */ +export function resolve(basePath: string, ...paths: string[]): AbsoluteFsPath { + return fs.resolve(basePath, ...paths); +} + +/** Returns true when the path provided is the root path. */ +export function isRoot(path: AbsoluteFsPath): boolean { + return fs.isRoot(path); +} + +/** + * Static access to `relative`. + */ +export function relative(from: T, to: T): PathSegment { + return fs.relative(from, to); +} + +/** + * Static access to `basename`. + */ +export function basename(filePath: PathString, extension?: string): PathSegment { + return fs.basename(filePath, extension) as PathSegment; +} diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts b/packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts new file mode 100644 index 0000000000..86023671a7 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts @@ -0,0 +1,48 @@ +/** + * @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 {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './types'; + +/** + * The default `FileSystem` that will always fail. + * + * This is a way of ensuring that the developer consciously chooses and + * configures the `FileSystem` before using it; particularly important when + * considering static functions like `absoluteFrom()` which rely on + * the `FileSystem` under the hood. + */ +export class InvalidFileSystem implements FileSystem { + exists(path: AbsoluteFsPath): boolean { throw makeError(); } + readFile(path: AbsoluteFsPath): string { throw makeError(); } + writeFile(path: AbsoluteFsPath, data: string): void { throw makeError(); } + symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void { throw makeError(); } + readdir(path: AbsoluteFsPath): PathSegment[] { throw makeError(); } + lstat(path: AbsoluteFsPath): FileStats { throw makeError(); } + stat(path: AbsoluteFsPath): FileStats { throw makeError(); } + pwd(): AbsoluteFsPath { throw makeError(); } + extname(path: AbsoluteFsPath|PathSegment): string { throw makeError(); } + copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { throw makeError(); } + moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { throw makeError(); } + mkdir(path: AbsoluteFsPath): void { throw makeError(); } + ensureDir(path: AbsoluteFsPath): void { throw makeError(); } + isCaseSensitive(): boolean { throw makeError(); } + resolve(...paths: string[]): AbsoluteFsPath { throw makeError(); } + dirname(file: T): T { throw makeError(); } + join(basePath: T, ...paths: string[]): T { throw makeError(); } + isRoot(path: AbsoluteFsPath): boolean { throw makeError(); } + isRooted(path: string): boolean { throw makeError(); } + relative(from: T, to: T): PathSegment { throw makeError(); } + basename(filePath: string, extension?: string): PathSegment { throw makeError(); } + realpath(filePath: AbsoluteFsPath): AbsoluteFsPath { throw makeError(); } + getDefaultLibLocation(): AbsoluteFsPath { throw makeError(); } + normalize(path: T): T { throw makeError(); } +} + +function makeError() { + return new Error( + 'FileSystem has not been configured. Please call `setFileSystem()` before calling this method.'); +} diff --git a/packages/compiler-cli/src/ngtsc/path/src/logical.ts b/packages/compiler-cli/src/ngtsc/file_system/src/logical.ts similarity index 91% rename from packages/compiler-cli/src/ngtsc/path/src/logical.ts rename to packages/compiler-cli/src/ngtsc/file_system/src/logical.ts index 4ad75a3f59..312cd0d6d4 100644 --- a/packages/compiler-cli/src/ngtsc/path/src/logical.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/logical.ts @@ -5,15 +5,14 @@ * 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 path from 'path'; - import * as ts from 'typescript'; +import {absoluteFrom, dirname, relative, resolve} from './helpers'; import {AbsoluteFsPath, BrandedPath, PathSegment} from './types'; import {stripExtension} from './util'; + + /** * A path that's relative to the logical root of a TypeScript project (one of the project's * rootDirs). @@ -30,9 +29,9 @@ export const LogicalProjectPath = { * importing from `to`. */ relativePathBetween: function(from: LogicalProjectPath, to: LogicalProjectPath): PathSegment { - let relativePath = path.posix.relative(path.posix.dirname(from), to); + let relativePath = relative(dirname(resolve(from)), resolve(to)); if (!relativePath.startsWith('../')) { - relativePath = ('./' + relativePath); + relativePath = ('./' + relativePath) as PathSegment; } return relativePath as PathSegment; }, @@ -64,10 +63,10 @@ export class LogicalFileSystem { * Get the logical path in the project of a `ts.SourceFile`. * * This method is provided as a convenient alternative to calling - * `logicalPathOfFile(AbsoluteFsPath.fromSourceFile(sf))`. + * `logicalPathOfFile(absoluteFromSourceFile(sf))`. */ logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null { - return this.logicalPathOfFile(AbsoluteFsPath.from(sf.fileName)); + return this.logicalPathOfFile(absoluteFrom(sf.fileName)); } /** diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts b/packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts new file mode 100644 index 0000000000..61349263cd --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/// +import * as fs from 'fs'; +import * as p from 'path'; +import {absoluteFrom, relativeFrom} from './helpers'; +import {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './types'; + +/** + * A wrapper around the Node.js file-system (i.e the `fs` package). + */ +export class NodeJSFileSystem implements FileSystem { + private _caseSensitive: boolean|undefined = undefined; + exists(path: AbsoluteFsPath): boolean { return fs.existsSync(path); } + readFile(path: AbsoluteFsPath): string { return fs.readFileSync(path, 'utf8'); } + writeFile(path: AbsoluteFsPath, data: string): void { + return fs.writeFileSync(path, data, 'utf8'); + } + symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void { fs.symlinkSync(target, path); } + readdir(path: AbsoluteFsPath): PathSegment[] { return fs.readdirSync(path) as PathSegment[]; } + lstat(path: AbsoluteFsPath): FileStats { return fs.lstatSync(path); } + stat(path: AbsoluteFsPath): FileStats { return fs.statSync(path); } + pwd(): AbsoluteFsPath { return this.normalize(process.cwd()) as AbsoluteFsPath; } + copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { fs.copyFileSync(from, to); } + moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { fs.renameSync(from, to); } + mkdir(path: AbsoluteFsPath): void { fs.mkdirSync(path); } + ensureDir(path: AbsoluteFsPath): void { + const parents: AbsoluteFsPath[] = []; + while (!this.isRoot(path) && !this.exists(path)) { + parents.push(path); + path = this.dirname(path); + } + while (parents.length) { + this.mkdir(parents.pop() !); + } + } + isCaseSensitive(): boolean { + if (this._caseSensitive === undefined) { + this._caseSensitive = this.exists(togglePathCase(__filename)); + } + return this._caseSensitive; + } + resolve(...paths: string[]): AbsoluteFsPath { + return this.normalize(p.resolve(...paths)) as AbsoluteFsPath; + } + + dirname(file: T): T { return this.normalize(p.dirname(file)) as T; } + join(basePath: T, ...paths: string[]): T { + return this.normalize(p.join(basePath, ...paths)) as T; + } + isRoot(path: AbsoluteFsPath): boolean { return this.dirname(path) === this.normalize(path); } + isRooted(path: string): boolean { return p.isAbsolute(path); } + relative(from: T, to: T): PathSegment { + return relativeFrom(this.normalize(p.relative(from, to))); + } + basename(filePath: string, extension?: string): PathSegment { + return p.basename(filePath, extension) as PathSegment; + } + extname(path: AbsoluteFsPath|PathSegment): string { return p.extname(path); } + realpath(path: AbsoluteFsPath): AbsoluteFsPath { return this.resolve(fs.realpathSync(path)); } + getDefaultLibLocation(): AbsoluteFsPath { + return this.resolve(require.resolve('typescript'), '..'); + } + normalize(path: T): T { + // Convert backslashes to forward slashes + return path.replace(/\\/g, '/') as T; + } +} + +/** + * Toggle the case of each character in a file path. + */ +function togglePathCase(str: string): AbsoluteFsPath { + return absoluteFrom( + str.replace(/\w/g, ch => ch.toUpperCase() === ch ? ch.toLowerCase() : ch.toUpperCase())); +} diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/types.ts b/packages/compiler-cli/src/ngtsc/file_system/src/types.ts new file mode 100644 index 0000000000..40cf6a6f87 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/src/types.ts @@ -0,0 +1,74 @@ +/** + * @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 + */ + +/** + * A `string` representing a specific type of path, with a particular brand `B`. + * + * A `string` is not assignable to a `BrandedPath`, but a `BrandedPath` is assignable to a `string`. + * Two `BrandedPath`s with different brands are not mutually assignable. + */ +export type BrandedPath = string & { + _brand: B; +}; + +/** + * A fully qualified path in the file system, in POSIX form. + */ +export type AbsoluteFsPath = BrandedPath<'AbsoluteFsPath'>; + +/** + * A path that's relative to another (unspecified) root. + * + * This does not necessarily have to refer to a physical file. + */ +export type PathSegment = BrandedPath<'PathSegment'>; + +/** + * A basic interface to abstract the underlying file-system. + * + * This makes it easier to provide mock file-systems in unit tests, + * but also to create clever file-systems that have features such as caching. + */ +export interface FileSystem { + exists(path: AbsoluteFsPath): boolean; + readFile(path: AbsoluteFsPath): string; + writeFile(path: AbsoluteFsPath, data: string): void; + symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void; + readdir(path: AbsoluteFsPath): PathSegment[]; + lstat(path: AbsoluteFsPath): FileStats; + stat(path: AbsoluteFsPath): FileStats; + pwd(): AbsoluteFsPath; + extname(path: AbsoluteFsPath|PathSegment): string; + copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void; + moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void; + mkdir(path: AbsoluteFsPath): void; + ensureDir(path: AbsoluteFsPath): void; + isCaseSensitive(): boolean; + isRoot(path: AbsoluteFsPath): boolean; + isRooted(path: string): boolean; + resolve(...paths: string[]): AbsoluteFsPath; + dirname(file: T): T; + join(basePath: T, ...paths: string[]): T; + relative(from: T, to: T): PathSegment; + basename(filePath: string, extension?: string): PathSegment; + realpath(filePath: AbsoluteFsPath): AbsoluteFsPath; + getDefaultLibLocation(): AbsoluteFsPath; + normalize(path: T): T; +} + +export type PathString = string | AbsoluteFsPath | PathSegment; + +/** + * Information about an object in the FileSystem. + * This is analogous to the `fs.Stats` class in Node.js. + */ +export interface FileStats { + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +} diff --git a/packages/compiler-cli/src/ngtsc/path/src/util.ts b/packages/compiler-cli/src/ngtsc/file_system/src/util.ts similarity index 60% rename from packages/compiler-cli/src/ngtsc/path/src/util.ts rename to packages/compiler-cli/src/ngtsc/file_system/src/util.ts index 267301dd97..be2863e9ac 100644 --- a/packages/compiler-cli/src/ngtsc/path/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/util.ts @@ -5,11 +5,10 @@ * 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 */ - -// TODO(alxhub): Unify this file with `util/src/path`. +import * as ts from 'typescript'; +import {AbsoluteFsPath} from './types'; const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/; -const ABSOLUTE_PATH = /^([a-zA-Z]:\/|\/)/; /** * Convert Windows-style separators to POSIX separators. @@ -26,10 +25,11 @@ export function stripExtension(path: string): string { return path.replace(TS_DTS_JS_EXTENSION, ''); } -/** - * Returns true if the normalized path is an absolute path. - */ -export function isAbsolutePath(path: string): boolean { - // TODO: use regExp based on OS in the future - return ABSOLUTE_PATH.test(path); +export function getSourceFileOrError(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile { + const sf = program.getSourceFile(fileName); + if (sf === undefined) { + throw new Error( + `Program does not contain "${fileName}" - available files are ${program.getSourceFiles().map(sf => sf.fileName).join(', ')}`); + } + return sf; } diff --git a/packages/compiler-cli/src/ngtsc/path/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/file_system/test/BUILD.bazel similarity index 79% rename from packages/compiler-cli/src/ngtsc/path/test/BUILD.bazel rename to packages/compiler-cli/src/ngtsc/file_system/test/BUILD.bazel index 8b1e2b62c2..1354f78b48 100644 --- a/packages/compiler-cli/src/ngtsc/path/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/file_system/test/BUILD.bazel @@ -10,7 +10,8 @@ ts_library( ]), deps = [ "//packages:types", - "//packages/compiler-cli/src/ngtsc/path", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", ], ) diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts new file mode 100644 index 0000000000..b14d44cc70 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts @@ -0,0 +1,47 @@ +/** + * @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 os from 'os'; + +import {absoluteFrom, relativeFrom, setFileSystem} from '../src/helpers'; +import {NodeJSFileSystem} from '../src/node_js_file_system'; + +describe('path types', () => { + beforeEach(() => { setFileSystem(new NodeJSFileSystem()); }); + + describe('absoluteFrom', () => { + it('should not throw when creating one from an absolute path', + () => { expect(() => absoluteFrom('/test.txt')).not.toThrow(); }); + + if (os.platform() === 'win32') { + it('should not throw when creating one from a windows absolute path', + () => { expect(absoluteFrom('C:\\test.txt')).toEqual('C:/test.txt'); }); + it('should not throw when creating one from a windows absolute path with POSIX separators', + () => { expect(absoluteFrom('C:/test.txt')).toEqual('C:/test.txt'); }); + it('should support windows drive letters', + () => { expect(absoluteFrom('D:\\foo\\test.txt')).toEqual('D:/foo/test.txt'); }); + it('should convert Windows path separators to POSIX separators', + () => { expect(absoluteFrom('C:\\foo\\test.txt')).toEqual('C:/foo/test.txt'); }); + } + + it('should throw when creating one from a non-absolute path', + () => { expect(() => absoluteFrom('test.txt')).toThrow(); }); + }); + + describe('relativeFrom', () => { + it('should not throw when creating one from a relative path', + () => { expect(() => relativeFrom('a/b/c.txt')).not.toThrow(); }); + + it('should throw when creating one from an absolute path', + () => { expect(() => relativeFrom('/a/b/c.txt')).toThrow(); }); + + if (os.platform() === 'win32') { + it('should throw when creating one from a Windows absolute path', + () => { expect(() => relativeFrom('C:/a/b/c.txt')).toThrow(); }); + } + }); +}); diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts new file mode 100644 index 0000000000..35d89aa107 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts @@ -0,0 +1,65 @@ +/** + * @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 {absoluteFrom} from '../src/helpers'; +import {LogicalFileSystem, LogicalProjectPath} from '../src/logical'; +import {runInEachFileSystem} from '../testing'; + +runInEachFileSystem(() => { + describe('logical paths', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); + + describe('LogicalFileSystem', () => { + it('should determine logical paths in a single root file system', () => { + const fs = new LogicalFileSystem([_('/test')]); + expect(fs.logicalPathOfFile(_('/test/foo/foo.ts'))) + .toEqual('/foo/foo' as LogicalProjectPath); + expect(fs.logicalPathOfFile(_('/test/bar/bar.ts'))) + .toEqual('/bar/bar' as LogicalProjectPath); + expect(fs.logicalPathOfFile(_('/not-test/bar.ts'))).toBeNull(); + }); + + it('should determine logical paths in a multi-root file system', () => { + const fs = new LogicalFileSystem([_('/test/foo'), _('/test/bar')]); + expect(fs.logicalPathOfFile(_('/test/foo/foo.ts'))).toEqual('/foo' as LogicalProjectPath); + expect(fs.logicalPathOfFile(_('/test/bar/bar.ts'))).toEqual('/bar' as LogicalProjectPath); + }); + + it('should continue to work when one root is a child of another', () => { + const fs = new LogicalFileSystem([_('/test'), _('/test/dist')]); + expect(fs.logicalPathOfFile(_('/test/foo.ts'))).toEqual('/foo' as LogicalProjectPath); + expect(fs.logicalPathOfFile(_('/test/dist/foo.ts'))).toEqual('/foo' as LogicalProjectPath); + }); + + it('should always return `/` prefixed logical paths', () => { + const rootFs = new LogicalFileSystem([_('/')]); + expect(rootFs.logicalPathOfFile(_('/foo/foo.ts'))) + .toEqual('/foo/foo' as LogicalProjectPath); + + const nonRootFs = new LogicalFileSystem([_('/test/')]); + expect(nonRootFs.logicalPathOfFile(_('/test/foo/foo.ts'))) + .toEqual('/foo/foo' as LogicalProjectPath); + }); + }); + + describe('utilities', () => { + it('should give a relative path between two adjacent logical files', () => { + const res = LogicalProjectPath.relativePathBetween( + '/foo' as LogicalProjectPath, '/bar' as LogicalProjectPath); + expect(res).toEqual('./bar'); + }); + + it('should give a relative path between two non-adjacent logical files', () => { + const res = LogicalProjectPath.relativePathBetween( + '/foo/index' as LogicalProjectPath, '/bar/index' as LogicalProjectPath); + expect(res).toEqual('../bar/index'); + }); + }); + }); +}); diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts new file mode 100644 index 0000000000..8ba2b3a69a --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts @@ -0,0 +1,148 @@ +/** + * @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 realFs from 'fs'; +import {absoluteFrom, relativeFrom, setFileSystem} from '../src/helpers'; +import {NodeJSFileSystem} from '../src/node_js_file_system'; +import {AbsoluteFsPath} from '../src/types'; + +describe('NodeJSFileSystem', () => { + let fs: NodeJSFileSystem; + let abcPath: AbsoluteFsPath; + let xyzPath: AbsoluteFsPath; + + beforeEach(() => { + fs = new NodeJSFileSystem(); + // Set the file-system so that calls like `absoluteFrom()` + // and `relativeFrom()` work correctly. + setFileSystem(fs); + abcPath = absoluteFrom('/a/b/c'); + xyzPath = absoluteFrom('/x/y/z'); + }); + + describe('exists()', () => { + it('should delegate to fs.existsSync()', () => { + const spy = spyOn(realFs, 'existsSync').and.returnValues(true, false); + expect(fs.exists(abcPath)).toBe(true); + expect(spy).toHaveBeenCalledWith(abcPath); + expect(fs.exists(xyzPath)).toBe(false); + expect(spy).toHaveBeenCalledWith(xyzPath); + }); + }); + + describe('readFile()', () => { + it('should delegate to fs.readFileSync()', () => { + const spy = spyOn(realFs, 'readFileSync').and.returnValue('Some contents'); + const result = fs.readFile(abcPath); + expect(result).toBe('Some contents'); + expect(spy).toHaveBeenCalledWith(abcPath, 'utf8'); + }); + }); + + describe('writeFile()', () => { + it('should delegate to fs.writeFileSync()', () => { + const spy = spyOn(realFs, 'writeFileSync'); + fs.writeFile(abcPath, 'Some contents'); + expect(spy).toHaveBeenCalledWith(abcPath, 'Some contents', 'utf8'); + }); + }); + + describe('readdir()', () => { + it('should delegate to fs.readdirSync()', () => { + const spy = spyOn(realFs, 'readdirSync').and.returnValue(['x', 'y/z']); + const result = fs.readdir(abcPath); + expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]); + expect(spy).toHaveBeenCalledWith(abcPath); + }); + }); + + describe('lstat()', () => { + it('should delegate to fs.lstatSync()', () => { + const stats = new realFs.Stats(); + const spy = spyOn(realFs, 'lstatSync').and.returnValue(stats); + const result = fs.lstat(abcPath); + expect(result).toBe(stats); + expect(spy).toHaveBeenCalledWith(abcPath); + }); + }); + + describe('stat()', () => { + it('should delegate to fs.statSync()', () => { + const stats = new realFs.Stats(); + const spy = spyOn(realFs, 'statSync').and.returnValue(stats); + const result = fs.stat(abcPath); + expect(result).toBe(stats); + expect(spy).toHaveBeenCalledWith(abcPath); + }); + }); + + describe('pwd()', () => { + it('should delegate to process.cwd()', () => { + const spy = spyOn(process, 'cwd').and.returnValue(abcPath); + const result = fs.pwd(); + expect(result).toEqual(abcPath); + expect(spy).toHaveBeenCalledWith(); + }); + }); + + describe('copyFile()', () => { + it('should delegate to fs.copyFileSync()', () => { + const spy = spyOn(realFs, 'copyFileSync'); + fs.copyFile(abcPath, xyzPath); + expect(spy).toHaveBeenCalledWith(abcPath, xyzPath); + }); + }); + + describe('moveFile()', () => { + it('should delegate to fs.renameSync()', () => { + const spy = spyOn(realFs, 'renameSync'); + fs.moveFile(abcPath, xyzPath); + expect(spy).toHaveBeenCalledWith(abcPath, xyzPath); + }); + }); + + describe('mkdir()', () => { + it('should delegate to fs.mkdirSync()', () => { + const spy = spyOn(realFs, 'mkdirSync'); + fs.mkdir(xyzPath); + expect(spy).toHaveBeenCalledWith(xyzPath); + }); + }); + + describe('ensureDir()', () => { + it('should call exists() and fs.mkdir()', () => { + const aPath = absoluteFrom('/a'); + const abPath = absoluteFrom('/a/b'); + const xPath = absoluteFrom('/x'); + const xyPath = absoluteFrom('/x/y'); + const mkdirCalls: string[] = []; + const existsCalls: string[] = []; + spyOn(realFs, 'mkdirSync').and.callFake((path: string) => mkdirCalls.push(path)); + spyOn(fs, 'exists').and.callFake((path: AbsoluteFsPath) => { + existsCalls.push(path); + switch (path) { + case aPath: + return true; + case abPath: + return true; + default: + return false; + } + }); + fs.ensureDir(abcPath); + expect(existsCalls).toEqual([abcPath, abPath]); + expect(mkdirCalls).toEqual([abcPath]); + + mkdirCalls.length = 0; + existsCalls.length = 0; + + fs.ensureDir(xyzPath); + expect(existsCalls).toEqual([xyzPath, xyPath, xPath]); + expect(mkdirCalls).toEqual([xPath, xyPath, xyzPath]); + }); + }); +}); \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/BUILD.bazel b/packages/compiler-cli/src/ngtsc/file_system/testing/BUILD.bazel new file mode 100644 index 0000000000..e28599e6ae --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/BUILD.bazel @@ -0,0 +1,16 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "testing", + testonly = True, + srcs = glob([ + "**/*.ts", + ]), + deps = [ + "//packages:types", + "//packages/compiler-cli/src/ngtsc/file_system", + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/index.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/index.ts new file mode 100644 index 0000000000..c41ab26f7b --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {Folder, MockFileSystem} from './src/mock_file_system'; +export {MockFileSystemNative} from './src/mock_file_system_native'; +export {MockFileSystemPosix} from './src/mock_file_system_posix'; +export {MockFileSystemWindows} from './src/mock_file_system_windows'; +export {TestFile, initMockFileSystem, runInEachFileSystem} from './src/test_helper'; diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts new file mode 100644 index 0000000000..a19aa08eab --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts @@ -0,0 +1,239 @@ +/** + * @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 {basename, dirname, resolve} from '../../src/helpers'; +import {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from '../../src/types'; + +/** + * An in-memory file system that can be used in unit tests. + */ +export abstract class MockFileSystem implements FileSystem { + private _fileTree: Folder = {}; + private _cwd: AbsoluteFsPath; + + + constructor(private _isCaseSensitive = false, cwd: AbsoluteFsPath = '/' as AbsoluteFsPath) { + this._cwd = this.normalize(cwd); + } + + isCaseSensitive() { return this._isCaseSensitive; } + + exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path).entity !== null; } + + readFile(path: AbsoluteFsPath): string { + const {entity} = this.findFromPath(path); + if (isFile(entity)) { + return entity; + } else { + throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); + } + } + + writeFile(path: AbsoluteFsPath, data: string): void { + const [folderPath, basename] = this.splitIntoFolderAndFile(path); + const {entity} = this.findFromPath(folderPath); + if (entity === null || !isFolder(entity)) { + throw new MockFileSystemError( + 'ENOENT', path, `Unable to write file "${path}". The containing folder does not exist.`); + } + entity[basename] = data; + } + + symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void { + const [folderPath, basename] = this.splitIntoFolderAndFile(path); + const {entity} = this.findFromPath(folderPath); + if (entity === null || !isFolder(entity)) { + throw new MockFileSystemError( + 'ENOENT', path, + `Unable to create symlink at "${path}". The containing folder does not exist.`); + } + entity[basename] = new SymLink(target); + } + + readdir(path: AbsoluteFsPath): PathSegment[] { + const {entity} = this.findFromPath(path); + if (entity === null) { + throw new MockFileSystemError( + 'ENOENT', path, `Unable to read directory "${path}". It does not exist.`); + } + if (isFile(entity)) { + throw new MockFileSystemError( + 'ENOTDIR', path, `Unable to read directory "${path}". It is a file.`); + } + return Object.keys(entity) as PathSegment[]; + } + + lstat(path: AbsoluteFsPath): FileStats { + const {entity} = this.findFromPath(path); + if (entity === null) { + throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); + } + return new MockFileStats(entity); + } + + stat(path: AbsoluteFsPath): FileStats { + const {entity} = this.findFromPath(path, {followSymLinks: true}); + if (entity === null) { + throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); + } + return new MockFileStats(entity); + } + + copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { + this.writeFile(to, this.readFile(from)); + } + + moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { + this.writeFile(to, this.readFile(from)); + const result = this.findFromPath(dirname(from)); + const folder = result.entity as Folder; + const name = basename(from); + delete folder[name]; + } + + mkdir(path: AbsoluteFsPath): void { this.ensureFolders(this._fileTree, this.splitPath(path)); } + + ensureDir(path: AbsoluteFsPath): void { + this.ensureFolders(this._fileTree, this.splitPath(path)); + } + + isRoot(path: AbsoluteFsPath): boolean { return this.dirname(path) === path; } + + extname(path: AbsoluteFsPath|PathSegment): string { + const match = /.+(\.[^.]*)$/.exec(path); + return match !== null ? match[1] : ''; + } + + realpath(filePath: AbsoluteFsPath): AbsoluteFsPath { + const result = this.findFromPath(filePath, {followSymLinks: true}); + if (result.entity === null) { + throw new MockFileSystemError( + 'ENOENT', filePath, `Unable to find the real path of "${filePath}". It does not exist.`); + } else { + return result.path; + } + } + + pwd(): AbsoluteFsPath { return this._cwd; } + + getDefaultLibLocation(): AbsoluteFsPath { return this.resolve('node_modules/typescript/lib'); } + + abstract resolve(...paths: string[]): AbsoluteFsPath; + abstract dirname(file: T): T; + abstract join(basePath: T, ...paths: string[]): T; + abstract relative(from: T, to: T): PathSegment; + abstract basename(filePath: string, extension?: string): PathSegment; + abstract isRooted(path: string): boolean; + abstract normalize(path: T): T; + protected abstract splitPath(path: T): string[]; + + dump(): Folder { return cloneFolder(this._fileTree); } + init(folder: Folder): void { this._fileTree = cloneFolder(folder); } + + protected findFromPath(path: AbsoluteFsPath, options?: {followSymLinks: boolean}): FindResult { + const followSymLinks = !!options && options.followSymLinks; + const segments = this.splitPath(path); + if (segments.length > 1 && segments[segments.length - 1] === '') { + // Remove a trailing slash (unless the path was only `/`) + segments.pop(); + } + // Convert the root folder to a canonical empty string `""` (on Windows it would be `C:`). + segments[0] = ''; + let current: Entity|null = this._fileTree; + while (segments.length) { + current = current[segments.shift() !]; + if (current === undefined) { + return {path, entity: null}; + } + if (segments.length > 0 && (!isFolder(current))) { + current = null; + break; + } + if (isFile(current)) { + break; + } + if (isSymLink(current)) { + if (followSymLinks) { + return this.findFromPath(resolve(current.path, ...segments), {followSymLinks}); + } else { + break; + } + } + } + return {path, entity: current}; + } + + protected splitIntoFolderAndFile(path: AbsoluteFsPath): [AbsoluteFsPath, string] { + const segments = this.splitPath(path); + const file = segments.pop() !; + return [path.substring(0, path.length - file.length - 1) as AbsoluteFsPath, file]; + } + + protected ensureFolders(current: Folder, segments: string[]): Folder { + // Convert the root folder to a canonical empty string `""` (on Windows it would be `C:`). + segments[0] = ''; + for (const segment of segments) { + if (isFile(current[segment])) { + throw new Error(`Folder already exists as a file.`); + } + if (!current[segment]) { + current[segment] = {}; + } + current = current[segment] as Folder; + } + return current; + } +} +export interface FindResult { + path: AbsoluteFsPath; + entity: Entity|null; +} +export type Entity = Folder | File | SymLink; +export interface Folder { [pathSegments: string]: Entity; } +export type File = string; +export class SymLink { + constructor(public path: AbsoluteFsPath) {} +} + +class MockFileStats implements FileStats { + constructor(private entity: Entity) {} + isFile(): boolean { return isFile(this.entity); } + isDirectory(): boolean { return isFolder(this.entity); } + isSymbolicLink(): boolean { return isSymLink(this.entity); } +} + +class MockFileSystemError extends Error { + constructor(public code: string, public path: string, message: string) { super(message); } +} + +export function isFile(item: Entity | null): item is File { + return typeof item === 'string'; +} + +export function isSymLink(item: Entity | null): item is SymLink { + return item instanceof SymLink; +} + +export function isFolder(item: Entity | null): item is Folder { + return item !== null && !isFile(item) && !isSymLink(item); +} + +function cloneFolder(folder: Folder): Folder { + const clone: Folder = {}; + for (const path in folder) { + const item = folder[path]; + if (isSymLink(item)) { + clone[path] = new SymLink(item.path); + } else if (isFolder(item)) { + clone[path] = cloneFolder(item); + } else { + clone[path] = folder[path]; + } + } + return clone; +} diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts new file mode 100644 index 0000000000..4528a62f60 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts @@ -0,0 +1,48 @@ +/** + * @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 {NodeJSFileSystem} from '../../src/node_js_file_system'; +import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types'; + +import {MockFileSystem} from './mock_file_system'; + +export class MockFileSystemNative extends MockFileSystem { + constructor(cwd: AbsoluteFsPath = '/' as AbsoluteFsPath) { super(undefined, cwd); } + + // Delegate to the real NodeJSFileSystem for these path related methods + + resolve(...paths: string[]): AbsoluteFsPath { + return NodeJSFileSystem.prototype.resolve.call(this, this.pwd(), ...paths); + } + dirname(file: T): T { + return NodeJSFileSystem.prototype.dirname.call(this, file) as T; + } + join(basePath: T, ...paths: string[]): T { + return NodeJSFileSystem.prototype.join.call(this, basePath, ...paths) as T; + } + relative(from: T, to: T): PathSegment { + return NodeJSFileSystem.prototype.relative.call(this, from, to); + } + + basename(filePath: string, extension?: string): PathSegment { + return NodeJSFileSystem.prototype.basename.call(this, filePath, extension); + } + + isCaseSensitive() { return NodeJSFileSystem.prototype.isCaseSensitive.call(this); } + + isRooted(path: string): boolean { return NodeJSFileSystem.prototype.isRooted.call(this, path); } + + isRoot(path: AbsoluteFsPath): boolean { + return NodeJSFileSystem.prototype.isRoot.call(this, path); + } + + normalize(path: T): T { + return NodeJSFileSystem.prototype.normalize.call(this, path) as T; + } + + protected splitPath(path: string): string[] { return path.split(/[\\\/]/); } +} diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts new file mode 100644 index 0000000000..2778997240 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/// +import * as p from 'path'; + +import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types'; +import {MockFileSystem} from './mock_file_system'; + +export class MockFileSystemPosix extends MockFileSystem { + resolve(...paths: string[]): AbsoluteFsPath { + const resolved = p.posix.resolve(this.pwd(), ...paths); + return this.normalize(resolved) as AbsoluteFsPath; + } + + dirname(file: T): T { return this.normalize(p.posix.dirname(file)) as T; } + + join(basePath: T, ...paths: string[]): T { + return this.normalize(p.posix.join(basePath, ...paths)) as T; + } + + relative(from: T, to: T): PathSegment { + return this.normalize(p.posix.relative(from, to)) as PathSegment; + } + + basename(filePath: string, extension?: string): PathSegment { + return p.posix.basename(filePath, extension) as PathSegment; + } + + isRooted(path: string): boolean { return path.startsWith('/'); } + + protected splitPath(path: T): string[] { return path.split('/'); } + + normalize(path: T): T { + return path.replace(/^[a-z]:\//i, '/').replace(/\\/g, '/') as T; + } +} diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts new file mode 100644 index 0000000000..3ec01f4ed8 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/// +import * as p from 'path'; + +import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types'; +import {MockFileSystem} from './mock_file_system'; + +export class MockFileSystemWindows extends MockFileSystem { + resolve(...paths: string[]): AbsoluteFsPath { + const resolved = p.win32.resolve(this.pwd(), ...paths); + return this.normalize(resolved as AbsoluteFsPath); + } + + dirname(path: T): T { return this.normalize(p.win32.dirname(path) as T); } + + join(basePath: T, ...paths: string[]): T { + return this.normalize(p.win32.join(basePath, ...paths)) as T; + } + + relative(from: T, to: T): PathSegment { + return this.normalize(p.win32.relative(from, to)) as PathSegment; + } + + basename(filePath: string, extension?: string): PathSegment { + return p.win32.basename(filePath, extension) as PathSegment; + } + + isRooted(path: string): boolean { return /^([A-Z]:)?([\\\/]|$)/i.test(path); } + + protected splitPath(path: T): string[] { return path.split(/[\\\/]/); } + + normalize(path: T): T { + return path.replace(/^[\/\\]/i, 'C:/').replace(/\\/g, '/') as T; + } +} diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts new file mode 100644 index 0000000000..b2b550d381 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts @@ -0,0 +1,149 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/// +import * as ts from 'typescript'; + +import {absoluteFrom, setFileSystem} from '../../src/helpers'; +import {AbsoluteFsPath} from '../../src/types'; + +import {MockFileSystem} from './mock_file_system'; +import {MockFileSystemNative} from './mock_file_system_native'; +import {MockFileSystemPosix} from './mock_file_system_posix'; +import {MockFileSystemWindows} from './mock_file_system_windows'; + +export interface TestFile { + name: AbsoluteFsPath; + contents: string; + isRoot?: boolean|undefined; +} + +export interface RunInEachFileSystemFn { + (callback: (os: string) => void): void; + windows(callback: (os: string) => void): void; + unix(callback: (os: string) => void): void; + native(callback: (os: string) => void): void; + osX(callback: (os: string) => void): void; +} + +const FS_NATIVE = 'Native'; +const FS_OS_X = 'OS/X'; +const FS_UNIX = 'Unix'; +const FS_WINDOWS = 'Windows'; +const FS_ALL = [FS_OS_X, FS_WINDOWS, FS_UNIX, FS_NATIVE]; + +function runInEachFileSystemFn(callback: (os: string) => void) { + FS_ALL.forEach(os => runInFileSystem(os, callback, false)); +} + +function runInFileSystem(os: string, callback: (os: string) => void, error: boolean) { + describe(`<>`, () => { + beforeEach(() => initMockFileSystem(os)); + callback(os); + if (error) { + afterAll(() => { throw new Error(`runInFileSystem limited to ${os}, cannot pass`); }); + } + }); +} + +export const runInEachFileSystem: RunInEachFileSystemFn = + runInEachFileSystemFn as RunInEachFileSystemFn; + +runInEachFileSystem.native = (callback: (os: string) => void) => + runInFileSystem(FS_NATIVE, callback, true); +runInEachFileSystem.osX = (callback: (os: string) => void) => + runInFileSystem(FS_OS_X, callback, true); +runInEachFileSystem.unix = (callback: (os: string) => void) => + runInFileSystem(FS_UNIX, callback, true); +runInEachFileSystem.windows = (callback: (os: string) => void) => + runInFileSystem(FS_WINDOWS, callback, true); + +export function initMockFileSystem(os: string, cwd?: AbsoluteFsPath): void { + const fs = createMockFileSystem(os, cwd); + setFileSystem(fs); + monkeyPatchTypeScript(os, fs); +} + +function createMockFileSystem(os: string, cwd?: AbsoluteFsPath): MockFileSystem { + switch (os) { + case 'OS/X': + return new MockFileSystemPosix(/* isCaseSensitive */ false, cwd); + case 'Unix': + return new MockFileSystemPosix(/* isCaseSensitive */ true, cwd); + case 'Windows': + return new MockFileSystemWindows(/* isCaseSensitive*/ false, cwd); + case 'Native': + return new MockFileSystemNative(cwd); + default: + throw new Error('FileSystem not supported'); + } +} + +function monkeyPatchTypeScript(os: string, fs: MockFileSystem) { + ts.sys.directoryExists = path => { + const absPath = fs.resolve(path); + return fs.exists(absPath) && fs.stat(absPath).isDirectory(); + }; + ts.sys.fileExists = path => { + const absPath = fs.resolve(path); + return fs.exists(absPath) && fs.stat(absPath).isFile(); + }; + ts.sys.getCurrentDirectory = () => fs.pwd(); + ts.sys.getDirectories = getDirectories; + ts.sys.readFile = fs.readFile.bind(fs); + ts.sys.resolvePath = fs.resolve.bind(fs); + ts.sys.writeFile = fs.writeFile.bind(fs); + ts.sys.readDirectory = readDirectory; + + function getDirectories(path: string): string[] { + return fs.readdir(absoluteFrom(path)).filter(p => fs.stat(fs.resolve(path, p)).isDirectory()); + } + + function getFileSystemEntries(path: string): FileSystemEntries { + const files: string[] = []; + const directories: string[] = []; + const absPath = fs.resolve(path); + const entries = fs.readdir(absPath); + for (const entry of entries) { + if (entry == '.' || entry === '..') { + continue; + } + const absPath = fs.resolve(path, entry); + const stat = fs.stat(absPath); + if (stat.isDirectory()) { + directories.push(absPath); + } else if (stat.isFile()) { + files.push(absPath); + } + } + return {files, directories}; + } + + function realPath(path: string): string { return fs.realpath(fs.resolve(path)); } + + // Rather than completely re-implementing we are using the `ts.matchFiles` function, + // which is internal to the `ts` namespace. + const tsMatchFiles: ( + path: string, extensions: ReadonlyArray| undefined, + excludes: ReadonlyArray| undefined, includes: ReadonlyArray| undefined, + useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined, + getFileSystemEntries: (path: string) => FileSystemEntries, + realpath: (path: string) => string) => string[] = (ts as any).matchFiles; + + function readDirectory( + path: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, + includes?: ReadonlyArray, depth?: number): string[] { + return tsMatchFiles( + path, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth, + getFileSystemEntries, realPath); + } +} + +interface FileSystemEntries { + readonly files: ReadonlyArray; + readonly directories: ReadonlyArray; +} diff --git a/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel b/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel index dbb06cc8f2..a7907091e9 100644 --- a/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel @@ -10,7 +10,7 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", - "//packages/compiler-cli/src/ngtsc/path", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/util", "@npm//@types/node", diff --git a/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts index 3131558ee8..0893ec6c97 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts @@ -5,19 +5,15 @@ * 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 {Expression, ExternalExpr, WrappedNodeExpr} from '@angular/compiler'; import {ExternalReference} from '@angular/compiler/src/compiler'; import * as ts from 'typescript'; - -import {LogicalFileSystem, LogicalProjectPath} from '../../path'; +import {LogicalFileSystem, LogicalProjectPath, absoluteFrom} from '../../file_system'; import {ReflectionHost} from '../../reflection'; -import {getSourceFile, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript'; - +import {getSourceFile, getSourceFileOrNull, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript'; import {findExportedNameOfNode} from './find_export'; import {ImportMode, Reference} from './references'; - /** * A host which supports an operation to convert a file name into a module name. * @@ -170,8 +166,9 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy { return null; } - const entryPointFile = this.program.getSourceFile(resolvedModule.resolvedFileName); - if (entryPointFile === undefined) { + const entryPointFile = + getSourceFileOrNull(this.program, absoluteFrom(resolvedModule.resolvedFileName)); + if (entryPointFile === null) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts index 309aee999a..7393f25798 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts @@ -5,10 +5,9 @@ * 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 {resolveModuleName} from '../../util/src/typescript'; +import {absoluteFrom} from '../../file_system'; +import {getSourceFileOrNull, resolveModuleName} from '../../util/src/typescript'; import {Reference} from './references'; export interface ReferenceResolver { @@ -33,6 +32,6 @@ export class ModuleResolver { if (resolved === undefined) { return null; } - return this.program.getSourceFile(resolved.resolvedFileName) || null; + return getSourceFileOrNull(this.program, absoluteFrom(resolved.resolvedFileName)); } } diff --git a/packages/compiler-cli/src/ngtsc/imports/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/imports/test/BUILD.bazel index 870fe78ec2..fef878a975 100644 --- a/packages/compiler-cli/src/ngtsc/imports/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/imports/test/BUILD.bazel @@ -10,6 +10,8 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/testing", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts b/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts index 2f9b284f09..defa93505a 100644 --- a/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts +++ b/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts @@ -5,86 +5,101 @@ * 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 {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {absoluteFrom} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; +import {getDeclaration, makeProgram} from '../../testing'; import {DefaultImportTracker} from '../src/default'; -describe('DefaultImportTracker', () => { - it('should prevent a default import from being elided if used', () => { - const {program, host} = makeProgram( - [ - {name: 'dep.ts', contents: `export default class Foo {}`}, - {name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`}, +runInEachFileSystem(() => { + describe('DefaultImportTracker', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); - // This control file is identical to the test file, but will not have its import marked - // for preservation. It exists to verify that it is in fact the action of - // DefaultImportTracker and not some other artifact of the test setup which causes the - // import to be preserved. It will also verify that DefaultImportTracker does not preserve - // imports which are not marked for preservation. - {name: 'ctrl.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`}, - ], - { - module: ts.ModuleKind.ES2015, - }); - const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause); - const fooId = fooClause.name !; - const fooDecl = fooClause.parent; + it('should prevent a default import from being elided if used', () => { + const {program, host} = makeProgram( + [ + {name: _('/dep.ts'), contents: `export default class Foo {}`}, + { + name: _('/test.ts'), + contents: `import Foo from './dep'; export function test(f: Foo) {}` + }, - const tracker = new DefaultImportTracker(); - tracker.recordImportedIdentifier(fooId, fooDecl); - tracker.recordUsedIdentifier(fooId); - program.emit(undefined, undefined, undefined, undefined, { - before: [tracker.importPreservingTransformer()], + // This control file is identical to the test file, but will not have its import marked + // for preservation. It exists to verify that it is in fact the action of + // DefaultImportTracker and not some other artifact of the test setup which causes the + // import to be preserved. It will also verify that DefaultImportTracker does not + // preserve imports which are not marked for preservation. + { + name: _('/ctrl.ts'), + contents: `import Foo from './dep'; export function test(f: Foo) {}` + }, + ], + { + module: ts.ModuleKind.ES2015, + }); + const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause); + const fooId = fooClause.name !; + const fooDecl = fooClause.parent; + + const tracker = new DefaultImportTracker(); + tracker.recordImportedIdentifier(fooId, fooDecl); + tracker.recordUsedIdentifier(fooId); + program.emit(undefined, undefined, undefined, undefined, { + before: [tracker.importPreservingTransformer()], + }); + const testContents = host.readFile('/test.js') !; + expect(testContents).toContain(`import Foo from './dep';`); + + // The control should have the import elided. + const ctrlContents = host.readFile('/ctrl.js'); + expect(ctrlContents).not.toContain(`import Foo from './dep';`); }); - const testContents = host.readFile('/test.js') !; - expect(testContents).toContain(`import Foo from './dep';`); - // The control should have the import elided. - const ctrlContents = host.readFile('/ctrl.js'); - expect(ctrlContents).not.toContain(`import Foo from './dep';`); + it('should transpile imports correctly into commonjs', () => { + const {program, host} = makeProgram( + [ + {name: _('/dep.ts'), contents: `export default class Foo {}`}, + { + name: _('/test.ts'), + contents: `import Foo from './dep'; export function test(f: Foo) {}` + }, + ], + { + module: ts.ModuleKind.CommonJS, + }); + const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause); + const fooId = ts.updateIdentifier(fooClause.name !); + const fooDecl = fooClause.parent; + + const tracker = new DefaultImportTracker(); + tracker.recordImportedIdentifier(fooId, fooDecl); + tracker.recordUsedIdentifier(fooId); + program.emit(undefined, undefined, undefined, undefined, { + before: [ + addReferenceTransformer(fooId), + tracker.importPreservingTransformer(), + ], + }); + const testContents = host.readFile('/test.js') !; + expect(testContents).toContain(`var dep_1 = require("./dep");`); + expect(testContents).toContain(`var ref = dep_1["default"];`); + }); }); - it('should transpile imports correctly into commonjs', () => { - const {program, host} = makeProgram( - [ - {name: 'dep.ts', contents: `export default class Foo {}`}, - {name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`}, - ], - { - module: ts.ModuleKind.CommonJS, - }); - const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause); - const fooId = ts.updateIdentifier(fooClause.name !); - const fooDecl = fooClause.parent; - - const tracker = new DefaultImportTracker(); - tracker.recordImportedIdentifier(fooId, fooDecl); - tracker.recordUsedIdentifier(fooId); - program.emit(undefined, undefined, undefined, undefined, { - before: [ - addReferenceTransformer(fooId), - tracker.importPreservingTransformer(), - ], - }); - const testContents = host.readFile('/test.js') !; - expect(testContents).toContain(`var dep_1 = require("./dep");`); - expect(testContents).toContain(`var ref = dep_1["default"];`); - }); -}); - -function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory { - return (context: ts.TransformationContext) => { - return (sf: ts.SourceFile) => { - if (id.getSourceFile().fileName === sf.fileName) { - return ts.updateSourceFileNode(sf, [ - ...sf.statements, ts.createVariableStatement(undefined, ts.createVariableDeclarationList([ - ts.createVariableDeclaration('ref', undefined, id), - ])) - ]); - } - return sf; + function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory { + return (context: ts.TransformationContext) => { + return (sf: ts.SourceFile) => { + if (id.getSourceFile().fileName === sf.fileName) { + return ts.updateSourceFileNode(sf, [ + ...sf.statements, + ts.createVariableStatement(undefined, ts.createVariableDeclarationList([ + ts.createVariableDeclaration('ref', undefined, id), + ])) + ]); + } + return sf; + }; }; - }; -} + } +}); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel index afc4e12861..cc98a4fbab 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel @@ -10,6 +10,8 @@ ts_library( ]), deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/metadata", diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/context_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/context_spec.ts index 410aa9c40d..070234844b 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/context_spec.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/context_spec.ts @@ -5,35 +5,37 @@ * 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 {ParseSourceFile} from '@angular/compiler'; +import {runInEachFileSystem} from '../../file_system/testing'; import {IndexingContext} from '../src/context'; import * as util from './util'; -describe('ComponentAnalysisContext', () => { - it('should store and return information about components', () => { - const context = new IndexingContext(); - const declaration = util.getComponentDeclaration('class C {};', 'C'); - const boundTemplate = util.getBoundTemplate('
'); +runInEachFileSystem(() => { + describe('ComponentAnalysisContext', () => { + it('should store and return information about components', () => { + const context = new IndexingContext(); + const declaration = util.getComponentDeclaration('class C {};', 'C'); + const boundTemplate = util.getBoundTemplate('
'); - context.addComponent({ - declaration, - selector: 'c-selector', boundTemplate, - templateMeta: { - isInline: false, - file: new ParseSourceFile('
', util.TESTFILE), - }, - }); - - expect(context.components).toEqual(new Set([ - { + context.addComponent({ declaration, selector: 'c-selector', boundTemplate, templateMeta: { isInline: false, - file: new ParseSourceFile('
', util.TESTFILE), + file: new ParseSourceFile('
', util.getTestFilePath()), }, - }, - ])); + }); + + expect(context.components).toEqual(new Set([ + { + declaration, + selector: 'c-selector', boundTemplate, + templateMeta: { + isInline: false, + file: new ParseSourceFile('
', util.getTestFilePath()), + }, + }, + ])); + }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts index e3cfcd9684..1f2d270dec 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts @@ -5,8 +5,8 @@ * 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 {BoundTarget, ParseSourceFile} from '@angular/compiler'; +import {runInEachFileSystem} from '../../file_system/testing'; import {DirectiveMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; import {IndexingContext} from '../src/context'; @@ -26,92 +26,96 @@ function populateContext( boundTemplate, templateMeta: { isInline, - file: new ParseSourceFile(template, util.TESTFILE), + file: new ParseSourceFile(template, util.getTestFilePath()), }, }); } -describe('generateAnalysis', () => { - it('should emit component and template analysis information', () => { - const context = new IndexingContext(); - const decl = util.getComponentDeclaration('class C {}', 'C'); - const template = '
{{foo}}
'; - populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template)); - const analysis = generateAnalysis(context); +runInEachFileSystem(() => { + describe('generateAnalysis', () => { + it('should emit component and template analysis information', () => { + const context = new IndexingContext(); + const decl = util.getComponentDeclaration('class C {}', 'C'); + const template = '
{{foo}}
'; + populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template)); + const analysis = generateAnalysis(context); - expect(analysis.size).toBe(1); + expect(analysis.size).toBe(1); - const info = analysis.get(decl); - expect(info).toEqual({ - name: 'C', - selector: 'c-selector', - file: new ParseSourceFile('class C {}', util.TESTFILE), - template: { - identifiers: getTemplateIdentifiers(util.getBoundTemplate('
{{foo}}
')), - usedComponents: new Set(), - isInline: false, - file: new ParseSourceFile('
{{foo}}
', util.TESTFILE), - } + const info = analysis.get(decl); + expect(info).toEqual({ + name: 'C', + selector: 'c-selector', + file: new ParseSourceFile('class C {}', util.getTestFilePath()), + template: { + identifiers: getTemplateIdentifiers(util.getBoundTemplate('
{{foo}}
')), + usedComponents: new Set(), + isInline: false, + file: new ParseSourceFile('
{{foo}}
', util.getTestFilePath()), + } + }); + }); + + it('should give inline templates the component source file', () => { + const context = new IndexingContext(); + const decl = util.getComponentDeclaration('class C {}', 'C'); + const template = '
{{foo}}
'; + populateContext( + context, decl, 'c-selector', '
{{foo}}
', util.getBoundTemplate(template), + /* inline template */ true); + const analysis = generateAnalysis(context); + + expect(analysis.size).toBe(1); + + const info = analysis.get(decl); + expect(info).toBeDefined(); + expect(info !.template.file) + .toEqual(new ParseSourceFile('class C {}', util.getTestFilePath())); + }); + + it('should give external templates their own source file', () => { + const context = new IndexingContext(); + const decl = util.getComponentDeclaration('class C {}', 'C'); + const template = '
{{foo}}
'; + populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template)); + const analysis = generateAnalysis(context); + + expect(analysis.size).toBe(1); + + const info = analysis.get(decl); + expect(info).toBeDefined(); + expect(info !.template.file) + .toEqual(new ParseSourceFile('
{{foo}}
', util.getTestFilePath())); + }); + + it('should emit used components', () => { + const context = new IndexingContext(); + + const templateA = ''; + const declA = util.getComponentDeclaration('class A {}', 'A'); + + const templateB = ''; + const declB = util.getComponentDeclaration('class B {}', 'B'); + + const boundA = + util.getBoundTemplate(templateA, {}, [{selector: 'b-selector', declaration: declB}]); + const boundB = + util.getBoundTemplate(templateB, {}, [{selector: 'a-selector', declaration: declA}]); + + populateContext(context, declA, 'a-selector', templateA, boundA); + populateContext(context, declB, 'b-selector', templateB, boundB); + + const analysis = generateAnalysis(context); + + expect(analysis.size).toBe(2); + + const infoA = analysis.get(declA); + expect(infoA).toBeDefined(); + expect(infoA !.template.usedComponents).toEqual(new Set([declB])); + + const infoB = analysis.get(declB); + expect(infoB).toBeDefined(); + expect(infoB !.template.usedComponents).toEqual(new Set([declA])); }); }); - - it('should give inline templates the component source file', () => { - const context = new IndexingContext(); - const decl = util.getComponentDeclaration('class C {}', 'C'); - const template = '
{{foo}}
'; - populateContext( - context, decl, 'c-selector', '
{{foo}}
', util.getBoundTemplate(template), - /* inline template */ true); - const analysis = generateAnalysis(context); - - expect(analysis.size).toBe(1); - - const info = analysis.get(decl); - expect(info).toBeDefined(); - expect(info !.template.file).toEqual(new ParseSourceFile('class C {}', util.TESTFILE)); - }); - - it('should give external templates their own source file', () => { - const context = new IndexingContext(); - const decl = util.getComponentDeclaration('class C {}', 'C'); - const template = '
{{foo}}
'; - populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template)); - const analysis = generateAnalysis(context); - - expect(analysis.size).toBe(1); - - const info = analysis.get(decl); - expect(info).toBeDefined(); - expect(info !.template.file).toEqual(new ParseSourceFile('
{{foo}}
', util.TESTFILE)); - }); - - it('should emit used components', () => { - const context = new IndexingContext(); - - const templateA = ''; - const declA = util.getComponentDeclaration('class A {}', 'A'); - - const templateB = ''; - const declB = util.getComponentDeclaration('class B {}', 'B'); - - const boundA = - util.getBoundTemplate(templateA, {}, [{selector: 'b-selector', declaration: declB}]); - const boundB = - util.getBoundTemplate(templateB, {}, [{selector: 'a-selector', declaration: declA}]); - - populateContext(context, declA, 'a-selector', templateA, boundA); - populateContext(context, declB, 'b-selector', templateB, boundB); - - const analysis = generateAnalysis(context); - - expect(analysis.size).toBe(2); - - const infoA = analysis.get(declA); - expect(infoA).toBeDefined(); - expect(infoA !.template.usedComponents).toEqual(new Set([declB])); - - const infoB = analysis.get(declB); - expect(infoB).toBeDefined(); - expect(infoB !.template.usedComponents).toEqual(new Set([declA])); - }); }); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts index a6ae55b45d..4d9d11d32a 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts @@ -8,22 +8,25 @@ import {BoundTarget, CssSelector, ParseTemplateOptions, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler'; import * as ts from 'typescript'; +import {AbsoluteFsPath, absoluteFrom} from '../../file_system'; import {Reference} from '../../imports'; import {DirectiveMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; /** Dummy file URL */ -export const TESTFILE = '/TESTFILE.ts'; +export function getTestFilePath(): AbsoluteFsPath { + return absoluteFrom('/TEST_FILE.ts'); +} /** * Creates a class declaration from a component source code. */ export function getComponentDeclaration(componentStr: string, className: string): ClassDeclaration { - const program = makeProgram([{name: TESTFILE, contents: componentStr}]); + const program = makeProgram([{name: getTestFilePath(), contents: componentStr}]); return getDeclaration( - program.program, TESTFILE, className, + program.program, getTestFilePath(), className, (value: ts.Declaration): value is ClassDeclaration => ts.isClassDeclaration(value)); } @@ -57,5 +60,5 @@ export function getBoundTemplate( }); const binder = new R3TargetBinder(matcher); - return binder.bind({template: parseTemplate(template, TESTFILE, options).nodes}); + return binder.bind({template: parseTemplate(template, getTestFilePath(), options).nodes}); } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/BUILD.bazel index f735524807..104de075b6 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/BUILD.bazel @@ -11,11 +11,12 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", - "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index 01fe596eea..79e1bd4bb8 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -5,293 +5,300 @@ * 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 {absoluteFrom, getSourceFileOrError} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {Reference} from '../../imports'; import {FunctionDefinition, TsHelperFn, TypeScriptReflectionHost} from '../../reflection'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; import {DynamicValue} from '../src/dynamic'; import {PartialEvaluator} from '../src/interface'; import {EnumValue} from '../src/result'; - import {evaluate, firstArgFfr, makeEvaluator, makeExpression, owningModuleOf} from './utils'; -describe('ngtsc metadata', () => { - it('reads a file correctly', () => { - const value = evaluate( - ` +runInEachFileSystem(() => { + describe('ngtsc metadata', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); + + it('reads a file correctly', () => { + const value = evaluate( + ` import {Y} from './other'; const A = Y; `, - 'A', [ - { - name: 'other.ts', - contents: ` + 'A', [ + { + name: _('/other.ts'), + contents: ` export const Y = 'test'; ` - }, - ]); + }, + ]); - expect(value).toEqual('test'); - }); + expect(value).toEqual('test'); + }); - it('map access works', - () => { expect(evaluate('const obj = {a: "test"};', 'obj.a')).toEqual('test'); }); + it('map access works', + () => { expect(evaluate('const obj = {a: "test"};', 'obj.a')).toEqual('test'); }); - it('resolves undefined property access', - () => { expect(evaluate('const obj: any = {}', 'obj.bar')).toEqual(undefined); }); + it('resolves undefined property access', + () => { expect(evaluate('const obj: any = {}', 'obj.bar')).toEqual(undefined); }); - it('function calls work', () => { - expect(evaluate(`function foo(bar) { return bar; }`, 'foo("test")')).toEqual('test'); - }); + it('function calls work', () => { + expect(evaluate(`function foo(bar) { return bar; }`, 'foo("test")')).toEqual('test'); + }); - it('function call default value works', () => { - expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo()')).toEqual(1); - expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo(2)')).toEqual(2); - expect(evaluate(`function foo(a, c = a) { return c; }; const a = 1;`, 'foo(2)')).toEqual(2); - }); + it('function call default value works', () => { + expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo()')).toEqual(1); + expect(evaluate(`function foo(bar = 1) { return bar; }`, 'foo(2)')).toEqual(2); + expect(evaluate(`function foo(a, c = a) { return c; }; const a = 1;`, 'foo(2)')).toEqual(2); + }); - it('function call spread works', () => { - expect(evaluate(`function foo(a, ...b) { return [a, b]; }`, 'foo(1, ...[2, 3])')).toEqual([ - 1, [2, 3] - ]); - }); + it('function call spread works', () => { + expect(evaluate(`function foo(a, ...b) { return [a, b]; }`, 'foo(1, ...[2, 3])')).toEqual([ + 1, [2, 3] + ]); + }); - it('conditionals work', () => { - expect(evaluate(`const x = false; const y = x ? 'true' : 'false';`, 'y')).toEqual('false'); - }); + it('conditionals work', () => { + expect(evaluate(`const x = false; const y = x ? 'true' : 'false';`, 'y')).toEqual('false'); + }); - it('addition works', () => { expect(evaluate(`const x = 1 + 2;`, 'x')).toEqual(3); }); + it('addition works', () => { expect(evaluate(`const x = 1 + 2;`, 'x')).toEqual(3); }); - it('static property on class works', - () => { expect(evaluate(`class Foo { static bar = 'test'; }`, 'Foo.bar')).toEqual('test'); }); + it('static property on class works', () => { + expect(evaluate(`class Foo { static bar = 'test'; }`, 'Foo.bar')).toEqual('test'); + }); - it('static property call works', () => { - expect(evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")')) - .toEqual('test'); - }); + it('static property call works', () => { + expect(evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")')) + .toEqual('test'); + }); - it('indirected static property call works', () => { - expect( - evaluate( - `class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, 'fn("test")')) - .toEqual('test'); - }); + it('indirected static property call works', () => { + expect( + evaluate( + `class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, 'fn("test")')) + .toEqual('test'); + }); - it('array works', () => { - expect(evaluate(`const x = 'test'; const y = [1, x, 2];`, 'y')).toEqual([1, 'test', 2]); - }); + it('array works', () => { + expect(evaluate(`const x = 'test'; const y = [1, x, 2];`, 'y')).toEqual([1, 'test', 2]); + }); - it('array spread works', () => { - expect(evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c')) - .toEqual([1, 2, 3, 4, 5]); - }); + it('array spread works', () => { + expect(evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c')) + .toEqual([1, 2, 3, 4, 5]); + }); - it('&& operations work', () => { - expect(evaluate(`const a = 'hello', b = 'world';`, 'a && b')).toEqual('world'); - expect(evaluate(`const a = false, b = 'world';`, 'a && b')).toEqual(false); - expect(evaluate(`const a = 'hello', b = 0;`, 'a && b')).toEqual(0); - }); + it('&& operations work', () => { + expect(evaluate(`const a = 'hello', b = 'world';`, 'a && b')).toEqual('world'); + expect(evaluate(`const a = false, b = 'world';`, 'a && b')).toEqual(false); + expect(evaluate(`const a = 'hello', b = 0;`, 'a && b')).toEqual(0); + }); - it('|| operations work', () => { - expect(evaluate(`const a = 'hello', b = 'world';`, 'a || b')).toEqual('hello'); - expect(evaluate(`const a = false, b = 'world';`, 'a || b')).toEqual('world'); - expect(evaluate(`const a = 'hello', b = 0;`, 'a || b')).toEqual('hello'); - }); + it('|| operations work', () => { + expect(evaluate(`const a = 'hello', b = 'world';`, 'a || b')).toEqual('hello'); + expect(evaluate(`const a = false, b = 'world';`, 'a || b')).toEqual('world'); + expect(evaluate(`const a = 'hello', b = 0;`, 'a || b')).toEqual('hello'); + }); - it('evaluates arithmetic operators', () => { - expect(evaluate('const a = 6, b = 3;', 'a + b')).toEqual(9); - expect(evaluate('const a = 6, b = 3;', 'a - b')).toEqual(3); - expect(evaluate('const a = 6, b = 3;', 'a * b')).toEqual(18); - expect(evaluate('const a = 6, b = 3;', 'a / b')).toEqual(2); - expect(evaluate('const a = 6, b = 3;', 'a % b')).toEqual(0); - expect(evaluate('const a = 6, b = 3;', 'a & b')).toEqual(2); - expect(evaluate('const a = 6, b = 3;', 'a | b')).toEqual(7); - expect(evaluate('const a = 6, b = 3;', 'a ^ b')).toEqual(5); - expect(evaluate('const a = 6, b = 3;', 'a ** b')).toEqual(216); - expect(evaluate('const a = 6, b = 3;', 'a << b')).toEqual(48); - expect(evaluate('const a = -6, b = 2;', 'a >> b')).toEqual(-2); - expect(evaluate('const a = -6, b = 2;', 'a >>> b')).toEqual(1073741822); - }); + it('evaluates arithmetic operators', () => { + expect(evaluate('const a = 6, b = 3;', 'a + b')).toEqual(9); + expect(evaluate('const a = 6, b = 3;', 'a - b')).toEqual(3); + expect(evaluate('const a = 6, b = 3;', 'a * b')).toEqual(18); + expect(evaluate('const a = 6, b = 3;', 'a / b')).toEqual(2); + expect(evaluate('const a = 6, b = 3;', 'a % b')).toEqual(0); + expect(evaluate('const a = 6, b = 3;', 'a & b')).toEqual(2); + expect(evaluate('const a = 6, b = 3;', 'a | b')).toEqual(7); + expect(evaluate('const a = 6, b = 3;', 'a ^ b')).toEqual(5); + expect(evaluate('const a = 6, b = 3;', 'a ** b')).toEqual(216); + expect(evaluate('const a = 6, b = 3;', 'a << b')).toEqual(48); + expect(evaluate('const a = -6, b = 2;', 'a >> b')).toEqual(-2); + expect(evaluate('const a = -6, b = 2;', 'a >>> b')).toEqual(1073741822); + }); - it('evaluates comparison operators', () => { - expect(evaluate('const a = 2, b = 3;', 'a < b')).toEqual(true); - expect(evaluate('const a = 3, b = 3;', 'a < b')).toEqual(false); + it('evaluates comparison operators', () => { + expect(evaluate('const a = 2, b = 3;', 'a < b')).toEqual(true); + expect(evaluate('const a = 3, b = 3;', 'a < b')).toEqual(false); - expect(evaluate('const a = 3, b = 3;', 'a <= b')).toEqual(true); - expect(evaluate('const a = 4, b = 3;', 'a <= b')).toEqual(false); + expect(evaluate('const a = 3, b = 3;', 'a <= b')).toEqual(true); + expect(evaluate('const a = 4, b = 3;', 'a <= b')).toEqual(false); - expect(evaluate('const a = 4, b = 3;', 'a > b')).toEqual(true); - expect(evaluate('const a = 3, b = 3;', 'a > b')).toEqual(false); + expect(evaluate('const a = 4, b = 3;', 'a > b')).toEqual(true); + expect(evaluate('const a = 3, b = 3;', 'a > b')).toEqual(false); - expect(evaluate('const a = 3, b = 3;', 'a >= b')).toEqual(true); - expect(evaluate('const a = 2, b = 3;', 'a >= b')).toEqual(false); + expect(evaluate('const a = 3, b = 3;', 'a >= b')).toEqual(true); + expect(evaluate('const a = 2, b = 3;', 'a >= b')).toEqual(false); - expect(evaluate('const a: any = 3, b = "3";', 'a == b')).toEqual(true); - expect(evaluate('const a: any = 2, b = "3";', 'a == b')).toEqual(false); + expect(evaluate('const a: any = 3, b = "3";', 'a == b')).toEqual(true); + expect(evaluate('const a: any = 2, b = "3";', 'a == b')).toEqual(false); - expect(evaluate('const a: any = 2, b = "3";', 'a != b')).toEqual(true); - expect(evaluate('const a: any = 3, b = "3";', 'a != b')).toEqual(false); + expect(evaluate('const a: any = 2, b = "3";', 'a != b')).toEqual(true); + expect(evaluate('const a: any = 3, b = "3";', 'a != b')).toEqual(false); - expect(evaluate('const a: any = 3, b = 3;', 'a === b')).toEqual(true); - expect(evaluate('const a: any = 3, b = "3";', 'a === b')).toEqual(false); + expect(evaluate('const a: any = 3, b = 3;', 'a === b')).toEqual(true); + expect(evaluate('const a: any = 3, b = "3";', 'a === b')).toEqual(false); - expect(evaluate('const a: any = 3, b = "3";', 'a !== b')).toEqual(true); - expect(evaluate('const a: any = 3, b = 3;', 'a !== b')).toEqual(false); - }); + expect(evaluate('const a: any = 3, b = "3";', 'a !== b')).toEqual(true); + expect(evaluate('const a: any = 3, b = 3;', 'a !== b')).toEqual(false); + }); - it('parentheticals work', - () => { expect(evaluate(`const a = 3, b = 4;`, 'a * (a + b)')).toEqual(21); }); + it('parentheticals work', + () => { expect(evaluate(`const a = 3, b = 4;`, 'a * (a + b)')).toEqual(21); }); - it('array access works', - () => { expect(evaluate(`const a = [1, 2, 3];`, 'a[1] + a[0]')).toEqual(3); }); + it('array access works', + () => { expect(evaluate(`const a = [1, 2, 3];`, 'a[1] + a[0]')).toEqual(3); }); - it('array `length` property access works', - () => { expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); }); + it('array `length` property access works', + () => { expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); }); - it('array `slice` function works', () => { - expect(evaluate(`const a = [1, 2, 3];`, 'a[\'slice\']()')).toEqual([1, 2, 3]); - }); + it('array `slice` function works', () => { + expect(evaluate(`const a = [1, 2, 3];`, 'a[\'slice\']()')).toEqual([1, 2, 3]); + }); - it('array `concat` function works', () => { - expect(evaluate(`const a = [1, 2], b = [3, 4];`, 'a[\'concat\'](b)')).toEqual([1, 2, 3, 4]); - expect(evaluate(`const a = [1, 2], b = 3;`, 'a[\'concat\'](b)')).toEqual([1, 2, 3]); - expect(evaluate(`const a = [1, 2], b = 3, c = [4, 5];`, 'a[\'concat\'](b, c)')).toEqual([ - 1, 2, 3, 4, 5 - ]); - expect(evaluate(`const a = [1, 2], b = [3, 4]`, 'a[\'concat\'](...b)')).toEqual([1, 2, 3, 4]); - }); + it('array `concat` function works', () => { + expect(evaluate(`const a = [1, 2], b = [3, 4];`, 'a[\'concat\'](b)')).toEqual([1, 2, 3, 4]); + expect(evaluate(`const a = [1, 2], b = 3;`, 'a[\'concat\'](b)')).toEqual([1, 2, 3]); + expect(evaluate(`const a = [1, 2], b = 3, c = [4, 5];`, 'a[\'concat\'](b, c)')).toEqual([ + 1, 2, 3, 4, 5 + ]); + expect(evaluate(`const a = [1, 2], b = [3, 4]`, 'a[\'concat\'](...b)')).toEqual([1, 2, 3, 4]); + }); - it('negation works', () => { - expect(evaluate(`const x = 3;`, '!x')).toEqual(false); - expect(evaluate(`const x = 3;`, '!!x')).toEqual(true); - }); + it('negation works', () => { + expect(evaluate(`const x = 3;`, '!x')).toEqual(false); + expect(evaluate(`const x = 3;`, '!!x')).toEqual(true); + }); - it('resolves access from external variable declarations as dynamic value', () => { - const value = evaluate('declare const window: any;', 'window.location'); - if (!(value instanceof DynamicValue)) { - return fail(`Should have resolved to a DynamicValue`); - } - expect(value.isFromDynamicInput()).toEqual(true); - expect(value.node.getText()).toEqual('window.location'); - if (!(value.reason instanceof DynamicValue)) { - return fail(`Should have a DynamicValue as reason`); - } - expect(value.reason.isFromExternalReference()).toEqual(true); - expect(value.reason.node.getText()).toEqual('window: any'); - }); + it('resolves access from external variable declarations as dynamic value', () => { + const value = evaluate('declare const window: any;', 'window.location'); + if (!(value instanceof DynamicValue)) { + return fail(`Should have resolved to a DynamicValue`); + } + expect(value.isFromDynamicInput()).toEqual(true); + expect(value.node.getText()).toEqual('window.location'); + if (!(value.reason instanceof DynamicValue)) { + return fail(`Should have a DynamicValue as reason`); + } + expect(value.reason.isFromExternalReference()).toEqual(true); + expect(value.reason.node.getText()).toEqual('window: any'); + }); - it('imports work', () => { - const {program} = makeProgram([ - {name: 'second.ts', contents: 'export function foo(bar) { return bar; }'}, - { - name: 'entry.ts', - contents: ` + it('imports work', () => { + const {program} = makeProgram([ + {name: _('/second.ts'), contents: 'export function foo(bar) { return bar; }'}, + { + name: _('/entry.ts'), + contents: ` import {foo} from './second'; const target$ = foo; ` - }, - ]); - const checker = program.getTypeChecker(); - const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); - const expr = result.initializer !; - const evaluator = makeEvaluator(checker); - const resolved = evaluator.evaluate(expr); - if (!(resolved instanceof Reference)) { - return fail('Expected expression to resolve to a reference'); - } - expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); - const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !); - if (reference === null) { - return fail('Expected to get an identifier'); - } - expect(reference.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); - }); + }, + ]); + const checker = program.getTypeChecker(); + const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration); + const expr = result.initializer !; + const evaluator = makeEvaluator(checker); + const resolved = evaluator.evaluate(expr); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to a reference'); + } + expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); + const reference = resolved.getIdentityIn(getSourceFileOrError(program, _('/entry.ts'))); + if (reference === null) { + return fail('Expected to get an identifier'); + } + expect(reference.getSourceFile()).toEqual(getSourceFileOrError(program, _('/entry.ts'))); + }); - it('absolute imports work', () => { - const {program} = makeProgram([ - {name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'}, - { - name: 'entry.ts', - contents: ` + it('absolute imports work', () => { + const {program} = makeProgram([ + { + name: _('/node_modules/some_library/index.d.ts'), + contents: 'export declare function foo(bar);' + }, + { + name: _('/entry.ts'), + contents: ` import {foo} from 'some_library'; const target$ = foo; ` - }, - ]); - const checker = program.getTypeChecker(); - const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); - const expr = result.initializer !; - const evaluator = makeEvaluator(checker); - const resolved = evaluator.evaluate(expr); - if (!(resolved instanceof Reference)) { - return fail('Expected expression to resolve to an absolute reference'); - } - expect(owningModuleOf(resolved)).toBe('some_library'); - expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); - const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !); - expect(reference).not.toBeNull(); - expect(reference !.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); - }); + }, + ]); + const checker = program.getTypeChecker(); + const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration); + const expr = result.initializer !; + const evaluator = makeEvaluator(checker); + const resolved = evaluator.evaluate(expr); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to an absolute reference'); + } + expect(owningModuleOf(resolved)).toBe('some_library'); + expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); + const reference = resolved.getIdentityIn(getSourceFileOrError(program, _('/entry.ts'))); + expect(reference).not.toBeNull(); + expect(reference !.getSourceFile()).toEqual(getSourceFileOrError(program, _('/entry.ts'))); + }); - it('reads values from default exports', () => { - const value = evaluate( - ` + it('reads values from default exports', () => { + const value = evaluate( + ` import mod from './second'; `, - 'mod.property', [ - {name: 'second.ts', contents: 'export default {property: "test"}'}, - ]); - expect(value).toEqual('test'); - }); - - it('reads values from named exports', () => { - const value = evaluate(`import * as mod from './second';`, 'mod.a.property', [ - {name: 'second.ts', contents: 'export const a = {property: "test"};'}, - ]); - expect(value).toEqual('test'); - }); - - it('chain of re-exports works', () => { - const value = evaluate(`import * as mod from './direct-reexport';`, 'mod.value.property', [ - {name: 'const.ts', contents: 'export const value = {property: "test"};'}, - {name: 'def.ts', contents: `import {value} from './const'; export default value;`}, - {name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`}, - {name: 'direct-reexport.ts', contents: `export {value} from './indirect-reexport';`}, - ]); - expect(value).toEqual('test'); - }); - - it('map spread works', () => { - const map: Map = evaluate>( - `const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, 'c'); - - const obj: {[key: string]: number} = {}; - map.forEach((value, key) => obj[key] = value); - expect(obj).toEqual({ - a: 1, - b: 2, - c: 3, + 'mod.property', [ + {name: _('/second.ts'), contents: 'export default {property: "test"}'}, + ]); + expect(value).toEqual('test'); }); - }); - it('indirected-via-object function call works', () => { - expect(evaluate( - ` + it('reads values from named exports', () => { + const value = evaluate(`import * as mod from './second';`, 'mod.a.property', [ + {name: _('/second.ts'), contents: 'export const a = {property: "test"};'}, + ]); + expect(value).toEqual('test'); + }); + + it('chain of re-exports works', () => { + const value = evaluate(`import * as mod from './direct-reexport';`, 'mod.value.property', [ + {name: _('/const.ts'), contents: 'export const value = {property: "test"};'}, + {name: _('/def.ts'), contents: `import {value} from './const'; export default value;`}, + {name: _('/indirect-reexport.ts'), contents: `import value from './def'; export {value};`}, + {name: _('/direct-reexport.ts'), contents: `export {value} from './indirect-reexport';`}, + ]); + expect(value).toEqual('test'); + }); + + it('map spread works', () => { + const map: Map = evaluate>( + `const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, 'c'); + + const obj: {[key: string]: number} = {}; + map.forEach((value, key) => obj[key] = value); + expect(obj).toEqual({ + a: 1, + b: 2, + c: 3, + }); + }); + + it('indirected-via-object function call works', () => { + expect(evaluate( + ` function fn(res) { return res; } const obj = {fn}; `, - 'obj.fn("test")')) - .toEqual('test'); - }); + 'obj.fn("test")')) + .toEqual('test'); + }); - it('template expressions work', - () => { expect(evaluate('const a = 2, b = 4;', '`1${a}3${b}5`')).toEqual('12345'); }); + it('template expressions work', + () => { expect(evaluate('const a = 2, b = 4;', '`1${a}3${b}5`')).toEqual('12345'); }); - it('enum resolution works', () => { - const result = evaluate( - ` + it('enum resolution works', () => { + const result = evaluate( + ` enum Foo { A, B, @@ -300,194 +307,209 @@ describe('ngtsc metadata', () => { const r = Foo.B; `, - 'r'); - if (!(result instanceof EnumValue)) { - return fail(`result is not an EnumValue`); - } - expect(result.enumRef.node.name.text).toBe('Foo'); - expect(result.name).toBe('B'); - }); + 'r'); + if (!(result instanceof EnumValue)) { + return fail(`result is not an EnumValue`); + } + expect(result.enumRef.node.name.text).toBe('Foo'); + expect(result.name).toBe('B'); + }); - it('variable declaration resolution works', () => { - const value = evaluate(`import {value} from './decl';`, 'value', [ - {name: 'decl.d.ts', contents: 'export declare let value: number;'}, - ]); - expect(value instanceof Reference).toBe(true); - }); + it('variable declaration resolution works', () => { + const value = evaluate(`import {value} from './decl';`, 'value', [ + {name: _('/decl.d.ts'), contents: 'export declare let value: number;'}, + ]); + expect(value instanceof Reference).toBe(true); + }); - it('should resolve shorthand properties to values', () => { - const {program} = makeProgram([ - {name: 'entry.ts', contents: `const prop = 42; const target$ = {prop};`}, - ]); - const checker = program.getTypeChecker(); - const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); - const expr = result.initializer !as ts.ObjectLiteralExpression; - const prop = expr.properties[0] as ts.ShorthandPropertyAssignment; - const evaluator = makeEvaluator(checker); - const resolved = evaluator.evaluate(prop.name); - expect(resolved).toBe(42); - }); + it('should resolve shorthand properties to values', () => { + const {program} = makeProgram([ + {name: _('/entry.ts'), contents: `const prop = 42; const target$ = {prop};`}, + ]); + const checker = program.getTypeChecker(); + const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration); + const expr = result.initializer !as ts.ObjectLiteralExpression; + const prop = expr.properties[0] as ts.ShorthandPropertyAssignment; + const evaluator = makeEvaluator(checker); + const resolved = evaluator.evaluate(prop.name); + expect(resolved).toBe(42); + }); - it('should resolve dynamic values in object literals', () => { - const {program} = makeProgram([ - {name: 'decl.d.ts', contents: 'export declare const fn: any;'}, - { - name: 'entry.ts', - contents: `import {fn} from './decl'; const prop = fn.foo(); const target$ = {value: prop};` - }, - ]); - const checker = program.getTypeChecker(); - const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); - const expr = result.initializer !as ts.ObjectLiteralExpression; - const evaluator = makeEvaluator(checker); - const resolved = evaluator.evaluate(expr); - if (!(resolved instanceof Map)) { - return fail('Should have resolved to a Map'); - } - const value = resolved.get('value') !; - if (!(value instanceof DynamicValue)) { - return fail(`Should have resolved 'value' to a DynamicValue`); - } - const prop = expr.properties[0] as ts.PropertyAssignment; - expect(value.node).toBe(prop.initializer); - }); + it('should resolve dynamic values in object literals', () => { + const {program} = makeProgram([ + {name: _('/decl.d.ts'), contents: 'export declare const fn: any;'}, + { + name: _('/entry.ts'), + contents: + `import {fn} from './decl'; const prop = fn.foo(); const target$ = {value: prop};` + }, + ]); + const checker = program.getTypeChecker(); + const result = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration); + const expr = result.initializer !as ts.ObjectLiteralExpression; + const evaluator = makeEvaluator(checker); + const resolved = evaluator.evaluate(expr); + if (!(resolved instanceof Map)) { + return fail('Should have resolved to a Map'); + } + const value = resolved.get('value') !; + if (!(value instanceof DynamicValue)) { + return fail(`Should have resolved 'value' to a DynamicValue`); + } + const prop = expr.properties[0] as ts.PropertyAssignment; + expect(value.node).toBe(prop.initializer); + }); - it('should resolve enums in template expressions', () => { - const value = - evaluate(`enum Test { VALUE = 'test', } const value = \`a.\${Test.VALUE}.b\`;`, 'value'); - expect(value).toBe('a.test.b'); - }); + it('should resolve enums in template expressions', () => { + const value = + evaluate(`enum Test { VALUE = 'test', } const value = \`a.\${Test.VALUE}.b\`;`, 'value'); + expect(value).toBe('a.test.b'); + }); - it('should not attach identifiers to FFR-resolved values', () => { - const value = evaluate( - ` + it('should not attach identifiers to FFR-resolved values', () => { + const value = evaluate( + ` declare function foo(arg: any): any; class Target {} const indir = foo(Target); const value = indir; `, - 'value', [], firstArgFfr); - if (!(value instanceof Reference)) { - return fail('Expected value to be a Reference'); - } - const id = value.getIdentityIn(value.node.getSourceFile()); - if (id === null) { - return fail('Expected value to have an identity'); - } - expect(id.text).toEqual('Target'); - }); + 'value', [], firstArgFfr); + if (!(value instanceof Reference)) { + return fail('Expected value to be a Reference'); + } + const id = value.getIdentityIn(value.node.getSourceFile()); + if (id === null) { + return fail('Expected value to have an identity'); + } + expect(id.text).toEqual('Target'); + }); - it('should resolve functions with more than one statement to an unknown value', () => { - const value = evaluate(`function foo(bar) { const b = bar; return b; }`, 'foo("test")'); + it('should resolve functions with more than one statement to an unknown value', () => { + const value = evaluate(`function foo(bar) { const b = bar; return b; }`, 'foo("test")'); - if (!(value instanceof DynamicValue)) { - return fail(`Should have resolved to a DynamicValue`); - } + if (!(value instanceof DynamicValue)) { + return fail(`Should have resolved to a DynamicValue`); + } - expect(value.isFromUnknown()).toBe(true); - expect((value.node as ts.CallExpression).expression.getText()).toBe('foo'); - }); + expect(value.isFromUnknown()).toBe(true); + expect((value.node as ts.CallExpression).expression.getText()).toBe('foo'); + }); - it('should evaluate TypeScript __spread helper', () => { - const {checker, expression} = makeExpression( - ` + it('should evaluate TypeScript __spread helper', () => { + const {checker, expression} = makeExpression( + ` import * as tslib from 'tslib'; const a = [1]; const b = [2, 3]; `, - 'tslib.__spread(a, b)', [ - { - name: 'node_modules/tslib/index.d.ts', - contents: ` + 'tslib.__spread(a, b)', [ + { + name: _('/node_modules/tslib/index.d.ts'), + contents: ` export declare function __spread(...args: any[]): any[]; ` - }, - ]); - const reflectionHost = new TsLibAwareReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker); - const value = evaluator.evaluate(expression); - expect(value).toEqual([1, 2, 3]); - }); - - describe('(visited file tracking)', () => { - it('should track each time a source file is visited', () => { - const trackFileDependency = jasmine.createSpy('DependencyTracker'); - const {expression, checker} = - makeExpression(`class A { static foo = 42; } function bar() { return A.foo; }`, 'bar()'); - const evaluator = makeEvaluator(checker, {trackFileDependency}); - evaluator.evaluate(expression); - expect(trackFileDependency).toHaveBeenCalledTimes(2); // two declaration visited - expect(trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName])) - .toEqual([['/entry.ts', '/entry.ts'], ['/entry.ts', '/entry.ts']]); + }, + ]); + const reflectionHost = new TsLibAwareReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker); + const value = evaluator.evaluate(expression); + expect(value).toEqual([1, 2, 3]); }); - it('should track imported source files', () => { - const trackFileDependency = jasmine.createSpy('DependencyTracker'); - const {expression, checker} = makeExpression(`import {Y} from './other'; const A = Y;`, 'A', [ - {name: 'other.ts', contents: `export const Y = 'test';`}, - {name: 'not-visited.ts', contents: `export const Z = 'nope';`} - ]); - const evaluator = makeEvaluator(checker, {trackFileDependency}); - evaluator.evaluate(expression); - expect(trackFileDependency).toHaveBeenCalledTimes(2); - expect(trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName])) - .toEqual([ - ['/entry.ts', '/entry.ts'], - ['/other.ts', '/entry.ts'], - ]); - }); + describe('(visited file tracking)', () => { + it('should track each time a source file is visited', () => { + const trackFileDependency = jasmine.createSpy('DependencyTracker'); + const {expression, checker} = makeExpression( + `class A { static foo = 42; } function bar() { return A.foo; }`, 'bar()'); + const evaluator = makeEvaluator(checker, {trackFileDependency}); + evaluator.evaluate(expression); + expect(trackFileDependency).toHaveBeenCalledTimes(2); // two declaration visited + expect( + trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName])) + .toEqual([[_('/entry.ts'), _('/entry.ts')], [_('/entry.ts'), _('/entry.ts')]]); + }); - it('should track files passed through during re-exports', () => { - const trackFileDependency = jasmine.createSpy('DependencyTracker'); - const {expression, checker} = - makeExpression(`import * as mod from './direct-reexport';`, 'mod.value.property', [ - {name: 'const.ts', contents: 'export const value = {property: "test"};'}, - {name: 'def.ts', contents: `import {value} from './const'; export default value;`}, - {name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`}, - {name: 'direct-reexport.ts', contents: `export {value} from './indirect-reexport';`}, - ]); - const evaluator = makeEvaluator(checker, {trackFileDependency}); - evaluator.evaluate(expression); - expect(trackFileDependency).toHaveBeenCalledTimes(2); - expect(trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName])) - .toEqual([ - ['/direct-reexport.ts', '/entry.ts'], - // Not '/indirect-reexport.ts' or '/def.ts'. - // TS skips through them when finding the original symbol for `value` - ['/const.ts', '/entry.ts'], - ]); + it('should track imported source files', () => { + const trackFileDependency = jasmine.createSpy('DependencyTracker'); + const {expression, checker} = + makeExpression(`import {Y} from './other'; const A = Y;`, 'A', [ + {name: _('/other.ts'), contents: `export const Y = 'test';`}, + {name: _('/not-visited.ts'), contents: `export const Z = 'nope';`} + ]); + const evaluator = makeEvaluator(checker, {trackFileDependency}); + evaluator.evaluate(expression); + expect(trackFileDependency).toHaveBeenCalledTimes(2); + expect( + trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName])) + .toEqual([ + [_('/entry.ts'), _('/entry.ts')], + [_('/other.ts'), _('/entry.ts')], + ]); + }); + + it('should track files passed through during re-exports', () => { + const trackFileDependency = jasmine.createSpy('DependencyTracker'); + const {expression, checker} = + makeExpression(`import * as mod from './direct-reexport';`, 'mod.value.property', [ + {name: _('/const.ts'), contents: 'export const value = {property: "test"};'}, + { + name: _('/def.ts'), + contents: `import {value} from './const'; export default value;` + }, + { + name: _('/indirect-reexport.ts'), + contents: `import value from './def'; export {value};` + }, + { + name: _('/direct-reexport.ts'), + contents: `export {value} from './indirect-reexport';` + }, + ]); + const evaluator = makeEvaluator(checker, {trackFileDependency}); + evaluator.evaluate(expression); + expect(trackFileDependency).toHaveBeenCalledTimes(2); + expect( + trackFileDependency.calls.allArgs().map(args => [args[0].fileName, args[1].fileName])) + .toEqual([ + [_('/direct-reexport.ts'), _('/entry.ts')], + // Not '/indirect-reexport.ts' or '/def.ts'. + // TS skips through them when finding the original symbol for `value` + [_('/const.ts'), _('/entry.ts')], + ]); + }); }); }); -}); -/** - * Customizes the resolution of functions to recognize functions from tslib. Such functions are not - * handled specially in the default TypeScript host, as only ngcc's ES5 host will have special - * powers to recognize functions from tslib. - */ -class TsLibAwareReflectionHost extends TypeScriptReflectionHost { - getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null { - if (ts.isFunctionDeclaration(node)) { - const helper = getTsHelperFn(node); - if (helper !== null) { - return { - node, - body: null, helper, - parameters: [], - }; + /** + * Customizes the resolution of functions to recognize functions from tslib. Such functions are + * not handled specially in the default TypeScript host, as only ngcc's ES5 host will have special + * powers to recognize functions from tslib. + */ + class TsLibAwareReflectionHost extends TypeScriptReflectionHost { + getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null { + if (ts.isFunctionDeclaration(node)) { + const helper = getTsHelperFn(node); + if (helper !== null) { + return { + node, + body: null, helper, + parameters: [], + }; + } } + return super.getDefinitionOfFunction(node); } - return super.getDefinitionOfFunction(node); } -} -function getTsHelperFn(node: ts.FunctionDeclaration): TsHelperFn|null { - const name = node.name !== undefined && ts.isIdentifier(node.name) && node.name.text; + function getTsHelperFn(node: ts.FunctionDeclaration): TsHelperFn|null { + const name = node.name !== undefined && ts.isIdentifier(node.name) && node.name.text; - if (name === '__spread') { - return TsHelperFn.Spread; - } else { - return null; + if (name === '__spread') { + return TsHelperFn.Spread; + } else { + return null; + } } -} +}); diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts index 6045e52cd5..e63983256e 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts @@ -5,27 +5,29 @@ * 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 {absoluteFrom} from '../../file_system'; +import {TestFile} from '../../file_system/testing'; import {Reference} from '../../imports'; import {TypeScriptReflectionHost} from '../../reflection'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; import {DependencyTracker, ForeignFunctionResolver, PartialEvaluator} from '../src/interface'; import {ResolvedValue} from '../src/result'; -export function makeExpression( - code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): { +export function makeExpression(code: string, expr: string, supportingFiles: TestFile[] = []): { expression: ts.Expression, host: ts.CompilerHost, checker: ts.TypeChecker, program: ts.Program, options: ts.CompilerOptions } { - const {program, options, host} = makeProgram( - [{name: 'entry.ts', contents: `${code}; const target$ = ${expr};`}, ...supportingFiles]); + const {program, options, host} = makeProgram([ + {name: absoluteFrom('/entry.ts'), contents: `${code}; const target$ = ${expr};`}, + ...supportingFiles + ]); const checker = program.getTypeChecker(); - const decl = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); + const decl = + getDeclaration(program, absoluteFrom('/entry.ts'), 'target$', ts.isVariableDeclaration); return { expression: decl.initializer !, host, @@ -42,7 +44,7 @@ export function makeEvaluator( } export function evaluate( - code: string, expr: string, supportingFiles: {name: string, contents: string}[] = [], + code: string, expr: string, supportingFiles: TestFile[] = [], foreignFunctionResolver?: ForeignFunctionResolver): T { const {expression, checker} = makeExpression(code, expr, supportingFiles); const evaluator = makeEvaluator(checker); diff --git a/packages/compiler-cli/src/ngtsc/path/README.md b/packages/compiler-cli/src/ngtsc/path/README.md deleted file mode 100644 index 546ae01539..0000000000 --- a/packages/compiler-cli/src/ngtsc/path/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# About paths in ngtsc - -Within the compiler, there are a number of different types of file system or URL "paths" which are manipulated as strings. While it's possible to declare the variables and fields which store these different kinds of paths using the 'string' type, this has significant drawbacks: - -* When calling a function which accepts a path as an argument, it's not clear what kind of path should be passed. -* It can be expensive to check whether a path is properly formatted, and without types it's easy to fall into the habit of normalizing different kinds of paths repeatedly. -* There is no static check to detect if paths are improperly used in the wrong context (e.g. a relative path passed where an absolute path was required). This can cause subtle bugs. -* When running on Windows, some paths can use different conventions (e.g. forward vs back slashes). It's not always clear when a path needs to be checked for the correct convention. - -To address these issues, ngtsc has specific static types for each kind of path in the system. These types are not mutually assignable, nor can they be directly assigned from `string`s (though they can be assigned _to_ `string`s). Conversion between `string`s and these specific path types happens through a narrow API which validates that all typed paths are valid. - -# The different path kinds - -All paths in the type system use POSIX format (`/` separators). - -## `AbsoluteFsPath` - -This path type represents an absolute path to a physical directory or file. For example, `/foo/bar.txt`. - -## `PathSegment` - -This path type represents a relative path to a directory or file. It only makes sense in the context of some directory (e.g. the working directory) or set of directories to search, and does not need to necessarily represent a relative path between two physical files. - -## `LogicalProjectPath` - -This path type represents a path to a file in TypeScript's logical file system. - -TypeScript supports multiple root directories for a given project, which are effectively overlayed to obtain a file layout. For example, if a project has two root directories `foo` and `bar` with the layout: - -```text -/foo -/foo/foo.ts -/bar -/bar/bar.ts -``` - -Then `foo.ts` could theoretically contain: - -```typescript -import {Bar} from './bar'; -``` - -This import of `./bar` is not a valid relative path from `foo.ts` to `bar.ts` on the physical filesystem, but is valid in the context of the project because the contents of the `foo` and `bar` directories are overlayed as far as TypeScript is concerned. - -In this example, `/foo/foo.ts` has a `LogicalProjectPath` of `/foo.ts` and `/bar/bar.ts` has a `LogicalProjectPath` of `/bar.ts`, allowing the module specifier in the import (`./bar`) to be resolved via standard path operations. \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/path/src/types.ts b/packages/compiler-cli/src/ngtsc/path/src/types.ts deleted file mode 100644 index 7286aeaaa7..0000000000 --- a/packages/compiler-cli/src/ngtsc/path/src/types.ts +++ /dev/null @@ -1,122 +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 * as path from 'path'; -import * as ts from 'typescript'; - -import {isAbsolutePath, normalizeSeparators} from './util'; - -/** - * A `string` representing a specific type of path, with a particular brand `B`. - * - * A `string` is not assignable to a `BrandedPath`, but a `BrandedPath` is assignable to a `string`. - * Two `BrandedPath`s with different brands are not mutually assignable. - */ -export type BrandedPath = string & { - _brand: B; -}; - -/** - * A fully qualified path in the file system, in POSIX form. - */ -export type AbsoluteFsPath = BrandedPath<'AbsoluteFsPath'>; - -/** - * A path that's relative to another (unspecified) root. - * - * This does not necessarily have to refer to a physical file. - */ -export type PathSegment = BrandedPath<'PathSegment'>; - -/** - * Contains utility functions for creating and manipulating `AbsoluteFsPath`s. - */ -export const AbsoluteFsPath = { - /** - * Convert the path `str` to an `AbsoluteFsPath`, throwing an error if it's not an absolute path. - */ - from: function(str: string): AbsoluteFsPath { - if (str.startsWith('/') && process.platform === 'win32') { - // in Windows if it's absolute path and starts with `/` we shall - // resolve it and return it including the drive. - str = path.resolve(str); - } - - const normalized = normalizeSeparators(str); - if (!isAbsolutePath(normalized)) { - throw new Error(`Internal Error: AbsoluteFsPath.from(${str}): path is not absolute`); - } - return normalized as AbsoluteFsPath; - }, - - /** - * Assume that the path `str` is an `AbsoluteFsPath` in the correct format already. - */ - fromUnchecked: function(str: string): AbsoluteFsPath { return str as AbsoluteFsPath;}, - - /** - * Extract an `AbsoluteFsPath` from a `ts.SourceFile`. - * - * This is cheaper than calling `AbsoluteFsPath.from(sf.fileName)`, as source files already have - * their file path in absolute POSIX format. - */ - fromSourceFile: function(sf: ts.SourceFile): AbsoluteFsPath { - // ts.SourceFile paths are always absolute. - return sf.fileName as AbsoluteFsPath; - }, - - /** - * Wrapper around `path.dirname` that returns an absolute path. - */ - dirname: function(file: AbsoluteFsPath): - AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked(path.dirname(file));}, - - /** - * Wrapper around `path.join` that returns an absolute path. - */ - join: function(basePath: AbsoluteFsPath, ...paths: string[]): - AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked(path.posix.join(basePath, ...paths));}, - - /** - * Wrapper around `path.resolve` that returns an absolute paths. - */ - resolve: function(basePath: string, ...paths: string[]): - AbsoluteFsPath { return AbsoluteFsPath.from(path.resolve(basePath, ...paths));}, - - /** Returns true when the path provided is the root path. */ - isRoot: function(path: AbsoluteFsPath): boolean { return AbsoluteFsPath.dirname(path) === path;}, -}; - -/** - * Contains utility functions for creating and manipulating `PathSegment`s. - */ -export const PathSegment = { - /** - * Convert the path `str` to a `PathSegment`, throwing an error if it's not a relative path. - */ - fromFsPath: function(str: string): PathSegment { - const normalized = normalizeSeparators(str); - if (isAbsolutePath(normalized)) { - throw new Error(`Internal Error: PathSegment.fromFsPath(${str}): path is not relative`); - } - return normalized as PathSegment; - }, - - /** - * Convert the path `str` to a `PathSegment`, while assuming that `str` is already normalized. - */ - fromUnchecked: function(str: string): PathSegment { return str as PathSegment;}, - - /** - * Wrapper around `path.relative` that returns a `PathSegment`. - */ - relative: function(from: AbsoluteFsPath, to: AbsoluteFsPath): - PathSegment { return PathSegment.fromFsPath(path.relative(from, to));}, - - basename: function(filePath: string, extension?: string): - PathSegment { return path.basename(filePath, extension) as PathSegment;} -}; diff --git a/packages/compiler-cli/src/ngtsc/path/test/logical_spec.ts b/packages/compiler-cli/src/ngtsc/path/test/logical_spec.ts deleted file mode 100644 index b2a8711cdb..0000000000 --- a/packages/compiler-cli/src/ngtsc/path/test/logical_spec.ts +++ /dev/null @@ -1,63 +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 {LogicalFileSystem, LogicalProjectPath} from '../src/logical'; -import {AbsoluteFsPath} from '../src/types'; - -describe('logical paths', () => { - describe('LogicalFileSystem', () => { - it('should determine logical paths in a single root file system', () => { - const fs = new LogicalFileSystem([abs('/test')]); - expect(fs.logicalPathOfFile(abs('/test/foo/foo.ts'))) - .toEqual('/foo/foo' as LogicalProjectPath); - expect(fs.logicalPathOfFile(abs('/test/bar/bar.ts'))) - .toEqual('/bar/bar' as LogicalProjectPath); - expect(fs.logicalPathOfFile(abs('/not-test/bar.ts'))).toBeNull(); - }); - - it('should determine logical paths in a multi-root file system', () => { - const fs = new LogicalFileSystem([abs('/test/foo'), abs('/test/bar')]); - expect(fs.logicalPathOfFile(abs('/test/foo/foo.ts'))).toEqual('/foo' as LogicalProjectPath); - expect(fs.logicalPathOfFile(abs('/test/bar/bar.ts'))).toEqual('/bar' as LogicalProjectPath); - }); - - it('should continue to work when one root is a child of another', () => { - const fs = new LogicalFileSystem([abs('/test'), abs('/test/dist')]); - expect(fs.logicalPathOfFile(abs('/test/foo.ts'))).toEqual('/foo' as LogicalProjectPath); - expect(fs.logicalPathOfFile(abs('/test/dist/foo.ts'))).toEqual('/foo' as LogicalProjectPath); - }); - - it('should always return `/` prefixed logical paths', () => { - const rootFs = new LogicalFileSystem([abs('/')]); - expect(rootFs.logicalPathOfFile(abs('/foo/foo.ts'))) - .toEqual('/foo/foo' as LogicalProjectPath); - - const nonRootFs = new LogicalFileSystem([abs('/test/')]); - expect(nonRootFs.logicalPathOfFile(abs('/test/foo/foo.ts'))) - .toEqual('/foo/foo' as LogicalProjectPath); - }); - }); - - describe('utilities', () => { - it('should give a relative path between two adjacent logical files', () => { - const res = LogicalProjectPath.relativePathBetween( - '/foo' as LogicalProjectPath, '/bar' as LogicalProjectPath); - expect(res).toEqual('./bar'); - }); - - it('should give a relative path between two non-adjacent logical files', () => { - const res = LogicalProjectPath.relativePathBetween( - '/foo/index' as LogicalProjectPath, '/bar/index' as LogicalProjectPath); - expect(res).toEqual('../bar/index'); - }); - }); -}); - -function abs(file: string): AbsoluteFsPath { - return AbsoluteFsPath.from(file); -} diff --git a/packages/compiler-cli/src/ngtsc/path/test/types_spec.ts b/packages/compiler-cli/src/ngtsc/path/test/types_spec.ts deleted file mode 100644 index 18910ceaca..0000000000 --- a/packages/compiler-cli/src/ngtsc/path/test/types_spec.ts +++ /dev/null @@ -1,26 +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 {AbsoluteFsPath} from '../src/types'; - -describe('path types', () => { - describe('AbsoluteFsPath', () => { - it('should not throw when creating one from an absolute path', - () => { expect(() => AbsoluteFsPath.from('/test.txt')).not.toThrow(); }); - it('should not throw when creating one from a windows absolute path', - () => { expect(AbsoluteFsPath.from('C:\\test.txt')).toEqual('C:/test.txt'); }); - it('should not throw when creating one from a windows absolute path with POSIX separators', - () => { expect(AbsoluteFsPath.from('C:/test.txt')).toEqual('C:/test.txt'); }); - it('should throw when creating one from a non-absolute path', - () => { expect(() => AbsoluteFsPath.from('test.txt')).toThrow(); }); - it('should support windows drive letters', - () => { expect(AbsoluteFsPath.from('D:\\foo\\test.txt')).toEqual('D:/foo/test.txt'); }); - it('should convert Windows path separators to POSIX separators', - () => { expect(AbsoluteFsPath.from('\\foo\\test.txt')).toEqual('/foo/test.txt'); }); - }); -}); diff --git a/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel b/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel index 53af9b1c4f..dd609410fd 100644 --- a/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel @@ -9,6 +9,7 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/file_system", "@npm//@types/node", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts b/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts index e8faef34ac..0eb6251b91 100644 --- a/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts +++ b/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts @@ -5,13 +5,10 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - /// import * as fs from 'fs'; -import * as path from 'path'; - import * as ts from 'typescript'; - +import {resolve} from '../../file_system'; import {PerfRecorder} from './api'; import {HrTime, mark, timeSinceInMicros} from './clock'; @@ -83,10 +80,10 @@ export class PerfTracker implements PerfRecorder { if (target.startsWith('ts:')) { target = target.substr('ts:'.length); - const outFile = path.posix.resolve(host.getCurrentDirectory(), target); + const outFile = resolve(host.getCurrentDirectory(), target); host.writeFile(outFile, json, false); } else { - const outFile = path.posix.resolve(host.getCurrentDirectory(), target); + const outFile = resolve(host.getCurrentDirectory(), target); fs.writeFileSync(outFile, json); } } diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index dcf334c664..cfcb171f38 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -17,13 +17,13 @@ import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {CycleAnalyzer, ImportGraph} from './cycles'; import {ErrorCode, ngErrorCode} from './diagnostics'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; +import {AbsoluteFsPath, LogicalFileSystem, absoluteFrom} from './file_system'; import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; import {IncrementalState} from './incremental'; import {IndexedComponent, IndexingContext} from './indexer'; import {generateAnalysis} from './indexer/src/transform'; import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata'; import {PartialEvaluator} from './partial_evaluator'; -import {AbsoluteFsPath, LogicalFileSystem} from './path'; import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf'; import {TypeScriptReflectionHost} from './reflection'; import {HostResourceLoader} from './resource_loader'; @@ -35,7 +35,7 @@ import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from ' import {aliasTransformFactory} from './transform/src/alias'; import {TypeCheckContext, TypeCheckingConfig, typeCheckFilePath} from './typecheck'; import {normalizeSeparators} from './util/src/path'; -import {getRootDirs, isDtsPath, resolveModuleName} from './util/src/typescript'; +import {getRootDirs, getSourceFileOrNull, isDtsPath, resolveModuleName} from './util/src/typescript'; export class NgtscProgram implements api.Program { private tsProgram: ts.Program; @@ -84,7 +84,7 @@ export class NgtscProgram implements api.Program { this.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.resourceManager = new HostResourceLoader(host, options); const shouldGenerateShims = options.allowEmptyCodegenFiles || false; - const normalizedRootNames = rootNames.map(n => AbsoluteFsPath.from(n)); + const normalizedRootNames = rootNames.map(n => absoluteFrom(n)); if (host.fileNameToModuleName !== undefined) { this.fileToModuleHost = host as FileToModuleHost; } @@ -115,7 +115,7 @@ export class NgtscProgram implements api.Program { generators.push(new TypeCheckShimGenerator(this.typeCheckFilePath)); rootFiles.push(this.typeCheckFilePath); - let entryPoint: string|null = null; + let entryPoint: AbsoluteFsPath|null = null; if (options.flatModuleOutFile !== undefined) { entryPoint = findFlatIndexEntryPoint(normalizedRootNames); if (entryPoint === null) { @@ -154,7 +154,7 @@ export class NgtscProgram implements api.Program { ts.createProgram(rootFiles, options, this.host, oldProgram && oldProgram.reuseTsProgram); this.reuseTsProgram = this.tsProgram; - this.entryPoint = entryPoint !== null ? this.tsProgram.getSourceFile(entryPoint) || null : null; + this.entryPoint = entryPoint !== null ? getSourceFileOrNull(this.tsProgram, entryPoint) : null; this.moduleResolver = new ModuleResolver(this.tsProgram, options, this.host); this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(this.moduleResolver)); this.defaultImportTracker = new DefaultImportTracker(); @@ -345,7 +345,7 @@ export class NgtscProgram implements api.Program { const emitSpan = this.perfRecorder.start('emit'); const emitResults: ts.EmitResult[] = []; - const typeCheckFile = this.tsProgram.getSourceFile(this.typeCheckFilePath); + const typeCheckFile = getSourceFileOrNull(this.tsProgram, this.typeCheckFilePath); for (const targetSourceFile of this.tsProgram.getSourceFiles()) { if (targetSourceFile.isDeclarationFile || targetSourceFile === typeCheckFile) { diff --git a/packages/compiler-cli/src/ngtsc/reflection/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/reflection/test/BUILD.bazel index 605b9dd910..b786eade07 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/reflection/test/BUILD.bazel @@ -10,6 +10,8 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts index 16dc055fc6..223a6d0b1a 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts @@ -5,47 +5,52 @@ * 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 {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {absoluteFrom} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; +import {getDeclaration, makeProgram} from '../../testing'; import {CtorParameter} from '../src/host'; import {TypeScriptReflectionHost} from '../src/typescript'; import {isNamedClassDeclaration} from '../src/util'; -describe('reflector', () => { - describe('ctor params', () => { - it('should reflect a single argument', () => { - const {program} = makeProgram([{ - name: 'entry.ts', - contents: ` +runInEachFileSystem(() => { + describe('reflector', () => { + let _: typeof absoluteFrom; + + beforeEach(() => _ = absoluteFrom); + + describe('ctor params', () => { + it('should reflect a single argument', () => { + const {program} = makeProgram([{ + name: _('/entry.ts'), + contents: ` class Bar {} class Foo { constructor(bar: Bar) {} } ` - }]); - const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration); - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const args = host.getConstructorParameters(clazz) !; - expect(args.length).toBe(1); - expectParameter(args[0], 'bar', 'Bar'); - }); + }]); + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(1); + expectParameter(args[0], 'bar', 'Bar'); + }); - it('should reflect a decorated argument', () => { - const {program} = makeProgram([ - { - name: 'dec.ts', - contents: ` + it('should reflect a decorated argument', () => { + const {program} = makeProgram([ + { + name: _('/dec.ts'), + contents: ` export function dec(target: any, key: string, index: number) { } ` - }, - { - name: 'entry.ts', - contents: ` + }, + { + name: _('/entry.ts'), + contents: ` import {dec} from './dec'; class Bar {} @@ -53,28 +58,28 @@ describe('reflector', () => { constructor(@dec bar: Bar) {} } ` - } - ]); - const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration); - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const args = host.getConstructorParameters(clazz) !; - expect(args.length).toBe(1); - expectParameter(args[0], 'bar', 'Bar', 'dec', './dec'); - }); + } + ]); + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(1); + expectParameter(args[0], 'bar', 'Bar', 'dec', './dec'); + }); - it('should reflect a decorated argument with a call', () => { - const {program} = makeProgram([ - { - name: 'dec.ts', - contents: ` + it('should reflect a decorated argument with a call', () => { + const {program} = makeProgram([ + { + name: _('/dec.ts'), + contents: ` export function dec(target: any, key: string, index: number) { } ` - }, - { - name: 'entry.ts', - contents: ` + }, + { + name: _('/entry.ts'), + contents: ` import {dec} from './dec'; class Bar {} @@ -82,27 +87,27 @@ describe('reflector', () => { constructor(@dec bar: Bar) {} } ` - } - ]); - const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration); - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const args = host.getConstructorParameters(clazz) !; - expect(args.length).toBe(1); - expectParameter(args[0], 'bar', 'Bar', 'dec', './dec'); - }); + } + ]); + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(1); + expectParameter(args[0], 'bar', 'Bar', 'dec', './dec'); + }); - it('should reflect a decorated argument with an indirection', () => { - const {program} = makeProgram([ - { - name: 'bar.ts', - contents: ` + it('should reflect a decorated argument with an indirection', () => { + const {program} = makeProgram([ + { + name: _('/bar.ts'), + contents: ` export class Bar {} ` - }, - { - name: 'entry.ts', - contents: ` + }, + { + name: _('/entry.ts'), + contents: ` import {Bar} from './bar'; import * as star from './bar'; @@ -110,187 +115,188 @@ describe('reflector', () => { constructor(bar: Bar, otherBar: star.Bar) {} } ` - } - ]); - const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration); - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const args = host.getConstructorParameters(clazz) !; - expect(args.length).toBe(2); - expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); - expectParameter(args[1], 'otherBar', {moduleName: './bar', name: 'Bar'}); - }); + } + ]); + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(2); + expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); + expectParameter(args[1], 'otherBar', {moduleName: './bar', name: 'Bar'}); + }); - it('should reflect an argument from an aliased import', () => { - const {program} = makeProgram([ - { - name: 'bar.ts', - contents: ` + it('should reflect an argument from an aliased import', () => { + const {program} = makeProgram([ + { + name: _('/bar.ts'), + contents: ` export class Bar {} ` - }, - { - name: 'entry.ts', - contents: ` + }, + { + name: _('/entry.ts'), + contents: ` import {Bar as LocalBar} from './bar'; class Foo { constructor(bar: LocalBar) {} } ` - } - ]); - const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration); - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const args = host.getConstructorParameters(clazz) !; - expect(args.length).toBe(1); - expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); - }); + } + ]); + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(1); + expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); + }); - it('should reflect an argument from a default import', () => { - const {program} = makeProgram([ - { - name: 'bar.ts', - contents: ` + it('should reflect an argument from a default import', () => { + const {program} = makeProgram([ + { + name: _('/bar.ts'), + contents: ` export default class Bar {} ` - }, - { - name: 'entry.ts', - contents: ` + }, + { + name: _('/entry.ts'), + contents: ` import Bar from './bar'; class Foo { constructor(bar: Bar) {} } ` + } + ]); + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(1); + const param = args[0].typeValueReference; + if (param === null || !param.local) { + return fail('Expected local parameter'); } - ]); - const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration); - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const args = host.getConstructorParameters(clazz) !; - expect(args.length).toBe(1); - const param = args[0].typeValueReference; - if (param === null || !param.local) { - return fail('Expected local parameter'); - } - expect(param).not.toBeNull(); - expect(param.defaultImportStatement).not.toBeNull(); - }); + expect(param).not.toBeNull(); + expect(param.defaultImportStatement).not.toBeNull(); + }); - it('should reflect a nullable argument', () => { - const {program} = makeProgram([ - { - name: 'bar.ts', - contents: ` + it('should reflect a nullable argument', () => { + const {program} = makeProgram([ + { + name: _('/bar.ts'), + contents: ` export class Bar {} ` - }, - { - name: 'entry.ts', - contents: ` + }, + { + name: _('/entry.ts'), + contents: ` import {Bar} from './bar'; class Foo { constructor(bar: Bar|null) {} } ` - } - ]); - const clazz = getDeclaration(program, 'entry.ts', 'Foo', isNamedClassDeclaration); - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const args = host.getConstructorParameters(clazz) !; - expect(args.length).toBe(1); - expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); + } + ]); + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(1); + expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); + }); }); - }); - it('should reflect a re-export', () => { - const {program} = makeProgram([ - {name: '/node_modules/absolute/index.ts', contents: 'export class Target {}'}, - {name: 'local1.ts', contents: `export {Target as AliasTarget} from 'absolute';`}, - {name: 'local2.ts', contents: `export {AliasTarget as Target} from './local1';`}, { - name: 'entry.ts', - contents: ` + it('should reflect a re-export', () => { + const {program} = makeProgram([ + {name: _('/node_modules/absolute/index.ts'), contents: 'export class Target {}'}, + {name: _('/local1.ts'), contents: `export {Target as AliasTarget} from 'absolute';`}, + {name: _('/local2.ts'), contents: `export {AliasTarget as Target} from './local1';`}, { + name: _('/entry.ts'), + contents: ` import {Target} from './local2'; import {Target as DirectTarget} from 'absolute'; const target = Target; const directTarget = DirectTarget; ` + } + ]); + const target = getDeclaration(program, _('/entry.ts'), 'target', ts.isVariableDeclaration); + if (target.initializer === undefined || !ts.isIdentifier(target.initializer)) { + return fail('Unexpected initializer for target'); } - ]); - const target = getDeclaration(program, 'entry.ts', 'target', ts.isVariableDeclaration); - if (target.initializer === undefined || !ts.isIdentifier(target.initializer)) { - return fail('Unexpected initializer for target'); - } - const directTarget = - getDeclaration(program, 'entry.ts', 'directTarget', ts.isVariableDeclaration); - if (directTarget.initializer === undefined || !ts.isIdentifier(directTarget.initializer)) { - return fail('Unexpected initializer for directTarget'); - } - const Target = target.initializer; - const DirectTarget = directTarget.initializer; + const directTarget = + getDeclaration(program, _('/entry.ts'), 'directTarget', ts.isVariableDeclaration); + if (directTarget.initializer === undefined || !ts.isIdentifier(directTarget.initializer)) { + return fail('Unexpected initializer for directTarget'); + } + const Target = target.initializer; + const DirectTarget = directTarget.initializer; - const checker = program.getTypeChecker(); - const host = new TypeScriptReflectionHost(checker); - const targetDecl = host.getDeclarationOfIdentifier(Target); - const directTargetDecl = host.getDeclarationOfIdentifier(DirectTarget); - if (targetDecl === null) { - return fail('No declaration found for Target'); - } else if (directTargetDecl === null) { - return fail('No declaration found for DirectTarget'); - } - expect(targetDecl.node.getSourceFile().fileName).toBe('/node_modules/absolute/index.ts'); - expect(ts.isClassDeclaration(targetDecl.node)).toBe(true); - expect(directTargetDecl.viaModule).toBe('absolute'); - expect(directTargetDecl.node).toBe(targetDecl.node); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const targetDecl = host.getDeclarationOfIdentifier(Target); + const directTargetDecl = host.getDeclarationOfIdentifier(DirectTarget); + if (targetDecl === null) { + return fail('No declaration found for Target'); + } else if (directTargetDecl === null) { + return fail('No declaration found for DirectTarget'); + } + expect(targetDecl.node.getSourceFile().fileName).toBe(_('/node_modules/absolute/index.ts')); + expect(ts.isClassDeclaration(targetDecl.node)).toBe(true); + expect(directTargetDecl.viaModule).toBe('absolute'); + expect(directTargetDecl.node).toBe(targetDecl.node); + }); }); -}); -function expectParameter( - param: CtorParameter, name: string, type?: string | {name: string, moduleName: string}, - decorator?: string, decoratorFrom?: string): void { - expect(param.name !).toEqual(name); - if (type === undefined) { - expect(param.typeValueReference).toBeNull(); - } else { - if (param.typeValueReference === null) { - return fail(`Expected parameter ${name} to have a typeValueReference`); - } - if (param.typeValueReference.local && typeof type === 'string') { - expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type); - } else if (!param.typeValueReference.local && typeof type !== 'string') { - expect(param.typeValueReference.moduleName).toEqual(type.moduleName); - expect(param.typeValueReference.name).toEqual(type.name); + function expectParameter( + param: CtorParameter, name: string, type?: string | {name: string, moduleName: string}, + decorator?: string, decoratorFrom?: string): void { + expect(param.name !).toEqual(name); + if (type === undefined) { + expect(param.typeValueReference).toBeNull(); } else { - return fail( - `Mismatch between typeValueReference and expected type: ${param.name} / ${param.typeValueReference.local}`); + if (param.typeValueReference === null) { + return fail(`Expected parameter ${name} to have a typeValueReference`); + } + if (param.typeValueReference.local && typeof type === 'string') { + expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type); + } else if (!param.typeValueReference.local && typeof type !== 'string') { + expect(param.typeValueReference.moduleName).toEqual(type.moduleName); + expect(param.typeValueReference.name).toEqual(type.name); + } else { + return fail( + `Mismatch between typeValueReference and expected type: ${param.name} / ${param.typeValueReference.local}`); + } + } + if (decorator !== undefined) { + expect(param.decorators).not.toBeNull(); + expect(param.decorators !.length).toBeGreaterThan(0); + expect(param.decorators !.some( + dec => dec.name === decorator && dec.import !== null && + dec.import.from === decoratorFrom)) + .toBe(true); } } - if (decorator !== undefined) { - expect(param.decorators).not.toBeNull(); - expect(param.decorators !.length).toBeGreaterThan(0); - expect(param.decorators !.some( - dec => dec.name === decorator && dec.import !== null && - dec.import.from === decoratorFrom)) - .toBe(true); - } -} -function argExpressionToString(name: ts.Node | null): string { - if (name == null) { - throw new Error('\'name\' argument can\'t be null'); - } + function argExpressionToString(name: ts.Node | null): string { + if (name == null) { + throw new Error('\'name\' argument can\'t be null'); + } - if (ts.isIdentifier(name)) { - return name.text; - } else if (ts.isPropertyAccessExpression(name)) { - return `${argExpressionToString(name.expression)}.${name.name.text}`; - } else { - throw new Error(`Unexpected node in arg expression: ${ts.SyntaxKind[name.kind]}.`); + if (ts.isIdentifier(name)) { + return name.text; + } else if (ts.isPropertyAccessExpression(name)) { + return `${argExpressionToString(name.expression)}.${name.name.text}`; + } else { + throw new Error(`Unexpected node in arg expression: ${ts.SyntaxKind[name.kind]}.`); + } } -} +}); diff --git a/packages/compiler-cli/src/ngtsc/resource_loader.ts b/packages/compiler-cli/src/ngtsc/resource_loader.ts index 0ca239dc15..c3a2c35ab9 100644 --- a/packages/compiler-cli/src/ngtsc/resource_loader.ts +++ b/packages/compiler-cli/src/ngtsc/resource_loader.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import * as fs from 'fs'; import * as ts from 'typescript'; import {CompilerHost} from '../transformers/api'; import {ResourceLoader} from './annotations/src/api'; @@ -99,7 +98,7 @@ export class HostResourceLoader implements ResourceLoader { } const result = this.host.readResource ? this.host.readResource(resolvedUrl) : - fs.readFileSync(resolvedUrl, 'utf8'); + this.host.readFile(resolvedUrl); if (typeof result !== 'string') { throw new Error(`HostResourceLoader: loader(${resolvedUrl}) returned a Promise`); } @@ -126,7 +125,7 @@ export class HostResourceLoader implements ResourceLoader { const candidateLocations = this.getCandidateLocations(url, fromFile); for (const candidate of candidateLocations) { - if (fs.existsSync(candidate)) { + if (this.host.fileExists(candidate)) { return candidate; } else if (CSS_PREPROCESSOR_EXT.test(candidate)) { /** @@ -135,7 +134,7 @@ export class HostResourceLoader implements ResourceLoader { * again. */ const cssFallbackUrl = candidate.replace(CSS_PREPROCESSOR_EXT, '.css'); - if (fs.existsSync(cssFallbackUrl)) { + if (this.host.fileExists(cssFallbackUrl)) { return cssFallbackUrl; } } diff --git a/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel index a322b229ab..4869bbf684 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel @@ -11,6 +11,8 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/reflection", diff --git a/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts index 66f942173b..1b7f592469 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts @@ -5,14 +5,14 @@ * 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 {ExternalExpr, ExternalReference} from '@angular/compiler'; import * as ts from 'typescript'; - +import {absoluteFrom} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; import {AliasGenerator, FileToModuleHost, Reference} from '../../imports'; import {DtsMetadataReader} from '../../metadata'; import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; -import {makeProgram} from '../../testing/in_memory_typescript'; +import {makeProgram} from '../../testing'; import {ExportScope} from '../src/api'; import {MetadataDtsModuleScopeResolver} from '../src/dependency'; @@ -49,7 +49,7 @@ function makeTestEnv( // Map the modules object to an array of files for `makeProgram`. const files = Object.keys(modules).map(moduleName => { return { - name: `node_modules/${moduleName}/index.d.ts`, + name: absoluteFrom(`/node_modules/${moduleName}/index.d.ts`), contents: PROLOG + (modules as any)[moduleName], }; }); @@ -79,10 +79,11 @@ function makeTestEnv( }; } -describe('MetadataDtsModuleScopeResolver', () => { - it('should produce an accurate scope for a basic NgModule', () => { - const {resolver, refs} = makeTestEnv({ - 'test': ` +runInEachFileSystem(() => { + describe('MetadataDtsModuleScopeResolver', () => { + it('should produce an accurate scope for a basic NgModule', () => { + const {resolver, refs} = makeTestEnv({ + 'test': ` export declare class Dir { static ngDirectiveDef: DirectiveMeta; @@ -92,15 +93,15 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } ` + }); + const {Dir, Module} = refs; + const scope = resolver.resolve(Module) !; + expect(scopeToRefs(scope)).toEqual([Dir]); }); - const {Dir, Module} = refs; - const scope = resolver.resolve(Module) !; - expect(scopeToRefs(scope)).toEqual([Dir]); - }); - it('should produce an accurate scope when a module is exported', () => { - const {resolver, refs} = makeTestEnv({ - 'test': ` + it('should produce an accurate scope when a module is exported', () => { + const {resolver, refs} = makeTestEnv({ + 'test': ` export declare class Dir { static ngDirectiveDef: DirectiveMeta; } @@ -113,15 +114,15 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } ` + }); + const {Dir, ModuleB} = refs; + const scope = resolver.resolve(ModuleB) !; + expect(scopeToRefs(scope)).toEqual([Dir]); }); - const {Dir, ModuleB} = refs; - const scope = resolver.resolve(ModuleB) !; - expect(scopeToRefs(scope)).toEqual([Dir]); - }); - it('should resolve correctly across modules', () => { - const {resolver, refs} = makeTestEnv({ - 'declaration': ` + it('should resolve correctly across modules', () => { + const {resolver, refs} = makeTestEnv({ + 'declaration': ` export declare class Dir { static ngDirectiveDef: DirectiveMeta; } @@ -130,26 +131,26 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - 'exported': ` + 'exported': ` import * as d from 'declaration'; export declare class ModuleB { static ngModuleDef: ModuleMeta; } ` + }); + const {Dir, ModuleB} = refs; + const scope = resolver.resolve(ModuleB) !; + expect(scopeToRefs(scope)).toEqual([Dir]); + + // Explicitly verify that the directive has the correct owning module. + expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration'); }); - const {Dir, ModuleB} = refs; - const scope = resolver.resolve(ModuleB) !; - expect(scopeToRefs(scope)).toEqual([Dir]); - // Explicitly verify that the directive has the correct owning module. - expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration'); - }); - - it('should write correct aliases for deep dependencies', () => { - const {resolver, refs} = makeTestEnv( - { - 'deep': ` + it('should write correct aliases for deep dependencies', () => { + const {resolver, refs} = makeTestEnv( + { + 'deep': ` export declare class DeepDir { static ngDirectiveDef: DirectiveMeta; } @@ -158,7 +159,7 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - 'middle': ` + 'middle': ` import * as deep from 'deep'; export declare class MiddleDir { @@ -169,7 +170,7 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - 'shallow': ` + 'shallow': ` import * as middle from 'middle'; export declare class ShallowDir { @@ -180,26 +181,26 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - }, - new AliasGenerator(testHost)); - const {ShallowModule} = refs; - const scope = resolver.resolve(ShallowModule) !; - const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); - expect(getAlias(DeepDir)).toEqual({ - moduleName: 'root/shallow', - name: 'ɵng$root$deep$$DeepDir', + }, + new AliasGenerator(testHost)); + const {ShallowModule} = refs; + const scope = resolver.resolve(ShallowModule) !; + const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); + expect(getAlias(DeepDir)).toEqual({ + moduleName: 'root/shallow', + name: 'ɵng$root$deep$$DeepDir', + }); + expect(getAlias(MiddleDir)).toEqual({ + moduleName: 'root/shallow', + name: 'ɵng$root$middle$$MiddleDir', + }); + expect(getAlias(ShallowDir)).toBeNull(); }); - expect(getAlias(MiddleDir)).toEqual({ - moduleName: 'root/shallow', - name: 'ɵng$root$middle$$MiddleDir', - }); - expect(getAlias(ShallowDir)).toBeNull(); - }); - it('should write correct aliases for bare directives in exports', () => { - const {resolver, refs} = makeTestEnv( - { - 'deep': ` + it('should write correct aliases for bare directives in exports', () => { + const {resolver, refs} = makeTestEnv( + { + 'deep': ` export declare class DeepDir { static ngDirectiveDef: DirectiveMeta; } @@ -208,7 +209,7 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - 'middle': ` + 'middle': ` import * as deep from 'deep'; export declare class MiddleDir { @@ -219,7 +220,7 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - 'shallow': ` + 'shallow': ` import * as middle from 'middle'; export declare class ShallowDir { @@ -230,27 +231,27 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - }, - new AliasGenerator(testHost)); - const {ShallowModule} = refs; - const scope = resolver.resolve(ShallowModule) !; - const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); - expect(getAlias(DeepDir)).toEqual({ - moduleName: 'root/shallow', - name: 'ɵng$root$deep$$DeepDir', + }, + new AliasGenerator(testHost)); + const {ShallowModule} = refs; + const scope = resolver.resolve(ShallowModule) !; + const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); + expect(getAlias(DeepDir)).toEqual({ + moduleName: 'root/shallow', + name: 'ɵng$root$deep$$DeepDir', + }); + expect(getAlias(MiddleDir)).toEqual({ + moduleName: 'root/shallow', + name: 'ɵng$root$middle$$MiddleDir', + }); + expect(getAlias(ShallowDir)).toBeNull(); }); - expect(getAlias(MiddleDir)).toEqual({ - moduleName: 'root/shallow', - name: 'ɵng$root$middle$$MiddleDir', - }); - expect(getAlias(ShallowDir)).toBeNull(); - }); - it('should not use an alias if a directive is declared in the same file as the re-exporting module', - () => { - const {resolver, refs} = makeTestEnv( - { - 'module': ` + it('should not use an alias if a directive is declared in the same file as the re-exporting module', + () => { + const {resolver, refs} = makeTestEnv( + { + 'module': ` export declare class DeepDir { static ngDirectiveDef: DirectiveMeta; } @@ -263,25 +264,26 @@ describe('MetadataDtsModuleScopeResolver', () => { static ngModuleDef: ModuleMeta; } `, - }, - new AliasGenerator(testHost)); - const {DeepExportModule} = refs; - const scope = resolver.resolve(DeepExportModule) !; - const [DeepDir] = scopeToRefs(scope); - expect(getAlias(DeepDir)).toBeNull(); - }); -}); + }, + new AliasGenerator(testHost)); + const {DeepExportModule} = refs; + const scope = resolver.resolve(DeepExportModule) !; + const [DeepDir] = scopeToRefs(scope); + expect(getAlias(DeepDir)).toBeNull(); + }); + }); -function scopeToRefs(scope: ExportScope): Reference[] { - const directives = scope.exported.directives.map(dir => dir.ref); - const pipes = scope.exported.pipes.map(pipe => pipe.ref); - return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !)); -} - -function getAlias(ref: Reference): ExternalReference|null { - if (ref.alias === null) { - return null; - } else { - return (ref.alias as ExternalExpr).value; + function scopeToRefs(scope: ExportScope): Reference[] { + const directives = scope.exported.directives.map(dir => dir.ref); + const pipes = scope.exported.pipes.map(pipe => pipe.ref); + return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !)); } -} + + function getAlias(ref: Reference): ExternalReference|null { + if (ref.alias === null) { + return null; + } else { + return (ref.alias as ExternalExpr).value; + } + } +}); diff --git a/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel b/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel index cdda7f0f75..8462a85af1 100644 --- a/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel @@ -9,8 +9,8 @@ ts_library( ]), deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/imports", - "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/util", "@npm//@types/node", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts index e1ed7126ad..945229f378 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts @@ -5,12 +5,10 @@ * 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 path from 'path'; import * as ts from 'typescript'; +import {AbsoluteFsPath, absoluteFrom, basename} from '../../file_system'; import {ImportRewriter} from '../../imports'; -import {AbsoluteFsPath} from '../../path/src/types'; import {isNonDeclarationTsPath} from '../../util/src/typescript'; import {ShimGenerator} from './host'; @@ -38,8 +36,7 @@ export class FactoryGenerator implements ShimGenerator { return null; } - const relativePathToSource = - './' + path.posix.basename(original.fileName).replace(TS_DTS_SUFFIX, ''); + const relativePathToSource = './' + basename(original.fileName).replace(TS_DTS_SUFFIX, ''); // Collect a list of classes that need to have factory types emitted for them. This list is // overly broad as at this point the ts.TypeChecker hasn't been created, and can't be used to // semantically understand which decorated types are actually decorated with Angular decorators. @@ -103,9 +100,8 @@ export class FactoryGenerator implements ShimGenerator { const map = new Map(); files.filter(sourceFile => isNonDeclarationTsPath(sourceFile)) .forEach( - sourceFile => map.set( - AbsoluteFsPath.fromUnchecked(sourceFile.replace(/\.ts$/, '.ngfactory.ts')), - sourceFile)); + sourceFile => + map.set(absoluteFrom(sourceFile.replace(/\.ts$/, '.ngfactory.ts')), sourceFile)); return new FactoryGenerator(map); } } diff --git a/packages/compiler-cli/src/ngtsc/shims/src/host.ts b/packages/compiler-cli/src/ngtsc/shims/src/host.ts index c5ee736033..cb5b9726c5 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/host.ts @@ -5,9 +5,8 @@ * 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 '../../path/src/types'; +import {AbsoluteFsPath, absoluteFrom, resolve} from '../../file_system'; export interface ShimGenerator { /** @@ -73,7 +72,7 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost { for (let i = 0; i < this.shimGenerators.length; i++) { const generator = this.shimGenerators[i]; // TypeScript internal paths are guaranteed to be POSIX-like absolute file paths. - const absoluteFsPath = AbsoluteFsPath.fromUnchecked(fileName); + const absoluteFsPath = resolve(fileName); if (generator.recognize(absoluteFsPath)) { const readFile = (originalFile: string) => { return this.delegate.getSourceFile( @@ -118,7 +117,7 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost { // Note that we can pass the file name as branded absolute fs path because TypeScript // internally only passes POSIX-like paths. return this.delegate.fileExists(fileName) || - this.shimGenerators.some(gen => gen.recognize(AbsoluteFsPath.fromUnchecked(fileName))); + this.shimGenerators.some(gen => gen.recognize(absoluteFrom(fileName))); } readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); } diff --git a/packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts index 9826dc7b4e..6085cb6045 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../path/src/types'; +import {AbsoluteFsPath, absoluteFrom} from '../../file_system'; import {isNonDeclarationTsPath} from '../../util/src/typescript'; import {ShimGenerator} from './host'; @@ -81,9 +81,8 @@ export class SummaryGenerator implements ShimGenerator { const map = new Map(); files.filter(sourceFile => isNonDeclarationTsPath(sourceFile)) .forEach( - sourceFile => map.set( - AbsoluteFsPath.fromUnchecked(sourceFile.replace(/\.ts$/, '.ngsummary.ts')), - sourceFile)); + sourceFile => + map.set(absoluteFrom(sourceFile.replace(/\.ts$/, '.ngsummary.ts')), sourceFile)); return new SummaryGenerator(map); } } diff --git a/packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts b/packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts index 5c0a8b0a0d..d444132266 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../path'; +import {AbsoluteFsPath} from '../../file_system'; import {ShimGenerator} from './host'; diff --git a/packages/compiler-cli/src/ngtsc/testing/BUILD.bazel b/packages/compiler-cli/src/ngtsc/testing/BUILD.bazel index 028ee75e41..e28599e6ae 100644 --- a/packages/compiler-cli/src/ngtsc/testing/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/testing/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/file_system", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts deleted file mode 100644 index 5ceffe4118..0000000000 --- a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts +++ /dev/null @@ -1,157 +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 * as path from 'path'; -import * as ts from 'typescript'; - -export function makeProgram( - files: {name: string, contents: string, isRoot?: boolean}[], options?: ts.CompilerOptions, - host: ts.CompilerHost = new InMemoryHost(), checkForErrors: boolean = true): - {program: ts.Program, host: ts.CompilerHost, options: ts.CompilerOptions} { - files.forEach(file => host.writeFile(file.name, file.contents, false, undefined, [])); - - const rootNames = - files.filter(file => file.isRoot !== false).map(file => host.getCanonicalFileName(file.name)); - const compilerOptions = { - noLib: true, - experimentalDecorators: true, - moduleResolution: ts.ModuleResolutionKind.NodeJs, ...options - }; - const program = ts.createProgram(rootNames, compilerOptions, host); - if (checkForErrors) { - const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()]; - if (diags.length > 0) { - const errors = diags.map(diagnostic => { - let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - if (diagnostic.file) { - const {line, character} = - diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start !); - message = `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`; - } - return `Error: ${message}`; - }); - throw new Error(`Typescript diagnostics failed! ${errors.join(', ')}`); - } - } - return {program, host, options: compilerOptions}; -} - -export class InMemoryHost implements ts.CompilerHost { - private fileSystem = new Map(); - - getSourceFile( - fileName: string, languageVersion: ts.ScriptTarget, - onError?: ((message: string) => void)|undefined, - shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined { - const contents = this.fileSystem.get(this.getCanonicalFileName(fileName)); - if (contents === undefined) { - onError && onError(`File does not exist: ${this.getCanonicalFileName(fileName)})`); - return undefined; - } - return ts.createSourceFile(fileName, contents, languageVersion); - } - - getDefaultLibFileName(options: ts.CompilerOptions): string { return '/lib.d.ts'; } - - writeFile( - fileName: string, data: string, writeByteOrderMark?: boolean, - onError?: ((message: string) => void)|undefined, - sourceFiles?: ReadonlyArray): void { - this.fileSystem.set(this.getCanonicalFileName(fileName), data); - } - - getCurrentDirectory(): string { return '/'; } - - getDirectories(dir: string): string[] { - const fullDir = this.getCanonicalFileName(dir) + '/'; - const dirSet = new Set(Array - // Look at all paths known to the host. - .from(this.fileSystem.keys()) - // Filter out those that aren't under the requested directory. - .filter(candidate => candidate.startsWith(fullDir)) - // Relativize the rest by the requested directory. - .map(candidate => candidate.substr(fullDir.length)) - // What's left are dir/.../file.txt entries, and file.txt entries. - // Get the dirname, which - // yields '.' for the latter and dir/... for the former. - .map(candidate => path.dirname(candidate)) - // Filter out the '.' entries, which were files. - .filter(candidate => candidate !== '.') - // Finally, split on / and grab the first entry. - .map(candidate => candidate.split('/', 1)[0])); - - // Get the resulting values out of the Set. - return Array.from(dirSet); - } - - getCanonicalFileName(fileName: string): string { - return path.posix.normalize(`${this.getCurrentDirectory()}/${fileName}`); - } - - useCaseSensitiveFileNames(): boolean { return true; } - - getNewLine(): string { return '\n'; } - - fileExists(fileName: string): boolean { return this.fileSystem.has(fileName); } - - readFile(fileName: string): string|undefined { return this.fileSystem.get(fileName); } -} - -function bindingNameEquals(node: ts.BindingName, name: string): boolean { - if (ts.isIdentifier(node)) { - return node.text === name; - } - return false; -} - -export function getDeclaration( - program: ts.Program, fileName: string, name: string, assert: (value: any) => value is T): T { - const sf = program.getSourceFile(fileName); - if (!sf) { - throw new Error(`No such file: ${fileName}`); - } - const chosenDecl = walkForDeclaration(sf); - - if (chosenDecl === null) { - throw new Error(`No such symbol: ${name} in ${fileName}`); - } - 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; - } -} diff --git a/packages/compiler-cli/src/ngtsc/path/index.ts b/packages/compiler-cli/src/ngtsc/testing/index.ts similarity index 61% rename from packages/compiler-cli/src/ngtsc/path/index.ts rename to packages/compiler-cli/src/ngtsc/testing/index.ts index ad4189411e..757e750cd7 100644 --- a/packages/compiler-cli/src/ngtsc/path/index.ts +++ b/packages/compiler-cli/src/ngtsc/testing/index.ts @@ -5,6 +5,4 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -export {LogicalFileSystem, LogicalProjectPath} from './src/logical'; -export {AbsoluteFsPath, PathSegment} from './src/types'; +export {getDeclaration, makeProgram} from './src/utils'; diff --git a/packages/compiler-cli/src/ngtsc/testing/src/utils.ts b/packages/compiler-cli/src/ngtsc/testing/src/utils.ts new file mode 100644 index 0000000000..27eb587c2d --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/testing/src/utils.ts @@ -0,0 +1,100 @@ +/** + * @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, NgtscCompilerHost, dirname, getFileSystem, getSourceFileOrError} from '../../file_system'; + +export function makeProgram( + files: {name: AbsoluteFsPath, contents: string, isRoot?: boolean}[], + options?: ts.CompilerOptions, host?: ts.CompilerHost, checkForErrors: boolean = true): + {program: ts.Program, host: ts.CompilerHost, options: ts.CompilerOptions} { + const fs = getFileSystem(); + files.forEach(file => { + fs.ensureDir(dirname(file.name)); + fs.writeFile(file.name, file.contents); + }); + + const compilerOptions = { + noLib: true, + experimentalDecorators: true, + moduleResolution: ts.ModuleResolutionKind.NodeJs, ...options + }; + const compilerHost = new NgtscCompilerHost(fs, compilerOptions); + const rootNames = files.filter(file => file.isRoot !== false) + .map(file => compilerHost.getCanonicalFileName(file.name)); + const program = ts.createProgram(rootNames, compilerOptions, compilerHost); + if (checkForErrors) { + const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()]; + if (diags.length > 0) { + const errors = diags.map(diagnostic => { + let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + if (diagnostic.file) { + const {line, character} = + diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start !); + message = `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`; + } + return `Error: ${message}`; + }); + throw new Error(`Typescript diagnostics failed! ${errors.join(', ')}`); + } + } + return {program, host: compilerHost, options: compilerOptions}; +} + +export function getDeclaration( + program: ts.Program, fileName: AbsoluteFsPath, name: string, + assert: (value: any) => value is T): T { + const sf = getSourceFileOrError(program, fileName); + const chosenDecl = walkForDeclaration(sf); + + if (chosenDecl === null) { + throw new Error(`No such symbol: ${name} in ${fileName}`); + } + 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; + } +} + +function bindingNameEquals(node: ts.BindingName, name: string): boolean { + if (ts.isIdentifier(node)) { + return node.text === name; + } + return false; +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel index 42d5ff643d..133b590583 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel @@ -8,9 +8,9 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/metadata", - "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 12f030c79f..c640d5ec4d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -9,8 +9,8 @@ import {BoundTarget} from '@angular/compiler'; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; -import {AbsoluteFsPath} from '../../path'; import {ClassDeclaration} from '../../reflection'; import {ImportManager} from '../../translator'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts index 98fef8e6e8..eadc68b5fd 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts @@ -5,13 +5,10 @@ * 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 path from 'path'; import * as ts from 'typescript'; +import {AbsoluteFsPath, join} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; -import {AbsoluteFsPath} from '../../path'; import {ClassDeclaration} from '../../reflection'; import {ImportManager} from '../../translator'; @@ -71,5 +68,5 @@ export class TypeCheckFile extends Environment { export function typeCheckFilePath(rootDirs: AbsoluteFsPath[]): AbsoluteFsPath { const shortest = rootDirs.concat([]).sort((a, b) => a.length - b.length)[0]; - return AbsoluteFsPath.fromUnchecked(path.posix.join(shortest, '__ng_typecheck__.ts')); + return join(shortest, '__ng_typecheck__.ts'); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel index bf5f2fa08b..a48813d7ad 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel @@ -11,11 +11,11 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/imports", - "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", - "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index c08b50c3e8..3b631bb92c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -5,25 +5,15 @@ * 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 {LogicalFileSystem, absoluteFrom, getSourceFileOrError} from '../../file_system'; +import {TestFile, runInEachFileSystem} from '../../file_system/testing'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports'; -import {AbsoluteFsPath, LogicalFileSystem} from '../../path'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getDeclaration, makeProgram} from '../../testing'; import {getRootDirs} from '../../util/src/typescript'; import {TypeCheckingConfig} from '../src/api'; import {TypeCheckContext} from '../src/context'; -import {TypeCheckProgramHost} from '../src/host'; - -const LIB_D_TS = { - name: 'lib.d.ts', - contents: ` - type Partial = { [P in keyof T]?: T[P]; }; - type Pick = { [P in K]: T[P]; }; - type NonNullable = T extends null | undefined ? never : T;` -}; const ALL_ENABLED_CONFIG: TypeCheckingConfig = { applyTemplateContextGuards: true, @@ -34,82 +24,100 @@ const ALL_ENABLED_CONFIG: TypeCheckingConfig = { strictSafeNavigationTypes: true, }; -describe('ngtsc typechecking', () => { - describe('ctors', () => { - it('compiles a basic type constructor', () => { - const files = [ - LIB_D_TS, { - name: 'main.ts', - contents: ` +runInEachFileSystem(() => { + describe('ngtsc typechecking', () => { + let _: typeof absoluteFrom; + let LIB_D_TS: TestFile; + + beforeEach(() => { + _ = absoluteFrom; + LIB_D_TS = { + name: _('/lib.d.ts'), + contents: ` + type Partial = { [P in keyof T]?: T[P]; }; + type Pick = { [P in K]: T[P]; }; + type NonNullable = T extends null | undefined ? never : T;` + }; + }); + + describe('ctors', () => { + it('compiles a basic type constructor', () => { + const files: TestFile[] = [ + LIB_D_TS, { + name: _('/main.ts'), + contents: ` class TestClass { value: T; } TestClass.ngTypeCtor({value: 'test'}); ` - } - ]; - const {program, host, options} = makeProgram(files, undefined, undefined, false); - const checker = program.getTypeChecker(); - const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); - const emitter = new ReferenceEmitter([ - new LocalIdentifierStrategy(), - new AbsoluteModuleStrategy( - program, checker, options, host, new TypeScriptReflectionHost(checker)), - new LogicalProjectStrategy(checker, logicalFs), - ]); - const ctx = new TypeCheckContext( - ALL_ENABLED_CONFIG, emitter, AbsoluteFsPath.fromUnchecked('/_typecheck_.ts')); - const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration); - ctx.addInlineTypeCtor(program.getSourceFile('main.ts') !, new Reference(TestClass), { - fnName: 'ngTypeCtor', - body: true, - fields: { - inputs: ['value'], - outputs: [], - queries: [], - }, + } + ]; + const {program, host, options} = makeProgram(files, undefined, undefined, false); + const checker = program.getTypeChecker(); + const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); + const emitter = new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new AbsoluteModuleStrategy( + program, checker, options, host, new TypeScriptReflectionHost(checker)), + new LogicalProjectStrategy(checker, logicalFs), + ]); + const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, _('/_typecheck_.ts')); + const TestClass = + getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); + ctx.addInlineTypeCtor( + getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), { + fnName: 'ngTypeCtor', + body: true, + fields: { + inputs: ['value'], + outputs: [], + queries: [], + }, + }); + ctx.calculateTemplateDiagnostics(program, host, options); }); - ctx.calculateTemplateDiagnostics(program, host, options); - }); - it('should not consider query fields', () => { - const files = [ - LIB_D_TS, { - name: 'main.ts', - contents: `class TestClass { value: any; }`, - } - ]; - const {program, host, options} = makeProgram(files, undefined, undefined, false); - const checker = program.getTypeChecker(); - const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); - const emitter = new ReferenceEmitter([ - new LocalIdentifierStrategy(), - new AbsoluteModuleStrategy( - program, checker, options, host, new TypeScriptReflectionHost(checker)), - new LogicalProjectStrategy(checker, logicalFs), - ]); - const ctx = new TypeCheckContext( - ALL_ENABLED_CONFIG, emitter, AbsoluteFsPath.fromUnchecked('/_typecheck_.ts')); - const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration); - ctx.addInlineTypeCtor(program.getSourceFile('main.ts') !, new Reference(TestClass), { - fnName: 'ngTypeCtor', - body: true, - fields: { - inputs: ['value'], - outputs: [], - queries: ['queryField'], - }, + it('should not consider query fields', () => { + const files: TestFile[] = [ + LIB_D_TS, { + name: _('/main.ts'), + contents: `class TestClass { value: any; }`, + } + ]; + const {program, host, options} = makeProgram(files, undefined, undefined, false); + const checker = program.getTypeChecker(); + const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); + const emitter = new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new AbsoluteModuleStrategy( + program, checker, options, host, new TypeScriptReflectionHost(checker)), + new LogicalProjectStrategy(checker, logicalFs), + ]); + const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, _('/_typecheck_.ts')); + const TestClass = + getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); + ctx.addInlineTypeCtor( + getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), { + fnName: 'ngTypeCtor', + body: true, + fields: { + inputs: ['value'], + outputs: [], + queries: ['queryField'], + }, + }); + const res = ctx.calculateTemplateDiagnostics(program, host, options); + const TestClassWithCtor = + getDeclaration(res.program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); + const typeCtor = TestClassWithCtor.members.find(isTypeCtor) !; + expect(typeCtor.getText()).not.toContain('queryField'); }); - const res = ctx.calculateTemplateDiagnostics(program, host, options); - const TestClassWithCtor = - getDeclaration(res.program, 'main.ts', 'TestClass', isNamedClassDeclaration); - const typeCtor = TestClassWithCtor.members.find(isTypeCtor) !; - expect(typeCtor.getText()).not.toContain('queryField'); }); }); -}); -function isTypeCtor(el: ts.ClassElement): el is ts.MethodDeclaration { - return ts.isMethodDeclaration(el) && ts.isIdentifier(el.name) && el.name.text === 'ngTypeCtor'; -} \ No newline at end of file + function isTypeCtor(el: ts.ClassElement): el is ts.MethodDeclaration { + return ts.isMethodDeclaration(el) && ts.isIdentifier(el.name) && el.name.text === 'ngTypeCtor'; + } +}); diff --git a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel index ecf6afaafb..3c2ed99875 100644 --- a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel @@ -9,7 +9,7 @@ ts_library( ]), deps = [ "//packages:types", - "//packages/compiler-cli/src/ngtsc/path", + "//packages/compiler-cli/src/ngtsc/file_system", "@npm//@types/node", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/util/src/path.ts b/packages/compiler-cli/src/ngtsc/util/src/path.ts index fc8300f017..d42b4e1415 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/path.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/path.ts @@ -5,26 +5,23 @@ * 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 path from 'path'; +import {dirname, relative, resolve} from '../../file_system'; const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/; export function relativePathBetween(from: string, to: string): string|null { - let relative = path.posix.relative(path.dirname(from), to).replace(TS_DTS_JS_EXTENSION, ''); + let relativePath = relative(dirname(resolve(from)), resolve(to)).replace(TS_DTS_JS_EXTENSION, ''); - if (relative === '') { + if (relativePath === '') { return null; } // path.relative() does not include the leading './'. - if (!relative.startsWith('.')) { - relative = `./${relative}`; + if (!relativePath.startsWith('.')) { + relativePath = `./${relativePath}`; } - return relative; + return relativePath; } export function normalizeSeparators(path: string): string { diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index dbd598f59e..58f89ac34b 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -10,7 +10,7 @@ const TS = /\.tsx?$/i; const D_TS = /\.d\.ts$/i; import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../path'; +import {AbsoluteFsPath, absoluteFrom} from '../../file_system'; export function isDtsPath(filePath: string): boolean { return D_TS.test(filePath); @@ -47,6 +47,12 @@ export function getSourceFile(node: ts.Node): ts.SourceFile { return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile(); } +export function getSourceFileOrNull(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile| + null { + return program.getSourceFile(fileName) || null; +} + + export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier|null { if (decl.name !== undefined && ts.isIdentifier(decl.name)) { return decl.name; @@ -83,7 +89,7 @@ export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions): // See: // https://github.com/Microsoft/TypeScript/blob/3f7357d37f66c842d70d835bc925ec2a873ecfec/src/compiler/sys.ts#L650 // Also compiler options might be set via an API which doesn't normalize paths - return rootDirs.map(rootDir => AbsoluteFsPath.from(rootDir)); + return rootDirs.map(rootDir => absoluteFrom(rootDir)); } export function nodeDebugInfo(node: ts.Node): string { diff --git a/packages/compiler-cli/src/ngtsc/util/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/util/test/BUILD.bazel index dde23724a8..a4ca97b5af 100644 --- a/packages/compiler-cli/src/ngtsc/util/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/util/test/BUILD.bazel @@ -10,6 +10,8 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts b/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts index 9fd8641508..7ecbc89aa2 100644 --- a/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts @@ -5,10 +5,10 @@ * 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 {makeProgram} from '../../testing/in_memory_typescript'; +import {absoluteFrom, getSourceFileOrError} from '../../file_system'; +import {runInEachFileSystem} from '../../file_system/testing'; +import {makeProgram} from '../../testing'; import {VisitListEntryResult, Visitor, visit} from '../src/visitor'; class TestAstVisitor extends Visitor { @@ -43,37 +43,41 @@ function testTransformerFactory(context: ts.TransformationContext): ts.Transform return (file: ts.SourceFile) => visit(file, new TestAstVisitor(), context); } -describe('AST Visitor', () => { - it('should add a statement before class in plain file', () => { - const {program, host} = - makeProgram([{name: 'main.ts', contents: `class A { static id = 3; }`}]); - const sf = program.getSourceFile('main.ts') !; - program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); - const main = host.readFile('/main.js'); - expect(main).toMatch(/^var A_id = 3;/); - }); +runInEachFileSystem(() => { + describe('AST Visitor', () => { + let _: typeof absoluteFrom; + beforeEach(() => _ = absoluteFrom); - it('should add a statement before class inside function definition', () => { - const {program, host} = makeProgram([{ - name: 'main.ts', - contents: ` + it('should add a statement before class in plain file', () => { + const {program, host} = + makeProgram([{name: _('/main.ts'), contents: `class A { static id = 3; }`}]); + const sf = getSourceFileOrError(program, _('/main.ts')); + program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); + const main = host.readFile('/main.js'); + expect(main).toMatch(/^var A_id = 3;/); + }); + + it('should add a statement before class inside function definition', () => { + const {program, host} = makeProgram([{ + name: _('/main.ts'), + contents: ` export function foo() { var x = 3; class A { static id = 2; } return A; } ` - }]); - const sf = program.getSourceFile('main.ts') !; - program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); - const main = host.readFile('/main.js'); - expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/); - }); + }]); + const sf = getSourceFileOrError(program, _('/main.ts')); + program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); + const main = host.readFile(_('/main.js')); + expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/); + }); - it('handles nested statements', () => { - const {program, host} = makeProgram([{ - name: 'main.ts', - contents: ` + it('handles nested statements', () => { + const {program, host} = makeProgram([{ + name: _('/main.ts'), + contents: ` export class A { static id = 3; @@ -84,11 +88,12 @@ describe('AST Visitor', () => { return B; } }` - }]); - const sf = program.getSourceFile('main.ts') !; - program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); - const main = host.readFile('/main.js'); - expect(main).toMatch(/var A_id = 3;\s+var A = /); - expect(main).toMatch(/var B_id = 4;\s+var B = /); + }]); + const sf = getSourceFileOrError(program, _('/main.ts')); + program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); + const main = host.readFile(_('/main.js')); + expect(main).toMatch(/var A_id = 3;\s+var A = /); + expect(main).toMatch(/var B_id = 4;\s+var B = /); + }); }); }); diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index 02a1733254..4d27b2b323 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -6,17 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Position, isSyntaxError, syntaxError} from '@angular/compiler'; -import * as fs from 'fs'; -import * as path from 'path'; +import {Position, isSyntaxError} from '@angular/compiler'; import * as ts from 'typescript'; - +import {AbsoluteFsPath, absoluteFrom, getFileSystem, relative, resolve} from '../src/ngtsc/file_system'; import * as api from './transformers/api'; import * as ng from './transformers/entry_points'; import {createMessageDiagnostic} from './transformers/util'; -const TS_EXT = /\.ts$/; - export type Diagnostics = ReadonlyArray; export function filterErrorsAndWarnings(diagnostics: Diagnostics): Diagnostics { @@ -30,7 +26,8 @@ const defaultFormatHost: ts.FormatDiagnosticsHost = { }; function displayFileName(fileName: string, host: ts.FormatDiagnosticsHost): string { - return path.relative(host.getCurrentDirectory(), host.getCanonicalFileName(fileName)); + return relative( + resolve(host.getCurrentDirectory()), resolve(host.getCanonicalFileName(fileName))); } export function formatDiagnosticPosition( @@ -110,11 +107,13 @@ export interface ParsedConfiguration { } export function calcProjectFileAndBasePath(project: string): - {projectFile: string, basePath: string} { - const projectIsDir = fs.lstatSync(project).isDirectory(); - const projectFile = projectIsDir ? path.join(project, 'tsconfig.json') : project; - const projectDir = projectIsDir ? project : path.dirname(project); - const basePath = path.resolve(process.cwd(), projectDir); + {projectFile: AbsoluteFsPath, basePath: AbsoluteFsPath} { + const fs = getFileSystem(); + const absProject = fs.resolve(project); + const projectIsDir = fs.lstat(absProject).isDirectory(); + const projectFile = projectIsDir ? fs.join(absProject, 'tsconfig.json') : absProject; + const projectDir = projectIsDir ? absProject : fs.dirname(absProject); + const basePath = fs.resolve(projectDir); return {projectFile, basePath}; } @@ -130,6 +129,7 @@ export function createNgCompilerOptions( export function readConfiguration( project: string, existingOptions?: ts.CompilerOptions): ParsedConfiguration { try { + const fs = getFileSystem(); const {projectFile, basePath} = calcProjectFileAndBasePath(project); const readExtendedConfigFile = @@ -149,11 +149,12 @@ export function readConfiguration( } if (config.extends) { - let extendedConfigPath = path.resolve(path.dirname(configFile), config.extends); - extendedConfigPath = path.extname(extendedConfigPath) ? extendedConfigPath : - `${extendedConfigPath}.json`; + let extendedConfigPath = fs.resolve(fs.dirname(configFile), config.extends); + extendedConfigPath = fs.extname(extendedConfigPath) ? + extendedConfigPath : + absoluteFrom(`${extendedConfigPath}.json`); - if (fs.existsSync(extendedConfigPath)) { + if (fs.exists(extendedConfigPath)) { // Call read config recursively as TypeScript only merges CompilerOptions return readExtendedConfigFile(extendedConfigPath, baseConfig); } @@ -175,14 +176,14 @@ export function readConfiguration( } const parseConfigHost = { useCaseSensitiveFileNames: true, - fileExists: fs.existsSync, + fileExists: fs.exists.bind(fs), readDirectory: ts.sys.readDirectory, readFile: ts.sys.readFile }; - const configFileName = path.resolve(process.cwd(), projectFile); + const configFileName = fs.resolve(fs.pwd(), projectFile); const parsed = ts.parseJsonConfigFileContent( config, parseConfigHost, basePath, existingOptions, configFileName); - const rootNames = parsed.fileNames.map(f => path.normalize(f)); + const rootNames = parsed.fileNames; const options = createNgCompilerOptions(basePath, config, parsed.options); let emitFlags = api.EmitFlags.Default; diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index a4f16c3548..517b974ec7 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompilerHost, EmitterVisitorContext, ExternalReference, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler'; +import {AotCompilerHost, EmitterVisitorContext, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler'; import * as path from 'path'; import * as ts from 'typescript'; import {TypeCheckHost} from '../diagnostics/translate_diagnostics'; -import {METADATA_VERSION, ModuleMetadata} from '../metadata/index'; +import {ModuleMetadata} from '../metadata/index'; +import {join} from '../ngtsc/file_system'; import {CompilerHost, CompilerOptions, LibrarySummary} from './api'; import {MetadataReaderHost, createMetadataReaderCache, readMetadata} from './metadata_reader'; @@ -253,7 +254,7 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHos const modulePath = importedFile.substring(0, importedFile.length - moduleName.length) + importedFilePackageName; const packageJson = require(modulePath + '/package.json'); - const packageTypings = path.posix.join(modulePath, packageJson.typings); + const packageTypings = join(modulePath, packageJson.typings); if (packageTypings === originalImportedFile) { moduleName = importedFilePackageName; } diff --git a/packages/compiler-cli/test/BUILD.bazel b/packages/compiler-cli/test/BUILD.bazel index 63ce1a1591..0861b23d07 100644 --- a/packages/compiler-cli/test/BUILD.bazel +++ b/packages/compiler-cli/test/BUILD.bazel @@ -7,18 +7,18 @@ ts_library( testonly = True, srcs = [ "mocks.ts", - "runfile_helpers.ts", "test_support.ts", ], visibility = [ ":__subpackages__", - "//packages/compiler-cli/ngcc/test:__subpackages__", "//packages/language-service/test:__subpackages__", ], deps = [ "//packages:types", "//packages/compiler", "//packages/compiler-cli", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/test/helpers", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/test/compliance/BUILD.bazel b/packages/compiler-cli/test/compliance/BUILD.bazel index 59d00a10ed..ad3a15b865 100644 --- a/packages/compiler-cli/test/compliance/BUILD.bazel +++ b/packages/compiler-cli/test/compliance/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( "//packages:types", "//packages/compiler", "//packages/compiler-cli", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler/test:test_utils", "@npm//typescript", ], diff --git a/packages/compiler-cli/test/compliance/mock_compile.ts b/packages/compiler-cli/test/compliance/mock_compile.ts index 332f1d9e59..47d2c1f171 100644 --- a/packages/compiler-cli/test/compliance/mock_compile.ts +++ b/packages/compiler-cli/test/compliance/mock_compile.ts @@ -5,15 +5,13 @@ * 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 {AotCompilerOptions} from '@angular/compiler'; import {escapeRegExp} from '@angular/compiler/src/util'; -import {MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, toMockFileArray} from '@angular/compiler/test/aot/test_util'; +import {MockCompilerHost, MockData, MockDirectory, arrayToMockDir, toMockFileArray} from '@angular/compiler/test/aot/test_util'; import * as ts from 'typescript'; - +import {NodeJSFileSystem, setFileSystem} from '../../src/ngtsc/file_system'; import {NgtscProgram} from '../../src/ngtsc/program'; - const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; const OPERATOR = /!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./; @@ -169,6 +167,7 @@ export function compile( errorCollector: (error: any, fileName?: string) => void = error => { throw error;}): { source: string, } { + setFileSystem(new NodeJSFileSystem()); const testFiles = toMockFileArray(data); const scripts = testFiles.map(entry => entry.fileName); const angularFilesArray = toMockFileArray(angularFiles); diff --git a/packages/compiler-cli/test/diagnostics/check_types_spec.ts b/packages/compiler-cli/test/diagnostics/check_types_spec.ts index 57778f5880..e03edfffcb 100644 --- a/packages/compiler-cli/test/diagnostics/check_types_spec.ts +++ b/packages/compiler-cli/test/diagnostics/check_types_spec.ts @@ -5,13 +5,11 @@ * 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 ng from '@angular/compiler-cli'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import * as ts from 'typescript'; - import {TestSupport, expectNoDiagnostics, setup} from '../test_support'; type MockFiles = { diff --git a/packages/compiler-cli/test/diagnostics/mocks.ts b/packages/compiler-cli/test/diagnostics/mocks.ts index 6542b4bd5d..ab3ab7ac19 100644 --- a/packages/compiler-cli/test/diagnostics/mocks.ts +++ b/packages/compiler-cli/test/diagnostics/mocks.ts @@ -114,8 +114,6 @@ export class DiagnosticContext { _reflector: StaticReflector|undefined; _errors: {e: any, path?: string}[] = []; _resolver: CompileMetadataResolver|undefined; - // TODO(issue/24571): remove '!'. - _refletor !: StaticReflector; // tslint:enable constructor( diff --git a/packages/compiler-cli/test/extract_i18n_spec.ts b/packages/compiler-cli/test/extract_i18n_spec.ts index 39d70991a5..c0f539de71 100644 --- a/packages/compiler-cli/test/extract_i18n_spec.ts +++ b/packages/compiler-cli/test/extract_i18n_spec.ts @@ -10,7 +10,7 @@ import * as fs from 'fs'; import * as path from 'path'; import {mainXi18n} from '../src/extract_i18n'; -import {makeTempDir, setup} from './test_support'; +import {setup} from './test_support'; const EXPECTED_XMB = ` +import {readFileSync, readdirSync, statSync} from 'fs'; +import {resolve} from 'path'; + +import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '..'; +import {AbsoluteFsPath, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system'; +import {Folder, MockFileSystemPosix, TestFile} from '../../../src/ngtsc/file_system/testing'; + +export function loadTestFiles(files: TestFile[]) { + const fs = getFileSystem(); + files.forEach(file => { + fs.ensureDir(fs.dirname(file.name)); + fs.writeFile(file.name, file.contents); + }); +} + +export function loadStandardTestFiles( + {fakeCore = true, rxjs = false}: {fakeCore?: boolean, rxjs?: boolean} = {}): Folder { + const tmpFs = new MockFileSystemPosix(true); + const basePath = '/' as AbsoluteFsPath; + + loadTestDirectory( + tmpFs, resolveNpmTreeArtifact('typescript'), + tmpFs.resolve(basePath, 'node_modules/typescript')); + + loadTestDirectory( + tmpFs, resolveNpmTreeArtifact('tslib'), tmpFs.resolve(basePath, 'node_modules/tslib')); + + + if (fakeCore) { + loadFakeCore(tmpFs, basePath); + } else { + getAngularPackagesFromRunfiles().forEach(({name, pkgPath}) => { + loadTestDirectory(tmpFs, pkgPath, tmpFs.resolve('/node_modules/@angular', name)); + }); + } + + if (rxjs) { + loadTestDirectory( + tmpFs, resolveNpmTreeArtifact('rxjs'), tmpFs.resolve(basePath, 'node_modules/rxjs')); + } + + return tmpFs.dump(); +} + +export function loadFakeCore(fs: FileSystem, basePath: string = '/') { + loadTestDirectory( + fs, resolveNpmTreeArtifact('angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package'), + fs.resolve(basePath, 'node_modules/@angular/core')); +} + +/** + * Load real files from the real file-system into a mock file-system. + * @param fs the file-system where the directory is to be loaded. + * @param directoryPath the path to the directory we want to load. + * @param mockPath the path within the mock file-system where the directory is to be loaded. + */ +function loadTestDirectory(fs: FileSystem, directoryPath: string, mockPath: AbsoluteFsPath): void { + readdirSync(directoryPath).forEach(item => { + const srcPath = resolve(directoryPath, item); + const targetPath = fs.resolve(mockPath, item); + try { + if (statSync(srcPath).isDirectory()) { + fs.ensureDir(targetPath); + loadTestDirectory(fs, srcPath, targetPath); + } else { + fs.ensureDir(fs.dirname(targetPath)); + fs.writeFile(targetPath, readFileSync(srcPath, 'utf-8')); + } + } catch (e) { + console.warn(`Failed to add ${srcPath} to the mock file-system: ${e.message}`); + } + }); +} diff --git a/packages/compiler-cli/test/runfile_helpers.ts b/packages/compiler-cli/test/helpers/src/runfile_helpers.ts similarity index 98% rename from packages/compiler-cli/test/runfile_helpers.ts rename to packages/compiler-cli/test/helpers/src/runfile_helpers.ts index 7842adb289..bb848881dd 100644 --- a/packages/compiler-cli/test/runfile_helpers.ts +++ b/packages/compiler-cli/test/helpers/src/runfile_helpers.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +/// import * as fs from 'fs'; import * as path from 'path'; diff --git a/packages/compiler-cli/test/metadata/evaluator_spec.ts b/packages/compiler-cli/test/metadata/evaluator_spec.ts index f44bc8795a..1a85c6f6f8 100644 --- a/packages/compiler-cli/test/metadata/evaluator_spec.ts +++ b/packages/compiler-cli/test/metadata/evaluator_spec.ts @@ -5,8 +5,6 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import * as fs from 'fs'; import * as ts from 'typescript'; import {Evaluator} from '../../src/metadata/evaluator'; diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 665bd37c98..8a256af8f9 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -2028,7 +2028,7 @@ describe('ngc transformer command-line', () => { const exitCode = main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)); expect(exitCode).toBe(1, 'Compile was expected to fail'); - const srcPathWithSep = `lib${path.sep}`; + const srcPathWithSep = `lib/`; expect(messages[0]) .toEqual( `${srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent' diff --git a/packages/compiler-cli/test/ngtsc/BUILD.bazel b/packages/compiler-cli/test/ngtsc/BUILD.bazel index 4041497eee..07dd83a2c3 100644 --- a/packages/compiler-cli/test/ngtsc/BUILD.bazel +++ b/packages/compiler-cli/test/ngtsc/BUILD.bazel @@ -8,11 +8,13 @@ ts_library( "//packages/compiler", "//packages/compiler-cli", "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/indexer", - "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/util", "//packages/compiler-cli/test:test_utils", + "//packages/compiler-cli/test/helpers", "@npm//@types/source-map", "@npm//source-map", "@npm//typescript", @@ -21,6 +23,7 @@ ts_library( jasmine_node_test( name = "ngtsc", + timeout = "long", bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"], data = [ "//packages/compiler-cli/test/ngtsc/fake_core:npm_package", diff --git a/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts index b7b2ad3155..554533177f 100644 --- a/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts +++ b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts @@ -5,25 +5,29 @@ * 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 {AbsoluteFsPath, resolve} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {AbsoluteSourceSpan, IdentifierKind} from '@angular/compiler-cli/src/ngtsc/indexer'; import {ParseSourceFile} from '@angular/compiler/src/compiler'; -import * as path from 'path'; + import {NgtscTestEnvironment} from './env'; -describe('ngtsc component indexing', () => { - let env !: NgtscTestEnvironment; +runInEachFileSystem(() => { + describe('ngtsc component indexing', () => { + let env !: NgtscTestEnvironment; + let testSourceFile: AbsoluteFsPath; + let testTemplateFile: AbsoluteFsPath; - function testPath(testFile: string): string { return path.posix.join(env.basePath, testFile); } + beforeEach(() => { + env = NgtscTestEnvironment.setup(); + env.tsconfig(); + testSourceFile = resolve(env.basePath, 'test.ts'); + testTemplateFile = resolve(env.basePath, 'test.html'); + }); - beforeEach(() => { - env = NgtscTestEnvironment.setup(); - env.tsconfig(); - }); - - describe('indexing metadata', () => { - it('should generate component metadata', () => { - const componentContent = ` + describe('indexing metadata', () => { + it('should generate component metadata', () => { + const componentContent = ` import {Component} from '@angular/core'; @Component({ @@ -32,22 +36,22 @@ describe('ngtsc component indexing', () => { }) export class TestCmp {} `; - env.write('test.ts', componentContent); - const indexed = env.driveIndexer(); - expect(indexed.size).toBe(1); + env.write(testSourceFile, componentContent); + const indexed = env.driveIndexer(); + expect(indexed.size).toBe(1); - const [[decl, indexedComp]] = Array.from(indexed.entries()); + const [[decl, indexedComp]] = Array.from(indexed.entries()); - expect(decl.getText()).toContain('export class TestCmp {}'); - expect(indexedComp).toEqual(jasmine.objectContaining({ - name: 'TestCmp', - selector: 'test-cmp', - file: new ParseSourceFile(componentContent, testPath('test.ts')), - })); - }); + expect(decl.getText()).toContain('export class TestCmp {}'); + expect(indexedComp).toEqual(jasmine.objectContaining({ + name: 'TestCmp', + selector: 'test-cmp', + file: new ParseSourceFile(componentContent, testSourceFile), + })); + }); - it('should index inline templates', () => { - const componentContent = ` + it('should index inline templates', () => { + const componentContent = ` import {Component} from '@angular/core'; @Component({ @@ -56,25 +60,25 @@ describe('ngtsc component indexing', () => { }) export class TestCmp { foo = 0; } `; - env.write('test.ts', componentContent); - const indexed = env.driveIndexer(); - const [[_, indexedComp]] = Array.from(indexed.entries()); - const template = indexedComp.template; + env.write(testSourceFile, componentContent); + const indexed = env.driveIndexer(); + const [[_, indexedComp]] = Array.from(indexed.entries()); + const template = indexedComp.template; - expect(template).toEqual({ - identifiers: new Set([{ - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(127, 130), - }]), - usedComponents: new Set(), - isInline: true, - file: new ParseSourceFile(componentContent, testPath('test.ts')), + expect(template).toEqual({ + identifiers: new Set([{ + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(127, 130), + }]), + usedComponents: new Set(), + isInline: true, + file: new ParseSourceFile(componentContent, testSourceFile), + }); }); - }); - it('should index external templates', () => { - env.write('test.ts', ` + it('should index external templates', () => { + env.write(testSourceFile, ` import {Component} from '@angular/core'; @Component({ @@ -83,29 +87,29 @@ describe('ngtsc component indexing', () => { }) export class TestCmp { foo = 0; } `); - env.write('test.html', '{{foo}}'); - const indexed = env.driveIndexer(); - const [[_, indexedComp]] = Array.from(indexed.entries()); - const template = indexedComp.template; + env.write(testTemplateFile, '{{foo}}'); + const indexed = env.driveIndexer(); + const [[_, indexedComp]] = Array.from(indexed.entries()); + const template = indexedComp.template; - expect(template).toEqual({ - identifiers: new Set([{ - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(2, 5), - }]), - usedComponents: new Set(), - isInline: false, - file: new ParseSourceFile('{{foo}}', testPath('test.html')), - }); - }); - - it('should index templates compiled without preserving whitespace', () => { - env.tsconfig({ - preserveWhitespaces: false, + expect(template).toEqual({ + identifiers: new Set([{ + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(2, 5), + }]), + usedComponents: new Set(), + isInline: false, + file: new ParseSourceFile('{{foo}}', testTemplateFile), + }); }); - env.write('test.ts', ` + it('should index templates compiled without preserving whitespace', () => { + env.tsconfig({ + preserveWhitespaces: false, + }); + + env.write(testSourceFile, ` import {Component} from '@angular/core'; @Component({ @@ -114,25 +118,25 @@ describe('ngtsc component indexing', () => { }) export class TestCmp { foo = 0; } `); - env.write('test.html', '
\n {{foo}}
'); - const indexed = env.driveIndexer(); - const [[_, indexedComp]] = Array.from(indexed.entries()); - const template = indexedComp.template; + env.write(testTemplateFile, '
\n {{foo}}
'); + const indexed = env.driveIndexer(); + const [[_, indexedComp]] = Array.from(indexed.entries()); + const template = indexedComp.template; - expect(template).toEqual({ - identifiers: new Set([{ - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(12, 15), - }]), - usedComponents: new Set(), - isInline: false, - file: new ParseSourceFile('
\n {{foo}}
', testPath('test.html')), + expect(template).toEqual({ + identifiers: new Set([{ + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(12, 15), + }]), + usedComponents: new Set(), + isInline: false, + file: new ParseSourceFile('
\n {{foo}}
', testTemplateFile), + }); }); - }); - it('should generated information about used components', () => { - env.write('test.ts', ` + it('should generate information about used components', () => { + env.write(testSourceFile, ` import {Component} from '@angular/core'; @Component({ @@ -141,8 +145,8 @@ describe('ngtsc component indexing', () => { }) export class TestCmp {} `); - env.write('test.html', '
'); - env.write('test_import.ts', ` + env.write(testTemplateFile, '
'); + env.write('test_import.ts', ` import {Component, NgModule} from '@angular/core'; import {TestCmp} from './test'; @@ -160,21 +164,22 @@ describe('ngtsc component indexing', () => { }) export class TestModule {} `); - env.write('test_import.html', ''); - const indexed = env.driveIndexer(); - expect(indexed.size).toBe(2); + env.write('test_import.html', ''); + const indexed = env.driveIndexer(); + expect(indexed.size).toBe(2); - const indexedComps = Array.from(indexed.values()); - const testComp = indexedComps.find(comp => comp.name === 'TestCmp'); - const testImportComp = indexedComps.find(cmp => cmp.name === 'TestImportCmp'); - expect(testComp).toBeDefined(); - expect(testImportComp).toBeDefined(); + const indexedComps = Array.from(indexed.values()); + const testComp = indexedComps.find(comp => comp.name === 'TestCmp'); + const testImportComp = indexedComps.find(cmp => cmp.name === 'TestImportCmp'); + expect(testComp).toBeDefined(); + expect(testImportComp).toBeDefined(); - expect(testComp !.template.usedComponents.size).toBe(0); - expect(testImportComp !.template.usedComponents.size).toBe(1); + expect(testComp !.template.usedComponents.size).toBe(0); + expect(testImportComp !.template.usedComponents.size).toBe(1); - const [usedComp] = Array.from(testImportComp !.template.usedComponents); - expect(indexed.get(usedComp)).toEqual(testComp); + const [usedComp] = Array.from(testImportComp !.template.usedComponents); + expect(indexed.get(usedComp)).toEqual(testComp); + }); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index eff7ac9fdc..efb1fe6793 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -7,32 +7,17 @@ */ import {CustomTransformers, Program} from '@angular/compiler-cli'; -import {IndexedComponent} from '@angular/compiler-cli/src/ngtsc/indexer'; -import {NgtscProgram} from '@angular/compiler-cli/src/ngtsc/program'; -import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host'; -import * as fs from 'fs'; -import * as path from 'path'; import * as ts from 'typescript'; import {createCompilerHost, createProgram} from '../../ngtools2'; import {main, mainDiagnosticsForTest, readNgcCommandLineAndConfiguration} from '../../src/main'; +import {AbsoluteFsPath, FileSystem, NgtscCompilerHost, absoluteFrom, getFileSystem} from '../../src/ngtsc/file_system'; +import {Folder, MockFileSystem} from '../../src/ngtsc/file_system/testing'; +import {IndexedComponent} from '../../src/ngtsc/indexer'; +import {NgtscProgram} from '../../src/ngtsc/program'; import {LazyRoute} from '../../src/ngtsc/routing'; -import {resolveNpmTreeArtifact} from '../runfile_helpers'; -import {TestSupport, setup} from '../test_support'; +import {setWrapHostForTest} from '../../src/transformers/compiler_host'; -function setupFakeCore(support: TestSupport): void { - if (!process.env.TEST_SRCDIR) { - throw new Error('`setupFakeCore` must be run within a Bazel test'); - } - - const fakeNpmPackageDir = - resolveNpmTreeArtifact('angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package'); - - const nodeModulesPath = path.join(support.basePath, 'node_modules'); - const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core'); - - fs.symlinkSync(fakeNpmPackageDir, angularCoreDirectory, 'junction'); -} /** * Manages a temporary testing directory structure and environment for testing ngtsc by feeding it @@ -43,24 +28,24 @@ export class NgtscTestEnvironment { private oldProgram: Program|null = null; private changedResources: Set|undefined = undefined; - private constructor(private support: TestSupport, readonly outDir: string) {} - - get basePath(): string { return this.support.basePath; } + private constructor( + private fs: FileSystem, readonly outDir: AbsoluteFsPath, readonly basePath: AbsoluteFsPath) {} /** * Set up a new testing environment. */ - static setup(): NgtscTestEnvironment { - const support = setup(); - const outDir = path.posix.join(support.basePath, 'built'); - process.chdir(support.basePath); + static setup(files?: Folder): NgtscTestEnvironment { + const fs = getFileSystem(); + if (files !== undefined && fs instanceof MockFileSystem) { + fs.init(files); + } - setupFakeCore(support); - setWrapHostForTest(null); + const host = new AugmentedCompilerHost(fs); + setWrapHostForTest(makeWrapHost(host)); - const env = new NgtscTestEnvironment(support, outDir); + const env = new NgtscTestEnvironment(fs, fs.resolve('/built'), absoluteFrom('/')); - env.write('tsconfig-base.json', `{ + env.write(absoluteFrom('/tsconfig-base.json'), `{ "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true, @@ -91,26 +76,26 @@ export class NgtscTestEnvironment { } assertExists(fileName: string) { - if (!fs.existsSync(path.resolve(this.outDir, fileName))) { + if (!this.fs.exists(this.fs.resolve(this.outDir, fileName))) { throw new Error(`Expected ${fileName} to be emitted (outDir: ${this.outDir})`); } } assertDoesNotExist(fileName: string) { - if (fs.existsSync(path.resolve(this.outDir, fileName))) { + if (this.fs.exists(this.fs.resolve(this.outDir, fileName))) { throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${this.outDir})`); } } getContents(fileName: string): string { this.assertExists(fileName); - const modulePath = path.resolve(this.outDir, fileName); - return fs.readFileSync(modulePath, 'utf8'); + const modulePath = this.fs.resolve(this.outDir, fileName); + return this.fs.readFile(modulePath); } enableMultipleCompilations(): void { this.changedResources = new Set(); - this.multiCompileHostExt = new MultiCompileHostExt(); + this.multiCompileHostExt = new MultiCompileHostExt(this.fs); setWrapHostForTest(makeWrapHost(this.multiCompileHostExt)); } @@ -126,31 +111,31 @@ export class NgtscTestEnvironment { if (this.multiCompileHostExt === null) { throw new Error(`Not tracking written files - call enableMultipleCompilations()`); } - const outDir = path.posix.join(this.support.basePath, 'built'); const writtenFiles = new Set(); this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => { - if (rawFile.startsWith(outDir)) { - writtenFiles.add(rawFile.substr(outDir.length)); + if (rawFile.startsWith(this.outDir)) { + writtenFiles.add(rawFile.substr(this.outDir.length)); } }); return writtenFiles; } write(fileName: string, content: string) { + const absFilePath = this.fs.resolve(this.basePath, fileName); if (this.multiCompileHostExt !== null) { - const absFilePath = path.resolve(this.support.basePath, fileName).replace(/\\/g, '/'); this.multiCompileHostExt.invalidate(absFilePath); this.changedResources !.add(absFilePath); } - this.support.write(fileName, content); + this.fs.ensureDir(this.fs.dirname(absFilePath)); + this.fs.writeFile(absFilePath, content); } invalidateCachedFile(fileName: string): void { + const absFilePath = this.fs.resolve(this.basePath, fileName); if (this.multiCompileHostExt === null) { throw new Error(`Not caching files - call enableMultipleCompilations()`); } - const fullFile = path.posix.join(this.support.basePath, fileName); - this.multiCompileHostExt.invalidate(fullFile); + this.multiCompileHostExt.invalidate(absFilePath); } tsconfig(extraOpts: {[key: string]: string | boolean} = {}, extraRootDirs?: string[]): void { @@ -166,7 +151,7 @@ export class NgtscTestEnvironment { this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2)); if (extraOpts['_useHostForImportGeneration'] === true) { - setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost())); + setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost(this.fs))); } } @@ -214,18 +199,15 @@ export class NgtscTestEnvironment { } } -class AugmentedCompilerHost { +class AugmentedCompilerHost extends NgtscCompilerHost { delegate !: ts.CompilerHost; } class FileNameToModuleNameHost extends AugmentedCompilerHost { - // CWD must be initialized lazily as `this.delegate` is not set until later. - private cwd: string|null = null; fileNameToModuleName(importedFilePath: string): string { - if (this.cwd === null) { - this.cwd = this.delegate.getCurrentDirectory(); - } - return 'root' + importedFilePath.substr(this.cwd.length).replace(/(\.d)?.ts$/, ''); + const relativeFilePath = this.fs.relative(this.fs.pwd(), this.fs.resolve(importedFilePath)); + const rootedPath = this.fs.join('root', relativeFilePath); + return rootedPath.replace(/(\.d)?.ts$/, ''); } } @@ -239,8 +221,7 @@ class MultiCompileHostExt extends AugmentedCompilerHost implements Partial void)|undefined, sourceFiles?: ReadonlyArray): void { - this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + super.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); this.writtenFiles.add(fileName); } diff --git a/packages/compiler-cli/test/ngtsc/incremental_spec.ts b/packages/compiler-cli/test/ngtsc/incremental_spec.ts index 96574a8d33..9cf9198feb 100644 --- a/packages/compiler-cli/test/ngtsc/incremental_spec.ts +++ b/packages/compiler-cli/test/ngtsc/incremental_spec.ts @@ -6,26 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; +import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; +import {loadStandardTestFiles} from '../helpers/src/mock_file_loading'; import {NgtscTestEnvironment} from './env'; -describe('ngtsc incremental compilation', () => { - let env !: NgtscTestEnvironment; - beforeEach(() => { - env = NgtscTestEnvironment.setup(); - env.enableMultipleCompilations(); - env.tsconfig(); - }); +const testFiles = loadStandardTestFiles(); - it('should skip unchanged services', () => { - env.write('service.ts', ` +runInEachFileSystem(() => { + describe('ngtsc incremental compilation', () => { + let env !: NgtscTestEnvironment; + + beforeEach(() => { + env = NgtscTestEnvironment.setup(testFiles); + env.enableMultipleCompilations(); + env.tsconfig(); + }); + + it('should skip unchanged services', () => { + env.write('service.ts', ` import {Injectable} from '@angular/core'; @Injectable() export class Service {} `); - env.write('test.ts', ` + env.write('test.ts', ` import {Component} from '@angular/core'; import {Service} from './service'; @@ -34,186 +39,186 @@ describe('ngtsc incremental compilation', () => { constructor(service: Service) {} } `); - env.driveMain(); - env.flushWrittenFileTracking(); + env.driveMain(); + env.flushWrittenFileTracking(); - // Pretend a change was made to test.ts. - env.invalidateCachedFile('test.ts'); - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); + // Pretend a change was made to test.ts. + env.invalidateCachedFile('test.ts'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); - // The changed file should be recompiled, but not the service. - expect(written).toContain('/test.js'); - expect(written).not.toContain('/service.js'); - }); + // The changed file should be recompiled, but not the service. + expect(written).toContain('/test.js'); + expect(written).not.toContain('/service.js'); + }); - it('should rebuild components that have changed', () => { - env.write('component1.ts', ` + it('should rebuild components that have changed', () => { + env.write('component1.ts', ` import {Component} from '@angular/core'; @Component({selector: 'cmp', template: 'cmp'}) export class Cmp1 {} `); - env.write('component2.ts', ` + env.write('component2.ts', ` import {Component} from '@angular/core'; @Component({selector: 'cmp2', template: 'cmp'}) export class Cmp2 {} `); - env.driveMain(); + env.driveMain(); - // Pretend a change was made to Cmp1 - env.flushWrittenFileTracking(); - env.invalidateCachedFile('component1.ts'); - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); - expect(written).toContain('/component1.js'); - expect(written).not.toContain('/component2.js'); - }); + // Pretend a change was made to Cmp1 + env.flushWrittenFileTracking(); + env.invalidateCachedFile('component1.ts'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + expect(written).toContain('/component1.js'); + expect(written).not.toContain('/component2.js'); + }); - it('should rebuild components whose templates have changed', () => { - env.write('component1.ts', ` + it('should rebuild components whose templates have changed', () => { + env.write('component1.ts', ` import {Component} from '@angular/core'; @Component({selector: 'cmp', templateUrl: './component1.template.html'}) export class Cmp1 {} `); - env.write('component1.template.html', 'cmp1'); - env.write('component2.ts', ` + env.write('component1.template.html', 'cmp1'); + env.write('component2.ts', ` import {Component} from '@angular/core'; @Component({selector: 'cmp2', templateUrl: './component2.template.html'}) export class Cmp2 {} `); - env.write('component2.template.html', 'cmp2'); + env.write('component2.template.html', 'cmp2'); - env.driveMain(); + env.driveMain(); - // Make a change to Cmp1 template - env.flushWrittenFileTracking(); - env.write('component1.template.html', 'changed'); - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); - expect(written).toContain('/component1.js'); - expect(written).not.toContain('/component2.js'); - }); + // Make a change to Cmp1 template + env.flushWrittenFileTracking(); + env.write('component1.template.html', 'changed'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + expect(written).toContain('/component1.js'); + expect(written).not.toContain('/component2.js'); + }); - it('should rebuild components whose partial-evaluation dependencies have changed', () => { - env.write('component1.ts', ` + it('should rebuild components whose partial-evaluation dependencies have changed', () => { + env.write('component1.ts', ` import {Component} from '@angular/core'; @Component({selector: 'cmp', template: 'cmp'}) export class Cmp1 {} `); - env.write('component2.ts', ` + env.write('component2.ts', ` import {Component} from '@angular/core'; import {SELECTOR} from './constants'; @Component({selector: SELECTOR, template: 'cmp'}) export class Cmp2 {} `); - env.write('constants.ts', ` + env.write('constants.ts', ` export const SELECTOR = 'cmp'; `); - env.driveMain(); + env.driveMain(); - // Pretend a change was made to SELECTOR - env.flushWrittenFileTracking(); - env.invalidateCachedFile('constants.ts'); - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); - expect(written).toContain('/constants.js'); - expect(written).not.toContain('/component1.js'); - expect(written).toContain('/component2.js'); + // Pretend a change was made to SELECTOR + env.flushWrittenFileTracking(); + env.invalidateCachedFile('constants.ts'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + expect(written).toContain('/constants.js'); + expect(written).not.toContain('/component1.js'); + expect(written).toContain('/component2.js'); + }); + + it('should rebuild components whose imported dependencies have changed', () => { + setupFooBarProgram(env); + + // Pretend a change was made to BarDir. + env.invalidateCachedFile('bar_directive.ts'); + env.driveMain(); + + let written = env.getFilesWrittenSinceLastFlush(); + expect(written).toContain('/bar_directive.js'); + expect(written).toContain('/bar_component.js'); + expect(written).toContain('/bar_module.js'); + expect(written).not.toContain('/foo_component.js'); + expect(written).not.toContain('/foo_pipe.js'); + expect(written).not.toContain('/foo_module.js'); + }); + + it('should rebuild components where their NgModule declared dependencies have changed', () => { + setupFooBarProgram(env); + + // Pretend a change was made to FooPipe. + env.invalidateCachedFile('foo_pipe.ts'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + expect(written).not.toContain('/bar_directive.js'); + expect(written).not.toContain('/bar_component.js'); + expect(written).not.toContain('/bar_module.js'); + expect(written).toContain('/foo_component.js'); + expect(written).toContain('/foo_pipe.js'); + expect(written).toContain('/foo_module.js'); + }); + + it('should rebuild components where their NgModule has changed', () => { + setupFooBarProgram(env); + + // Pretend a change was made to FooPipe. + env.invalidateCachedFile('foo_module.ts'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + expect(written).not.toContain('/bar_directive.js'); + expect(written).not.toContain('/bar_component.js'); + expect(written).not.toContain('/bar_module.js'); + expect(written).toContain('/foo_component.js'); + expect(written).toContain('/foo_pipe.js'); + expect(written).toContain('/foo_module.js'); + }); + + it('should rebuild everything if a typings file changes', () => { + setupFooBarProgram(env); + + // Pretend a change was made to a typings file. + env.invalidateCachedFile('foo_selector.d.ts'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + expect(written).toContain('/bar_directive.js'); + expect(written).toContain('/bar_component.js'); + expect(written).toContain('/bar_module.js'); + expect(written).toContain('/foo_component.js'); + expect(written).toContain('/foo_pipe.js'); + expect(written).toContain('/foo_module.js'); + }); + + it('should compile incrementally with template type-checking turned on', () => { + env.tsconfig({ivyTemplateTypeCheck: true}); + env.write('main.ts', 'export class Foo {}'); + env.driveMain(); + env.invalidateCachedFile('main.ts'); + env.driveMain(); + // If program reuse were configured incorrectly (as was responsible for + // https://github.com/angular/angular/issues/30079), this would have crashed. + }); }); - it('should rebuild components whose imported dependencies have changed', () => { - setupFooBarProgram(env); - - // Pretend a change was made to BarDir. - env.invalidateCachedFile('bar_directive.ts'); - env.driveMain(); - - let written = env.getFilesWrittenSinceLastFlush(); - expect(written).toContain('/bar_directive.js'); - expect(written).toContain('/bar_component.js'); - expect(written).toContain('/bar_module.js'); - expect(written).not.toContain('/foo_component.js'); - expect(written).not.toContain('/foo_pipe.js'); - expect(written).not.toContain('/foo_module.js'); - }); - - it('should rebuild components where their NgModule declared dependencies have changed', () => { - setupFooBarProgram(env); - - // Pretend a change was made to FooPipe. - env.invalidateCachedFile('foo_pipe.ts'); - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); - expect(written).not.toContain('/bar_directive.js'); - expect(written).not.toContain('/bar_component.js'); - expect(written).not.toContain('/bar_module.js'); - expect(written).toContain('/foo_component.js'); - expect(written).toContain('/foo_pipe.js'); - expect(written).toContain('/foo_module.js'); - }); - - it('should rebuild components where their NgModule has changed', () => { - setupFooBarProgram(env); - - // Pretend a change was made to FooPipe. - env.invalidateCachedFile('foo_module.ts'); - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); - expect(written).not.toContain('/bar_directive.js'); - expect(written).not.toContain('/bar_component.js'); - expect(written).not.toContain('/bar_module.js'); - expect(written).toContain('/foo_component.js'); - expect(written).toContain('/foo_pipe.js'); - expect(written).toContain('/foo_module.js'); - }); - - it('should rebuild everything if a typings file changes', () => { - setupFooBarProgram(env); - - // Pretend a change was made to a typings file. - env.invalidateCachedFile('foo_selector.d.ts'); - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); - expect(written).toContain('/bar_directive.js'); - expect(written).toContain('/bar_component.js'); - expect(written).toContain('/bar_module.js'); - expect(written).toContain('/foo_component.js'); - expect(written).toContain('/foo_pipe.js'); - expect(written).toContain('/foo_module.js'); - }); - - it('should compile incrementally with template type-checking turned on', () => { - env.tsconfig({ivyTemplateTypeCheck: true}); - env.write('main.ts', 'export class Foo {}'); - env.driveMain(); - env.invalidateCachedFile('main.ts'); - env.driveMain(); - // If program reuse were configured incorrectly (as was responsible for - // https://github.com/angular/angular/issues/30079), this would have crashed. - }); -}); - -function setupFooBarProgram(env: NgtscTestEnvironment) { - env.write('foo_component.ts', ` + function setupFooBarProgram(env: NgtscTestEnvironment) { + env.write('foo_component.ts', ` import {Component} from '@angular/core'; import {fooSelector} from './foo_selector'; @Component({selector: fooSelector, template: 'foo'}) export class FooCmp {} `); - env.write('foo_pipe.ts', ` + env.write('foo_pipe.ts', ` import {Pipe} from '@angular/core'; @Pipe({name: 'foo'}) export class FooPipe {} `); - env.write('foo_module.ts', ` + env.write('foo_module.ts', ` import {NgModule} from '@angular/core'; import {FooCmp} from './foo_component'; import {FooPipe} from './foo_pipe'; @@ -224,19 +229,19 @@ function setupFooBarProgram(env: NgtscTestEnvironment) { }) export class FooModule {} `); - env.write('bar_component.ts', ` + env.write('bar_component.ts', ` import {Component} from '@angular/core'; @Component({selector: 'bar', template: 'bar'}) export class BarCmp {} `); - env.write('bar_directive.ts', ` + env.write('bar_directive.ts', ` import {Directive} from '@angular/core'; @Directive({selector: '[bar]'}) export class BarDir {} `); - env.write('bar_module.ts', ` + env.write('bar_module.ts', ` import {NgModule} from '@angular/core'; import {BarCmp} from './bar_component'; import {BarDir} from './bar_directive'; @@ -246,9 +251,10 @@ function setupFooBarProgram(env: NgtscTestEnvironment) { }) export class BarModule {} `); - env.write('foo_selector.d.ts', ` + env.write('foo_selector.d.ts', ` export const fooSelector = 'foo'; `); - env.driveMain(); - env.flushWrittenFileTracking(); -} + env.driveMain(); + env.flushWrittenFileTracking(); + } +}); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 320b68b078..d239b66842 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -6,11 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics'; -import {LazyRoute} from '@angular/compiler-cli/src/ngtsc/routing'; -import * as path from 'path'; +import {platform} from 'os'; import * as ts from 'typescript'; +import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics'; +import {absoluteFrom} from '../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; +import {LazyRoute} from '../../src/ngtsc/routing'; +import {loadStandardTestFiles} from '../helpers/src/mock_file_loading'; + import {NgtscTestEnvironment} from './env'; const trim = (input: string): string => input.replace(/\s+/g, ' ').trim(); @@ -30,14 +34,19 @@ const contentQueryRegExp = (predicate: string, descend: boolean, ref?: string): const setClassMetadataRegExp = (expectedType: string): RegExp => new RegExp(`setClassMetadata(.*?${expectedType}.*?)`); -describe('ngtsc behavioral tests', () => { - let env !: NgtscTestEnvironment; +const testFiles = loadStandardTestFiles(); - beforeEach(() => { env = NgtscTestEnvironment.setup(); }); +runInEachFileSystem(os => { + describe('ngtsc behavioral tests', () => { + let env !: NgtscTestEnvironment; - it('should compile Injectables without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + beforeEach(() => { + env = NgtscTestEnvironment.setup(testFiles); + env.tsconfig(); + }); + + it('should compile Injectables without errors', () => { + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable() @@ -48,40 +57,37 @@ describe('ngtsc behavioral tests', () => { constructor(dep: Dep) {} } `); - - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Dep.ngInjectableDef ='); - expect(jsContents).toContain('Service.ngInjectableDef ='); - expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); - expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Dep.ngInjectableDef ='); + expect(jsContents).toContain('Service.ngInjectableDef ='); + expect(jsContents).not.toContain('__decorate'); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + }); - it('should compile Injectables with a generic service', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Injectables with a generic service', () => { + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable() export class Store {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Store.ngInjectableDef ='); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef>;'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Store.ngInjectableDef ='); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef>;'); + }); - it('should compile Injectables with providedIn without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Injectables with providedIn without errors', () => { + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable() @@ -93,23 +99,22 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Dep.ngInjectableDef ='); - expect(jsContents).toContain('Service.ngInjectableDef ='); - expect(jsContents) - .toContain('return new (t || Service)(i0.ɵɵinject(Dep)); }, providedIn: \'root\' });'); - expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); - expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Dep.ngInjectableDef ='); + expect(jsContents).toContain('Service.ngInjectableDef ='); + expect(jsContents) + .toContain('return new (t || Service)(i0.ɵɵinject(Dep)); }, providedIn: \'root\' });'); + expect(jsContents).not.toContain('__decorate'); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + }); - it('should compile Injectables with providedIn and factory without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Injectables with providedIn and factory without errors', () => { + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root', useFactory: () => new Service() }) @@ -118,23 +123,22 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Service.ngInjectableDef ='); - expect(jsContents).toContain('(r = new t());'); - expect(jsContents).toContain('(r = (function () { return new Service(); })());'); - expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {'); - expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); - expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Service.ngInjectableDef ='); + expect(jsContents).toContain('(r = new t());'); + expect(jsContents).toContain('(r = (function () { return new Service(); })());'); + expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {'); + expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); + expect(jsContents).not.toContain('__decorate'); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + }); - it('should compile Injectables with providedIn and factory with deps without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Injectables with providedIn and factory with deps without errors', () => { + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable() @@ -146,24 +150,23 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Service.ngInjectableDef ='); - expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {'); - expect(jsContents).toContain('(r = new t(i0.ɵɵinject(Dep)));'); - expect(jsContents) - .toContain('(r = (function (dep) { return new Service(dep); })(i0.ɵɵinject(Dep)));'); - expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); - expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Service.ngInjectableDef ='); + expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {'); + expect(jsContents).toContain('(r = new t(i0.ɵɵinject(Dep)));'); + expect(jsContents) + .toContain('(r = (function (dep) { return new Service(dep); })(i0.ɵɵinject(Dep)));'); + expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); + expect(jsContents).not.toContain('__decorate'); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + }); - it('should compile @Injectable with an @Optional dependency', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile @Injectable with an @Optional dependency', () => { + env.write('test.ts', ` import {Injectable, Optional as Opt} from '@angular/core'; @Injectable() @@ -174,14 +177,13 @@ describe('ngtsc behavioral tests', () => { constructor(@Opt() dep: Dep) {} } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('inject(Dep, 8)'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('inject(Dep, 8)'); + }); - it('should compile Components (inline template) without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Components (inline template) without errors', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -191,21 +193,20 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); - expect(jsContents).not.toContain('__decorate'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); + expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); + }); - it('should compile Components (dynamic inline template) without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Components (dynamic inline template) without errors', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -215,21 +216,20 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); - expect(jsContents).not.toContain('__decorate'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); + expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); + }); - it('should compile Components (function call inline template) without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Components (function call inline template) without errors', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; function getTemplate() { @@ -242,42 +242,20 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); - expect(jsContents).not.toContain('__decorate'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); + expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); - }); - - it('should compile Components (external template) without errors', () => { - env.tsconfig(); - env.write('test.ts', ` - import {Component} from '@angular/core'; - - @Component({ - selector: 'test-cmp', - templateUrl: './dir/test.html', - }) - export class TestCmp {} - `); - env.write('dir/test.html', '

Hello World

'); - - env.driveMain(); - - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Hello World'); - }); - - it('should add @nocollapse to static fields when closure annotations are requested', () => { - env.tsconfig({ - 'annotateForClosureCompiler': true, + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); }); - env.write('test.ts', ` + + it('should compile Components (external template) without errors', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -286,17 +264,42 @@ describe('ngtsc behavioral tests', () => { }) export class TestCmp {} `); - env.write('dir/test.html', '

Hello World

'); + env.write('dir/test.html', '

Hello World

'); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('/** @nocollapse */ TestCmp.ngComponentDef'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Hello World'); + }); - it('should recognize aliased decorators', () => { - env.tsconfig({}); - env.write('test.ts', ` + // This test triggers the Tsickle compiler which asserts that the file-paths + // are valid for the real OS. When on non-Windows systems it doesn't like paths + // that start with `C:`. + if (os !== 'Windows' || platform() === 'win32') { + it('should add @nocollapse to static fields when closure annotations are requested', () => { + env.tsconfig({ + 'annotateForClosureCompiler': true, + }); + env.write('test.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + templateUrl: './dir/test.html', + }) + export class TestCmp {} + `); + env.write('dir/test.html', '

Hello World

'); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('/** @nocollapse */ TestCmp.ngComponentDef'); + }); + } + + it('should recognize aliased decorators', () => { + env.write('test.ts', ` import { Component as AngularComponent, Directive as AngularDirective, @@ -349,24 +352,24 @@ describe('ngtsc behavioral tests', () => { class MyModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestBase.ngBaseDef = i0.ɵɵdefineBase'); - expect(jsContents).toContain('TestComponent.ngComponentDef = i0.ɵɵdefineComponent'); - expect(jsContents).toContain('TestDirective.ngDirectiveDef = i0.ɵɵdefineDirective'); - expect(jsContents).toContain('TestPipe.ngPipeDef = i0.ɵɵdefinePipe'); - expect(jsContents).toContain('TestInjectable.ngInjectableDef = i0.ɵɵdefineInjectable'); - expect(jsContents).toContain('MyModule.ngModuleDef = i0.ɵɵdefineNgModule'); - expect(jsContents).toContain('MyModule.ngInjectorDef = i0.ɵɵdefineInjector'); - expect(jsContents).toContain('inputs: { input: "input" }'); - expect(jsContents).toContain('outputs: { output: "output" }'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('TestBase.ngBaseDef = i0.ɵɵdefineBase'); + expect(jsContents).toContain('TestComponent.ngComponentDef = i0.ɵɵdefineComponent'); + expect(jsContents).toContain('TestDirective.ngDirectiveDef = i0.ɵɵdefineDirective'); + expect(jsContents).toContain('TestPipe.ngPipeDef = i0.ɵɵdefinePipe'); + expect(jsContents).toContain('TestInjectable.ngInjectableDef = i0.ɵɵdefineInjectable'); + expect(jsContents).toContain('MyModule.ngModuleDef = i0.ɵɵdefineNgModule'); + expect(jsContents).toContain('MyModule.ngInjectorDef = i0.ɵɵdefineInjector'); + expect(jsContents).toContain('inputs: { input: "input" }'); + expect(jsContents).toContain('outputs: { output: "output" }'); + }); - it('should compile Components with a templateUrl in a different rootDir', () => { - env.tsconfig({}, ['./extraRootDir']); - env.write('extraRootDir/test.html', '

Hello World

'); - env.write('test.ts', ` + it('should compile Components with a templateUrl in a different rootDir', () => { + env.tsconfig({}, ['./extraRootDir']); + env.write('extraRootDir/test.html', '

Hello World

'); + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -376,15 +379,14 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Hello World'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Hello World'); + }); - it('should compile components with styleUrls', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile components with styleUrls', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -394,17 +396,16 @@ describe('ngtsc behavioral tests', () => { }) export class TestCmp {} `); - env.write('dir/style.css', ':host { background-color: blue; }'); + env.write('dir/style.css', ':host { background-color: blue; }'); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('background-color: blue'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('background-color: blue'); + }); - it('should compile components with styleUrls with fallback to .css extension', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile components with styleUrls with fallback to .css extension', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -414,17 +415,16 @@ describe('ngtsc behavioral tests', () => { }) export class TestCmp {} `); - env.write('dir/style.css', ':host { background-color: blue; }'); + env.write('dir/style.css', ':host { background-color: blue; }'); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('background-color: blue'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('background-color: blue'); + }); - it('should compile NgModules without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules without errors', () => { + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -440,80 +440,77 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('i0.ɵɵdefineNgModule({ type: TestModule, bootstrap: [TestCmp] });'); - expect(jsContents) - .toContain('/*@__PURE__*/ i0.ɵɵsetNgModuleScope(TestModule, { declarations: [TestCmp] });'); - expect(jsContents) - .toContain( - 'i0.ɵɵdefineInjector({ factory: ' + - 'function TestModule_Factory(t) { return new (t || TestModule)(); } });'); + const jsContents = env.getContents('test.js'); + expect(jsContents) + .toContain('i0.ɵɵdefineNgModule({ type: TestModule, bootstrap: [TestCmp] });'); + expect(jsContents) + .toContain( + '/*@__PURE__*/ i0.ɵɵsetNgModuleScope(TestModule, { declarations: [TestCmp] });'); + expect(jsContents) + .toContain( + 'i0.ɵɵdefineInjector({ factory: ' + + 'function TestModule_Factory(t) { return new (t || TestModule)(); } });'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); - expect(dtsContents) - .toContain( - 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); - expect(dtsContents).not.toContain('__decorate'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); + expect(dtsContents) + .toContain( + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); + expect(dtsContents).not.toContain('__decorate'); + }); - it('should not emit a ɵɵsetNgModuleScope call when no scope metadata is present', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should not emit a ɵɵsetNgModuleScope call when no scope metadata is present', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); - expect(jsContents).not.toContain('ɵɵsetNgModuleScope(TestModule,'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); + expect(jsContents).not.toContain('ɵɵsetNgModuleScope(TestModule,'); + }); - it('should emit the id when the module\'s id is a string', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should emit the id when the module\'s id is a string', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({id: 'test'}) export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain(`i0.ɵɵdefineNgModule({ type: TestModule, id: 'test' })`); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain(`i0.ɵɵdefineNgModule({ type: TestModule, id: 'test' })`); + }); - it('should emit the id when the module\'s id is defined as `module.id`', () => { - env.tsconfig(); - env.write('index.d.ts', ` + it('should emit the id when the module\'s id is defined as `module.id`', () => { + env.write('index.d.ts', ` declare const module = {id: string}; `); - env.write('test.ts', ` + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({id: module.id}) export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule, id: module.id })'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule, id: module.id })'); + }); - it('should filter out directives and pipes from module exports in the injector def', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should filter out directives and pipes from module exports in the injector def', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterComp, RouterModule} from '@angular/router'; import {Dir, OtherDir, MyPipe, Comp} from './decls'; @@ -533,7 +530,7 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write(`decls.ts`, ` + env.write(`decls.ts`, ` import {Component, Directive, Pipe} from '@angular/core'; @Directive({selector: '[dir]'}) @@ -548,7 +545,7 @@ describe('ngtsc behavioral tests', () => { @Component({selector: 'test', template: ''}) export class Comp {} `); - env.write('node_modules/@angular/router/index.d.ts', ` + env.write('node_modules/@angular/router/index.d.ts', ` import {ɵɵComponentDefWithMeta, ModuleWithProviders, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class RouterComp { @@ -561,19 +558,18 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'i0.ɵɵdefineInjector({ factory: function TestModule_Factory(t) ' + - '{ return new (t || TestModule)(); }, imports: [[OtherModule, RouterModule.forRoot()],' + - '\n OtherModule,\n RouterModule] });'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents) + .toContain( + 'i0.ɵɵdefineInjector({ factory: function TestModule_Factory(t) ' + + '{ return new (t || TestModule)(); }, imports: [[OtherModule, RouterModule.forRoot()],' + + '\n OtherModule,\n RouterModule] });'); + }); - it('should compile NgModules with services without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with services without errors', () => { + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; export class Token {} @@ -595,26 +591,25 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); - expect(jsContents) - .toContain( - `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + - `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + - `Token, useValue: 'test' }], imports: [[OtherModule]] });`); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); + expect(jsContents) + .toContain( + `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + + `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + + `Token, useValue: 'test' }], imports: [[OtherModule]] });`); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); - expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); + expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); + }); - it('should compile NgModules with factory providers without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with factory providers without errors', () => { + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; export class Token {} @@ -636,26 +631,25 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); - expect(jsContents) - .toContain( - `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + - `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + - `Token, useFactory: function () { return new Token(); } }], imports: [[OtherModule]] });`); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); + expect(jsContents) + .toContain( + `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + + `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + + `Token, useFactory: function () { return new Token(); } }], imports: [[OtherModule]] });`); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); - expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); + expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); + }); - it('should compile NgModules with factory providers and deps without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with factory providers and deps without errors', () => { + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; export class Dep {} @@ -681,26 +675,25 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); - expect(jsContents) - .toContain( - `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + - `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + - `Token, useFactory: function (dep) { return new Token(dep); }, deps: [Dep] }], imports: [[OtherModule]] });`); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); + expect(jsContents) + .toContain( + `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + + `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + + `Token, useFactory: function (dep) { return new Token(dep); }, deps: [Dep] }], imports: [[OtherModule]] });`); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); - expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); + expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); + }); - it('should compile NgModules with references to local components', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with references to local components', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {Foo} from './foo'; @@ -709,25 +702,24 @@ describe('ngtsc behavioral tests', () => { }) export class FooModule {} `); - env.write('foo.ts', ` + env.write('foo.ts', ` import {Component} from '@angular/core'; @Component({selector: 'foo', template: ''}) export class Foo {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - const dtsContents = env.getContents('test.d.ts'); + const jsContents = env.getContents('test.js'); + const dtsContents = env.getContents('test.d.ts'); - expect(jsContents).toContain('import { Foo } from \'./foo\';'); - expect(jsContents).not.toMatch(/as i[0-9] from ".\/foo"/); - expect(dtsContents).toContain('as i1 from "./foo";'); - }); + expect(jsContents).toContain('import { Foo } from \'./foo\';'); + expect(jsContents).not.toMatch(/as i[0-9] from ".\/foo"/); + expect(dtsContents).toContain('as i1 from "./foo";'); + }); - it('should compile NgModules with references to absolute components', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with references to absolute components', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {Foo} from 'foo'; @@ -736,7 +728,7 @@ describe('ngtsc behavioral tests', () => { }) export class FooModule {} `); - env.write('node_modules/foo/index.ts', ` + env.write('node_modules/foo/index.ts', ` import {Component} from '@angular/core'; @Component({ @@ -747,19 +739,18 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - const dtsContents = env.getContents('test.d.ts'); + const jsContents = env.getContents('test.js'); + const dtsContents = env.getContents('test.d.ts'); - expect(jsContents).toContain('import { Foo } from \'foo\';'); - expect(jsContents).not.toMatch(/as i[0-9] from "foo"/); - expect(dtsContents).toContain('as i1 from "foo";'); - }); + expect(jsContents).toContain('import { Foo } from \'foo\';'); + expect(jsContents).not.toMatch(/as i[0-9] from "foo"/); + expect(dtsContents).toContain('as i1 from "foo";'); + }); - it('should compile NgModules with references to forward declared bootstrap components', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with references to forward declared bootstrap components', () => { + env.write('test.ts', ` import {Component, forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -771,15 +762,14 @@ describe('ngtsc behavioral tests', () => { export class Foo {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('bootstrap: function () { return [Foo]; }'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('bootstrap: function () { return [Foo]; }'); + }); - it('should compile NgModules with references to forward declared directives', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with references to forward declared directives', () => { + env.write('test.ts', ` import {Directive, forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -791,15 +781,14 @@ describe('ngtsc behavioral tests', () => { export class Foo {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('declarations: function () { return [Foo]; }'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('declarations: function () { return [Foo]; }'); + }); - it('should compile NgModules with references to forward declared imports', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with references to forward declared imports', () => { + env.write('test.ts', ` import {forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -811,15 +800,14 @@ describe('ngtsc behavioral tests', () => { export class BarModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: function () { return [BarModule]; }'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: function () { return [BarModule]; }'); + }); - it('should compile NgModules with references to forward declared exports', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile NgModules with references to forward declared exports', () => { + env.write('test.ts', ` import {forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -831,15 +819,14 @@ describe('ngtsc behavioral tests', () => { export class BarModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('exports: function () { return [BarModule]; }'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('exports: function () { return [BarModule]; }'); + }); - it('should compile Pipes without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Pipes without errors', () => { + env.write('test.ts', ` import {Pipe} from '@angular/core'; @Pipe({ @@ -849,21 +836,21 @@ describe('ngtsc behavioral tests', () => { export class TestPipe {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - const dtsContents = env.getContents('test.d.ts'); + const jsContents = env.getContents('test.js'); + const dtsContents = env.getContents('test.d.ts'); - expect(jsContents) - .toContain( - 'TestPipe.ngPipeDef = i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, ' + - 'factory: function TestPipe_Factory(t) { return new (t || TestPipe)(); }, pure: false })'); - expect(dtsContents).toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta;'); - }); + expect(jsContents) + .toContain( + 'TestPipe.ngPipeDef = i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, ' + + 'factory: function TestPipe_Factory(t) { return new (t || TestPipe)(); }, pure: false })'); + expect(dtsContents) + .toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta;'); + }); - it('should compile pure Pipes without errors', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile pure Pipes without errors', () => { + env.write('test.ts', ` import {Pipe} from '@angular/core'; @Pipe({ @@ -872,21 +859,21 @@ describe('ngtsc behavioral tests', () => { export class TestPipe {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - const dtsContents = env.getContents('test.d.ts'); + const jsContents = env.getContents('test.js'); + const dtsContents = env.getContents('test.d.ts'); - expect(jsContents) - .toContain( - 'TestPipe.ngPipeDef = i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, ' + - 'factory: function TestPipe_Factory(t) { return new (t || TestPipe)(); }, pure: true })'); - expect(dtsContents).toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta;'); - }); + expect(jsContents) + .toContain( + 'TestPipe.ngPipeDef = i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, ' + + 'factory: function TestPipe_Factory(t) { return new (t || TestPipe)(); }, pure: true })'); + expect(dtsContents) + .toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta;'); + }); - it('should compile Pipes with dependencies', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Pipes with dependencies', () => { + env.write('test.ts', ` import {Pipe} from '@angular/core'; export class Dep {} @@ -900,15 +887,14 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('return new (t || TestPipe)(i0.ɵɵdirectiveInject(Dep));'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('return new (t || TestPipe)(i0.ɵɵdirectiveInject(Dep));'); + }); - it('should compile Pipes with generic types', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile Pipes with generic types', () => { + env.write('test.ts', ` import {Pipe} from '@angular/core'; @Pipe({ @@ -917,18 +903,17 @@ describe('ngtsc behavioral tests', () => { export class TestPipe {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestPipe.ngPipeDef ='); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta, "test-pipe">;'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('TestPipe.ngPipeDef ='); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta, "test-pipe">;'); + }); - it('should include @Pipes in @NgModule scopes', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should include @Pipes in @NgModule scopes', () => { + env.write('test.ts', ` import {Component, NgModule, Pipe} from '@angular/core'; @Pipe({name: 'test'}) @@ -941,21 +926,20 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('pipes: [TestPipe]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('pipes: [TestPipe]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDefWithMeta'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'i0.ɵɵNgModuleDefWithMeta'); + }); - describe('empty and missing selectors', () => { - it('should use default selector for Components when no selector present', () => { - env.tsconfig({}); - env.write('test.ts', ` + describe('empty and missing selectors', () => { + it('should use default selector for Components when no selector present', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -964,15 +948,14 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('selectors: [["ng-component"]]'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('selectors: [["ng-component"]]'); + }); - it('should use default selector for Components with empty string selector', () => { - env.tsconfig({}); - env.write('test.ts', ` + it('should use default selector for Components with empty string selector', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -982,15 +965,14 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('selectors: [["ng-component"]]'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('selectors: [["ng-component"]]'); + }); - it('should throw if selector is missing in Directive decorator params', () => { - env.tsconfig({}); - env.write('test.ts', ` + it('should throw if selector is missing in Directive decorator params', () => { + env.write('test.ts', ` import {Directive} from '@angular/core'; @Directive({ @@ -999,14 +981,13 @@ describe('ngtsc behavioral tests', () => { export class TestDir {} `); - const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Directive TestDir has no selector, please add it!'); - }); + const errors = env.driveDiagnostics(); + expect(trim(errors[0].messageText as string)) + .toContain('Directive TestDir has no selector, please add it!'); + }); - it('should throw if Directive selector is an empty string', () => { - env.tsconfig({}); - env.write('test.ts', ` + it('should throw if Directive selector is an empty string', () => { + env.write('test.ts', ` import {Directive} from '@angular/core'; @Directive({ @@ -1015,9 +996,10 @@ describe('ngtsc behavioral tests', () => { export class TestDir {} `); - const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Directive TestDir has no selector, please add it!'); + const errors = env.driveDiagnostics(); + expect(trim(errors[0].messageText as string)) + .toContain('Directive TestDir has no selector, please add it!'); + }); }); it('should throw error if content queries share a property with inputs', () => { @@ -1085,12 +1067,10 @@ describe('ngtsc behavioral tests', () => { expect(trim(messageText as string)) .toContain('Query decorator must go on a property-type member'); }); - }); - describe('multiple decorators on classes', () => { - it('should compile @Injectable on Components, Directives, Pipes, and Modules', () => { - env.tsconfig(); - env.write('test.ts', ` + describe('multiple decorators on classes', () => { + it('should compile @Injectable on Components, Directives, Pipes, and Modules', () => { + env.write('test.ts', ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'test', template: 'test'}) @@ -1110,38 +1090,37 @@ describe('ngtsc behavioral tests', () => { export class TestNgModule {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const dtsContents = env.getContents('test.d.ts'); + env.driveMain(); + const jsContents = env.getContents('test.js'); + const dtsContents = env.getContents('test.d.ts'); - // Validate that each class has the primary definition. - expect(jsContents).toContain('TestCmp.ngComponentDef ='); - expect(jsContents).toContain('TestDir.ngDirectiveDef ='); - expect(jsContents).toContain('TestPipe.ngPipeDef ='); - expect(jsContents).toContain('TestNgModule.ngModuleDef ='); + // Validate that each class has the primary definition. + expect(jsContents).toContain('TestCmp.ngComponentDef ='); + expect(jsContents).toContain('TestDir.ngDirectiveDef ='); + expect(jsContents).toContain('TestPipe.ngPipeDef ='); + expect(jsContents).toContain('TestNgModule.ngModuleDef ='); - // Validate that each class also has an injectable definition. - expect(jsContents).toContain('TestCmp.ngInjectableDef ='); - expect(jsContents).toContain('TestDir.ngInjectableDef ='); - expect(jsContents).toContain('TestPipe.ngInjectableDef ='); - expect(jsContents).toContain('TestNgModule.ngInjectableDef ='); + // Validate that each class also has an injectable definition. + expect(jsContents).toContain('TestCmp.ngInjectableDef ='); + expect(jsContents).toContain('TestDir.ngInjectableDef ='); + expect(jsContents).toContain('TestPipe.ngInjectableDef ='); + expect(jsContents).toContain('TestNgModule.ngInjectableDef ='); - // Validate that each class's .d.ts declaration has the primary definition. - expect(dtsContents).toContain('ComponentDefWithMeta { - env.tsconfig(); - env.write('test.ts', ` + it('should not compile a component and a directive annotation on the same class', () => { + env.write('test.ts', ` import {Component, Directive} from '@angular/core'; @Component({selector: 'test', template: 'test'}) @@ -1149,16 +1128,15 @@ describe('ngtsc behavioral tests', () => { class ShouldNotCompile {} `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(errors[0].messageText).toContain('Two incompatible decorators on class'); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(errors[0].messageText).toContain('Two incompatible decorators on class'); + }); - it('should leave decorators present on jit: true directives', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should leave decorators present on jit: true directives', () => { + env.write('test.ts', ` import {Directive, Inject} from '@angular/core'; @Directive({ @@ -1169,19 +1147,19 @@ describe('ngtsc behavioral tests', () => { constructor(@Inject('foo') foo: string) {} } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Directive({'); - expect(jsContents).toContain('__param(0, Inject'); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Directive({'); + expect(jsContents).toContain('__param(0, Inject'); + }); }); - }); - describe('compiling invalid @Injectables', () => { - describe('with strictInjectionParameters = true', () => { - it('should give a compile-time error if an invalid @Injectable is used with no arguments', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + describe('compiling invalid @Injectables', () => { + describe('with strictInjectionParameters = true', () => { + it('should give a compile-time error if an invalid @Injectable is used with no arguments', + () => { + env.tsconfig({strictInjectionParameters: true}); + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable() @@ -1190,15 +1168,15 @@ describe('ngtsc behavioral tests', () => { } `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(errors[0].messageText).toContain('No suitable injection token for parameter'); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(errors[0].messageText).toContain('No suitable injection token for parameter'); + }); - it('should give a compile-time error if an invalid @Injectable is used with an argument', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + it('should give a compile-time error if an invalid @Injectable is used with an argument', + () => { + env.tsconfig({strictInjectionParameters: true}); + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable() @@ -1207,15 +1185,15 @@ describe('ngtsc behavioral tests', () => { } `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(errors[0].messageText).toContain('No suitable injection token for parameter'); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(errors[0].messageText).toContain('No suitable injection token for parameter'); + }); - it('should not give a compile-time error if an invalid @Injectable is used with useValue', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + it('should not give a compile-time error if an invalid @Injectable is used with useValue', + () => { + env.tsconfig({strictInjectionParameters: true}); + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable({ @@ -1227,16 +1205,16 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toMatch(/if \(t\).*throw new Error.* else .* '42'/ms); - }); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/if \(t\).*throw new Error.* else .* '42'/ms); + }); + }); - describe('with strictInjectionParameters = false', () => { - it('should compile an @Injectable on a class with a non-injectable constructor', () => { - env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + describe('with strictInjectionParameters = false', () => { + it('should compile an @Injectable on a class with a non-injectable constructor', () => { + env.tsconfig({strictInjectionParameters: false}); + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable() @@ -1245,15 +1223,15 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error('); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error('); + }); - it('should compile an @Injectable provided in the root on a class with a non-injectable constructor', - () => { - env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + it('should compile an @Injectable provided in the root on a class with a non-injectable constructor', + () => { + env.tsconfig({strictInjectionParameters: false}); + env.write('test.ts', ` import {Injectable} from '@angular/core'; @Injectable({providedIn: 'root'}) export class Test { @@ -1261,43 +1239,42 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error('); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error('); + }); + }); }); - }); - describe('templateUrl and styleUrls processing', () => { - const testsForResource = (resource: string) => [ - // [component location, resource location, resource reference] + describe('templateUrl and styleUrls processing', () => { + const testsForResource = (resource: string) => [ + // [component location, resource location, resource reference] - // component and resource are in the same folder - [`a/app.ts`, `a/${resource}`, `./${resource}`], // - [`a/app.ts`, `a/${resource}`, resource], // - [`a/app.ts`, `a/${resource}`, `/a/${resource}`], + // component and resource are in the same folder + [`a/app.ts`, `a/${resource}`, `./${resource}`], // + [`a/app.ts`, `a/${resource}`, resource], // + [`a/app.ts`, `a/${resource}`, `/a/${resource}`], - // resource is one level up - [`a/app.ts`, resource, `../${resource}`], // - [`a/app.ts`, resource, `/${resource}`], + // resource is one level up + [`a/app.ts`, resource, `../${resource}`], // + [`a/app.ts`, resource, `/${resource}`], - // component and resource are in different folders - [`a/app.ts`, `b/${resource}`, `../b/${resource}`], // - [`a/app.ts`, `b/${resource}`, `/b/${resource}`], + // component and resource are in different folders + [`a/app.ts`, `b/${resource}`, `../b/${resource}`], // + [`a/app.ts`, `b/${resource}`, `/b/${resource}`], - // resource is in subfolder of component directory - [`a/app.ts`, `a/b/c/${resource}`, `./b/c/${resource}`], // - [`a/app.ts`, `a/b/c/${resource}`, `b/c/${resource}`], // - [`a/app.ts`, `a/b/c/${resource}`, `/a/b/c/${resource}`], - ]; + // resource is in subfolder of component directory + [`a/app.ts`, `a/b/c/${resource}`, `./b/c/${resource}`], // + [`a/app.ts`, `a/b/c/${resource}`, `b/c/${resource}`], // + [`a/app.ts`, `a/b/c/${resource}`, `/a/b/c/${resource}`], + ]; - testsForResource('style.css').forEach((test) => { - const [compLoc, styleLoc, styleRef] = test; - it(`should handle ${styleRef}`, () => { - env.tsconfig(); - env.write(styleLoc, ':host { background-color: blue; }'); - env.write(compLoc, ` + testsForResource('style.css').forEach((test) => { + const [compLoc, styleLoc, styleRef] = test; + it(`should handle ${styleRef}`, () => { + env.write(styleLoc, ':host { background-color: blue; }'); + env.write(compLoc, ` import {Component} from '@angular/core'; @Component({ @@ -1308,19 +1285,18 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents(compLoc.replace('.ts', '.js')); - expect(jsContents).toContain('background-color: blue'); + const jsContents = env.getContents(compLoc.replace('.ts', '.js')); + expect(jsContents).toContain('background-color: blue'); + }); }); - }); - testsForResource('template.html').forEach((test) => { - const [compLoc, templateLoc, templateRef] = test; - it(`should handle ${templateRef}`, () => { - env.tsconfig(); - env.write(templateLoc, 'Template Content'); - env.write(compLoc, ` + testsForResource('template.html').forEach((test) => { + const [compLoc, templateLoc, templateRef] = test; + it(`should handle ${templateRef}`, () => { + env.write(templateLoc, 'Template Content'); + env.write(compLoc, ` import {Component} from '@angular/core'; @Component({ @@ -1330,18 +1306,17 @@ describe('ngtsc behavioral tests', () => { export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents(compLoc.replace('.ts', '.js')); - expect(jsContents).toContain('Template Content'); + const jsContents = env.getContents(compLoc.replace('.ts', '.js')); + expect(jsContents).toContain('Template Content'); + }); }); }); - }); - describe('former View Engine AST transform bugs', () => { - it('should compile array literals behind conditionals', () => { - env.tsconfig(); - env.write('test.ts', ` + describe('former View Engine AST transform bugs', () => { + it('should compile array literals behind conditionals', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -1354,13 +1329,12 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); - }); + env.driveMain(); + expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); + }); - it('should compile array literals inside function arguments', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile array literals inside function arguments', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -1376,15 +1350,14 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); + env.driveMain(); + expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); + }); }); - }); - describe('unwrapping ModuleWithProviders functions', () => { - it('should extract the generic type and include it in the module\'s declaration', () => { - env.tsconfig(); - env.write(`test.ts`, ` + describe('unwrapping ModuleWithProviders functions', () => { + it('should extract the generic type and include it in the module\'s declaration', () => { + env.write(`test.ts`, ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @@ -1392,7 +1365,7 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.write('node_modules/router/index.d.ts', ` + env.write('node_modules/router/index.d.ts', ` import {ModuleWithProviders, ɵɵNgModuleDefWithMeta} from '@angular/core'; declare class RouterModule { @@ -1401,21 +1374,20 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDefWithMeta'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router";`); + expect(dtsContents) + .toContain( + 'i0.ɵɵNgModuleDefWithMeta'); + }); - it('should extract the generic type if it is provided as qualified type name', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should extract the generic type if it is provided as qualified type name', () => { + env.write(`test.ts`, ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @@ -1423,7 +1395,7 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.write('node_modules/router/index.d.ts', ` + env.write('node_modules/router/index.d.ts', ` import {ModuleWithProviders} from '@angular/core'; import * as internal from './internal'; export {InternalRouterModule} from './internal'; @@ -1434,29 +1406,28 @@ describe('ngtsc behavioral tests', () => { `); - env.write('node_modules/router/internal.d.ts', ` + env.write('node_modules/router/internal.d.ts', ` import {ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class InternalRouterModule { static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDefWithMeta'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router";`); + expect(dtsContents) + .toContain( + 'i0.ɵɵNgModuleDefWithMeta'); + }); - it('should not reference a constant with a ModuleWithProviders value in ngModuleDef imports', - () => { - env.tsconfig(); - env.write('dep.d.ts', ` + it('should not reference a constant with a ModuleWithProviders value in ngModuleDef imports', + () => { + env.write('dep.d.ts', ` import {ModuleWithProviders, ɵɵNgModuleDefWithMeta as ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class DepModule { @@ -1464,7 +1435,7 @@ describe('ngtsc behavioral tests', () => { static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); - env.write('test.ts', ` + env.write('test.ts', ` import {NgModule, ModuleWithProviders} from '@angular/core'; import {DepModule} from './dep'; @@ -1478,16 +1449,15 @@ describe('ngtsc behavioral tests', () => { }) export class Module {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [i1.DepModule]'); - }); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [i1.DepModule]'); + }); + }); - it('should unwrap a ModuleWithProviders-like function if a matching literal type is provided for it', - () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should unwrap a ModuleWithProviders-like function if a matching literal type is provided for it', + () => { + env.write(`test.ts`, ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @@ -1495,7 +1465,7 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.write('node_modules/router/index.d.ts', ` + env.write('node_modules/router/index.d.ts', ` import {ModuleWithProviders, ɵɵNgModuleDefWithMeta} from '@angular/core'; export interface MyType extends ModuleWithProviders {} @@ -1506,22 +1476,21 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDefWithMeta'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router";`); + expect(dtsContents) + .toContain( + 'i0.ɵɵNgModuleDefWithMeta'); + }); - it('should unwrap a namespace imported ModuleWithProviders function if a generic type is provided for it', - () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should unwrap a namespace imported ModuleWithProviders function if a generic type is provided for it', + () => { + env.write(`test.ts`, ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @@ -1529,7 +1498,7 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.write('node_modules/router/index.d.ts', ` + env.write('node_modules/router/index.d.ts', ` import * as core from '@angular/core'; import {RouterModule} from 'router'; @@ -1539,21 +1508,20 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDefWithMeta'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router";`); + expect(dtsContents) + .toContain( + 'i0.ɵɵNgModuleDefWithMeta'); + }); - it('should inject special types according to the metadata', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should inject special types according to the metadata', () => { + env.write(`test.ts`, ` import { Attribute, ChangeDetectorRef, @@ -1582,16 +1550,15 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents) + .toContain( + `factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`); + }); - it('should generate queries for components', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should generate queries for components', () => { + env.write(`test.ts`, ` import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core'; @Component({ @@ -1610,21 +1577,20 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toMatch(varRegExp('bar')); - expect(jsContents).toMatch(varRegExp('test1')); - expect(jsContents).toMatch(varRegExp('test2')); - expect(jsContents).toMatch(varRegExp('accessor')); - // match `i0.ɵɵcontentQuery(dirIndex, _c1, true, TemplateRef)` - expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef')); - // match `i0.ɵɵviewQuery(_c2, true, null)` - expect(jsContents).toMatch(viewQueryRegExp(true)); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(varRegExp('bar')); + expect(jsContents).toMatch(varRegExp('test1')); + expect(jsContents).toMatch(varRegExp('test2')); + expect(jsContents).toMatch(varRegExp('accessor')); + // match `i0.ɵɵcontentQuery(dirIndex, _c1, true, TemplateRef)` + expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef')); + // match `i0.ɵɵviewQuery(_c2, true, null)` + expect(jsContents).toMatch(viewQueryRegExp(true)); + }); - it('should generate queries for directives', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should generate queries for directives', () => { + env.write(`test.ts`, ` import {Directive, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core'; @Directive({ @@ -1642,24 +1608,23 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toMatch(varRegExp('bar')); - expect(jsContents).toMatch(varRegExp('test1')); - expect(jsContents).toMatch(varRegExp('test2')); - expect(jsContents).toMatch(varRegExp('accessor')); - // match `i0.ɵɵcontentQuery(dirIndex, _c1, true, TemplateRef)` - expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef')); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(varRegExp('bar')); + expect(jsContents).toMatch(varRegExp('test1')); + expect(jsContents).toMatch(varRegExp('test2')); + expect(jsContents).toMatch(varRegExp('accessor')); + // match `i0.ɵɵcontentQuery(dirIndex, _c1, true, TemplateRef)` + expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef')); - // match `i0.ɵɵviewQuery(_c2, true, null)` - // Note that while ViewQuery doesn't necessarily make sense on a directive, because it doesn't - // have a view, we still need to handle it because a component could extend the directive. - expect(jsContents).toMatch(viewQueryRegExp(true)); - }); + // match `i0.ɵɵviewQuery(_c2, true, null)` + // Note that while ViewQuery doesn't necessarily make sense on a directive, because it doesn't + // have a view, we still need to handle it because a component could extend the directive. + expect(jsContents).toMatch(viewQueryRegExp(true)); + }); - it('should handle queries that use forwardRef', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should handle queries that use forwardRef', () => { + env.write(`test.ts`, ` import {Component, ContentChild, TemplateRef, ViewContainerRef, forwardRef} from '@angular/core'; @Component({ @@ -1675,20 +1640,19 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - // match `i0.ɵɵcontentQuery(dirIndex, TemplateRef, true, null)` - expect(jsContents).toMatch(contentQueryRegExp('TemplateRef', true)); - // match `i0.ɵɵcontentQuery(dirIndex, ViewContainerRef, true, null)` - expect(jsContents).toMatch(contentQueryRegExp('ViewContainerRef', true)); - // match `i0.ɵɵcontentQuery(dirIndex, _c0, true, null)` - expect(jsContents).toContain('_c0 = ["parens"];'); - expect(jsContents).toMatch(contentQueryRegExp('_c0', true)); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + // match `i0.ɵɵcontentQuery(dirIndex, TemplateRef, true, null)` + expect(jsContents).toMatch(contentQueryRegExp('TemplateRef', true)); + // match `i0.ɵɵcontentQuery(dirIndex, ViewContainerRef, true, null)` + expect(jsContents).toMatch(contentQueryRegExp('ViewContainerRef', true)); + // match `i0.ɵɵcontentQuery(dirIndex, _c0, true, null)` + expect(jsContents).toContain('_c0 = ["parens"];'); + expect(jsContents).toMatch(contentQueryRegExp('_c0', true)); + }); - it('should compile expressions that write keys', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should compile expressions that write keys', () => { + env.write(`test.ts`, ` import {Component, ContentChild, TemplateRef, ViewContainerRef, forwardRef} from '@angular/core'; @Component({ @@ -1701,13 +1665,12 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - expect(env.getContents('test.js')).toContain('test[key] = $event'); - }); + env.driveMain(); + expect(env.getContents('test.js')).toContain('test[key] = $event'); + }); - it('should generate host listeners for components', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should generate host listeners for components', () => { + env.write(`test.ts`, ` import {Component, HostListener} from '@angular/core'; @Component({ @@ -1726,9 +1689,9 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); }); @@ -1737,12 +1700,11 @@ describe('ngtsc behavioral tests', () => { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); - it('should throw in case unknown global target is provided', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should throw in case unknown global target is provided', () => { + env.write(`test.ts`, ` import {Component, HostListener} from '@angular/core'; @Component({ @@ -1754,15 +1716,14 @@ describe('ngtsc behavioral tests', () => { onClick(event: any): void {} } `); - const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain( - `Unexpected global target 'UnknownTarget' defined for 'click' event. Supported list of global targets: window,document,body.`); - }); + const errors = env.driveDiagnostics(); + expect(trim(errors[0].messageText as string)) + .toContain( + `Unexpected global target 'UnknownTarget' defined for 'click' event. Supported list of global targets: window,document,body.`); + }); - it('should throw in case pipes are used in host listeners', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should throw in case pipes are used in host listeners', () => { + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ @@ -1774,14 +1735,13 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Cannot have a pipe in an action expression'); - }); + const errors = env.driveDiagnostics(); + expect(trim(errors[0].messageText as string)) + .toContain('Cannot have a pipe in an action expression'); + }); - it('should throw in case pipes are used in host bindings', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should throw in case pipes are used in host bindings', () => { + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ @@ -1793,14 +1753,13 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Host binding expression cannot contain pipes'); - }); + const errors = env.driveDiagnostics(); + expect(trim(errors[0].messageText as string)) + .toContain('Host binding expression cannot contain pipes'); + }); - it('should generate host bindings for directives', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should generate host bindings for directives', () => { + env.write(`test.ts`, ` import {Component, HostBinding, HostListener, TemplateRef} from '@angular/core'; @Component({ @@ -1824,9 +1783,9 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵallocHostVars(2); @@ -1843,15 +1802,14 @@ describe('ngtsc behavioral tests', () => { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); - it('should accept dynamic host attribute bindings', () => { - env.tsconfig(); - env.write('other.d.ts', ` + it('should accept dynamic host attribute bindings', () => { + env.write('other.d.ts', ` export declare const foo: any; `); - env.write('test.ts', ` + env.write('test.ts', ` import {Component} from '@angular/core'; import {foo} from './other'; @@ -1866,14 +1824,13 @@ describe('ngtsc behavioral tests', () => { }) export class TestCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.ɵɵelementHostAttrs(["test", test])'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵelementHostAttrs(["test", test])'); + }); - it('should accept enum values as host bindings', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should accept enum values as host bindings', () => { + env.write(`test.ts`, ` import {Component, HostBinding, HostListener, TemplateRef} from '@angular/core'; enum HostBindings { @@ -1892,13 +1849,12 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - expect(env.getContents('test.js')).toContain('i0.ɵɵattribute("hello", ctx.foo)'); - }); + env.driveMain(); + expect(env.getContents('test.js')).toContain('i0.ɵɵattribute("hello", ctx.foo)'); + }); - it('should generate host listeners for directives within hostBindings section', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should generate host listeners for directives within hostBindings section', () => { + env.write(`test.ts`, ` import {Directive, HostListener} from '@angular/core'; @Directive({ @@ -1910,21 +1866,21 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostBindings: function Dir_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵlistener("change", function Dir_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg); }); } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); - it('should use proper default value for preserveWhitespaces config param', () => { - env.tsconfig(); // default is `false` - env.write(`test.ts`, ` + it('should use proper default value for preserveWhitespaces config param', () => { + env.tsconfig(); // default is `false` + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -1937,14 +1893,14 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('text(1, " Template with whitespaces ");'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('text(1, " Template with whitespaces ");'); + }); - it('should take preserveWhitespaces config option into account', () => { - env.tsconfig({preserveWhitespaces: true}); - env.write(`test.ts`, ` + it('should take preserveWhitespaces config option into account', () => { + env.tsconfig({preserveWhitespaces: true}); + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -1956,15 +1912,15 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('text(2, "\\n Template with whitespaces\\n ");'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents) + .toContain('text(2, "\\n Template with whitespaces\\n ");'); + }); - it('@Component\'s preserveWhitespaces should override the one defined in config', () => { - env.tsconfig({preserveWhitespaces: true}); - env.write(`test.ts`, ` + it('@Component\'s preserveWhitespaces should override the one defined in config', () => { + env.tsconfig({preserveWhitespaces: true}); + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -1977,14 +1933,14 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('text(1, " Template with whitespaces ");'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('text(1, " Template with whitespaces ");'); + }); - it('should use proper default value for i18nUseExternalIds config param', () => { - env.tsconfig(); // default is `true` - env.write(`test.ts`, ` + it('should use proper default value for i18nUseExternalIds config param', () => { + env.tsconfig(); // default is `true` + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -1992,14 +1948,14 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('MSG_EXTERNAL_8321000940098097247$$TEST_TS_1'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('MSG_EXTERNAL_8321000940098097247$$TEST_TS_1'); + }); - it('should take i18nUseExternalIds config option into account', () => { - env.tsconfig({i18nUseExternalIds: false}); - env.write(`test.ts`, ` + it('should take i18nUseExternalIds config option into account', () => { + env.tsconfig({i18nUseExternalIds: false}); + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -2007,14 +1963,13 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).not.toContain('MSG_EXTERNAL_'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).not.toContain('MSG_EXTERNAL_'); + }); - it('@Component\'s `interpolation` should override default interpolation config', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('@Component\'s `interpolation` should override default interpolation config', () => { + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-with-custom-interpolation-a', @@ -2026,14 +1981,13 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('ɵɵtextInterpolate(ctx.text)'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('ɵɵtextInterpolate(ctx.text)'); + }); - it('should handle `encapsulation` field', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should handle `encapsulation` field', () => { + env.write(`test.ts`, ` import {Component, ViewEncapsulation} from '@angular/core'; @Component({ selector: 'comp-a', @@ -2043,14 +1997,13 @@ describe('ngtsc behavioral tests', () => { class CompA {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('encapsulation: 2'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('encapsulation: 2'); + }); - it('should throw if `encapsulation` contains invalid value', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should throw if `encapsulation` contains invalid value', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ selector: 'comp-a', @@ -2059,14 +2012,13 @@ describe('ngtsc behavioral tests', () => { }) class CompA {} `); - const errors = env.driveDiagnostics(); - expect(errors[0].messageText) - .toContain('encapsulation must be a member of ViewEncapsulation enum from @angular/core'); - }); + const errors = env.driveDiagnostics(); + expect(errors[0].messageText) + .toContain('encapsulation must be a member of ViewEncapsulation enum from @angular/core'); + }); - it('should handle `changeDetection` field', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should handle `changeDetection` field', () => { + env.write(`test.ts`, ` import {Component, ChangeDetectionStrategy} from '@angular/core'; @Component({ selector: 'comp-a', @@ -2076,14 +2028,13 @@ describe('ngtsc behavioral tests', () => { class CompA {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('changeDetection: 0'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('changeDetection: 0'); + }); - it('should throw if `changeDetection` contains invalid value', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should throw if `changeDetection` contains invalid value', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ selector: 'comp-a', @@ -2092,15 +2043,14 @@ describe('ngtsc behavioral tests', () => { }) class CompA {} `); - const errors = env.driveDiagnostics(); - expect(errors[0].messageText) - .toContain( - 'changeDetection must be a member of ChangeDetectionStrategy enum from @angular/core'); - }); + const errors = env.driveDiagnostics(); + expect(errors[0].messageText) + .toContain( + 'changeDetection must be a member of ChangeDetectionStrategy enum from @angular/core'); + }); - it('should ignore empty bindings', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should ignore empty bindings', () => { + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -2108,14 +2058,13 @@ describe('ngtsc behavioral tests', () => { }) class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).not.toContain('i0.ɵɵproperty'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).not.toContain('i0.ɵɵproperty'); + }); - it('should correctly recognize local symbols', () => { - env.tsconfig(); - env.write('module.ts', ` + it('should correctly recognize local symbols', () => { + env.write('module.ts', ` import {NgModule} from '@angular/core'; import {Dir, Comp} from './test'; @@ -2125,7 +2074,7 @@ describe('ngtsc behavioral tests', () => { }) class Module {} `); - env.write(`test.ts`, ` + env.write(`test.ts`, ` import {Component, Directive} from '@angular/core'; @Directive({ @@ -2140,14 +2089,13 @@ describe('ngtsc behavioral tests', () => { export class Comp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).not.toMatch(/import \* as i[0-9] from ['"].\/test['"]/); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).not.toMatch(/import \* as i[0-9] from ['"].\/test['"]/); + }); - it('should generate exportAs declarations', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should generate exportAs declarations', () => { + env.write('test.ts', ` import {Component, Directive} from '@angular/core'; @Directive({ @@ -2157,15 +2105,14 @@ describe('ngtsc behavioral tests', () => { class Dir {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain(`exportAs: ["foo"]`); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain(`exportAs: ["foo"]`); + }); - it('should generate multiple exportAs declarations', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should generate multiple exportAs declarations', () => { + env.write('test.ts', ` import {Component, Directive} from '@angular/core'; @Directive({ @@ -2175,16 +2122,16 @@ describe('ngtsc behavioral tests', () => { class Dir {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain(`exportAs: ["foo", "bar"]`); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain(`exportAs: ["foo", "bar"]`); + }); - it('should generate correct factory stubs for a test module', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); + it('should generate correct factory stubs for a test module', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - env.write('test.ts', ` + env.write('test.ts', ` import {Injectable, NgModule} from '@angular/core'; @Injectable() @@ -2194,31 +2141,31 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.write('empty.ts', ` + env.write('empty.ts', ` import {Injectable} from '@angular/core'; @Injectable() export class NotAModule {} `); - env.driveMain(); + env.driveMain(); - const factoryContents = env.getContents('test.ngfactory.js'); - expect(factoryContents).toContain(`import * as i0 from '@angular/core';`); - expect(factoryContents).toContain(`import { NotAModule, TestModule } from './test';`); - expect(factoryContents) - .toContain(`export var TestModuleNgFactory = new i0.\u0275NgModuleFactory(TestModule);`); - expect(factoryContents).not.toContain(`NotAModuleNgFactory`); - expect(factoryContents).not.toContain('\u0275NonEmptyModule'); + const factoryContents = env.getContents('test.ngfactory.js'); + expect(factoryContents).toContain(`import * as i0 from '@angular/core';`); + expect(factoryContents).toContain(`import { NotAModule, TestModule } from './test';`); + expect(factoryContents) + .toContain(`export var TestModuleNgFactory = new i0.\u0275NgModuleFactory(TestModule);`); + expect(factoryContents).not.toContain(`NotAModuleNgFactory`); + expect(factoryContents).not.toContain('\u0275NonEmptyModule'); - const emptyFactory = env.getContents('empty.ngfactory.js'); - expect(emptyFactory).toContain(`import * as i0 from '@angular/core';`); - expect(emptyFactory).toContain(`export var \u0275NonEmptyModule = true;`); - }); + const emptyFactory = env.getContents('empty.ngfactory.js'); + expect(emptyFactory).toContain(`import * as i0 from '@angular/core';`); + expect(emptyFactory).toContain(`export var \u0275NonEmptyModule = true;`); + }); - it('should generate correct type annotation for NgModuleFactory calls in ngfactories', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); - env.write('test.ts', ` + it('should generate correct type annotation for NgModuleFactory calls in ngfactories', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -2226,72 +2173,71 @@ describe('ngtsc behavioral tests', () => { }) export class TestCmp {} `); - env.driveMain(); + env.driveMain(); - const ngfactoryContents = env.getContents('test.ngfactory.d.ts'); - expect(ngfactoryContents).toContain(`i0.ɵNgModuleFactory`); - }); + const ngfactoryContents = env.getContents('test.ngfactory.d.ts'); + expect(ngfactoryContents).toContain(`i0.ɵNgModuleFactory`); + }); - it('should copy a top-level comment into a factory stub', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); + it('should copy a top-level comment into a factory stub', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - env.write('test.ts', `/** I am a top-level comment. */ + env.write('test.ts', `/** I am a top-level comment. */ import {NgModule} from '@angular/core'; @NgModule({}) export class TestModule {} `); + env.driveMain(); - env.driveMain(); + const factoryContents = env.getContents('test.ngfactory.js'); + expect(factoryContents).toMatch(/^\/\*\* I am a top-level comment\. \*\//); + }); - const factoryContents = env.getContents('test.ngfactory.js'); - expect(factoryContents).toMatch(/^\/\*\* I am a top-level comment\. \*\//); - }); + it('should be able to compile an app using the factory shim', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - it('should be able to compile an app using the factory shim', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); - - env.write('test.ts', ` + env.write('test.ts', ` export {MyModuleNgFactory} from './my-module.ngfactory'; `); - env.write('my-module.ts', ` + env.write('my-module.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class MyModule {} `); - env.driveMain(); - }); + env.driveMain(); + }); - it('should generate correct imports in factory stubs when compiling @angular/core', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); + it('should generate correct imports in factory stubs when compiling @angular/core', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - env.write('test.ts', ` + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class TestModule {} `); - // Trick the compiler into thinking it's compiling @angular/core. - env.write('r3_symbols.ts', 'export const ITS_JUST_ANGULAR = true;'); + // Trick the compiler into thinking it's compiling @angular/core. + env.write('r3_symbols.ts', 'export const ITS_JUST_ANGULAR = true;'); - env.driveMain(); + env.driveMain(); - const factoryContents = env.getContents('test.ngfactory.js'); - expect(normalize(factoryContents)).toBe(normalize(` + const factoryContents = env.getContents('test.ngfactory.js'); + expect(normalize(factoryContents)).toBe(normalize(` import * as i0 from "./r3_symbols"; import { TestModule } from './test'; export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule); `)); - }); + }); - it('should generate a summary stub for decorated classes in the input file only', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); + it('should generate a summary stub for decorated classes in the input file only', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - env.write('test.ts', ` + env.write('test.ts', ` import {Injectable, NgModule} from '@angular/core'; export class NotAModule {} @@ -2300,16 +2246,16 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const summaryContents = env.getContents('test.ngsummary.js'); - expect(summaryContents).toEqual(`export var TestModuleNgSummary = null;\n`); - }); + const summaryContents = env.getContents('test.ngsummary.js'); + expect(summaryContents).toEqual(`export var TestModuleNgSummary = null;\n`); + }); - it('should generate a summary stub for classes exported via exports', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); + it('should generate a summary stub for classes exported via exports', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - env.write('test.ts', ` + env.write('test.ts', ` import {Injectable, NgModule} from '@angular/core'; @NgModule({}) @@ -2318,29 +2264,28 @@ describe('ngtsc behavioral tests', () => { export {NotDirectlyExported}; `); - env.driveMain(); + env.driveMain(); - const summaryContents = env.getContents('test.ngsummary.js'); - expect(summaryContents).toEqual(`export var NotDirectlyExportedNgSummary = null;\n`); - }); + const summaryContents = env.getContents('test.ngsummary.js'); + expect(summaryContents).toEqual(`export var NotDirectlyExportedNgSummary = null;\n`); + }); - it('it should generate empty export when there are no other summary symbols, to ensure the output is a valid ES module', - () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); - env.write('empty.ts', ` + it('it should generate empty export when there are no other summary symbols, to ensure the output is a valid ES module', + () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); + env.write('empty.ts', ` export class NotAModule {} `); - env.driveMain(); + env.driveMain(); - const emptySummary = env.getContents('empty.ngsummary.js'); - // The empty export ensures this js file is still an ES module. - expect(emptySummary).toEqual(`export var \u0275empty = null;\n`); - }); + const emptySummary = env.getContents('empty.ngsummary.js'); + // The empty export ensures this js file is still an ES module. + expect(emptySummary).toEqual(`export var \u0275empty = null;\n`); + }); - it('should compile a banana-in-a-box inside of a template', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile a banana-in-a-box inside of a template', () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -2350,12 +2295,11 @@ describe('ngtsc behavioral tests', () => { class TestCmp {} `); - env.driveMain(); - }); + env.driveMain(); + }); - it('generates inherited factory definitions', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('generates inherited factory definitions', () => { + env.write(`test.ts`, ` import {Injectable} from '@angular/core'; class Dep {} @@ -2377,21 +2321,20 @@ describe('ngtsc behavioral tests', () => { `); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('function Base_Factory(t) { return new (t || Base)(i0.ɵɵinject(Dep)); }'); - expect(jsContents).toContain('var \u0275Child_BaseFactory = i0.ɵɵgetInheritedFactory(Child)'); - expect(jsContents) - .toContain('function Child_Factory(t) { return \u0275Child_BaseFactory(t || Child); }'); - expect(jsContents) - .toContain('function GrandChild_Factory(t) { return new (t || GrandChild)(); }'); - }); + expect(jsContents) + .toContain('function Base_Factory(t) { return new (t || Base)(i0.ɵɵinject(Dep)); }'); + expect(jsContents).toContain('var \u0275Child_BaseFactory = i0.ɵɵgetInheritedFactory(Child)'); + expect(jsContents) + .toContain('function Child_Factory(t) { return \u0275Child_BaseFactory(t || Child); }'); + expect(jsContents) + .toContain('function GrandChild_Factory(t) { return new (t || GrandChild)(); }'); + }); - it('generates base factories for directives', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('generates base factories for directives', () => { + env.write(`test.ts`, ` import {Directive} from '@angular/core'; class Base {} @@ -2404,16 +2347,15 @@ describe('ngtsc behavioral tests', () => { `); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('var \u0275Dir_BaseFactory = i0.ɵɵgetInheritedFactory(Dir)'); - }); + expect(jsContents).toContain('var \u0275Dir_BaseFactory = i0.ɵɵgetInheritedFactory(Dir)'); + }); - it('should wrap "directives" in component metadata in a closure when forward references are present', - () => { - env.tsconfig(); - env.write('test.ts', ` + it('should wrap "directives" in component metadata in a closure when forward references are present', + () => { + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -2434,15 +2376,14 @@ describe('ngtsc behavioral tests', () => { class Module {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('directives: function () { return [CmpB]; }'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('directives: function () { return [CmpB]; }'); + }); - it('should emit setClassMetadata calls for all types', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should emit setClassMetadata calls for all types', () => { + env.write('test.ts', ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {} @@ -2452,23 +2393,22 @@ describe('ngtsc behavioral tests', () => { @Pipe({name: 'pipe'}) class TestPipe {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('\u0275setClassMetadata(TestComponent, '); - expect(jsContents).toContain('\u0275setClassMetadata(TestDirective, '); - expect(jsContents).toContain('\u0275setClassMetadata(TestInjectable, '); - expect(jsContents).toContain('\u0275setClassMetadata(TestNgModule, '); - expect(jsContents).toContain('\u0275setClassMetadata(TestPipe, '); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('\u0275setClassMetadata(TestComponent, '); + expect(jsContents).toContain('\u0275setClassMetadata(TestDirective, '); + expect(jsContents).toContain('\u0275setClassMetadata(TestInjectable, '); + expect(jsContents).toContain('\u0275setClassMetadata(TestNgModule, '); + expect(jsContents).toContain('\u0275setClassMetadata(TestPipe, '); + }); - it('should use imported types in setClassMetadata if they can be represented as values', () => { - env.tsconfig({}); + it('should use imported types in setClassMetadata if they can be represented as values', () => { - env.write(`types.ts`, ` + env.write(`types.ts`, ` export class MyTypeA {} export class MyTypeB {} `); - env.write(`test.ts`, ` + env.write(`test.ts`, ` import {Component, Inject, Injectable} from '@angular/core'; import {MyTypeA, MyTypeB} from './types'; @@ -2486,22 +2426,21 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(`import * as i1 from "./types";`); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeA')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeB')); - }); + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain(`import * as i1 from "./types";`); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeA')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeB')); + }); - it('should use imported types in setClassMetadata if they can be represented as values and imported as `* as foo`', - () => { - env.tsconfig({}); + it('should use imported types in setClassMetadata if they can be represented as values and imported as `* as foo`', + () => { - env.write(`types.ts`, ` + env.write(`types.ts`, ` export class MyTypeA {} export class MyTypeB {} `); - env.write(`test.ts`, ` + env.write(`test.ts`, ` import {Component, Inject, Injectable} from '@angular/core'; import * as types from './types'; @@ -2519,21 +2458,20 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(`import * as i1 from "./types";`); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeA')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeB')); - }); + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain(`import * as i1 from "./types";`); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeA')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeB')); + }); - it('should use default-imported types if they can be represented as values', () => { - env.tsconfig({}); + it('should use default-imported types if they can be represented as values', () => { - env.write(`types.ts`, ` + env.write(`types.ts`, ` export default class Default {} export class Other {} `); - env.write(`test.ts`, ` + env.write(`test.ts`, ` import {Component} from '@angular/core'; import {Other} from './types'; import Default from './types'; @@ -2544,23 +2482,23 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(`import Default from './types';`); - expect(jsContents).toContain(`import * as i1 from "./types";`); - expect(jsContents).toContain('i0.ɵɵdirectiveInject(Default)'); - expect(jsContents).toContain('i0.ɵɵdirectiveInject(i1.Other)'); - expect(jsContents).toMatch(setClassMetadataRegExp('type: Default')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.Other')); - }); + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain(`import Default from './types';`); + expect(jsContents).toContain(`import * as i1 from "./types";`); + expect(jsContents).toContain('i0.ɵɵdirectiveInject(Default)'); + expect(jsContents).toContain('i0.ɵɵdirectiveInject(i1.Other)'); + expect(jsContents).toMatch(setClassMetadataRegExp('type: Default')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.Other')); + }); - it('should use `undefined` in setClassMetadata if types can\'t be represented as values', () => { - env.tsconfig({}); + it('should use `undefined` in setClassMetadata if types can\'t be represented as values', + () => { - env.write(`types.ts`, ` + env.write(`types.ts`, ` export type MyType = Map; `); - env.write(`test.ts`, ` + env.write(`test.ts`, ` import {Component, Inject, Injectable} from '@angular/core'; import {MyType} from './types'; @@ -2573,17 +2511,16 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).not.toContain(`import { MyType } from './types';`); - // Note: `type: undefined` below, since MyType can't be represented as a value - expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined')); - }); + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).not.toContain(`import { MyType } from './types';`); + // Note: `type: undefined` below, since MyType can't be represented as a value + expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined')); + }); - it('should not throw in case whitespaces and HTML comments are present inside ', - () => { - env.tsconfig(); - env.write('test.ts', ` + it('should not throw in case whitespaces and HTML comments are present inside ', + () => { + env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -2596,13 +2533,12 @@ describe('ngtsc behavioral tests', () => { }) class CmpA {} `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(0); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(0); + }); - it('should compile a template using multiple directives with the same selector', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should compile a template using multiple directives with the same selector', () => { + env.write('test.ts', ` import {Component, Directive, NgModule} from '@angular/core'; @Directive({selector: '[test]'}) @@ -2622,15 +2558,14 @@ describe('ngtsc behavioral tests', () => { class Module {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toMatch(/directives: \[DirA,\s+DirB\]/); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/directives: \[DirA,\s+DirB\]/); + }); - describe('cycle detection', () => { - it('should detect a simple cycle and use remote component scoping', () => { - env.tsconfig(); - env.write('test.ts', ` + describe('cycle detection', () => { + it('should detect a simple cycle and use remote component scoping', () => { + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; import {NormalComponent} from './cyclic'; @@ -2646,7 +2581,7 @@ describe('ngtsc behavioral tests', () => { export class Module {} `); - env.write('cyclic.ts', ` + env.write('cyclic.ts', ` import {Component} from '@angular/core'; @Component({ @@ -2656,17 +2591,16 @@ describe('ngtsc behavioral tests', () => { export class NormalComponent {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch( - /i\d\.ɵɵsetComponentScope\(NormalComponent,\s+\[NormalComponent,\s+CyclicComponent\],\s+\[\]\)/); - expect(jsContents).not.toContain('/*__PURE__*/ i0.ɵɵsetComponentScope'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents) + .toMatch( + /i\d\.ɵɵsetComponentScope\(NormalComponent,\s+\[NormalComponent,\s+CyclicComponent\],\s+\[\]\)/); + expect(jsContents).not.toContain('/*__PURE__*/ i0.ɵɵsetComponentScope'); + }); - it('should detect a cycle added entirely during compilation', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should detect a cycle added entirely during compilation', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {ACmp} from './a'; import {BCmp} from './b'; @@ -2674,7 +2608,7 @@ describe('ngtsc behavioral tests', () => { @NgModule({declarations: [ACmp, BCmp]}) export class Module {} `); - env.write('a.ts', ` + env.write('a.ts', ` import {Component} from '@angular/core'; @Component({ @@ -2683,7 +2617,7 @@ describe('ngtsc behavioral tests', () => { }) export class ACmp {} `); - env.write('b.ts', ` + env.write('b.ts', ` import {Component} from '@angular/core'; @Component({ @@ -2692,16 +2626,15 @@ describe('ngtsc behavioral tests', () => { }) export class BCmp {} `); - env.driveMain(); - const aJsContents = env.getContents('a.js'); - const bJsContents = env.getContents('b.js'); - expect(aJsContents).toMatch(/import \* as i\d? from ".\/b"/); - expect(bJsContents).not.toMatch(/import \* as i\d? from ".\/a"/); - }); + env.driveMain(); + const aJsContents = env.getContents('a.js'); + const bJsContents = env.getContents('b.js'); + expect(aJsContents).toMatch(/import \* as i\d? from ".\/b"/); + expect(bJsContents).not.toMatch(/import \* as i\d? from ".\/a"/); + }); - it('should not detect a potential cycle if it doesn\'t actually happen', () => { - env.tsconfig(); - env.write('test.ts', ` + it('should not detect a potential cycle if it doesn\'t actually happen', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {ACmp} from './a'; import {BCmp} from './b'; @@ -2709,7 +2642,7 @@ describe('ngtsc behavioral tests', () => { @NgModule({declarations: [ACmp, BCmp]}) export class Module {} `); - env.write('a.ts', ` + env.write('a.ts', ` import {Component} from '@angular/core'; @Component({ @@ -2718,7 +2651,7 @@ describe('ngtsc behavioral tests', () => { }) export class ACmp {} `); - env.write('b.ts', ` + env.write('b.ts', ` import {Component} from '@angular/core'; @Component({ @@ -2727,14 +2660,14 @@ describe('ngtsc behavioral tests', () => { }) export class BCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).not.toContain('setComponentScope'); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).not.toContain('setComponentScope'); + }); }); - }); - describe('multiple local refs', () => { - const getComponentScript = (template: string): string => ` + describe('multiple local refs', () => { + const getComponentScript = (template: string): string => ` import {Component, Directive, NgModule} from '@angular/core'; @Component({selector: 'my-cmp', template: \`${template}\`}) @@ -2744,46 +2677,44 @@ describe('ngtsc behavioral tests', () => { class Module {} `; - const cases = [ - ` + const cases = [ + `
`, - ` + `
`, - ` + `
`, - ` + `
`, - ` + `
` - ]; + ]; - cases.forEach(template => { - it('should not throw', () => { - env.tsconfig(); - env.write('test.ts', getComponentScript(template)); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(0); + cases.forEach(template => { + it('should not throw', () => { + env.write('test.ts', getComponentScript(template)); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(0); + }); }); }); - }); - it('should wrap "inputs" and "outputs" keys if they contain unsafe characters', () => { - env.tsconfig({}); - env.write(`test.ts`, ` + it('should wrap "inputs" and "outputs" keys if they contain unsafe characters', () => { + env.write(`test.ts`, ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -2797,9 +2728,9 @@ describe('ngtsc behavioral tests', () => { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const inputsAndOutputs = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const inputsAndOutputs = ` inputs: { "track-type": "track-type", "track-name": "track-name", @@ -2815,16 +2746,16 @@ describe('ngtsc behavioral tests', () => { "output.event": "output.event" } `; - expect(trim(jsContents)).toContain(trim(inputsAndOutputs)); - }); + expect(trim(jsContents)).toContain(trim(inputsAndOutputs)); + }); - it('should compile programs with typeRoots', () => { - // Write out a custom tsconfig.json that includes 'typeRoots' and 'files'. 'files' is necessary - // because otherwise TS picks up the testTypeRoot/test/index.d.ts file into the program - // automatically. Shims are also turned on (via allowEmptyCodegenFiles) because the shim - // ts.CompilerHost wrapper can break typeRoot functionality (which this test is meant to - // detect). - env.write('tsconfig.json', `{ + it('should compile programs with typeRoots', () => { + // Write out a custom tsconfig.json that includes 'typeRoots' and 'files'. 'files' is + // necessary because otherwise TS picks up the testTypeRoot/test/index.d.ts file into the + // program automatically. Shims are also turned on (via allowEmptyCodegenFiles) because the + // shim ts.CompilerHost wrapper can break typeRoot functionality (which this test is meant to + // detect). + env.write('tsconfig.json', `{ "extends": "./tsconfig-base.json", "angularCompilerOptions": { "allowEmptyCodegenFiles": true @@ -2834,26 +2765,25 @@ describe('ngtsc behavioral tests', () => { }, "files": ["./test.ts"] }`); - env.write('test.ts', ` + env.write('test.ts', ` import {Test} from 'ambient'; console.log(Test); `); - env.write('testTypeRoot/.exists', ''); - env.write('testTypeRoot/test/index.d.ts', ` + env.write('testTypeRoot/.exists', ''); + env.write('testTypeRoot/test/index.d.ts', ` declare module 'ambient' { export const Test = 'This is a test'; } `); - env.driveMain(); + env.driveMain(); - // Success is enough to indicate that this passes. - }); + // Success is enough to indicate that this passes. + }); - describe('when processing external directives', () => { - it('should not emit multiple references to the same directive', () => { - env.tsconfig(); - env.write('node_modules/external/index.d.ts', ` + describe('when processing external directives', () => { + it('should not emit multiple references to the same directive', () => { + env.write('node_modules/external/index.d.ts', ` import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class ExternalDir { @@ -2864,7 +2794,7 @@ describe('ngtsc behavioral tests', () => { static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); - env.write('test.ts', ` + env.write('test.ts', ` import {Component, Directive, NgModule} from '@angular/core'; import {ExternalModule} from 'external'; @@ -2882,14 +2812,13 @@ describe('ngtsc behavioral tests', () => { class Module {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toMatch(/directives: \[i1\.ExternalDir\]/); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/directives: \[i1\.ExternalDir\]/); + }); - it('should import directives by their external name', () => { - env.tsconfig(); - env.write('node_modules/external/index.d.ts', ` + it('should import directives by their external name', () => { + env.write('node_modules/external/index.d.ts', ` import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; import {InternalDir} from './internal'; @@ -2899,13 +2828,13 @@ describe('ngtsc behavioral tests', () => { static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); - env.write('node_modules/external/internal.d.ts', ` + env.write('node_modules/external/internal.d.ts', ` export declare class InternalDir { static ngDirectiveDef: ɵɵDirectiveDefWithMeta; } `); - env.write('test.ts', ` + env.write('test.ts', ` import {Component, Directive, NgModule} from '@angular/core'; import {ExternalModule} from 'external'; @@ -2921,78 +2850,78 @@ describe('ngtsc behavioral tests', () => { class Module {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toMatch(/directives: \[i1\.ExternalDir\]/); - }); - }); - - describe('flat module indices', () => { - it('should generate a basic flat module index', () => { - env.tsconfig({ - 'flatModuleOutFile': 'flat.js', + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/directives: \[i1\.ExternalDir\]/); }); - env.write('test.ts', 'export const TEST = "this is a test";'); - - env.driveMain(); - const jsContents = env.getContents('flat.js'); - expect(jsContents).toContain('export * from \'./test\';'); }); - it('should determine the flat module entry-point within multiple root files', () => { - env.tsconfig({ - 'flatModuleOutFile': 'flat.js', - }); - env.write('ignored.ts', 'export const TEST = "this is ignored";'); - env.write('index.ts', 'export const ENTRY = "this is the entry";'); + describe('flat module indices', () => { + it('should generate a basic flat module index', () => { + env.tsconfig({ + 'flatModuleOutFile': 'flat.js', + }); + env.write('test.ts', 'export const TEST = "this is a test";'); - env.driveMain(); - const jsContents = env.getContents('flat.js'); - expect(jsContents) - .toContain( - 'export * from \'./index\';', - 'Should detect the "index.ts" file as flat module entry-point.'); - }); - - it('should generate a flat module with an id', () => { - env.tsconfig({ - 'flatModuleOutFile': 'flat.js', - 'flatModuleId': '@mymodule', - }); - env.write('test.ts', 'export const TEST = "this is a test";'); - - env.driveMain(); - const dtsContents = env.getContents('flat.d.ts'); - expect(dtsContents).toContain('/// '); - }); - - it('should generate a proper flat module index file when nested', () => { - env.tsconfig({ - 'flatModuleOutFile': './public-api/index.js', + env.driveMain(); + const jsContents = env.getContents('flat.js'); + expect(jsContents).toContain('export * from \'./test\';'); }); - env.write('test.ts', `export const SOME_EXPORT = 'some-export'`); - env.driveMain(); + it('should determine the flat module entry-point within multiple root files', () => { + env.tsconfig({ + 'flatModuleOutFile': 'flat.js', + }); + env.write('ignored.ts', 'export const TEST = "this is ignored";'); + env.write('index.ts', 'export const ENTRY = "this is the entry";'); - expect(env.getContents('./public-api/index.js')).toContain(`export * from '../test';`); - }); + env.driveMain(); + const jsContents = env.getContents('flat.js'); + expect(jsContents) + .toContain( + 'export * from \'./index\';', + 'Should detect the "index.ts" file as flat module entry-point.'); + }); - it('should report an error when a flat module index is requested but no entrypoint can be determined', - () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', 'export class Foo {}'); - env.write('test2.ts', 'export class Bar {}'); + it('should generate a flat module with an id', () => { + env.tsconfig({ + 'flatModuleOutFile': 'flat.js', + 'flatModuleId': '@mymodule', + }); + env.write('test.ts', 'export const TEST = "this is a test";'); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(errors[0].messageText) - .toBe( - 'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.'); - }); + env.driveMain(); + const dtsContents = env.getContents('flat.d.ts'); + expect(dtsContents).toContain('/// '); + }); - it('should report an error when a visible directive is not exported', () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + it('should generate a proper flat module index file when nested', () => { + env.tsconfig({ + 'flatModuleOutFile': './public-api/index.js', + }); + + env.write('test.ts', `export const SOME_EXPORT = 'some-export'`); + env.driveMain(); + + expect(env.getContents('./public-api/index.js')).toContain(`export * from '../test';`); + }); + + it('should report an error when a flat module index is requested but no entrypoint can be determined', + () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write('test.ts', 'export class Foo {}'); + env.write('test2.ts', 'export class Bar {}'); + + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(errors[0].messageText) + .toBe( + 'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.'); + }); + + it('should report an error when a visible directive is not exported', () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write('test.ts', ` import {Directive, NgModule} from '@angular/core'; // The directive is not exported. @@ -3004,23 +2933,23 @@ describe('ngtsc behavioral tests', () => { export class Module {} `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(errors[0].messageText) - .toBe( - 'Unsupported private class Dir. This class is visible ' + - 'to consumers via Module -> Dir, but is not exported from the top-level library ' + - 'entrypoint.'); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(errors[0].messageText) + .toBe( + 'Unsupported private class Dir. This class is visible ' + + 'to consumers via Module -> Dir, but is not exported from the top-level library ' + + 'entrypoint.'); - // Verify that the error is for the correct class. - const id = expectTokenAtPosition(errors[0].file !, errors[0].start !, ts.isIdentifier); - expect(id.text).toBe('Dir'); - expect(ts.isClassDeclaration(id.parent)).toBe(true); - }); + // Verify that the error is for the correct class. + const id = expectTokenAtPosition(errors[0].file !, errors[0].start !, ts.isIdentifier); + expect(id.text).toBe('Dir'); + expect(ts.isClassDeclaration(id.parent)).toBe(true); + }); - it('should report an error when a deeply visible directive is not exported', () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + it('should report an error when a deeply visible directive is not exported', () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write('test.ts', ` import {Directive, NgModule} from '@angular/core'; // The directive is not exported. @@ -3036,23 +2965,23 @@ describe('ngtsc behavioral tests', () => { export class Module {} `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(2); - expect(errors[0].messageText) - .toBe( - 'Unsupported private class DirModule. This class is ' + - 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + - 'library entrypoint.'); - expect(errors[1].messageText) - .toBe( - 'Unsupported private class Dir. This class is visible ' + - 'to consumers via Module -> DirModule -> Dir, but is not exported from the top-level ' + - 'library entrypoint.'); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(2); + expect(errors[0].messageText) + .toBe( + 'Unsupported private class DirModule. This class is ' + + 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + + 'library entrypoint.'); + expect(errors[1].messageText) + .toBe( + 'Unsupported private class Dir. This class is visible ' + + 'to consumers via Module -> DirModule -> Dir, but is not exported from the top-level ' + + 'library entrypoint.'); + }); - it('should report an error when a deeply visible module is not exported', () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + it('should report an error when a deeply visible module is not exported', () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write('test.ts', ` import {Directive, NgModule} from '@angular/core'; // The directive is exported. @@ -3068,18 +2997,19 @@ describe('ngtsc behavioral tests', () => { export class Module {} `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(errors[0].messageText) - .toBe( - 'Unsupported private class DirModule. This class is ' + - 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + - 'library entrypoint.'); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(errors[0].messageText) + .toBe( + 'Unsupported private class DirModule. This class is ' + + 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + + 'library entrypoint.'); + }); - it('should not report an error when a non-exported module is imported by a visible one', () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + it('should not report an error when a non-exported module is imported by a visible one', + () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write('test.ts', ` import {Directive, NgModule} from '@angular/core'; // The directive is not exported. @@ -3096,13 +3026,13 @@ describe('ngtsc behavioral tests', () => { export class Module {} `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(0); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(0); + }); - it('should not report an error when re-exporting an external symbol', () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + it('should not report an error when re-exporting an external symbol', () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write('test.ts', ` import {Directive, NgModule} from '@angular/core'; import {ExternalModule} from 'external'; @@ -3110,7 +3040,7 @@ describe('ngtsc behavioral tests', () => { @NgModule({exports: [ExternalModule]}) export class Module {} `); - env.write('node_modules/external/index.d.ts', ` + env.write('node_modules/external/index.d.ts', ` import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class ExternalDir { @@ -3122,44 +3052,47 @@ describe('ngtsc behavioral tests', () => { } `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(0); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(0); + }); }); - }); - it('should execute custom transformers', () => { - let beforeCount = 0; - let afterCount = 0; + it('should execute custom transformers', () => { + let beforeCount = 0; + let afterCount = 0; - env.tsconfig(); - env.write('test.ts', ` + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) class Module {} `); - env.driveMain({ - beforeTs: [() => (sourceFile: ts.SourceFile) => { - beforeCount++; - return sourceFile; - }], - afterTs: [() => (sourceFile: ts.SourceFile) => { - afterCount++; - return sourceFile; - }], + env.driveMain({ + beforeTs: [() => (sourceFile: ts.SourceFile) => { + beforeCount++; + return sourceFile; + }], + afterTs: [() => (sourceFile: ts.SourceFile) => { + afterCount++; + return sourceFile; + }], + }); + + expect(beforeCount).toBe(1); + expect(afterCount).toBe(1); }); - expect(beforeCount).toBe(1); - expect(afterCount).toBe(1); - }); - - describe('@fileoverview Closure annotations', () => { - it('should be produced if not present in source file', () => { - env.tsconfig({ - 'annotateForClosureCompiler': true, - }); - env.write(`test.ts`, ` + // These tests trigger the Tsickle compiler which asserts that the file-paths + // are valid for the real OS. When on non-Windows systems it doesn't like paths + // that start with `C:`. + if (os !== 'Windows' || platform() === 'win32') { + describe('@fileoverview Closure annotations', () => { + it('should be produced if not present in source file', () => { + env.tsconfig({ + 'annotateForClosureCompiler': true, + }); + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ @@ -3168,39 +3101,39 @@ describe('ngtsc behavioral tests', () => { export class SomeComp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const fileoverview = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const fileoverview = ` /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ `; - expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); - }); + expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); + }); - it('should be produced for empty source files', () => { - env.tsconfig({ - 'annotateForClosureCompiler': true, - }); - env.write(`test.ts`, ``); + it('should be produced for empty source files', () => { + env.tsconfig({ + 'annotateForClosureCompiler': true, + }); + env.write(`test.ts`, ``); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const fileoverview = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const fileoverview = ` /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ `; - expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); - }); + expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); + }); - it('should always be at the very beginning of a script (if placed above imports)', () => { - env.tsconfig({ - 'annotateForClosureCompiler': true, - }); - env.write(`test.ts`, ` + it('should always be at the very beginning of a script (if placed above imports)', () => { + env.tsconfig({ + 'annotateForClosureCompiler': true, + }); + env.write(`test.ts`, ` /** * @fileoverview Some Comp overview * @modName {some_comp} @@ -3214,9 +3147,9 @@ describe('ngtsc behavioral tests', () => { export class SomeComp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const fileoverview = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const fileoverview = ` /** * * @fileoverview Some Comp overview @@ -3225,14 +3158,15 @@ describe('ngtsc behavioral tests', () => { * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ `; - expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); - }); + expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); + }); - it('should always be at the very beginning of a script (if placed above non-imports)', () => { - env.tsconfig({ - 'annotateForClosureCompiler': true, - }); - env.write(`test.ts`, ` + it('should always be at the very beginning of a script (if placed above non-imports)', + () => { + env.tsconfig({ + 'annotateForClosureCompiler': true, + }); + env.write(`test.ts`, ` /** * @fileoverview Some Comp overview * @modName {some_comp} @@ -3242,9 +3176,9 @@ describe('ngtsc behavioral tests', () => { const testFn = function() { return true; } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const fileoverview = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const fileoverview = ` /** * * @fileoverview Some Comp overview @@ -3253,14 +3187,15 @@ describe('ngtsc behavioral tests', () => { * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ `; - expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); - }); - }); + expect(trim(jsContents).startsWith(trim(fileoverview))).toBeTruthy(); + }); + }); + } - describe('sanitization', () => { - it('should generate sanitizers for unsafe attributes in hostBindings fn in Directives', () => { - env.tsconfig(); - env.write(`test.ts`, ` + describe('sanitization', () => { + it('should generate sanitizers for unsafe attributes in hostBindings fn in Directives', + () => { + env.write(`test.ts`, ` import {Component, Directive, HostBinding} from '@angular/core'; @Directive({ @@ -3293,9 +3228,9 @@ describe('ngtsc behavioral tests', () => { class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostBindings: function UnsafeAttrsDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵallocHostVars(6); @@ -3310,12 +3245,12 @@ describe('ngtsc behavioral tests', () => { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); - it('should generate sanitizers for unsafe properties in hostBindings fn in Directives', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should generate sanitizers for unsafe properties in hostBindings fn in Directives', + () => { + env.write(`test.ts`, ` import {Component, Directive, HostBinding} from '@angular/core'; @Directive({ @@ -3348,9 +3283,9 @@ describe('ngtsc behavioral tests', () => { class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostBindings: function UnsafePropsDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵallocHostVars(6); @@ -3365,12 +3300,12 @@ describe('ngtsc behavioral tests', () => { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); - it('should not generate sanitizers for URL properties in hostBindings fn in Component', () => { - env.tsconfig(); - env.write(`test.ts`, ` + it('should not generate sanitizers for URL properties in hostBindings fn in Component', + () => { + env.write(`test.ts`, ` import {Component} from '@angular/core'; @Component({ @@ -3388,9 +3323,9 @@ describe('ngtsc behavioral tests', () => { class FooCmp {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵallocHostVars(6); @@ -3405,12 +3340,12 @@ describe('ngtsc behavioral tests', () => { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); }); - }); - describe('listLazyRoutes()', () => { - // clang-format off + describe('listLazyRoutes()', () => { + // clang-format off const lazyRouteMatching = ( route: string, fromModulePath: RegExp, fromModuleName: string, toModulePath: RegExp, toModuleName: string) => { @@ -3426,11 +3361,10 @@ describe('ngtsc behavioral tests', () => { }), } as unknown as LazyRoute; }; - // clang-format on + // clang-format on - beforeEach(() => { - env.tsconfig(); - env.write('node_modules/@angular/router/index.d.ts', ` + beforeEach(() => { + env.write('node_modules/@angular/router/index.d.ts', ` import {ModuleWithProviders, ɵɵNgModuleDefWithMeta as ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare var ROUTES; @@ -3440,11 +3374,11 @@ describe('ngtsc behavioral tests', () => { static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); - }); + }); - describe('when called without arguments', () => { - it('should list all routes', () => { - env.write('test.ts', ` + describe('when called without arguments', () => { + it('should list all routes', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3458,13 +3392,13 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('lazy/lazy-1.ts', ` + env.write('lazy/lazy-1.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy1Module {} `); - env.write('lazy/lazy-2.ts', ` + env.write('lazy/lazy-2.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3477,29 +3411,29 @@ describe('ngtsc behavioral tests', () => { }) export class Lazy2Module {} `); - env.write('lazy/lazy-3.ts', ` + env.write('lazy/lazy-3.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy3Module {} `); - const routes = env.driveRoutes(); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy-3#Lazy3Module', /\/lazy\/lazy-2\.ts$/, 'Lazy2Module', /\/lazy\/lazy-3\.ts$/, - 'Lazy3Module'), - lazyRouteMatching( - './lazy/lazy-1#Lazy1Module', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy-1\.ts$/, - 'Lazy1Module'), - lazyRouteMatching( - './lazy/lazy-2#Lazy2Module', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy-2\.ts$/, - 'Lazy2Module'), - ]); - }); + const routes = env.driveRoutes(); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-3#Lazy3Module', /\/lazy\/lazy-2\.ts$/, 'Lazy2Module', /\/lazy\/lazy-3\.ts$/, + 'Lazy3Module'), + lazyRouteMatching( + './lazy/lazy-1#Lazy1Module', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy-1\.ts$/, + 'Lazy1Module'), + lazyRouteMatching( + './lazy/lazy-2#Lazy2Module', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy-2\.ts$/, + 'Lazy2Module'), + ]); + }); - it('should detect lazy routes in simple children routes', () => { - env.write('test.ts', ` + it('should detect lazy routes in simple children routes', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3521,7 +3455,7 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('lazy.ts', ` + env.write('lazy.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3529,16 +3463,16 @@ describe('ngtsc behavioral tests', () => { export class LazyModule {} `); - const routes = env.driveRoutes(); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\.ts$/, 'LazyModule'), - ]); - }); + const routes = env.driveRoutes(); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\.ts$/, 'LazyModule'), + ]); + }); - it('should detect lazy routes in all root directories', () => { - env.tsconfig({}, ['./foo/other-root-dir', './bar/other-root-dir']); - env.write('src/test.ts', ` + it('should detect lazy routes in all root directories', () => { + env.tsconfig({}, ['./foo/other-root-dir', './bar/other-root-dir']); + env.write('src/test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3551,7 +3485,7 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('foo/other-root-dir/src/lazy-foo.ts', ` + env.write('foo/other-root-dir/src/lazy-foo.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3564,7 +3498,7 @@ describe('ngtsc behavioral tests', () => { }) export class LazyFooModule {} `); - env.write('bar/other-root-dir/src/lazy-bar.ts', ` + env.write('bar/other-root-dir/src/lazy-bar.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3577,32 +3511,32 @@ describe('ngtsc behavioral tests', () => { }) export class LazyBarModule {} `); - env.write('bar/other-root-dir/src/lazier-bar.ts', ` + env.write('bar/other-root-dir/src/lazier-bar.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class LazierBarModule {} `); - const routes = env.driveRoutes(); + const routes = env.driveRoutes(); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy-foo#LazyFooModule', /\/test\.ts$/, 'TestModule', - /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, 'LazyFooModule'), - lazyRouteMatching( - './lazy-bar#LazyBarModule', /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, - 'LazyFooModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, 'LazyBarModule'), - lazyRouteMatching( - './lazier-bar#LazierBarModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, - 'LazyBarModule', /\/bar\/other-root-dir\/src\/lazier-bar\.ts$/, 'LazierBarModule'), - ]); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-foo#LazyFooModule', /\/test\.ts$/, 'TestModule', + /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, 'LazyFooModule'), + lazyRouteMatching( + './lazy-bar#LazyBarModule', /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, + 'LazyFooModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, 'LazyBarModule'), + lazyRouteMatching( + './lazier-bar#LazierBarModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, + 'LazyBarModule', /\/bar\/other-root-dir\/src\/lazier-bar\.ts$/, 'LazierBarModule'), + ]); + }); }); - }); - describe('when called with entry module', () => { - it('should throw if the entry module hasn\'t been analyzed', () => { - env.write('test.ts', ` + describe('when called with entry module', () => { + it('should throw if the entry module hasn\'t been analyzed', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3616,19 +3550,19 @@ describe('ngtsc behavioral tests', () => { export class TestModule {} `); - const entryModule1 = path.posix.join(env.basePath, 'test#TestModule'); - const entryModule2 = path.posix.join(env.basePath, 'not-test#TestModule'); - const entryModule3 = path.posix.join(env.basePath, 'test#NotTestModule'); + const entryModule1 = absoluteFrom('/test#TestModule'); + const entryModule2 = absoluteFrom('/not-test#TestModule'); + const entryModule3 = absoluteFrom('/test#NotTestModule'); - expect(() => env.driveRoutes(entryModule1)).not.toThrow(); - expect(() => env.driveRoutes(entryModule2)) - .toThrowError(`Failed to list lazy routes: Unknown module '${entryModule2}'.`); - expect(() => env.driveRoutes(entryModule3)) - .toThrowError(`Failed to list lazy routes: Unknown module '${entryModule3}'.`); - }); + expect(() => env.driveRoutes(entryModule1)).not.toThrow(); + expect(() => env.driveRoutes(entryModule2)) + .toThrowError(`Failed to list lazy routes: Unknown module '${entryModule2}'.`); + expect(() => env.driveRoutes(entryModule3)) + .toThrowError(`Failed to list lazy routes: Unknown module '${entryModule3}'.`); + }); - it('should list all transitive lazy routes', () => { - env.write('test.ts', ` + it('should list all transitive lazy routes', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; import {Test1Module as Test1ModuleRenamed} from './test-1'; @@ -3647,7 +3581,7 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('test-1.ts', ` + env.write('test-1.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3660,7 +3594,7 @@ describe('ngtsc behavioral tests', () => { }) export class Test1Module {} `); - env.write('test-2.ts', ` + env.write('test-2.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3673,42 +3607,42 @@ describe('ngtsc behavioral tests', () => { }) export class Test2Module {} `); - env.write('lazy/lazy.ts', ` + env.write('lazy/lazy.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class LazyModule {} `); - env.write('lazy-1/lazy-1.ts', ` + env.write('lazy-1/lazy-1.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy1Module {} `); - env.write('lazy-2/lazy-2.ts', ` + env.write('lazy-2/lazy-2.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy2Module {} `); - const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + const routes = env.driveRoutes(absoluteFrom('/test#TestModule')); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, - 'LazyModule'), - lazyRouteMatching( - './lazy-1/lazy-1#Lazy1Module', /\/test-1\.ts$/, 'Test1Module', - /\/lazy-1\/lazy-1\.ts$/, 'Lazy1Module'), - lazyRouteMatching( - './lazy-2/lazy-2#Lazy2Module', /\/test-2\.ts$/, 'Test2Module', - /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), - ]); - }); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + lazyRouteMatching( + './lazy-1/lazy-1#Lazy1Module', /\/test-1\.ts$/, 'Test1Module', + /\/lazy-1\/lazy-1\.ts$/, 'Lazy1Module'), + lazyRouteMatching( + './lazy-2/lazy-2#Lazy2Module', /\/test-2\.ts$/, 'Test2Module', + /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), + ]); + }); - it('should ignore exports that do not refer to an `NgModule`', () => { - env.write('test-1.ts', ` + it('should ignore exports that do not refer to an `NgModule`', () => { + env.write('test-1.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; import {Test2Component, Test2Module} from './test-2'; @@ -3727,7 +3661,7 @@ describe('ngtsc behavioral tests', () => { }) export class Test1Module {} `); - env.write('test-2.ts', ` + env.write('test-2.ts', ` import {Component, NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3750,33 +3684,33 @@ describe('ngtsc behavioral tests', () => { }) export class Test2Module {} `); - env.write('lazy-1/lazy-1.ts', ` + env.write('lazy-1/lazy-1.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy1Module {} `); - env.write('lazy-2/lazy-2.ts', ` + env.write('lazy-2/lazy-2.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy2Module {} `); - const routes = env.driveRoutes(path.join(env.basePath, 'test-1#Test1Module')); + const routes = env.driveRoutes(absoluteFrom('/test-1#Test1Module')); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy-1/lazy-1#Lazy1Module', /\/test-1\.ts$/, 'Test1Module', - /\/lazy-1\/lazy-1\.ts$/, 'Lazy1Module'), - lazyRouteMatching( - './lazy-2/lazy-2#Lazy2Module', /\/test-2\.ts$/, 'Test2Module', - /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), - ]); - }); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-1/lazy-1#Lazy1Module', /\/test-1\.ts$/, 'Test1Module', + /\/lazy-1\/lazy-1\.ts$/, 'Lazy1Module'), + lazyRouteMatching( + './lazy-2/lazy-2#Lazy2Module', /\/test-2\.ts$/, 'Test2Module', + /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), + ]); + }); - it('should support `ModuleWithProviders`', () => { - env.write('test.ts', ` + it('should support `ModuleWithProviders`', () => { + env.write('test.ts', ` import {ModuleWithProviders, NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3806,33 +3740,33 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('lazy-1/lazy-1.ts', ` + env.write('lazy-1/lazy-1.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy1Module {} `); - env.write('lazy-2/lazy-2.ts', ` + env.write('lazy-2/lazy-2.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class Lazy2Module {} `); - const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + const routes = env.driveRoutes(absoluteFrom('/test#TestModule')); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy-1/lazy-1#Lazy1Module', /\/test\.ts$/, 'TestModule', /\/lazy-1\/lazy-1\.ts$/, - 'Lazy1Module'), - lazyRouteMatching( - './lazy-2/lazy-2#Lazy2Module', /\/test\.ts$/, 'TestRoutingModule', - /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), - ]); - }); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-1/lazy-1#Lazy1Module', /\/test\.ts$/, 'TestModule', /\/lazy-1\/lazy-1\.ts$/, + 'Lazy1Module'), + lazyRouteMatching( + './lazy-2/lazy-2#Lazy2Module', /\/test\.ts$/, 'TestRoutingModule', + /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), + ]); + }); - it('should only process each module once', () => { - env.write('test.ts', ` + it('should only process each module once', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3855,7 +3789,7 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('lazy/lazy.ts', ` + env.write('lazy/lazy.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3868,33 +3802,33 @@ describe('ngtsc behavioral tests', () => { }) export class LazyModule {} `); - env.write('lazier/lazier.ts', ` + env.write('lazier/lazier.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class LazierModule {} `); - const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + const routes = env.driveRoutes(absoluteFrom('/test#TestModule')); - // `LazyModule` is referenced in both `SharedModule` and `TestModule`, - // but it is only processed once (hence one `LazierModule` entry). - expect(routes).toEqual([ - lazyRouteMatching( - './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, - 'LazyModule'), - lazyRouteMatching( - './lazy/lazy#LazyModule', /\/test\.ts$/, 'SharedModule', /\/lazy\/lazy\.ts$/, - 'LazyModule'), - lazyRouteMatching( - '../lazier/lazier#LazierModule', /\/lazy\/lazy\.ts$/, 'LazyModule', - /\/lazier\/lazier\.ts$/, 'LazierModule'), - ]); - }); + // `LazyModule` is referenced in both `SharedModule` and `TestModule`, + // but it is only processed once (hence one `LazierModule` entry). + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'SharedModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + lazyRouteMatching( + '../lazier/lazier#LazierModule', /\/lazy\/lazy\.ts$/, 'LazyModule', + /\/lazier\/lazier\.ts$/, 'LazierModule'), + ]); + }); - it('should detect lazy routes in all root directories', () => { - env.tsconfig({}, ['./foo/other-root-dir', './bar/other-root-dir']); - env.write('src/test.ts', ` + it('should detect lazy routes in all root directories', () => { + env.tsconfig({}, ['./foo/other-root-dir', './bar/other-root-dir']); + env.write('src/test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3907,7 +3841,7 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('foo/other-root-dir/src/lazy-foo.ts', ` + env.write('foo/other-root-dir/src/lazy-foo.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3920,7 +3854,7 @@ describe('ngtsc behavioral tests', () => { }) export class LazyFooModule {} `); - env.write('bar/other-root-dir/src/lazy-bar.ts', ` + env.write('bar/other-root-dir/src/lazy-bar.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3933,30 +3867,30 @@ describe('ngtsc behavioral tests', () => { }) export class LazyBarModule {} `); - env.write('bar/other-root-dir/src/lazier-bar.ts', ` + env.write('bar/other-root-dir/src/lazier-bar.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class LazierBarModule {} `); - const routes = env.driveRoutes(path.join(env.basePath, 'src/test#TestModule')); + const routes = env.driveRoutes(absoluteFrom('/src/test#TestModule')); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy-foo#LazyFooModule', /\/test\.ts$/, 'TestModule', - /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, 'LazyFooModule'), - lazyRouteMatching( - './lazy-bar#LazyBarModule', /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, - 'LazyFooModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, 'LazyBarModule'), - lazyRouteMatching( - './lazier-bar#LazierBarModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, - 'LazyBarModule', /\/bar\/other-root-dir\/src\/lazier-bar\.ts$/, 'LazierBarModule'), - ]); - }); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-foo#LazyFooModule', /\/test\.ts$/, 'TestModule', + /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, 'LazyFooModule'), + lazyRouteMatching( + './lazy-bar#LazyBarModule', /\/foo\/other-root-dir\/src\/lazy-foo\.ts$/, + 'LazyFooModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, 'LazyBarModule'), + lazyRouteMatching( + './lazier-bar#LazierBarModule', /\/bar\/other-root-dir\/src\/lazy-bar\.ts$/, + 'LazyBarModule', /\/bar\/other-root-dir\/src\/lazier-bar\.ts$/, 'LazierBarModule'), + ]); + }); - it('should ignore modules not (transitively) referenced by the entry module', () => { - env.write('test.ts', ` + it('should ignore modules not (transitively) referenced by the entry module', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -3978,7 +3912,7 @@ describe('ngtsc behavioral tests', () => { }) export class Test2Module {} `); - env.write('lazy/lazy.ts', ` + env.write('lazy/lazy.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) @@ -3988,17 +3922,17 @@ describe('ngtsc behavioral tests', () => { export class Lazy2Module {} `); - const routes = env.driveRoutes(path.join(env.basePath, 'test#Test1Module')); + const routes = env.driveRoutes(absoluteFrom('/test#Test1Module')); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy/lazy#Lazy1Module', /\/test\.ts$/, 'Test1Module', /\/lazy\/lazy\.ts$/, - 'Lazy1Module'), - ]); - }); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#Lazy1Module', /\/test\.ts$/, 'Test1Module', /\/lazy\/lazy\.ts$/, + 'Lazy1Module'), + ]); + }); - it('should ignore routes to unknown modules', () => { - env.write('test.ts', ` + it('should ignore routes to unknown modules', () => { + env.write('test.ts', ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @@ -4012,61 +3946,61 @@ describe('ngtsc behavioral tests', () => { }) export class TestModule {} `); - env.write('lazy/lazy.ts', ` + env.write('lazy/lazy.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class LazyModule {} `); - const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + const routes = env.driveRoutes(absoluteFrom('/test#TestModule')); - expect(routes).toEqual([ - lazyRouteMatching( - './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, - 'LazyModule'), - ]); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + ]); + }); }); }); - }); - describe('ivy switch mode', () => { - it('should allow for symbols to be renamed when they use a SWITCH_IVY naming mechanism', () => { - env.tsconfig(); - env.write('test.ts', ` + describe('ivy switch mode', () => { + it('should allow for symbols to be renamed when they use a SWITCH_IVY naming mechanism', + () => { + env.write('test.ts', ` export const FooCmp__POST_R3__ = 1; export const FooCmp__PRE_R3__ = 2; export const FooCmp = FooCmp__PRE_R3__;`); - env.driveMain(); + env.driveMain(); - const source = env.getContents('test.js'); - expect(source).toContain(`export var FooCmp = FooCmp__POST_R3__`); - expect(source).not.toContain(`export var FooCmp = FooCmp__PRE_R3__`); - }); + const source = env.getContents('test.js'); + expect(source).toContain(`export var FooCmp = FooCmp__POST_R3__`); + expect(source).not.toContain(`export var FooCmp = FooCmp__PRE_R3__`); + }); - it('should allow for SWITCH_IVY naming even even if it occurs outside of core', () => { - const content = ` + it('should allow for SWITCH_IVY naming even even if it occurs outside of core', () => { + const content = ` export const Foo__POST_R3__ = 1; export const Foo__PRE_R3__ = 2; export const Foo = Foo__PRE_R3__; `; - env.tsconfig(); - env.write('test_outside_angular_core.ts', content); - env.write('test_inside_angular_core.ts', content + '\nexport const ITS_JUST_ANGULAR = true;'); - env.driveMain(); + env.write('test_outside_angular_core.ts', content); + env.write( + 'test_inside_angular_core.ts', content + '\nexport const ITS_JUST_ANGULAR = true;'); + env.driveMain(); - const sourceTestOutsideAngularCore = env.getContents('test_outside_angular_core.js'); - const sourceTestInsideAngularCore = env.getContents('test_inside_angular_core.js'); - expect(sourceTestInsideAngularCore).toContain(sourceTestOutsideAngularCore); + const sourceTestOutsideAngularCore = env.getContents('test_outside_angular_core.js'); + const sourceTestInsideAngularCore = env.getContents('test_inside_angular_core.js'); + expect(sourceTestInsideAngularCore).toContain(sourceTestOutsideAngularCore); + }); }); - }); - describe('NgModule export aliasing', () => { - it('should use an alias to import a directive from a deep dependency', () => { - env.tsconfig({'_useHostForImportGeneration': true}); + describe('NgModule export aliasing', () => { + it('should use an alias to import a directive from a deep dependency', () => { + env.tsconfig({'_useHostForImportGeneration': true}); - // 'alpha' declares the directive which will ultimately be imported. - env.write('alpha.d.ts', ` + // 'alpha' declares the directive which will ultimately be imported. + env.write('alpha.d.ts', ` import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class ExternalDir { @@ -4078,8 +4012,8 @@ export const Foo = Foo__PRE_R3__; } `); - // 'beta' re-exports AlphaModule from alpha. - env.write('beta.d.ts', ` + // 'beta' re-exports AlphaModule from alpha. + env.write('beta.d.ts', ` import {ɵɵNgModuleDefWithMeta} from '@angular/core'; import {AlphaModule} from './alpha'; @@ -4088,8 +4022,9 @@ export const Foo = Foo__PRE_R3__; } `); - // The application imports BetaModule from beta, gaining visibility of ExternalDir from alpha. - env.write('test.ts', ` + // The application imports BetaModule from beta, gaining visibility of ExternalDir from + // alpha. + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; import {BetaModule} from './beta'; @@ -4105,17 +4040,17 @@ export const Foo = Foo__PRE_R3__; }) export class Module {} `); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - // Expect that ExternalDir from alpha is imported via the re-export from beta. - expect(jsContents).toContain('import * as i1 from "root/beta";'); - expect(jsContents).toContain('directives: [i1.\u0275ng$root$alpha$$ExternalDir]'); - }); + // Expect that ExternalDir from alpha is imported via the re-export from beta. + expect(jsContents).toContain('import * as i1 from "root/beta";'); + expect(jsContents).toContain('directives: [i1.\u0275ng$root$alpha$$ExternalDir]'); + }); - it('should write alias ES2015 exports for NgModule exported directives', () => { - env.tsconfig({'_useHostForImportGeneration': true}); - env.write('external.d.ts', ` + it('should write alias ES2015 exports for NgModule exported directives', () => { + env.tsconfig({'_useHostForImportGeneration': true}); + env.write('external.d.ts', ` import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; import {LibModule} from './lib'; @@ -4127,7 +4062,7 @@ export const Foo = Foo__PRE_R3__; static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); - env.write('lib.d.ts', ` + env.write('lib.d.ts', ` import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class LibDir { @@ -4138,7 +4073,7 @@ export const Foo = Foo__PRE_R3__; static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); - env.write('foo.ts', ` + env.write('foo.ts', ` import {Directive, NgModule} from '@angular/core'; import {ExternalModule} from './external'; @@ -4151,7 +4086,7 @@ export const Foo = Foo__PRE_R3__; }) export class FooModule {} `); - env.write('index.ts', ` + env.write('index.ts', ` import {Component, NgModule} from '@angular/core'; import {FooModule} from './foo'; @@ -4167,15 +4102,15 @@ export const Foo = Foo__PRE_R3__; }) export class IndexModule {} `); - env.driveMain(); - const jsContents = env.getContents('index.js'); - expect(jsContents) - .toContain('export { FooDir as \u0275ng$root$foo$$FooDir } from "root/foo";'); - }); + env.driveMain(); + const jsContents = env.getContents('index.js'); + expect(jsContents) + .toContain('export { FooDir as \u0275ng$root$foo$$FooDir } from "root/foo";'); + }); - it('should escape unusual characters in aliased filenames', () => { - env.tsconfig({'_useHostForImportGeneration': true}); - env.write('other._$test.ts', ` + it('should escape unusual characters in aliased filenames', () => { + env.tsconfig({'_useHostForImportGeneration': true}); + env.write('other._$test.ts', ` import {Directive, NgModule} from '@angular/core'; @Directive({selector: 'test'}) @@ -4187,7 +4122,7 @@ export const Foo = Foo__PRE_R3__; }) export class OtherModule {} `); - env.write('index.ts', ` + env.write('index.ts', ` import {NgModule} from '@angular/core'; import {OtherModule} from './other._$test'; @@ -4196,18 +4131,17 @@ export const Foo = Foo__PRE_R3__; }) export class IndexModule {} `); - env.driveMain(); - const jsContents = env.getContents('index.js'); - expect(jsContents) - .toContain( - 'export { TestDir as \u0275ng$root$other___test$$TestDir } from "root/other._$test";'); + env.driveMain(); + const jsContents = env.getContents('index.js'); + expect(jsContents) + .toContain( + 'export { TestDir as \u0275ng$root$other___test$$TestDir } from "root/other._$test";'); + }); }); - }); - describe('inline resources', () => { - it('should process inline