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
This commit is contained in:
Pete Bacon Darwin 2019-06-06 20:22:32 +01:00 committed by Kara Erickson
parent 1e7e065423
commit 7186f9c016
177 changed files with 16598 additions and 14829 deletions

View File

@ -27,12 +27,12 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/entry_point", "//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/imports",
"//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/routing",

View File

@ -5,6 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {AotCompilerHost, AotCompilerHost as StaticReflectorHost, StaticReflector, StaticSymbol} from '@angular/compiler';
export {DiagnosticTemplateInfo, getExpressionScope, getTemplateExpressionDiagnostics} from './src/diagnostics/expression_diagnostics'; export {DiagnosticTemplateInfo, getExpressionScope, getTemplateExpressionDiagnostics} from './src/diagnostics/expression_diagnostics';
export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expression_type'; 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 {ngToTsDiagnostic} from './src/transformers/util';
export {NgTscPlugin} from './src/ngtsc/tsc_plugin'; export {NgTscPlugin} from './src/ngtsc/tsc_plugin';
setFileSystem(new NodeJSFileSystem());

View File

@ -8,15 +8,16 @@ ts_library(
"*.ts", "*.ts",
"**/*.ts", "**/*.ts",
]), ]),
tsconfig = "//packages/compiler-cli:tsconfig",
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/scope",
@ -25,7 +26,6 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/util", "//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/convert-source-map", "@npm//@types/convert-source-map",
"@npm//@types/node", "@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/source-map", "@npm//@types/source-map",
"@npm//@types/yargs", "@npm//@types/yargs",
"@npm//canonical-path", "@npm//canonical-path",

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 {hasBeenProcessed as _hasBeenProcessed} from './src/packages/build_marker';
import {EntryPointJsonProperty, EntryPointPackageJson} from './src/packages/entry_point'; 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. // We are wrapping this function to hide the internal types.
return _hasBeenProcessed(packageJson as EntryPointPackageJson, format as EntryPointJsonProperty); return _hasBeenProcessed(packageJson as EntryPointPackageJson, format as EntryPointJsonProperty);
} }
// Configure the file-system for external users.
setFileSystem(new NodeJSFileSystem());

View File

@ -8,7 +8,7 @@
*/ */
import * as yargs from 'yargs'; 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 {mainNgcc} from './src/main';
import {ConsoleLogger, LogLevel} from './src/logging/console_logger'; 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.'); 'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
process.exit(1); 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 propertiesToConsider: string[] = options['p'];
const targetEntryPointPath = options['t'] ? options['t'] : undefined; const targetEntryPointPath = options['t'] ? options['t'] : undefined;
const compileAllFormats = !options['first-only']; const compileAllFormats = !options['first-only'];

View File

@ -10,14 +10,13 @@ import * as ts from 'typescript';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles'; 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 {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata'; import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path';
import {ClassDeclaration, ClassSymbol, Decorator} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, ClassSymbol, Decorator} from '../../../src/ngtsc/reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform'; import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {FileSystem} from '../file_system/file_system';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils'; import {isDefined} from '../utils';
@ -57,9 +56,9 @@ class NgccResourceLoader implements ResourceLoader {
constructor(private fs: FileSystem) {} constructor(private fs: FileSystem) {}
canPreload = false; canPreload = false;
preload(): undefined|Promise<void> { throw new Error('Not implemented.'); } preload(): undefined|Promise<void> { 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 { resolve(url: string, containingFile: string): string {
return AbsoluteFsPath.resolve(AbsoluteFsPath.dirname(AbsoluteFsPath.from(containingFile)), url); return resolve(dirname(absoluteFrom(containingFile)), url);
} }
} }

View File

@ -7,7 +7,7 @@
*/ */
import * as ts from 'typescript'; 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 {Declaration} from '../../../src/ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {hasNameIdentifier, isDefined} from '../utils'; import {hasNameIdentifier, isDefined} from '../utils';
@ -94,12 +94,11 @@ export class PrivateDeclarationsAnalyzer {
}); });
return Array.from(privateDeclarations.keys()).map(id => { return Array.from(privateDeclarations.keys()).map(id => {
const from = AbsoluteFsPath.fromSourceFile(id.getSourceFile()); const from = absoluteFromSourceFile(id.getSourceFile());
const declaration = privateDeclarations.get(id) !; const declaration = privateDeclarations.get(id) !;
const alias = exportAliasDeclarations.has(id) ? exportAliasDeclarations.get(id) ! : null; const alias = exportAliasDeclarations.has(id) ? exportAliasDeclarations.get(id) ! : null;
const dtsDeclaration = this.host.getDtsDeclaration(declaration.node); const dtsDeclaration = this.host.getDtsDeclaration(declaration.node);
const dtsFrom = const dtsFrom = dtsDeclaration && absoluteFromSourceFile(dtsDeclaration.getSourceFile());
dtsDeclaration && AbsoluteFsPath.fromSourceFile(dtsDeclaration.getSourceFile());
return {identifier: id.text, from, dtsFrom, alias}; return {identifier: id.text, from, dtsFrom, alias};
}); });

View File

@ -6,11 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {isRequireCall} from '../host/commonjs_host'; import {isRequireCall} from '../host/commonjs_host';
import {DependencyHost, DependencyInfo} from './dependency_host'; import {DependencyHost, DependencyInfo} from './dependency_host';
import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 { export interface DependencyHost {
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo; findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo;

View File

@ -7,8 +7,7 @@
*/ */
import {DepGraph} from 'dependency-graph'; import {DepGraph} from 'dependency-graph';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {AbsoluteFsPath, FileSystem, resolve} from '../../../src/ngtsc/file_system';
import {FileSystem} from '../file_system/file_system';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point'; import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point';
import {DependencyHost} from './dependency_host'; import {DependencyHost} from './dependency_host';
@ -176,7 +175,7 @@ export class DependencyResolver {
if (format === 'esm2015' || format === 'esm5' || format === 'umd' || format === 'commonjs') { if (format === 'esm2015' || format === 'esm5' || format === 'umd' || format === 'commonjs') {
const formatPath = entryPoint.packageJson[property] !; const formatPath = entryPoint.packageJson[property] !;
return {format, path: AbsoluteFsPath.resolve(entryPoint.path, formatPath)}; return {format, path: resolve(entryPoint.path, formatPath)};
} }
} }
throw new Error( throw new Error(

View File

@ -6,13 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {DependencyHost, DependencyInfo} from './dependency_host'; import {DependencyHost, DependencyInfo} from './dependency_host';
import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';
/** /**
* Helper functions for computing dependencies. * Helper functions for computing dependencies.
*/ */

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, isRoot, join, resolve} from '../../../src/ngtsc/file_system';
import {FileSystem} from '../file_system/file_system';
import {PathMappings, isRelativePath} from '../utils'; import {PathMappings, isRelativePath} from '../utils';
/** /**
* This is a very cut-down implementation of the TypeScript module resolution strategy. * 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. * Convert the `pathMappings` into a collection of `PathMapper` functions.
*/ */
private processPathMappings(pathMappings: PathMappings): ProcessedPathMapping[] { private processPathMappings(pathMappings: PathMappings): ProcessedPathMapping[] {
const baseUrl = AbsoluteFsPath.from(pathMappings.baseUrl); const baseUrl = absoluteFrom(pathMappings.baseUrl);
return Object.keys(pathMappings.paths).map(pathPattern => { return Object.keys(pathMappings.paths).map(pathPattern => {
const matcher = splitOnStar(pathPattern); const matcher = splitOnStar(pathPattern);
const templates = pathMappings.paths[pathPattern].map(splitOnStar); 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`. * If neither of these files exist then the method returns `null`.
*/ */
private resolveAsRelativePath(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { private resolveAsRelativePath(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null {
const resolvedPath = this.resolvePath( const resolvedPath =
AbsoluteFsPath.resolve(AbsoluteFsPath.dirname(fromPath), moduleName), this.resolvePath(resolve(dirname(fromPath), moduleName), this.relativeExtensions);
this.relativeExtensions);
return resolvedPath && new ResolvedRelativeModule(resolvedPath); return resolvedPath && new ResolvedRelativeModule(resolvedPath);
} }
@ -118,13 +118,13 @@ export class ModuleResolver {
*/ */
private resolveAsEntryPoint(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { private resolveAsEntryPoint(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null {
let folder = fromPath; let folder = fromPath;
while (!AbsoluteFsPath.isRoot(folder)) { while (!isRoot(folder)) {
folder = AbsoluteFsPath.dirname(folder); folder = dirname(folder);
if (folder.endsWith('node_modules')) { if (folder.endsWith('node_modules')) {
// Skip up if the folder already ends in 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)) { if (this.isEntryPoint(modulePath)) {
return new ResolvedExternalModule(modulePath); return new ResolvedExternalModule(modulePath);
} else if (this.resolveAsRelativePath(modulePath, fromPath)) { } else if (this.resolveAsRelativePath(modulePath, fromPath)) {
@ -141,7 +141,7 @@ export class ModuleResolver {
*/ */
private resolvePath(path: AbsoluteFsPath, postFixes: string[]): AbsoluteFsPath|null { private resolvePath(path: AbsoluteFsPath, postFixes: string[]): AbsoluteFsPath|null {
for (const postFix of postFixes) { for (const postFix of postFixes) {
const testPath = AbsoluteFsPath.fromUnchecked(path + postFix); const testPath = absoluteFrom(path + postFix);
if (this.fs.exists(testPath)) { if (this.fs.exists(testPath)) {
return testPath; return testPath;
} }
@ -155,7 +155,7 @@ export class ModuleResolver {
* This is achieved by checking for the existence of `${modulePath}/package.json`. * This is achieved by checking for the existence of `${modulePath}/package.json`.
*/ */
private isEntryPoint(modulePath: AbsoluteFsPath): boolean { 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) { private computeMappedTemplates(mapping: ProcessedPathMapping, match: string) {
return mapping.templates.map( return mapping.templates.map(
template => template => resolve(mapping.baseUrl, template.prefix + match + template.postfix));
AbsoluteFsPath.resolve(mapping.baseUrl, template.prefix + match + template.postfix));
} }
/** /**
@ -225,9 +224,9 @@ export class ModuleResolver {
*/ */
private findPackagePath(path: AbsoluteFsPath): AbsoluteFsPath|null { private findPackagePath(path: AbsoluteFsPath): AbsoluteFsPath|null {
let folder = path; let folder = path;
while (!AbsoluteFsPath.isRoot(folder)) { while (!isRoot(folder)) {
folder = AbsoluteFsPath.dirname(folder); folder = dirname(folder);
if (this.fs.exists(AbsoluteFsPath.join(folder, 'package.json'))) { if (this.fs.exists(join(folder, 'package.json'))) {
return folder; return folder;
} }
} }

View File

@ -6,16 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {getImportsOfUmdModule, parseStatementForUmdModule} from '../host/umd_host'; import {getImportsOfUmdModule, parseStatementForUmdModule} from '../host/umd_host';
import {DependencyHost, DependencyInfo} from './dependency_host'; import {DependencyHost, DependencyInfo} from './dependency_host';
import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';
/** /**
* Helper functions for computing dependencies. * Helper functions for computing dependencies.
*/ */

View File

@ -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;
}

View File

@ -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); }
}

View File

@ -7,7 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {Declaration, Import} from '../../../src/ngtsc/reflection'; import {Declaration, Import} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program'; import {BundleProgram} from '../packages/bundle_program';
@ -122,13 +122,13 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
if (this.compilerHost.resolveModuleNames) { if (this.compilerHost.resolveModuleNames) {
const moduleInfo = const moduleInfo =
this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0]; this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0];
return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName); return moduleInfo && this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedFileName));
} else { } else {
const moduleInfo = ts.resolveModuleName( const moduleInfo = ts.resolveModuleName(
moduleName, containingFile.fileName, this.program.getCompilerOptions(), moduleName, containingFile.fileName, this.program.getCompilerOptions(),
this.compilerHost); this.compilerHost);
return moduleInfo.resolvedModule && return moduleInfo.resolvedModule &&
this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName); this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedModule.resolvedFileName));
} }
} }
} }

View File

@ -8,6 +8,7 @@
import * as ts from 'typescript'; 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 {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program'; 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. * @param dtsProgram The program containing all the typings files.
* @returns a map of class names to class declarations. * @returns a map of class names to class declarations.
*/ */
protected computeDtsDeclarationMap(dtsRootFileName: string, dtsProgram: ts.Program): protected computeDtsDeclarationMap(dtsRootFileName: AbsoluteFsPath, dtsProgram: ts.Program):
Map<string, ts.Declaration> { Map<string, ts.Declaration> {
const dtsDeclarationMap = new Map<string, ts.Declaration>(); const dtsDeclarationMap = new Map<string, ts.Declaration>();
const checker = dtsProgram.getTypeChecker(); const checker = dtsProgram.getTypeChecker();

View File

@ -8,6 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {Declaration, Import} from '../../../src/ngtsc/reflection'; import {Declaration, Import} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program'; import {BundleProgram} from '../packages/bundle_program';
@ -154,13 +155,13 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
if (this.compilerHost.resolveModuleNames) { if (this.compilerHost.resolveModuleNames) {
const moduleInfo = const moduleInfo =
this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0]; this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0];
return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName); return moduleInfo && this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedFileName));
} else { } else {
const moduleInfo = ts.resolveModuleName( const moduleInfo = ts.resolveModuleName(
moduleName, containingFile.fileName, this.program.getCompilerOptions(), moduleName, containingFile.fileName, this.program.getCompilerOptions(),
this.compilerHost); this.compilerHost);
return moduleInfo.resolvedModule && return moduleInfo.resolvedModule &&
this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName); this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedModule.resolvedFileName));
} }
} }
} }

View File

@ -5,15 +5,12 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {CommonJsDependencyHost} from './dependencies/commonjs_dependency_host';
import {DependencyResolver} from './dependencies/dependency_resolver'; import {DependencyResolver} from './dependencies/dependency_resolver';
import {EsmDependencyHost} from './dependencies/esm_dependency_host'; import {EsmDependencyHost} from './dependencies/esm_dependency_host';
import {ModuleResolver} from './dependencies/module_resolver'; import {ModuleResolver} from './dependencies/module_resolver';
import {UmdDependencyHost} from './dependencies/umd_dependency_host'; 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 {ConsoleLogger, LogLevel} from './logging/console_logger';
import {Logger} from './logging/logger'; import {Logger} from './logging/logger';
import {hasBeenProcessed, markAsProcessed} from './packages/build_marker'; import {hasBeenProcessed, markAsProcessed} from './packages/build_marker';
@ -79,7 +76,7 @@ export function mainNgcc(
{basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
compileAllFormats = true, createNewEntryPointFormats = false, compileAllFormats = true, createNewEntryPointFormats = false,
logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void {
const fs = new NodeJSFileSystem(); const fs = getFileSystem();
const transformer = new Transformer(fs, logger); const transformer = new Transformer(fs, logger);
const moduleResolver = new ModuleResolver(fs, pathMappings); const moduleResolver = new ModuleResolver(fs, pathMappings);
const esmDependencyHost = new EsmDependencyHost(fs, moduleResolver); const esmDependencyHost = new EsmDependencyHost(fs, moduleResolver);
@ -95,7 +92,7 @@ export function mainNgcc(
const fileWriter = getFileWriter(fs, createNewEntryPointFormats); const fileWriter = getFileWriter(fs, createNewEntryPointFormats);
const absoluteTargetEntryPointPath = const absoluteTargetEntryPointPath =
targetEntryPointPath ? AbsoluteFsPath.resolve(basePath, targetEntryPointPath) : undefined; targetEntryPointPath ? resolve(basePath, targetEntryPointPath) : undefined;
if (absoluteTargetEntryPointPath && if (absoluteTargetEntryPointPath &&
hasProcessedTargetEntryPoint( hasProcessedTargetEntryPoint(
@ -104,8 +101,8 @@ export function mainNgcc(
return; return;
} }
const {entryPoints, invalidEntryPoints} = finder.findEntryPoints( const {entryPoints, invalidEntryPoints} =
AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath, pathMappings); finder.findEntryPoints(absoluteFrom(basePath), absoluteTargetEntryPointPath, pathMappings);
invalidEntryPoints.forEach(invalidEntryPoint => { invalidEntryPoints.forEach(invalidEntryPoint => {
logger.debug( logger.debug(
@ -119,14 +116,13 @@ export function mainNgcc(
return; return;
} }
entryPoints.forEach(entryPoint => { for (const entryPoint of entryPoints) {
// Are we compiling the Angular core? // Are we compiling the Angular core?
const isCore = entryPoint.name === '@angular/core'; const isCore = entryPoint.name === '@angular/core';
const compiledFormats = new Set<string>(); const compiledFormats = new Set<string>();
const entryPointPackageJson = entryPoint.packageJson; const entryPointPackageJson = entryPoint.packageJson;
const entryPointPackageJsonPath = const entryPointPackageJsonPath = fs.resolve(entryPoint.path, 'package.json');
AbsoluteFsPath.fromUnchecked(`${entryPoint.path}/package.json`);
const hasProcessedDts = hasBeenProcessed(entryPointPackageJson, 'typings'); const hasProcessedDts = hasBeenProcessed(entryPointPackageJson, 'typings');
@ -180,7 +176,7 @@ export function mainNgcc(
throw new Error( throw new Error(
`Failed to compile any formats for entry-point at (${entryPoint.path}). Tried ${propertiesToConsider}.`); `Failed to compile any formats for entry-point at (${entryPoint.path}). Tried ${propertiesToConsider}.`);
} }
}); }
} }
function getFileWriter(fs: FileSystem, createNewEntryPointFormats: boolean): FileWriter { function getFileWriter(fs: FileSystem, createNewEntryPointFormats: boolean): FileWriter {
@ -190,7 +186,7 @@ function getFileWriter(fs: FileSystem, createNewEntryPointFormats: boolean): Fil
function hasProcessedTargetEntryPoint( function hasProcessedTargetEntryPoint(
fs: FileSystem, targetPath: AbsoluteFsPath, propertiesToConsider: string[], fs: FileSystem, targetPath: AbsoluteFsPath, propertiesToConsider: string[],
compileAllFormats: boolean) { compileAllFormats: boolean) {
const packageJsonPath = AbsoluteFsPath.resolve(targetPath, 'package.json'); const packageJsonPath = resolve(targetPath, 'package.json');
const packageJson = JSON.parse(fs.readFile(packageJsonPath)); const packageJson = JSON.parse(fs.readFile(packageJsonPath));
for (const property of propertiesToConsider) { for (const property of propertiesToConsider) {
@ -221,7 +217,7 @@ function hasProcessedTargetEntryPoint(
*/ */
function markNonAngularPackageAsProcessed( function markNonAngularPackageAsProcessed(
fs: FileSystem, path: AbsoluteFsPath, propertiesToConsider: string[]) { 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)); const packageJson = JSON.parse(fs.readFile(packageJsonPath));
propertiesToConsider.forEach(formatProperty => { propertiesToConsider.forEach(formatProperty => {
if (packageJson[formatProperty]) if (packageJson[formatProperty])

View File

@ -5,9 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {EntryPointJsonProperty, EntryPointPackageJson} from './entry_point'; import {EntryPointJsonProperty, EntryPointPackageJson} from './entry_point';
export const NGCC_VERSION = '0.0.0-PLACEHOLDER'; export const NGCC_VERSION = '0.0.0-PLACEHOLDER';

View File

@ -6,9 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, dirname, resolve} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from './patch_ts_expando_initializer'; import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from './patch_ts_expando_initializer';
/** /**
@ -35,12 +33,14 @@ export interface BundleProgram {
export function makeBundleProgram( export function makeBundleProgram(
fs: FileSystem, isCore: boolean, path: AbsoluteFsPath, r3FileName: string, fs: FileSystem, isCore: boolean, path: AbsoluteFsPath, r3FileName: string,
options: ts.CompilerOptions, host: ts.CompilerHost): BundleProgram { options: ts.CompilerOptions, host: ts.CompilerHost): BundleProgram {
const r3SymbolsPath = const r3SymbolsPath = isCore ? findR3SymbolsPath(fs, dirname(path), r3FileName) : null;
isCore ? findR3SymbolsPath(fs, AbsoluteFsPath.dirname(path), r3FileName) : null;
const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path]; const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path];
const originalGetExpandoInitializer = patchTsGetExpandoInitializer(); const originalGetExpandoInitializer = patchTsGetExpandoInitializer();
const program = ts.createProgram(rootPaths, options, host); 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); restoreGetExpandoInitializer(originalGetExpandoInitializer);
const file = program.getSourceFile(path) !; const file = program.getSourceFile(path) !;
@ -54,7 +54,7 @@ export function makeBundleProgram(
*/ */
export function findR3SymbolsPath( export function findR3SymbolsPath(
fs: FileSystem, directory: AbsoluteFsPath, filename: string): AbsoluteFsPath|null { fs: FileSystem, directory: AbsoluteFsPath, filename: string): AbsoluteFsPath|null {
const r3SymbolsFilePath = AbsoluteFsPath.resolve(directory, filename); const r3SymbolsFilePath = resolve(directory, filename);
if (fs.exists(r3SymbolsFilePath)) { if (fs.exists(r3SymbolsFilePath)) {
return r3SymbolsFilePath; return r3SymbolsFilePath;
} }
@ -67,13 +67,12 @@ export function findR3SymbolsPath(
.filter(p => p !== 'node_modules') .filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks) // Only interested in directories (and only those that are not symlinks)
.filter(p => { .filter(p => {
const stat = fs.lstat(AbsoluteFsPath.resolve(directory, p)); const stat = fs.lstat(resolve(directory, p));
return stat.isDirectory() && !stat.isSymbolicLink(); return stat.isDirectory() && !stat.isSymbolicLink();
}); });
for (const subDirectory of subDirectories) { for (const subDirectory of subDirectories) {
const r3SymbolsFilePath = const r3SymbolsFilePath = findR3SymbolsPath(fs, resolve(directory, subDirectory), filename);
findR3SymbolsPath(fs, AbsoluteFsPath.resolve(directory, subDirectory), filename);
if (r3SymbolsFilePath) { if (r3SymbolsFilePath) {
return r3SymbolsFilePath; return r3SymbolsFilePath;
} }

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {AbsoluteFsPath, FileSystem, join, resolve} from '../../../src/ngtsc/file_system';
import {FileSystem} from '../file_system/file_system';
import {parseStatementForUmdModule} from '../host/umd_host'; import {parseStatementForUmdModule} from '../host/umd_host';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
@ -70,7 +69,7 @@ export const SUPPORTED_FORMAT_PROPERTIES: EntryPointJsonProperty[] =
export function getEntryPointInfo( export function getEntryPointInfo(
fs: FileSystem, logger: Logger, packagePath: AbsoluteFsPath, fs: FileSystem, logger: Logger, packagePath: AbsoluteFsPath,
entryPointPath: AbsoluteFsPath): EntryPoint|null { entryPointPath: AbsoluteFsPath): EntryPoint|null {
const packageJsonPath = AbsoluteFsPath.resolve(entryPointPath, 'package.json'); const packageJsonPath = resolve(entryPointPath, 'package.json');
if (!fs.exists(packageJsonPath)) { if (!fs.exists(packageJsonPath)) {
return null; return null;
} }
@ -88,15 +87,14 @@ export function getEntryPointInfo(
} }
// Also there must exist a `metadata.json` file next to the typings entry-point. // Also there must exist a `metadata.json` file next to the typings entry-point.
const metadataPath = const metadataPath = resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
AbsoluteFsPath.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
const entryPointInfo: EntryPoint = { const entryPointInfo: EntryPoint = {
name: entryPointPackageJson.name, name: entryPointPackageJson.name,
packageJson: entryPointPackageJson, packageJson: entryPointPackageJson,
package: packagePath, package: packagePath,
path: entryPointPath, path: entryPointPath,
typings: AbsoluteFsPath.resolve(entryPointPath, typings), typings: resolve(entryPointPath, typings),
compiledByAngular: fs.exists(metadataPath), compiledByAngular: fs.exists(metadataPath),
}; };
@ -127,7 +125,7 @@ export function getEntryPointFormat(
if (mainFile === undefined) { if (mainFile === undefined) {
return undefined; return undefined;
} }
const pathToMain = AbsoluteFsPath.join(entryPoint.path, mainFile); const pathToMain = join(entryPoint.path, mainFile);
return isUmdModule(fs, pathToMain) ? 'umd' : 'commonjs'; return isUmdModule(fs, pathToMain) ? 'umd' : 'commonjs';
case 'module': case 'module':
return 'esm5'; return 'esm5';

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, absoluteFrom, resolve} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {NgtscCompilerHost} from '../../../src/ngtsc/file_system/src/compiler_host';
import {FileSystem} from '../file_system/file_system';
import {PathMappings} from '../utils'; import {PathMappings} from '../utils';
import {BundleProgram, makeBundleProgram} from './bundle_program'; import {BundleProgram, makeBundleProgram} from './bundle_program';
import {EntryPointFormat, EntryPointJsonProperty} from './entry_point'; 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 * A bundle of files and paths (and TS programs) that correspond to a particular
@ -49,17 +49,16 @@ export function makeEntryPointBundle(
rootDir: entryPointPath, ...pathMappings rootDir: entryPointPath, ...pathMappings
}; };
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPointPath); const srcHost = new NgccSourcesCompilerHost(fs, options, entryPointPath);
const dtsHost = new NgccCompilerHost(fs, options); const dtsHost = new NgtscCompilerHost(fs, options);
const rootDirs = [AbsoluteFsPath.from(entryPointPath)]; const rootDirs = [absoluteFrom(entryPointPath)];
// Create the bundle programs, as necessary. // Create the bundle programs, as necessary.
const src = makeBundleProgram( const src = makeBundleProgram(
fs, isCore, AbsoluteFsPath.resolve(entryPointPath, formatPath), 'r3_symbols.js', options, fs, isCore, resolve(entryPointPath, formatPath), 'r3_symbols.js', options, srcHost);
srcHost); const dts = transformDts ?
const dts = transformDts ? makeBundleProgram( makeBundleProgram(
fs, isCore, AbsoluteFsPath.resolve(entryPointPath, typingsPath), fs, isCore, resolve(entryPointPath, typingsPath), 'r3_symbols.d.ts', options, dtsHost) :
'r3_symbols.d.ts', options, dtsHost) : null;
null;
const isFlatCore = isCore && src.r3SymbolsFile === null; const isFlatCore = isCore && src.r3SymbolsFile === null;
return {format, formatProperty, rootDirs, isCore, isFlatCore, src, dts}; return {format, formatProperty, rootDirs, isCore, isFlatCore, src, dts};

View File

@ -5,11 +5,11 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
import {FileSystem} from '../file_system/file_system';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {PathMappings} from '../utils'; import {PathMappings} from '../utils';
import {EntryPoint, getEntryPointInfo} from './entry_point'; import {EntryPoint, getEntryPointInfo} from './entry_point';
export class EntryPointFinder { export class EntryPointFinder {
@ -55,9 +55,9 @@ export class EntryPointFinder {
AbsoluteFsPath[] { AbsoluteFsPath[] {
const basePaths = [sourceDirectory]; const basePaths = [sourceDirectory];
if (pathMappings) { if (pathMappings) {
const baseUrl = AbsoluteFsPath.resolve(pathMappings.baseUrl); const baseUrl = resolve(pathMappings.baseUrl);
values(pathMappings.paths).forEach(paths => paths.forEach(path => { 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. basePaths.sort(); // Get the paths in order with the shorter ones first.
@ -84,17 +84,17 @@ export class EntryPointFinder {
.filter(p => p !== 'node_modules') .filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks) // Only interested in directories (and only those that are not symlinks)
.filter(p => { .filter(p => {
const stat = this.fs.lstat(AbsoluteFsPath.resolve(sourceDirectory, p)); const stat = this.fs.lstat(resolve(sourceDirectory, p));
return stat.isDirectory() && !stat.isSymbolicLink(); return stat.isDirectory() && !stat.isSymbolicLink();
}) })
.forEach(p => { .forEach(p => {
// Either the directory is a potential package or a namespace containing packages (e.g // Either the directory is a potential package or a namespace containing packages (e.g
// `@angular`). // `@angular`).
const packagePath = AbsoluteFsPath.join(sourceDirectory, p); const packagePath = join(sourceDirectory, p);
entryPoints.push(...this.walkDirectoryForEntryPoints(packagePath)); entryPoints.push(...this.walkDirectoryForEntryPoints(packagePath));
// Also check for any nested node_modules in this package // 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)) { if (this.fs.exists(nestedNodeModulesPath)) {
entryPoints.push(...this.walkDirectoryForEntryPoints(nestedNodeModulesPath)); entryPoints.push(...this.walkDirectoryForEntryPoints(nestedNodeModulesPath));
} }
@ -145,11 +145,11 @@ export class EntryPointFinder {
.filter(p => p !== 'node_modules') .filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks) // Only interested in directories (and only those that are not symlinks)
.filter(p => { .filter(p => {
const stat = this.fs.lstat(AbsoluteFsPath.resolve(dir, p)); const stat = this.fs.lstat(resolve(dir, p));
return stat.isDirectory() && !stat.isSymbolicLink(); return stat.isDirectory() && !stat.isSymbolicLink();
}) })
.forEach(subDir => { .forEach(subDir => {
const resolvedSubDir = AbsoluteFsPath.resolve(dir, subDir); const resolvedSubDir = resolve(dir, subDir);
fn(resolvedSubDir); fn(resolvedSubDir);
this.walkDirectory(resolvedSubDir, fn); this.walkDirectory(resolvedSubDir, fn);
}); });

View File

@ -5,74 +5,19 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as os from 'os';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../../../src/ngtsc/file_system';
import {FileSystem} from '../file_system/file_system'; import {NgtscCompilerHost} from '../../../src/ngtsc/file_system/src/compiler_host';
import {isRelativePath} from '../utils'; 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 * 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 * 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 * 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. * 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( private cache = ts.createModuleResolutionCache(
this.getCurrentDirectory(), file => this.getCanonicalFileName(file)); this.getCurrentDirectory(), file => this.getCanonicalFileName(file));

View File

@ -6,13 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {FileSystem} from '../../../src/ngtsc/file_system';
import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer'; import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer';
import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analysis/module_with_providers_analyzer'; import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analysis/module_with_providers_analyzer';
import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry';
import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer'; import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer';
import {FileSystem} from '../file_system/file_system';
import {CommonJsReflectionHost} from '../host/commonjs_host'; import {CommonJsReflectionHost} from '../host/commonjs_host';
import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm2015ReflectionHost} from '../host/esm2015_host';
import {Esm5ReflectionHost} from '../host/esm5_host'; import {Esm5ReflectionHost} from '../host/esm5_host';
@ -27,11 +26,8 @@ import {Renderer} from '../rendering/renderer';
import {RenderingFormatter} from '../rendering/rendering_formatter'; import {RenderingFormatter} from '../rendering/rendering_formatter';
import {UmdRenderingFormatter} from '../rendering/umd_rendering_formatter'; import {UmdRenderingFormatter} from '../rendering/umd_rendering_formatter';
import {FileToWrite} from '../rendering/utils'; import {FileToWrite} from '../rendering/utils';
import {EntryPointBundle} from './entry_point_bundle'; import {EntryPointBundle} from './entry_point_bundle';
/** /**
* A Package is stored in a directory on disk and that directory can contain one or more package * 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). * formats - e.g. fesm2015, UMD, etc. Additionally, each package provides typings (`.d.ts` files).

View File

@ -7,20 +7,19 @@
*/ */
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; 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 {translateType, ImportManager} from '../../../src/ngtsc/translator';
import {DecorationAnalyses} from '../analysis/decoration_analyzer'; import {DecorationAnalyses} from '../analysis/decoration_analyzer';
import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer'; import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer';
import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer'; import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer';
import {IMPORT_PREFIX} from '../constants'; import {IMPORT_PREFIX} from '../constants';
import {FileSystem} from '../file_system/file_system';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {FileToWrite, getImportRewriter} from './utils'; import {FileToWrite, getImportRewriter} from './utils';
import {RenderingFormatter} from './rendering_formatter'; import {RenderingFormatter} from './rendering_formatter';
import {extractSourceMap, renderSourceAndMap} from './source_maps'; 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 * A structure that captures information about what needs to be rendered

View File

@ -7,7 +7,7 @@
*/ */
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; 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 {Import, ImportManager} from '../../../src/ngtsc/translator';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {CompiledClass} from '../analysis/decoration_analyzer'; import {CompiledClass} from '../analysis/decoration_analyzer';
@ -46,8 +46,7 @@ export class EsmRenderingFormatter implements RenderingFormatter {
if (from) { if (from) {
const basePath = stripExtension(from); const basePath = stripExtension(from);
const relativePath = const relativePath = './' + relative(dirname(entryPointBasePath), basePath);
'./' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath);
exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : ''; exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : '';
} }
@ -136,12 +135,11 @@ export class EsmRenderingFormatter implements RenderingFormatter {
importManager: ImportManager): void { importManager: ImportManager): void {
moduleWithProviders.forEach(info => { moduleWithProviders.forEach(info => {
const ngModuleName = info.ngModule.node.name.text; const ngModuleName = info.ngModule.node.name.text;
const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile()); const declarationFile = absoluteFromSourceFile(info.declaration.getSourceFile());
const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile()); const ngModuleFile = absoluteFromSourceFile(info.ngModule.node.getSourceFile());
const importPath = info.ngModule.viaModule || const importPath = info.ngModule.viaModule ||
(declarationFile !== ngModuleFile ? (declarationFile !== ngModuleFile ?
stripExtension( stripExtension(`./${relative(dirname(declarationFile), ngModuleFile)}`) :
`./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) :
null); null);
const ngModule = generateImportString(importManager, importPath, ngModuleName); const ngModule = generateImportString(importManager, importPath, ngModuleName);

View File

@ -8,14 +8,13 @@
import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler'; import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports';
import {translateStatement, ImportManager} from '../../../src/ngtsc/translator'; import {translateStatement, ImportManager} from '../../../src/ngtsc/translator';
import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer'; import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer';
import {PrivateDeclarationsAnalyses} from '../analysis/private_declarations_analyzer'; import {PrivateDeclarationsAnalyses} from '../analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer';
import {IMPORT_PREFIX} from '../constants'; 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 {NgccReflectionHost} from '../host/ngcc_host';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';

View File

@ -9,8 +9,7 @@ import {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, gene
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map'; import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {resolve, FileSystem, absoluteFromSourceFile, dirname, basename, absoluteFrom} from '../../../src/ngtsc/file_system';
import {FileSystem} from '../file_system/file_system';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {FileToWrite} from './utils'; import {FileToWrite} from './utils';
@ -39,19 +38,18 @@ export function extractSourceMap(
let externalSourceMap: SourceMapConverter|null = null; let externalSourceMap: SourceMapConverter|null = null;
try { try {
const fileName = external[1] || external[2]; const fileName = external[1] || external[2];
const filePath = AbsoluteFsPath.resolve( const filePath = resolve(dirname(absoluteFromSourceFile(file)), fileName);
AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName);
const mappingFile = fs.readFile(filePath); const mappingFile = fs.readFile(filePath);
externalSourceMap = fromJSON(mappingFile); externalSourceMap = fromJSON(mappingFile);
} catch (e) { } catch (e) {
if (e.code === 'ENOENT') { if (e.code === 'ENOENT') {
logger.warn( logger.warn(
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`); `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'); const mapPath = absoluteFrom(file.fileName + '.map');
if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) && fs.exists(mapPath) && if (basename(e.path) !== basename(mapPath) && fs.exists(mapPath) &&
fs.stat(mapPath).isFile()) { fs.stat(mapPath).isFile()) {
logger.warn( 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 { try {
externalSourceMap = fromObject(JSON.parse(fs.readFile(mapPath))); externalSourceMap = fromObject(JSON.parse(fs.readFile(mapPath)));
} catch (e) { } catch (e) {
@ -76,9 +74,9 @@ export function extractSourceMap(
*/ */
export function renderSourceAndMap( export function renderSourceAndMap(
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileToWrite[] { sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileToWrite[] {
const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile); const outputPath = absoluteFromSourceFile(sourceFile);
const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`); const outputMapPath = absoluteFrom(`${outputPath}.map`);
const relativeSourcePath = PathSegment.basename(outputPath); const relativeSourcePath = basename(outputPath);
const relativeMapPath = `${relativeSourcePath}.map`; const relativeMapPath = `${relativeSourcePath}.map`;
const outputMap = output.generateMap({ const outputMap = output.generateMap({

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter} from '../../../src/ngtsc/imports'; import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter} from '../../../src/ngtsc/imports';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; import {NgccFlatImportRewriter} from './ngcc_import_rewriter';
/** /**

View File

@ -10,7 +10,6 @@ import {EntryPoint} from '../packages/entry_point';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
import {FileToWrite} from '../rendering/utils'; import {FileToWrite} from '../rendering/utils';
/** /**
* Responsible for writing out the transformed files to disk. * Responsible for writing out the transformed files to disk.
*/ */

View File

@ -6,8 +6,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem, absoluteFrom, dirname} from '../../../src/ngtsc/file_system';
import {FileSystem} from '../file_system/file_system';
import {EntryPoint} from '../packages/entry_point'; import {EntryPoint} from '../packages/entry_point';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
import {FileToWrite} from '../rendering/utils'; import {FileToWrite} from '../rendering/utils';
@ -25,8 +24,8 @@ export class InPlaceFileWriter implements FileWriter {
} }
protected writeFileAndBackup(file: FileToWrite): void { protected writeFileAndBackup(file: FileToWrite): void {
this.fs.ensureDir(AbsoluteFsPath.dirname(file.path)); this.fs.ensureDir(dirname(file.path));
const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`); const backPath = absoluteFrom(`${file.path}.__ivy_ngcc_bak`);
if (this.fs.exists(backPath)) { if (this.fs.exists(backPath)) {
throw new Error( throw new Error(
`Tried to overwrite ${backPath} with an ngcc back up file, which is disallowed.`); `Tried to overwrite ${backPath} with an ngcc back up file, which is disallowed.`);

View File

@ -6,7 +6,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
@ -27,7 +27,7 @@ const NGCC_DIRECTORY = '__ivy_ngcc__';
export class NewEntryPointFileWriter extends InPlaceFileWriter { export class NewEntryPointFileWriter extends InPlaceFileWriter {
writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]) { writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]) {
// The new folder is at the root of the overall package // 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); this.copyBundle(bundle, entryPoint.package, ngccFolder);
transformedFiles.forEach(file => this.writeFile(file, entryPoint.package, ngccFolder)); transformedFiles.forEach(file => this.writeFile(file, entryPoint.package, ngccFolder));
this.updatePackageJson(entryPoint, bundle.formatProperty, ngccFolder); this.updatePackageJson(entryPoint, bundle.formatProperty, ngccFolder);
@ -36,13 +36,12 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter {
protected copyBundle( protected copyBundle(
bundle: EntryPointBundle, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath) { bundle: EntryPointBundle, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath) {
bundle.src.program.getSourceFiles().forEach(sourceFile => { bundle.src.program.getSourceFiles().forEach(sourceFile => {
const relativePath = const relativePath = relative(packagePath, absoluteFromSourceFile(sourceFile));
PathSegment.relative(packagePath, AbsoluteFsPath.fromSourceFile(sourceFile));
const isOutsidePackage = relativePath.startsWith('..'); const isOutsidePackage = relativePath.startsWith('..');
if (!sourceFile.isDeclarationFile && !isOutsidePackage) { if (!sourceFile.isDeclarationFile && !isOutsidePackage) {
const newFilePath = AbsoluteFsPath.join(ngccFolder, relativePath); const newFilePath = join(ngccFolder, relativePath);
this.fs.ensureDir(AbsoluteFsPath.dirname(newFilePath)); this.fs.ensureDir(dirname(newFilePath));
this.fs.copyFile(AbsoluteFsPath.fromSourceFile(sourceFile), 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 // This is either `.d.ts` or `.d.ts.map` file
super.writeFileAndBackup(file); super.writeFileAndBackup(file);
} else { } else {
const relativePath = PathSegment.relative(packagePath, file.path); const relativePath = relative(packagePath, file.path);
const newFilePath = AbsoluteFsPath.join(ngccFolder, relativePath); const newFilePath = join(ngccFolder, relativePath);
this.fs.ensureDir(AbsoluteFsPath.dirname(newFilePath)); this.fs.ensureDir(dirname(newFilePath));
this.fs.writeFile(newFilePath, file.contents); this.fs.writeFile(newFilePath, file.contents);
} }
} }
protected updatePackageJson( protected updatePackageJson(
entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, ngccFolder: AbsoluteFsPath) { entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, ngccFolder: AbsoluteFsPath) {
const formatPath = const formatPath = join(entryPoint.path, entryPoint.packageJson[formatProperty] !);
AbsoluteFsPath.join(entryPoint.path, entryPoint.packageJson[formatProperty] !); const newFormatPath = join(ngccFolder, relative(entryPoint.package, formatPath));
const newFormatPath =
AbsoluteFsPath.join(ngccFolder, PathSegment.relative(entryPoint.package, formatPath));
const newFormatProperty = formatProperty + '_ivy_ngcc'; const newFormatProperty = formatProperty + '_ivy_ngcc';
(entryPoint.packageJson as any)[newFormatProperty] = (entryPoint.packageJson as any)[newFormatProperty] = relative(entryPoint.path, newFormatPath);
PathSegment.relative(entryPoint.path, newFormatPath);
this.fs.writeFile( this.fs.writeFile(
AbsoluteFsPath.join(entryPoint.path, 'package.json'), join(entryPoint.path, 'package.json'), JSON.stringify(entryPoint.packageJson));
JSON.stringify(entryPoint.packageJson));
} }
} }

View File

@ -12,17 +12,17 @@ ts_library(
deps = [ deps = [
"//packages/compiler-cli/ngcc", "//packages/compiler-cli/ngcc",
"//packages/compiler-cli/ngcc/test/helpers", "//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/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/test:test_utils", "//packages/compiler-cli/test/helpers",
"@npm//@types/convert-source-map", "@npm//@types/convert-source-map",
"@npm//@types/mock-fs", "@npm//convert-source-map",
"@npm//canonical-path",
"@npm//magic-string", "@npm//magic-string",
"@npm//typescript", "@npm//typescript",
], ],
@ -31,12 +31,12 @@ ts_library(
jasmine_node_test( jasmine_node_test(
name = "test", name = "test",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"], bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
data = [
"//packages/compiler-cli/test/ngtsc/fake_core:npm_package",
],
deps = [ deps = [
":test_lib", ":test_lib",
"//tools/testing:node_no_angular", "//tools/testing:node_no_angular",
"@npm//canonical-path",
"@npm//convert-source-map",
"@npm//shelljs",
], ],
) )
@ -49,21 +49,24 @@ ts_library(
deps = [ deps = [
"//packages/compiler-cli/ngcc", "//packages/compiler-cli/ngcc",
"//packages/compiler-cli/ngcc/test/helpers", "//packages/compiler-cli/ngcc/test/helpers",
"//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/test:test_utils", "//packages/compiler-cli/src/ngtsc/file_system/testing",
"@npm//@types/mock-fs", "//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/test/helpers",
"@npm//rxjs", "@npm//rxjs",
], ],
) )
jasmine_node_test( jasmine_node_test(
name = "integration", name = "integration",
timeout = "long",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"], bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
data = [ data = [
"//packages/common:npm_package", "//packages/common:npm_package",
"//packages/core:npm_package", "//packages/core:npm_package",
"@npm//rxjs", "@npm//rxjs",
], ],
shard_count = 4,
tags = [ tags = [
# Disabled in AOT mode because we want ngcc to compile non-AOT Angular packages. # Disabled in AOT mode because we want ngcc to compile non-AOT Angular packages.
"no-ivy-aot", "no-ivy-aot",
@ -73,6 +76,5 @@ jasmine_node_test(
"//tools/testing:node_no_angular", "//tools/testing:node_no_angular",
"@npm//canonical-path", "@npm//canonical-path",
"@npm//convert-source-map", "@npm//convert-source-map",
"@npm//shelljs",
], ],
) )

View File

@ -7,238 +7,267 @@
*/ */
import * as ts from 'typescript'; 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 {Decorator} from '../../../src/ngtsc/reflection';
import {DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform'; import {DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Folder, MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {createFileSystemFromProgramFiles, makeTestBundleProgram} from '../helpers/utils'; import {getRootFiles, 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,
}
];
type DecoratorHandlerWithResolve = DecoratorHandler<any, any>& { type DecoratorHandlerWithResolve = DecoratorHandler<any, any>& {
resolve: NonNullable<DecoratorHandler<any, any>['resolve']>; resolve: NonNullable<DecoratorHandler<any, any>['resolve']>;
}; };
describe('DecorationAnalyzer', () => { runInEachFileSystem(() => {
describe('analyzeProgram()', () => { describe('DecorationAnalyzer', () => {
let logs: string[]; let _: typeof absoluteFrom;
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
let result: DecorationAnalyses;
// Helpers beforeEach(() => { _ = absoluteFrom; });
const createTestHandler = () => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect',
'analyze',
'resolve',
'compile',
]);
// Only detect the Component and Directive decorators
handler.detect.and.callFake(
(node: ts.Declaration, decorators: Decorator[] | null): DetectResult<any>| 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 setUpAndAnalyzeProgram = (...progArgs: Parameters<typeof makeTestBundleProgram>) => { describe('analyzeProgram()', () => {
logs = []; let logs: string[];
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
let result: DecorationAnalyses;
const {options, host, ...bundle} = makeTestBundleProgram(...progArgs); // Helpers
program = bundle.program; const createTestHandler = () => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect',
'analyze',
'resolve',
'compile',
]);
// Only detect the Component and Directive decorators
handler.detect.and.callFake(
(node: ts.Declaration, decorators: Decorator[] | null): DetectResult<any>|
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 = function setUpAndAnalyzeProgram(testFiles: TestFile[]) {
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); logs = [];
const referencesRegistry = new NgccReferencesRegistry(reflectionHost); loadTestFiles(testFiles);
const fs = new MockFileSystem(createFileSystemFromProgramFiles(...progArgs)); loadFakeCore(getFileSystem());
const analyzer = new DecorationAnalyzer( const rootFiles = getRootFiles(testFiles);
fs, program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, const {options, host, ...bundle} = makeTestBundleProgram(rootFiles[0]);
[AbsoluteFsPath.fromUnchecked('/')], false); program = bundle.program;
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram();
};
describe('basic usage', () => { const reflectionHost =
beforeEach(() => setUpAndAnalyzeProgram(TEST_PROGRAM)); 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', () => { describe('basic usage', () => {
TEST_PROGRAM.forEach(({name}) => { beforeEach(() => {
const file = program.getSourceFile(name) !; const TEST_PROGRAM = [
expect(result.get(file) !.sourceFile).toBe(file); {
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', describe('internal components', () => {
() => { beforeEach(() => {
expect(testHandler.detect).toHaveBeenCalledTimes(5); const INTERNAL_COMPONENT_PROGRAM = [
expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([ {
null, name: _('/entrypoint.js'),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]), contents: `
jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]), import {Component, NgModule} from '@angular/core';
jasmine.arrayContaining([jasmine.objectContaining({name: 'Injectable'})]), import {ImportedComponent} from './component';
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]),
]);
});
it('should return an object containing the classes that were analyzed', () => { export class LocalComponent {}
const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !; LocalComponent.decorators = [{type: Component}];
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 = program.getSourceFile(TEST_PROGRAM[1].name) !; export class MyModule {}
const compiledFile2 = result.get(file2) !; MyModule.decorators = [{type: NgModule, args: [{
expect(compiledFile2.compiledClasses.length).toEqual(1); declarations: [ImportedComponent, LocalComponent],
expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({ exports: [ImportedComponent, LocalComponent],
name: 'MyOtherComponent', compilation: ['@Component (compiled)'], },] }];
} as unknown as CompiledClass)); `
}); },
{
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', () => { // The problem of exposing the type of these internal components in the .d.ts typing
expect(logs).toEqual([ // files is not yet solved.
// Classes without decorators should also be detected. it('should analyze an internally imported component, which is not publicly exported from the entry-point',
'detect: InjectionToken (no decorators)', () => {
// First detect and (potentially) analyze. const file = getSourceFileOrError(program, _('/component.js'));
'detect: MyComponent@Component', const analysis = result.get(file) !;
'analyze: MyComponent@Component', expect(analysis).toBeDefined();
'detect: MyDirective@Directive', const ImportedComponent =
'analyze: MyDirective@Directive', analysis.compiledClasses.find(f => f.name === 'ImportedComponent') !;
'detect: MyService@Injectable', expect(ImportedComponent).toBeDefined();
'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)',
]);
});
});
describe('internal components', () => { it('should analyze an internally defined component, which is not exported at all', () => {
beforeEach(() => setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM)); const file = getSourceFileOrError(program, _('/entrypoint.js'));
const analysis = result.get(file) !;
// The problem of exposing the type of these internal components in the .d.ts typing files expect(analysis).toBeDefined();
// is not yet solved. const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !;
it('should analyze an internally imported component, which is not publicly exported from the entry-point', expect(LocalComponent).toBeDefined();
() => { });
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();
}); });
}); });
}); });

View File

@ -7,397 +7,415 @@
*/ */
import * as ts from 'typescript'; 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 {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {BundleProgram} from '../../src/packages/bundle_program'; import {BundleProgram} from '../../src/packages/bundle_program';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils';
const TEST_PROGRAM = [ runInEachFileSystem(() => {
{ describe('ModuleWithProvidersAnalyzer', () => {
name: '/src/entry-point.js', describe('analyzeProgram()', () => {
contents: ` let _: typeof absoluteFrom;
export * from './explicit'; let analyses: ModuleWithProvidersAnalyses;
export * from './any'; let program: ts.Program;
export * from './implicit'; let dtsProgram: BundleProgram|null;
export * from './no-providers'; let referencesRegistry: NgccReferencesRegistry;
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<ExplicitInternalModule>;
export declare function explicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function explicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare class ExplicitClass {
static explicitInternalMethod(): ModuleWithProviders<ExplicitInternalModule>;
static explicitExternalMethod(): ModuleWithProviders<ExternalModule>;
static explicitLibraryMethod(): ModuleWithProviders<LibraryModule>;
}
`
},
{
name: '/typings/any.d.ts',
contents: `
import {ModuleWithProviders} from './core';
export declare class AnyInternalModule {}
export declare function anyInternalFunction(): ModuleWithProviders<any>;
export declare function anyExternalFunction(): ModuleWithProviders<any>;
export declare function anyLibraryFunction(): ModuleWithProviders<any>;
export declare class AnyClass {
static anyInternalMethod(): ModuleWithProviders<any>;
static anyExternalMethod(): ModuleWithProviders<any>;
static anyLibraryMethod(): ModuleWithProviders<any>;
}
`
},
{
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<NoProvidersInternalModule>;
export declare function noProvExplicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function noProvExplicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare function noProvAnyInternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyExternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyLibraryFunction(): ModuleWithProviders<any>;
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: `
export declare interface Type<T> { beforeEach(() => {
new (...args: any[]): T _ = absoluteFrom;
}
export declare type Provider = any;
export declare interface ModuleWithProviders<T> {
ngModule: Type<T>
providers?: Provider[]
}
`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
describe('ModuleWithProvidersAnalyzer', () => { const TEST_PROGRAM: TestFile[] = [
describe('analyzeProgram()', () => { {
let analyses: ModuleWithProvidersAnalyses; name: _('/src/entry-point.js'),
let program: ts.Program; contents: `
let dtsProgram: BundleProgram; export * from './explicit';
let referencesRegistry: NgccReferencesRegistry; 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<ExplicitInternalModule>;
export declare function explicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function explicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare class ExplicitClass {
static explicitInternalMethod(): ModuleWithProviders<ExplicitInternalModule>;
static explicitExternalMethod(): ModuleWithProviders<ExternalModule>;
static explicitLibraryMethod(): ModuleWithProviders<LibraryModule>;
}
`
},
{
name: _('/typings/any.d.ts'),
contents: `
import {ModuleWithProviders} from './core';
export declare class AnyInternalModule {}
export declare function anyInternalFunction(): ModuleWithProviders<any>;
export declare function anyExternalFunction(): ModuleWithProviders<any>;
export declare function anyLibraryFunction(): ModuleWithProviders<any>;
export declare class AnyClass {
static anyInternalMethod(): ModuleWithProviders<any>;
static anyExternalMethod(): ModuleWithProviders<any>;
static anyLibraryMethod(): ModuleWithProviders<any>;
}
`
},
{
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<NoProvidersInternalModule>;
export declare function noProvExplicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function noProvExplicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare function noProvAnyInternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyExternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyLibraryFunction(): ModuleWithProviders<any>;
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(() => { export declare interface Type<T> {
program = makeTestProgram(...TEST_PROGRAM); new (...args: any[]): T
dtsProgram = makeTestBundleProgram(TEST_DTS_PROGRAM); }
const host = export declare type Provider = any;
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dtsProgram); export declare interface ModuleWithProviders<T> {
referencesRegistry = new NgccReferencesRegistry(host); ngModule: Type<T>
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); const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry);
analyses = analyzer.analyzeProgram(program); 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]) :
[];
}
}); });
}); });

View File

@ -5,241 +5,242 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; 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 {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 {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {MockLogger} from '../helpers/mock_logger'; 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', () => { const TEST_PROGRAM: TestFile[] = [
describe('analyzeProgram()', () => { {
name: _('/src/entry_point.js'),
const TEST_PROGRAM = [ isRoot: true,
{ contents: `
name: '/src/entry_point.js', export {PublicComponent} from './a';
isRoot: true, export {ModuleA} from './mod';
contents: ` export {ModuleB} from './b';
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';
` `
}, },
{ {
name: '/src/a.js', name: _('/src/a.js'),
isRoot: false, isRoot: false,
contents: ` contents: `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
export class ComponentOne {} export class PublicComponent {}
ComponentOne.decorators = [ PublicComponent.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]} {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/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', () => { addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'PublicComponent');
const {program, referencesRegistry, analyzer} = addToReferencesRegistry(
setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM); program, referencesRegistry, _('/src/b.js'), 'PrivateComponent1');
addToReferencesRegistry(
program, referencesRegistry, _('/src/c.js'), 'InternalComponent1');
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentOne'); const analyses = analyzer.analyzeProgram(program);
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentTwo'); // 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); it('should find all non-public declarations that were aliased', () => {
expect(analyses).toEqual([{ const _ = absoluteFrom;
identifier: 'ComponentOne', const ALIASED_EXPORTS_PROGRAM = [
from: _('/src/a.js'), {
dtsFrom: null, name: _('/src/entry_point.js'),
alias: 'aliasedComponentOne', 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));
}

View File

@ -5,51 +5,63 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; 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 {Reference} from '../../../src/ngtsc/imports';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; 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 {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {makeTestBundleProgram} from '../helpers/utils';
describe('NgccReferencesRegistry', () => { runInEachFileSystem(() => {
it('should return a mapping from resolved reference identifiers to their declarations', () => { describe('NgccReferencesRegistry', () => {
const {program, options, host} = makeProgram([{ it('should return a mapping from resolved reference identifiers to their declarations', () => {
name: 'index.ts', const _ = absoluteFrom;
contents: ` const TEST_FILES: TestFile[] = [{
name: _('/index.ts'),
contents: `
export class SomeClass {} export class SomeClass {}
export function someFunction() {} export function someFunction() {}
export const someVariable = 42; export const someVariable = 42;
export const testArray = [SomeClass, someFunction, someVariable]; 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 = const indexPath = _('/index.ts');
getDeclaration(program, 'index.ts', 'testArray', ts.isVariableDeclaration); const testArrayDeclaration =
const someClassDecl = getDeclaration(program, 'index.ts', 'SomeClass', ts.isClassDeclaration); getDeclaration(program, indexPath, 'testArray', ts.isVariableDeclaration);
const someFunctionDecl = const someClassDecl = getDeclaration(program, indexPath, 'SomeClass', ts.isClassDeclaration);
getDeclaration(program, 'index.ts', 'someFunction', ts.isFunctionDeclaration); const someFunctionDecl =
const someVariableDecl = getDeclaration(program, indexPath, 'someFunction', ts.isFunctionDeclaration);
getDeclaration(program, 'index.ts', 'someVariable', ts.isVariableDeclaration); const someVariableDecl =
const testArrayExpression = testArrayDeclaration.initializer !; getDeclaration(program, indexPath, 'someVariable', ts.isVariableDeclaration);
const testArrayExpression = testArrayDeclaration.initializer !;
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker); const evaluator = new PartialEvaluator(reflectionHost, checker);
const registry = new NgccReferencesRegistry(reflectionHost); const registry = new NgccReferencesRegistry(reflectionHost);
const references = (evaluator.evaluate(testArrayExpression) as any[]) const references = (evaluator.evaluate(testArrayExpression) as any[]).filter(isReference);
.filter(ref => ref instanceof Reference) as Reference<ts.Declaration>[]; registry.add(null !, ...references);
registry.add(null !, ...references);
const map = registry.getDeclarationMap(); const map = registry.getDeclarationMap();
expect(map.size).toEqual(2); expect(map.size).toEqual(2);
expect(map.get(someClassDecl.name !) !.node).toBe(someClassDecl); expect(map.get(someClassDecl.name !) !.node).toBe(someClassDecl);
expect(map.get(someFunctionDecl.name !) !.node).toBe(someFunctionDecl); expect(map.get(someFunctionDecl.name !) !.node).toBe(someFunctionDecl);
expect(map.has(someVariableDecl.name as ts.Identifier)).toBe(false); expect(map.has(someVariableDecl.name as ts.Identifier)).toBe(false);
});
}); });
function isReference(ref: any): ref is Reference<ts.Declaration> {
return ref instanceof Reference;
}
}); });

View File

@ -5,71 +5,77 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {makeTestProgram} from '../helpers/utils'; import {getRootFiles, makeTestBundleProgram} from '../helpers/utils';
const TEST_PROGRAM = [ runInEachFileSystem(() => {
{ describe('SwitchMarkerAnalyzer', () => {
name: 'entrypoint.js', describe('analyzeProgram()', () => {
contents: ` it('should check for switchable markers in all the files of the program', () => {
import {a} from './a'; const _ = absoluteFrom;
import {b} from './b'; const TEST_PROGRAM: TestFile[] = [
` {
}, name: _('/entrypoint.js'),
{ contents: `
name: 'a.js', import {a} from './a';
contents: ` import {b} from './b';
import {c} from './c'; `
export const a = 1; },
` {
}, name: _('/a.js'),
{ contents: `
name: 'b.js', import {c} from './c';
contents: ` export const a = 1;
export const b = 42; `
var factoryB = factory__PRE_R3__; },
` {
}, name: _('/b.js'),
{ contents: `
name: 'c.js', export const b = 42;
contents: ` var factoryB = factory__PRE_R3__;
export const c = 'So long, and thanks for all the fish!'; `
var factoryC = factory__PRE_R3__; },
var factoryD = 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', () => { const entrypoint = getSourceFileOrError(program, _('/entrypoint.js'));
describe('analyzeProgram()', () => { const a = getSourceFileOrError(program, _('/a.js'));
it('should check for switchable markers in all the files of the program', () => { const b = getSourceFileOrError(program, _('/b.js'));
const program = makeTestProgram(...TEST_PROGRAM); const c = getSourceFileOrError(program, _('/c.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const analyzer = new SwitchMarkerAnalyzer(host);
const analysis = analyzer.analyzeProgram(program);
const entrypoint = program.getSourceFile('entrypoint.js') !; expect(analysis.size).toEqual(2);
const a = program.getSourceFile('a.js') !; expect(analysis.has(entrypoint)).toBe(false);
const b = program.getSourceFile('b.js') !; expect(analysis.has(a)).toBe(false);
const c = program.getSourceFile('c.js') !; 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(c)).toBe(true);
expect(analysis.has(entrypoint)).toBe(false); expect(analysis.get(c) !.sourceFile).toBe(c);
expect(analysis.has(a)).toBe(false); expect(analysis.get(c) !.declarations.map(decl => decl.getText())).toEqual([
expect(analysis.has(b)).toBe(true); 'factoryC = factory__PRE_R3__',
expect(analysis.get(b) !.sourceFile).toBe(b); 'factoryD = factory__PRE_R3__',
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__',
]);
}); });
}); });
}); });

View File

@ -6,180 +6,216 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {CommonJsDependencyHost} from '../../src/dependencies/commonjs_dependency_host'; import {CommonJsDependencyHost} from '../../src/dependencies/commonjs_dependency_host';
import {ModuleResolver} from '../../src/dependencies/module_resolver'; 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', () => { beforeEach(() => {
let host: CommonJsDependencyHost; _ = absoluteFrom;
beforeEach(() => { loadTestFiles([
const fs = createMockFileSystem(); {
host = new CommonJsDependencyHost(fs, new ModuleResolver(fs)); name: _('/no/imports/or/re-exports/index.js'),
}); contents: '// some text but no import-like statements'
},
describe('getDependencies()', () => { {name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
it('should not generate a TS AST if the source does not contain any require calls', () => { {name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
spyOn(ts, 'createSourceFile'); {name: _('/external/imports/index.js'), contents: commonJs(['lib_1', 'lib_1/sub_1'])},
host.findDependencies(_('/no/imports/or/re-exports/index.js')); {name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'},
expect(ts.createSourceFile).not.toHaveBeenCalled(); {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', () => { describe('getDependencies()', () => {
const {dependencies, missing, deepImports} = it('should not generate a TS AST if the source does not contain any require calls', () => {
host.findDependencies(_('/external/imports/index.js')); spyOn(ts, 'createSourceFile');
expect(dependencies.size).toBe(2); host.findDependencies(_('/no/imports/or/re-exports/index.js'));
expect(missing.size).toBe(0); expect(ts.createSourceFile).not.toHaveBeenCalled();
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', () => { it('should resolve all the external imports of the source file', () => {
const {dependencies, missing, deepImports} = const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/re-exports/index.js')); host.findDependencies(_('/external/imports/index.js'));
expect(dependencies.size).toBe(2); expect(dependencies.size).toBe(2);
expect(missing.size).toBe(0); expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0); expect(deepImports.size).toBe(0);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
}); });
it('should capture missing external imports', () => { it('should resolve all the external re-exports of the source file', () => {
const {dependencies, missing, deepImports} = const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/imports-missing/index.js')); 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); it('should capture missing external imports', () => {
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); const {dependencies, missing, deepImports} =
expect(missing.size).toBe(1); host.findDependencies(_('/external/imports-missing/index.js'));
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true);
expect(deepImports.size).toBe(0);
});
it('should not register deep imports as missing', () => { expect(dependencies.size).toBe(1);
// This scenario verifies the behavior of the dependency analysis when an external import expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
// is found that does not map to an entry-point but still exists on disk, i.e. a deep import. expect(missing.size).toBe(1);
// Such deep imports are captured for diagnostics purposes. expect(missing.has(relativeFrom('missing'))).toBe(true);
const {dependencies, missing, deepImports} = expect(deepImports.size).toBe(0);
host.findDependencies(_('/external/deep-import/index.js')); });
expect(dependencies.size).toBe(0); it('should not register deep imports as missing', () => {
expect(missing.size).toBe(0); // This scenario verifies the behavior of the dependency analysis when an external import
expect(deepImports.size).toBe(1); // is found that does not map to an entry-point but still exists on disk, i.e. a deep
expect(deepImports.has(_('/node_modules/lib_1/deep/import'))).toBe(true); // 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', () => { expect(dependencies.size).toBe(0);
const {dependencies, missing, deepImports} = expect(missing.size).toBe(0);
host.findDependencies(_('/internal/outer/index.js')); expect(deepImports.size).toBe(1);
expect(deepImports.has(_('/node_modules/lib_1/deep/import'))).toBe(true);
});
expect(dependencies.size).toBe(1); it('should recurse into internal dependencies', () => {
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); const {dependencies, missing, deepImports} =
expect(missing.size).toBe(0); host.findDependencies(_('/internal/outer/index.js'));
expect(deepImports.size).toBe(0);
});
it('should handle circular internal dependencies', () => { expect(dependencies.size).toBe(1);
const {dependencies, missing, deepImports} = expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
host.findDependencies(_('/internal/circular_a/index.js')); expect(missing.size).toBe(0);
expect(dependencies.size).toBe(2); 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(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
});
it('should support `paths` alias mappings when resolving modules', () => { it('should handle circular internal dependencies', () => {
const fs = createMockFileSystem(); const {dependencies, missing, deepImports} =
host = new CommonJsDependencyHost(fs, new ModuleResolver(fs, { host.findDependencies(_('/internal/circular_a/index.js'));
baseUrl: '/dist', expect(dependencies.size).toBe(2);
paths: { expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
'@app/*': ['*'], expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
'@lib/*/test': ['lib/*/test'], expect(missing.size).toBe(0);
} expect(deepImports.size).toBe(0);
})); });
const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js'));
expect(dependencies.size).toBe(4); it('should support `paths` alias mappings when resolving modules', () => {
expect(dependencies.has(_('/dist/components'))).toBe(true); const fs = getFileSystem();
expect(dependencies.has(_('/dist/shared'))).toBe(true); host = new CommonJsDependencyHost(fs, new ModuleResolver(fs, {
expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true); baseUrl: '/dist',
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); paths: {
expect(missing.size).toBe(0); '@app/*': ['*'],
expect(deepImports.size).toBe(0); '@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() { function commonJs(importPaths: string[], exportNames: string[] = []) {
return new MockFileSystem({ const commonJsRequires =
'/no/imports/or/re-exports/index.js': '// some text but no import-like statements', importPaths
'/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', .map(
'/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', p =>
'/external/imports/index.js': commonJs(['lib_1', 'lib_1/sub_1']), `var ${p.replace('@angular/', '').replace(/\.?\.?\//g, '').replace(/@/,'')} = require('${p}');`)
'/external/imports/package.json': '{"esm2015": "./index.js"}', .join('\n');
'/external/imports/index.metadata.json': 'MOCK METADATA', const exportStatements =
'/external/re-exports/index.js': exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
commonJs(['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']), return `${commonJsRequires}
'/external/re-exports/package.json': '{"esm2015": "./index.js"}', ${exportStatements}`;
'/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}`;
}

View File

@ -5,187 +5,207 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver';
import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
import {ModuleResolver} from '../../src/dependencies/module_resolver'; import {ModuleResolver} from '../../src/dependencies/module_resolver';
import {FileSystem} from '../../src/file_system/file_system';
import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPoint} from '../../src/packages/entry_point';
import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.from; interface DepMap {
[path: string]: {resolved: string[], missing: string[]};
}
describe('DependencyResolver', () => { runInEachFileSystem(() => {
let host: EsmDependencyHost; describe('DependencyResolver', () => {
let resolver: DependencyResolver; let _: typeof absoluteFrom;
let fs: FileSystem; let host: EsmDependencyHost;
let moduleResolver: ModuleResolver; let resolver: DependencyResolver;
beforeEach(() => { let fs: FileSystem;
fs = new MockFileSystem(); let moduleResolver: ModuleResolver;
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;
const dependencies = { beforeEach(() => {
[_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []}, _ = absoluteFrom;
[_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []}, fs = getFileSystem();
[_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []}, moduleResolver = new ModuleResolver(fs);
[_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []}, host = new EsmDependencyHost(fs, moduleResolver);
[_('/fifth/index.js')]: {resolved: [], missing: []}, resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host});
};
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', () => { describe('sortEntryPointsByDependency()', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ let first: EntryPoint;
[_('/first/index.js')]: {resolved: [], missing: ['/missing']}, let second: EntryPoint;
[_('/second/sub/index.js')]: {resolved: [], missing: []}, let third: EntryPoint;
})); let fourth: EntryPoint;
const result = resolver.sortEntryPointsByDependency([first, second]); let fifth: EntryPoint;
expect(result.entryPoints).toEqual([second]); let dependencies: DepMap;
expect(result.invalidEntryPoints).toEqual([
{entryPoint: first, missingDependencies: ['/missing']}, 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};
};
}
}); });
}); });

View File

@ -7,222 +7,262 @@
*/ */
import * as ts from 'typescript'; 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 {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
import {ModuleResolver} from '../../src/dependencies/module_resolver'; import {ModuleResolver} from '../../src/dependencies/module_resolver';
import {MockFileSystem} from '../helpers/mock_file_system';
const _ = AbsoluteFsPath.from; runInEachFileSystem(() => {
describe('EsmDependencyHost', () => { describe('EsmDependencyHost', () => {
let host: EsmDependencyHost; let _: typeof absoluteFrom;
beforeEach(() => { let host: EsmDependencyHost;
const fs = createMockFileSystem(); beforeEach(() => {
host = new EsmDependencyHost(fs, new ModuleResolver(fs)); _ = absoluteFrom;
}); setupMockFileSystem();
const fs = getFileSystem();
describe('getDependencies()', () => { host = new EsmDependencyHost(fs, new ModuleResolver(fs));
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', () => { describe('getDependencies()', () => {
const {dependencies, missing, deepImports} = it('should not generate a TS AST if the source does not contain any imports or re-exports',
host.findDependencies(_('/external/re-exports/index.js')); () => {
expect(dependencies.size).toBe(2); spyOn(ts, 'createSourceFile');
expect(missing.size).toBe(0); host.findDependencies(_('/no/imports/or/re-exports/index.js'));
expect(deepImports.size).toBe(0); expect(ts.createSourceFile).not.toHaveBeenCalled();
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 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', () => { function setupMockFileSystem(): void {
const {dependencies, missing, deepImports} = loadTestFiles([
host.findDependencies(_('/external/imports-missing/index.js')); {
name: _('/no/imports/or/re-exports/index.js'),
expect(dependencies.size).toBe(1); contents: '// some text but no import-like statements'
expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); },
expect(missing.size).toBe(1); {name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); {name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
expect(deepImports.size).toBe(0); {
}); name: _('/external/imports/index.js'),
contents: `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`
it('should not register deep imports as missing', () => { },
// This scenario verifies the behavior of the dependency analysis when an external import {name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'},
// is found that does not map to an entry-point but still exists on disk, i.e. a deep import. {name: _('/external/imports/index.metadata.json'), contents: 'MOCK METADATA'},
// Such deep imports are captured for diagnostics purposes. {
const {dependencies, missing, deepImports} = name: _('/external/re-exports/index.js'),
host.findDependencies(_('/external/deep-import/index.js')); contents: `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`
},
expect(dependencies.size).toBe(0); {name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
expect(missing.size).toBe(0); {name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
expect(deepImports.size).toBe(1); {
expect(deepImports.has(_('/node_modules/lib-1/deep/import'))).toBe(true); name: _('/external/imports-missing/index.js'),
}); contents: `import {X} from 'lib-1';\nimport {Y} from 'missing';`
},
it('should recurse into internal dependencies', () => { {name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'},
const {dependencies, missing, deepImports} = {name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'},
host.findDependencies(_('/internal/outer/index.js')); {
name: _('/external/deep-import/index.js'),
expect(dependencies.size).toBe(1); contents: `import {Y} from 'lib-1/deep/import';`
expect(dependencies.has(_('/node_modules/lib-1/sub-1'))).toBe(true); },
expect(missing.size).toBe(0); {name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'},
expect(deepImports.size).toBe(0); {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"}'},
it('should handle circular internal dependencies', () => { {name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'},
const {dependencies, missing, deepImports} = {
host.findDependencies(_('/internal/circular-a/index.js')); name: _('/internal/inner/index.js'),
expect(dependencies.size).toBe(2); contents: `import {Y} from 'lib-1/sub-1'; export declare class X {}`
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); name: _('/internal/circular-a/index.js'),
expect(deepImports.size).toBe(0); contents:
}); `import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`
},
it('should support `paths` alias mappings when resolving modules', () => { {
const fs = createMockFileSystem(); name: _('/internal/circular-b/index.js'),
host = new EsmDependencyHost(fs, new ModuleResolver(fs, { contents:
baseUrl: '/dist', `import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';`
paths: { },
'@app/*': ['*'], {name: _('/internal/circular-a/package.json'), contents: '{"esm2015": "./index.js"}'},
'@lib/*/test': ['lib/*/test'], {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"}'},
const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); {name: _('/re-directed/index.metadata.json'), contents: 'MOCK METADATA'},
expect(dependencies.size).toBe(4); {
expect(dependencies.has(_('/dist/components'))).toBe(true); name: _('/path-alias/index.js'),
expect(dependencies.has(_('/dist/shared'))).toBe(true); contents:
expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true); `import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';`
expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); },
expect(missing.size).toBe(0); {name: _('/path-alias/package.json'), contents: '{"esm2015": "./index.js"}'},
expect(deepImports.size).toBe(0); {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'},
function createMockFileSystem() { {
return new MockFileSystem({ name: _('/node_modules/lib-1/deep/import/index.js'),
'/no/imports/or/re-exports/index.js': '// some text but no import-like statements', contents: 'export declare class DeepImport {}'
'/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', },
'/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', {name: _('/node_modules/lib-1/sub-1/index.js'), contents: 'export declare class Y {}'},
'/external/imports/index.js': `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`, {name: _('/node_modules/lib-1/sub-1/package.json'), contents: '{"esm2015": "./index.js"}'},
'/external/imports/package.json': '{"esm2015": "./index.js"}', {name: _('/node_modules/lib-1/sub-1/index.metadata.json'), contents: 'MOCK METADATA'},
'/external/imports/index.metadata.json': 'MOCK METADATA', {name: _('/node_modules/lib-1/sub-2.js'), contents: `export * from './sub-2/sub-2';`},
'/external/re-exports/index.js': `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`, {name: _('/node_modules/lib-1/sub-2/sub-2.js'), contents: `export declare class Z {}';`},
'/external/re-exports/package.json': '{"esm2015": "./index.js"}', {name: _('/node_modules/lib-1/sub-2/package.json'), contents: '{"esm2015": "./sub-2.js"}'},
'/external/re-exports/index.metadata.json': 'MOCK METADATA', {name: _('/node_modules/lib-1/sub-2/sub-2.metadata.json'), contents: 'MOCK METADATA'},
'/external/imports-missing/index.js': `import {X} from 'lib-1';\nimport {Y} from 'missing';`, {name: _('/dist/components/index.js'), contents: `class MyComponent {};`},
'/external/imports-missing/package.json': '{"esm2015": "./index.js"}', {name: _('/dist/components/package.json'), contents: '{"esm2015": "./index.js"}'},
'/external/imports-missing/index.metadata.json': 'MOCK METADATA', {name: _('/dist/components/index.metadata.json'), contents: 'MOCK METADATA'},
'/external/deep-import/index.js': `import {Y} from 'lib-1/deep/import';`, {
'/external/deep-import/package.json': '{"esm2015": "./index.js"}', name: _('/dist/shared/index.js'),
'/external/deep-import/index.metadata.json': 'MOCK METADATA', contents: `import {X} from 'lib-1';\nexport class Service {}`
'/internal/outer/index.js': `import {X} from '../inner';`, },
'/internal/outer/package.json': '{"esm2015": "./index.js"}', {name: _('/dist/shared/package.json'), contents: '{"esm2015": "./index.js"}'},
'/internal/outer/index.metadata.json': 'MOCK METADATA', {name: _('/dist/shared/index.metadata.json'), contents: 'MOCK METADATA'},
'/internal/inner/index.js': `import {Y} from 'lib-1/sub-1'; export declare class X {}`, {name: _('/dist/lib/shared/test/index.js'), contents: `export class TestHelper {}`},
'/internal/circular-a/index.js': {name: _('/dist/lib/shared/test/package.json'), contents: '{"esm2015": "./index.js"}'},
`import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`, {name: _('/dist/lib/shared/test/index.metadata.json'), contents: 'MOCK METADATA'},
'/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];
} }
});
describe('hasImportOrReexportStatements', () => { describe('isStringImportOrReexport', () => {
it('should return true if there is an import statement', () => { it('should return true if the statement is an import', () => {
expect(host.hasImportOrReexportStatements('import {X} from "some/x";')).toBe(true); expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";')))
expect(host.hasImportOrReexportStatements('import * as X from "some/x";')).toBe(true); .toBe(true);
expect( expect(host.isStringImportOrReexport(createStatement('import * as X from "some/x";')))
host.hasImportOrReexportStatements('blah blah\n\n import {X} from "some/x";\nblah blah')) .toBe(true);
.toBe(true); });
expect(host.hasImportOrReexportStatements('\t\timport {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); describe('hasImportOrReexportStatements', () => {
expect( it('should return true if there is an import statement', () => {
host.hasImportOrReexportStatements('blah blah\n\n export {X} from "some/x";\nblah blah')) expect(host.hasImportOrReexportStatements('import {X} from "some/x";')).toBe(true);
.toBe(true); expect(host.hasImportOrReexportStatements('import * as X from "some/x";')).toBe(true);
expect(host.hasImportOrReexportStatements('\t\texport {X} from "some/x";')).toBe(true); expect(host.hasImportOrReexportStatements(
expect(host.hasImportOrReexportStatements( 'blah blah\n\n import {X} from "some/x";\nblah blah'))
'blah blah\n\n export * from "@angular/core;\nblah blah')) .toBe(true);
.toBe(true); expect(host.hasImportOrReexportStatements('\t\timport {X} from "some/x";')).toBe(true);
}); });
it('should return false if there is no import nor re-export statement', () => { it('should return true if there is a re-export statement', () => {
expect(host.hasImportOrReexportStatements('blah blah')).toBe(false); expect(host.hasImportOrReexportStatements('export {X} from "some/x";')).toBe(true);
expect(host.hasImportOrReexportStatements('export function moo() {}')).toBe(false); expect(host.hasImportOrReexportStatements(
expect( 'blah blah\n\n export {X} from "some/x";\nblah blah'))
host.hasImportOrReexportStatements('Some text that happens to include the word import')) .toBe(true);
.toBe(false); 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);
});
}); });
}); });
}); });

View File

@ -5,202 +5,200 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {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() { beforeEach(() => {
return new MockFileSystem({ _ = absoluteFrom;
'/libs': { loadTestFiles([
'local-package': { {name: _('/libs/local-package/package.json'), contents: 'PACKAGE.JSON for local-package'},
'package.json': 'PACKAGE.JSON for local-package', {name: _('/libs/local-package/index.js'), contents: `import {X} from './x';`},
'index.js': `import {X} from './x';`, {name: _('/libs/local-package/x.js'), contents: `export class X {}`},
'x.js': `export class X {}`, {name: _('/libs/local-package/sub-folder/index.js'), contents: `import {X} from '../x';`},
'sub-folder': { {
'index.js': `import {X} from '../x';`, name: _('/libs/local-package/node_modules/package-1/sub-folder/index.js'),
contents: `export class Z {}`
}, },
'node_modules': { {
'package-1': { name: _('/libs/local-package/node_modules/package-1/package.json'),
'sub-folder': {'index.js': `export class Z {}`}, contents: 'PACKAGE.JSON for package-1'
'package.json': 'PACKAGE.JSON for package-1',
},
}, },
}, {
'node_modules': { name: _('/libs/node_modules/package-2/package.json'),
'package-2': { contents: 'PACKAGE.JSON for 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/node_modules/package-3/package.json'),
'/dist': { contents: 'PACKAGE.JSON for package-3'
'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',
}, },
'package-5': { {name: _('/dist/package-4/x.js'), contents: `export class X {}`},
'package.json': 'PACKAGE.JSON for package-5', {name: _('/dist/package-4/package.json'), contents: 'PACKAGE.JSON for package-4'},
'post-fix': { {
'package.json': 'PACKAGE.JSON for package-5/post-fix', name: _('/dist/package-4/sub-folder/index.js'),
} contents: `import {X} from '@shared/package-4/x';`
}, },
} {
}, name: _('/dist/sub-folder/package-4/package.json'),
'/node_modules': { contents: 'PACKAGE.JSON for package-4'
'top-package': { },
'package.json': 'PACKAGE.JSON for top-package', {
} 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'
describe('ModuleResolver', () => { },
describe('resolveModule()', () => { {
describe('with relative paths', () => { name: _('/node_modules/top-package/package.json'),
it('should resolve sibling, child and aunt modules', () => { contents: 'PACKAGE.JSON for top-package'
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);
});
}); });
describe('with non-mapped external paths', () => { describe('resolveModule()', () => {
it('should resolve to the package.json of a local node_modules package', () => { describe('with relative paths', () => {
const resolver = new ModuleResolver(createMockFileSystem()); it('should resolve sibling, child and aunt modules', () => {
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js'))) const resolver = new ModuleResolver(getFileSystem());
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js')))
expect( .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js')));
resolver.resolveModuleImport('package-1', _('/libs/local-package/sub-folder/index.js'))) expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); .toEqual(new ResolvedRelativeModule(_('/libs/local-package/sub-folder/index.js')));
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js'))) expect(resolver.resolveModuleImport('../x', _('/libs/local-package/sub-folder/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js')));
});
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/*': ['*'],
}
}); });
// We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map to it('should return `null` if the resolved module relative module does not exist', () => {
// `*` and so the final resolved path will not include the `sub-folder` segment. const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport( expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null);
'@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', () => { describe('with non-mapped external paths', () => {
let resolver: ModuleResolver; 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(); it('should resolve to the package.json of a higher node_modules package', () => {
resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4'))); .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/*', '*']}}); it('should return `null` if the package cannot be found', () => {
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) const resolver = new ModuleResolver(getFileSystem());
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); 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', () => { describe('with mapped path external modules', () => {
const resolver = new ModuleResolver( it('should resolve to the package.json of simple mapped packages', () => {
createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); const resolver = new ModuleResolver(
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) getFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}});
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix')));
});
it('should match paths against complex path matchers', () => { expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
const resolver = new ModuleResolver( .toEqual(new ResolvedExternalModule(_('/dist/package-4')));
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);
});
it('should resolve path as "relative" if the mapped path is inside the current package', expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
() => { .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5')));
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')));
});
it('should resolve paths where the wildcard matches more than one path segment', () => { it('should select the best match by the length of prefix before the *', () => {
const resolver = new ModuleResolver( const resolver = new ModuleResolver(getFileSystem(), {
createMockFileSystem(), baseUrl: '/dist',
{baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); paths: {
expect( '@lib/*': ['*'],
resolver.resolveModuleImport( '@lib/sub-folder/*': ['*'],
'@shared/sub-folder/package-5/post-fix', _('/dist/package-4/sub-folder/index.js'))) }
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); });
// 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')));
});
}); });
}); });
}); });

View File

@ -7,180 +7,231 @@
*/ */
import * as ts from 'typescript'; 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 {ModuleResolver} from '../../src/dependencies/module_resolver';
import {UmdDependencyHost} from '../../src/dependencies/umd_dependency_host'; 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', () => { beforeEach(() => {
let host: UmdDependencyHost; _ = absoluteFrom;
beforeEach(() => { setupMockFileSystem();
const fs = createMockFileSystem(); const fs = getFileSystem();
host = new UmdDependencyHost(fs, new ModuleResolver(fs)); 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()', () => { function umd(moduleName: string, importPaths: string[], exportNames: string[] = []) {
it('should not generate a TS AST if the source does not contain any require calls', () => { const commonJsRequires = importPaths.map(p => `,require('${p}')`).join('');
spyOn(ts, 'createSourceFile'); const amdDeps = importPaths.map(p => `,'${p}'`).join('');
host.findDependencies(_('/no/imports/or/re-exports/index.js')); const globalParams =
expect(ts.createSourceFile).not.toHaveBeenCalled(); importPaths.map(p => `,global.${p.replace('@angular/', 'ng.').replace(/\//g, '')}`)
}); .join('');
const params =
it('should resolve all the external imports of the source file', () => { importPaths.map(p => `,${p.replace('@angular/', '').replace(/\.?\.?\//g, '')}`).join('');
const {dependencies, missing, deepImports} = const exportStatements =
host.findDependencies(_('/external/imports/index.js')); exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
expect(dependencies.size).toBe(2); return `
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 (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports${commonJsRequires}) : typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports${commonJsRequires}) :
typeof define === 'function' && define.amd ? define('${moduleName}', ['exports'${amdDeps}], factory) : typeof define === 'function' && define.amd ? define('${moduleName}', ['exports'${amdDeps}], factory) :
@ -189,4 +240,5 @@ function umd(moduleName: string, importPaths: string[], exportNames: string[] =
${exportStatements} ${exportStatements}
}))); })));
`; `;
} }
});

View File

@ -10,8 +10,8 @@ ts_library(
]), ]),
deps = [ deps = [
"//packages/compiler-cli/ngcc", "//packages/compiler-cli/ngcc",
"//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/file_system/testing",
"@npm//typescript", "@npm//typescript",
], ],
) )

View File

@ -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);
}

View File

@ -5,19 +5,13 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import {AbsoluteFsPath, NgtscCompilerHost, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {TestFile} from '../../../src/ngtsc/file_system/testing';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {BundleProgram, makeBundleProgram} from '../../src/packages/bundle_program';
import {makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript';
import {BundleProgram} from '../../src/packages/bundle_program';
import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point'; import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from '../../src/packages/patch_ts_expando_initializer'; import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
import {Folder} from './mock_file_system';
export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript';
const _ = AbsoluteFsPath.fromUnchecked;
/** /**
* *
* @param format The format of the bundle. * @param format The format of the bundle.
@ -26,86 +20,31 @@ const _ = AbsoluteFsPath.fromUnchecked;
*/ */
export function makeTestEntryPointBundle( export function makeTestEntryPointBundle(
formatProperty: EntryPointJsonProperty, format: EntryPointFormat, isCore: boolean, formatProperty: EntryPointJsonProperty, format: EntryPointFormat, isCore: boolean,
files: {name: string, contents: string, isRoot?: boolean}[], srcRootNames: AbsoluteFsPath[], dtsRootNames?: AbsoluteFsPath[]): EntryPointBundle {
dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]): EntryPointBundle { const src = makeTestBundleProgram(srcRootNames[0], isCore);
const src = makeTestBundleProgram(files); const dts = dtsRootNames ? makeTestDtsBundleProgram(dtsRootNames[0], isCore) : null;
const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null;
const isFlatCore = isCore && src.r3SymbolsFile === null; const isFlatCore = isCore && src.r3SymbolsFile === null;
return {formatProperty, format, rootDirs: [_('/')], src, dts, isCore, isFlatCore}; return {formatProperty, format, rootDirs: [absoluteFrom('/')], src, dts, isCore, isFlatCore};
} }
/** export function makeTestBundleProgram(
* Create a bundle program for testing. path: AbsoluteFsPath, isCore: boolean = false): BundleProgram {
* @param files The source files of the bundle program. const fs = getFileSystem();
*/ const options = {allowJs: true, checkJs: false};
export function makeTestBundleProgram(files: {name: string, contents: string}[]): BundleProgram { const entryPointPath = fs.dirname(path);
const {program, options, host} = makeTestProgramInternal(...files); const host = new NgccSourcesCompilerHost(fs, options, entryPointPath);
const path = _(files[0].name); return makeBundleProgram(fs, isCore, path, 'r3_symbols.js', options, host);
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};
} }
function makeTestProgramInternal( export function makeTestDtsBundleProgram(
...files: {name: string, contents: string, isRoot?: boolean | undefined}[]): { path: AbsoluteFsPath, isCore: boolean = false): BundleProgram {
program: ts.Program, const fs = getFileSystem();
host: ts.CompilerHost, const options = {};
options: ts.CompilerOptions, const host = new NgtscCompilerHost(fs, options);
} { return makeBundleProgram(fs, isCore, path, 'r3_symbols.d.ts', options, host);
const originalTsGetExpandoInitializer = patchTsGetExpandoInitializer();
const program =
makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false});
restoreGetExpandoInitializer(originalTsGetExpandoInitializer);
return program;
} }
export function makeTestProgram( export function convertToDirectTsLibImport(filesystem: TestFile[]) {
...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<T> = (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<T = any> {}
`
};
}
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}[]) {
return filesystem.map(file => { return filesystem.map(file => {
const contents = const contents =
file.contents file.contents
@ -117,10 +56,6 @@ export function convertToDirectTsLibImport(filesystem: {name: string, contents:
}); });
} }
export function createFileSystemFromProgramFiles( export function getRootFiles(testFiles: TestFile[]): AbsoluteFsPath[] {
...fileCollections: ({name: string, contents: string}[] | undefined)[]): Folder { return testFiles.filter(f => f.isRoot !== false).map(f => absoluteFrom(f.name));
const folder: Folder = {};
fileCollections.forEach(
files => files && files.forEach(file => folder[file.name] = file.contents));
return folder;
} }

File diff suppressed because it is too large Load Diff

View File

@ -8,17 +8,28 @@
import * as ts from 'typescript'; 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 {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 {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils'; import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util'; import {expectTypeValueReferencesForParameters} from './util';
const FILES = [ runInEachFileSystem(() => {
{ describe('Fesm2015ReflectionHost [import helper style]', () => {
name: '/some_directive.js', let _: typeof absoluteFrom;
contents: ` let FILES: {[label: string]: TestFile[]};
beforeEach(() => {
_ = absoluteFrom;
const NAMESPACED_IMPORT_FILES = [
{
name: _('/some_directive.js'),
contents: `
import * as tslib_1 from 'tslib'; import * as tslib_1 from 'tslib';
import { Directive, Inject, InjectionToken, Input } from '@angular/core'; import { Directive, Inject, InjectionToken, Input } from '@angular/core';
const INJECTED_TOKEN = new InjectionToken('injected'); const INJECTED_TOKEN = new InjectionToken('injected');
@ -52,10 +63,10 @@ const FILES = [
], SomeDirective); ], SomeDirective);
export { SomeDirective }; export { SomeDirective };
`, `,
}, },
{ {
name: '/node_modules/@angular/core/some_directive.js', name: _('/node_modules/@angular/core/some_directive.js'),
contents: ` contents: `
import * as tslib_1 from 'tslib'; import * as tslib_1 from 'tslib';
import { Directive, Input } from './directives'; import { Directive, Input } from './directives';
let SomeDirective = class SomeDirective { let SomeDirective = class SomeDirective {
@ -70,10 +81,10 @@ const FILES = [
], SomeDirective); ], SomeDirective);
export { SomeDirective }; export { SomeDirective };
`, `,
}, },
{ {
name: 'ngmodule.js', name: _('/ngmodule.js'),
contents: ` contents: `
import * as tslib_1 from 'tslib'; import * as tslib_1 from 'tslib';
import { NgModule } from './directives'; import { NgModule } from './directives';
var HttpClientXsrfModule_1; var HttpClientXsrfModule_1;
@ -96,311 +107,340 @@ const FILES = [
nonDecoratedVar = 43; nonDecoratedVar = 43;
export { HttpClientXsrfModule }; export { HttpClientXsrfModule };
` `
}, },
]; ];
describe('Fesm2015ReflectionHost [import helper style]', () => { const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES);
[{files: FILES, label: 'namespaced'},
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
].forEach(fileSystem => {
describe(`[${fileSystem.label}]`, () => {
describe('getDecoratorsOfDeclaration()', () => { FILES = {
it('should find the decorators on a class', () => { 'namespaced': NAMESPACED_IMPORT_FILES,
const program = makeTestProgram(fileSystem.files[0]); 'direct import': DIRECT_IMPORT_FILES,
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).toBeDefined(); ['namespaced', 'direct import'].forEach(label => {
expect(decorators.length).toEqual(1); describe(`[${label}]`, () => {
beforeEach(() => {
const decorator = decorators[0]; const fs = getFileSystem();
expect(decorator.name).toEqual('Directive'); loadFakeCore(fs);
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); loadTestFiles(FILES[label]);
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { describe('getDecoratorsOfDeclaration()', () => {
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') it('should find the decorators on a class', () => {
.and.callFake( const {program} = makeTestBundleProgram(_('/some_directive.js'));
(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]);
const host = const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration( const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode); const decorators = host.getDecoratorsOfDeclaration(classNode) !;
const decorators = parameters ![2].decorators !;
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.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; const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(typeIdentifier.text).toBe('Inject'); 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( function findVariableDeclaration(
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined { node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
if (!node) { if (!node) {
return; 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));
}
}); });

File diff suppressed because it is too large Load Diff

View File

@ -5,20 +5,38 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; 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 {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 {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils'; import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util'; import {expectTypeValueReferencesForParameters} from './util';
const FILES = [ runInEachFileSystem(() => {
{ describe('Esm5ReflectionHost [import helper style]', () => {
name: '/some_directive.js', let _: typeof absoluteFrom;
contents: ` 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 * as tslib_1 from 'tslib';
import { Directive, Inject, InjectionToken, Input } from '@angular/core'; import { Directive, Inject, InjectionToken, Input } from '@angular/core';
var INJECTED_TOKEN = new InjectionToken('injected'); var INJECTED_TOKEN = new InjectionToken('injected');
@ -59,10 +77,10 @@ const FILES = [
}()); }());
export { SomeDirective }; export { SomeDirective };
`, `,
}, },
{ {
name: '/node_modules/@angular/core/some_directive.js', name: _('/node_modules/@angular/core/some_directive.js'),
contents: ` contents: `
import * as tslib_1 from 'tslib'; import * as tslib_1 from 'tslib';
import { Directive, Input } from './directives'; import { Directive, Input } from './directives';
var SomeDirective = /** @class */ (function () { var SomeDirective = /** @class */ (function () {
@ -80,10 +98,10 @@ const FILES = [
}()); }());
export { SomeDirective }; export { SomeDirective };
`, `,
}, },
{ {
name: '/ngmodule.js', name: _('/ngmodule.js'),
contents: ` contents: `
import * as tslib_1 from 'tslib'; import * as tslib_1 from 'tslib';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
var HttpClientXsrfModule = /** @class */ (function () { var HttpClientXsrfModule = /** @class */ (function () {
@ -110,366 +128,380 @@ export { SomeDirective };
nonDecoratedVar = 43; nonDecoratedVar = 43;
export { HttpClientXsrfModule }; export { HttpClientXsrfModule };
` `
}, },
]; ];
describe('Esm5ReflectionHost [import helper style]', () => { const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES);
[{files: FILES, label: 'namespaced'},
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
].forEach(fileSystem => {
describe(`[${fileSystem.label}]`, () => {
describe('getDecoratorsOfDeclaration()', () => { FILES = {
it('should find the decorators on a class', () => { 'namespaced': NAMESPACED_IMPORT_FILES,
const program = makeTestProgram(fileSystem.files[0]); 'direct import': DIRECT_IMPORT_FILES,
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).toBeDefined(); ['namespaced', 'direct import'].forEach(label => {
expect(decorators.length).toEqual(1); describe(`[${label}]`, () => {
beforeEach(() => {
const decorator = decorators[0]; const fs = getFileSystem();
expect(decorator.name).toEqual('Directive'); loadFakeCore(fs);
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); loadTestFiles(FILES[label]);
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { describe('getDecoratorsOfDeclaration()', () => {
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') it('should find the decorators on a class', () => {
.and.callFake( const {program} = makeTestBundleProgram(_('/some_directive.js'));
(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]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration( const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration); program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode); const decorators = host.getDecoratorsOfDeclaration(classNode) !;
const decorators = parameters ![2].decorators !;
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.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; const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(typeIdentifier.text).toBe('Inject'); 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()', () => { function findVariableDeclaration(
it('should return an array of all classes in the given source file', () => { node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
const program = makeTestProgram(...fileSystem.files); if (!node) {
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); return;
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()}.`);
} }
expect(value.getText()).toBe('HttpClientXsrfModule'); if (isNamedVariableDeclaration(node) && node.name.text === variableName) {
}); return node;
}
it('should return undefined if the variable has no assignment', () => { return node.forEachChild(node => findVariableDeclaration(node, variableName));
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);
});
}); });
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';
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CtorParameter} from '../../../src/ngtsc/reflection'; import {CtorParameter} from '../../../src/ngtsc/reflection';
/** /**

View File

@ -5,422 +5,397 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; import {Folder, MockFileSystem, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {existsSync, readFileSync, readdirSync, statSync, symlinkSync} from 'fs'; import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
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 {mainNgcc} from '../../src/main'; import {mainNgcc} from '../../src/main';
import {markAsProcessed} from '../../src/packages/build_marker'; import {markAsProcessed} from '../../src/packages/build_marker';
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.from; const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true});
describe('ngcc main()', () => { runInEachFileSystem(() => {
beforeEach(createMockFileSystem); describe('ngcc main()', () => {
afterEach(restoreRealFileSystem); let _: typeof absoluteFrom;
let fs: FileSystem;
it('should run ngcc without errors for esm2015', () => { beforeEach(() => {
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']})) _ = absoluteFrom;
.not.toThrow(); fs = getFileSystem();
}); initMockFileSystem(fs, testFiles);
});
it('should run ngcc without errors for esm5', () => { it('should run ngcc without errors for esm2015', () => {
expect(() => mainNgcc({ 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', basePath: '/node_modules',
propertiesToConsider: ['esm5'], propertiesToConsider: ['esm5'],
compileAllFormats: false,
logger: new MockLogger(), logger: new MockLogger(),
})) });
.not.toThrow(); expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
}); esm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
it('should run ngcc without errors when "main" property is not present', () => { typings: '0.0.0-PLACEHOLDER',
mainNgcc({ });
basePath: '/dist', });
propertiesToConsider: ['main', 'es2015'],
logger: new MockLogger(),
}); });
expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({ describe('with createNewEntryPointFormats', () => {
es2015: '0.0.0-PLACEHOLDER', it('should create new files rather than overwriting the originals', () => {
typings: '0.0.0-PLACEHOLDER', const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/;
});
});
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({ mainNgcc({
basePath: '/node_modules', 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(); const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']);
mainNgcc({ mainNgcc({
basePath: '/node_modules', basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing', propertiesToConsider: ['esm2015'], logger,
propertiesToConsider: ['fesm2015', 'esm5', 'esm2015'], logger,
}); });
expect(logger.logs.debug).not.toContain([ expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']);
'The target entry-point has already been processed'
]);
}); });
}); });
describe('[compileAllFormats === false]', () => { describe('with pathMappings', () => {
it('should process the target if the first matching `propertyToConsider` is not marked as processed', it('should find and compile packages accessible via the pathMappings', () => {
() => { mainNgcc({
const logger = new MockLogger(); basePath: '/node_modules',
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']); propertiesToConsider: ['es2015'],
mainNgcc({ pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'},
basePath: '/node_modules', });
targetEntryPointPath: '@angular/common/http/testing', expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
propertiesToConsider: ['esm5', 'esm2015'], es2015: '0.0.0-PLACEHOLDER',
compileAllFormats: false, logger, typings: '0.0.0-PLACEHOLDER',
}); });
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
expect(logger.logs.debug).not.toContain([ es2015: '0.0.0-PLACEHOLDER',
'The target entry-point has already been processed' typings: '0.0.0-PLACEHOLDER',
]); });
});
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',
}); });
}); });
it('should cope with compiling the same entry-point multiple times with different formats', function loadPackage(
() => { packageName: string, basePath: AbsoluteFsPath = _('/node_modules')): EntryPointPackageJson {
mainNgcc({ return JSON.parse(fs.readFile(fs.resolve(basePath, packageName, 'package.json')));
basePath: '/node_modules', }
propertiesToConsider: ['module'],
compileAllFormats: false,
logger: new MockLogger(),
}); function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ if (fs instanceof MockFileSystem) {
module: '0.0.0-PLACEHOLDER', fs.init(testFiles);
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',
});
});
});
describe('with createNewEntryPointFormats', () => { // a random test package that no metadata.json file so not compiled by Angular.
it('should create new files rather than overwriting the originals', () => { loadTestFiles([
const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/; {
mainNgcc({ name: _('/node_modules/test-package/package.json'),
basePath: '/node_modules', contents: '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
createNewEntryPointFormats: true, },
propertiesToConsider: ['esm5'], {
logger: new MockLogger(), 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;'
},
]);
}); // An Angular package that has been built locally and stored in the `dist` directory.
loadTestFiles([
// Updates the package.json {
expect(loadPackage('@angular/common').esm5).toEqual('./esm5/common.js'); name: _('/dist/local-package/package.json'),
expect((loadPackage('@angular/common') as any).esm5_ivy_ngcc) contents: '{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
.toEqual('__ivy_ngcc__/esm5/common.js'); },
{name: _('/dist/local-package/index.metadata.json'), contents: 'DUMMY DATA'},
// Doesn't touch original files {
expect(readFileSync(`/node_modules/@angular/common/esm5/src/common_module.js`, 'utf8')) name: _('/dist/local-package/index.js'),
.not.toMatch(ANGULAR_CORE_IMPORT_REGEX); contents:
// Or create a backup of the original `import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '<h2>Hello</h2>'}] }\n];`
expect(existsSync(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`)) },
.toBe(false); {
name: _('/dist/local-package/index.d.ts'),
// Creates new files contents: `export declare class AppComponent {};`
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: '<h2>Hello</h2>'}] }
];`,
'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');
} }
}); });
});
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'));
}

View File

@ -5,168 +5,159 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker';
import {MockFileSystem} from '../helpers/mock_file_system';
function createMockFileSystem() { runInEachFileSystem(() => {
return new MockFileSystem({ describe('Marker files', () => {
'/node_modules/@angular/common': { let _: typeof absoluteFrom;
'package.json': `{ beforeEach(() => {
"fesm2015": "./fesm2015/common.js", _ = absoluteFrom;
"fesm5": "./fesm5/common.js", loadTestFiles([
"typings": "./common.d.ts" {
}`, name: _('/node_modules/@angular/common/package.json'),
'fesm2015': { contents:
'common.js': 'DUMMY CONTENT', `{"fesm2015": "./fesm2015/common.js", "fesm5": "./fesm5/common.js", "typings": "./common.d.ts"}`
'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"
}`,
}, },
}, {name: _('/node_modules/@angular/common/fesm2015/common.js'), contents: 'DUMMY CONTENT'},
'other': { {name: _('/node_modules/@angular/common/fesm2015/http.js'), contents: 'DUMMY CONTENT'},
'package.json': '{ }', {
}, name: _('/node_modules/@angular/common/fesm2015/http/testing.js'),
'testing': { contents: 'DUMMY CONTENT'
'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/testing.js'), contents: 'DUMMY CONTENT'},
}, {
'/node_modules/@angular/no-typings': { name: _('/node_modules/@angular/common/http/package.json'),
'package.json': `{ contents:
"fesm2015": "./fesm2015/index.js" `{"fesm2015": "../fesm2015/http.js", "fesm5": "../fesm5/http.js", "typings": "./http.d.ts"}`
}`,
'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': '{ }',
}, },
}, {
'not_node_modules': { name: _('/node_modules/@angular/common/http/testing/package.json'),
'lib2': { contents:
'package.json': '{ }', `{"fesm2015": "../../fesm2015/http/testing.js", "fesm5": "../../fesm5/http/testing.js", "typings": "../http/testing.d.ts" }`
}, },
}, {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"}`
describe('Marker files', () => { },
const COMMON_PACKAGE_PATH = AbsoluteFsPath.from('/node_modules/@angular/common/package.json'); {name: _('/node_modules/@angular/common/node_modules/tslib/package.json'), contents: '{ }'},
{
describe('markAsProcessed', () => { name: _(
it('should write a property in the package.json containing the version placeholder', () => { '/node_modules/@angular/common/node_modules/tslib/node_modules/other-lib/package.json'),
const fs = createMockFileSystem(); contents: '{ }'
},
let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); {
expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); name: _('/node_modules/@angular/no-typings/package.json'),
expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); contents: `{ "fesm2015": "./fesm2015/index.js" }`
},
markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); {name: _('/node_modules/@angular/no-typings/fesm2015/index.js'), contents: 'DUMMY CONTENT'},
pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); {
expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); name: _('/node_modules/@angular/no-typings/fesm2015/index.d.ts'),
expect(pkg.__processed_by_ivy_ngcc__.esm5).toBeUndefined(); contents: 'DUMMY CONTENT'
},
markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'esm5'); {
pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); name: _('/node_modules/@angular/other/not-package.json'),
expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); contents: '{ "fesm2015": "./fesm2015/other.js" }'
expect(pkg.__processed_by_ivy_ngcc__.esm5).toEqual('0.0.0-PLACEHOLDER'); },
{
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', () => { describe('markAsProcessed', () => {
const fs = createMockFileSystem(); it('should write a property in the package.json containing the version placeholder', () => {
let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); const COMMON_PACKAGE_PATH = _('/node_modules/@angular/common/package.json');
expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); const fs = getFileSystem();
markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); let 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__).toBeUndefined();
}); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined();
});
describe('hasBeenProcessed', () => { markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015');
it('should return true if the marker exists for the given format property', () => { pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH));
expect(hasBeenProcessed( expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER');
{name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}}, expect(pkg.__processed_by_ivy_ngcc__.esm5).toBeUndefined();
'fesm2015'))
.toBe(true); 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( describe('hasBeenProcessed', () => {
{name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}}, it('should return true if the marker exists for the given format property', () => {
'module')) expect(hasBeenProcessed(
.toBe(false); {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}},
}); 'fesm2015'))
it('should return false if no markers exist', .toBe(true);
() => { expect(hasBeenProcessed({name: 'test'}, 'module')).toBe(false); }); });
it('should throw an Error if the format has been compiled with a different version.', () => { it('should return false if the marker does not exist for the given format property', () => {
expect( expect(hasBeenProcessed(
() => hasBeenProcessed( {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '0.0.0-PLACEHOLDER'}},
{name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'fesm2015')) 'module'))
.toThrowError( .toBe(false);
'The ngcc compiler has changed since the last ngcc build.\n' + });
'Please completely remove `node_modules` and try again.'); it('should return false if no markers exist',
}); () => { expect(hasBeenProcessed({name: 'test'}, 'module')).toBe(false); });
it('should throw an Error if any format has been compiled with a different version.', () => { it('should throw an Error if the format has been compiled with a different version.', () => {
expect( expect(
() => hasBeenProcessed( () => hasBeenProcessed(
{name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'module')) {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'fesm2015'))
.toThrowError( .toThrowError(
'The ngcc compiler has changed since the last ngcc build.\n' + 'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.'); 'Please completely remove `node_modules` and try again.');
expect( });
() => hasBeenProcessed( it('should throw an Error if any format has been compiled with a different version.', () => {
{ expect(
name: 'test', () => hasBeenProcessed(
__processed_by_ivy_ngcc__: {'module': '0.0.0-PLACEHOLDER', 'fesm2015': '8.0.0'} {name: 'test', __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}}, 'module'))
}, .toThrowError(
'module')) 'The ngcc compiler has changed since the last ngcc build.\n' +
.toThrowError( 'Please completely remove `node_modules` and try again.');
'The ngcc compiler has changed since the last ngcc build.\n' + expect(
'Please completely remove `node_modules` and try again.'); () => hasBeenProcessed(
expect( {
() => hasBeenProcessed( name: 'test',
{ __processed_by_ivy_ngcc__: {'module': '0.0.0-PLACEHOLDER', 'fesm2015': '8.0.0'}
name: 'test', },
__processed_by_ivy_ngcc__: {'module': '0.0.0-PLACEHOLDER', 'fesm2015': '8.0.0'} 'module'))
}, .toThrowError(
'fesm2015')) 'The ngcc compiler has changed since the last ngcc build.\n' +
.toThrowError( 'Please completely remove `node_modules` and try again.');
'The ngcc compiler has changed since the last ngcc build.\n' + expect(
'Please completely remove `node_modules` and try again.'); () => 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.');
});
}); });
}); });
}); });

View File

@ -5,118 +5,161 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
import {MockFileSystem} from '../helpers/mock_file_system';
const _ = AbsoluteFsPath.from; runInEachFileSystem(() => {
describe('entry point bundle', () => {
function createMockFileSystem() { function setupMockFileSystem(): void {
return new MockFileSystem({ const _ = absoluteFrom;
'/node_modules/test': { loadTestFiles([
'package.json': {
'{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', name: _('/node_modules/test/package.json'),
'index.d.ts': 'export * from "./public_api";', contents:
'index.js': 'export * from "./public_api";', '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}'
'index.metadata.json': '...', },
'public_api.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 "test/secondary";
export * from "./nested"; export * from "./nested";
export declare class TestClass {}; export declare class TestClass {};
`, `
'public_api.js': ` },
{
name: _('/node_modules/test/public_api.js'),
contents: `
export * from "test/secondary"; export * from "test/secondary";
export * from "./nested"; export * from "./nested";
export const TestClass = function() {}; export const TestClass = function() {};
`, `
'root.d.ts': ` },
{
name: _('/node_modules/test/root.d.ts'),
contents: `
import * from 'other'; import * from 'other';
export declare class RootClass {}; export declare class RootClass {};
`, `
'root.js': ` },
{
name: _('/node_modules/test/root.js'),
contents: `
import * from 'other'; import * from 'other';
export const RootClass = function() {}; export const RootClass = function() {};
`, `
'nested': { },
'index.d.ts': 'export * from "../root";', {name: _('/node_modules/test/nested/index.d.ts'), contents: 'export * from "../root";'},
'index.js': '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";'},
'es2015': { {
'index.js': 'export * from "./public_api";', name: _('/node_modules/test/es2015/public_api.js'),
'public_api.js': 'export class TestClass {};', contents: 'export class TestClass {};'
'root.js': ` },
{
name: _('/node_modules/test/es2015/root.js'),
contents: `
import * from 'other'; import * from 'other';
export class RootClass {}; export class RootClass {};
`, `
'nested': {
'index.js': 'export * from "../root";',
}, },
}, {
'secondary': { name: _('/node_modules/test/es2015/nested/index.js'),
'package.json': contents: 'export * from "../root";'
'{"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/secondary/package.json'),
'/node_modules/other': { contents:
'package.json': '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}'
'{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', },
'index.d.ts': 'export * from "./public_api";', {
'index.js': 'export * from "./public_api";', name: _('/node_modules/test/secondary/index.d.ts'),
'index.metadata.json': '...', contents: 'export * from "./public_api";'
'public_api.d.ts': 'export declare class OtherClass {};', },
'public_api.js': 'export class OtherClass {};', {
'es2015': { name: _('/node_modules/test/secondary/index.js'),
'index.js': 'export * from "./public_api";', contents: 'export * from "./public_api";'
'public_api.js': 'export class OtherClass {};', },
}, {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
// https://github.com/angular/angular/issues/29939 it('should resolve JavaScript sources instead of declaration files if they are adjacent',
it('should resolve JavaScript sources instead of declaration files if they are adjacent', () => { () => {
const fs = createMockFileSystem(); setupMockFileSystem();
const esm5bundle = makeEntryPointBundle( const fs = getFileSystem();
fs, '/node_modules/test', './index.js', './index.d.ts', false, 'esm5', 'esm5', true) !; 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)) expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toEqual(jasmine.arrayWithExactContents([ .toEqual(jasmine.arrayWithExactContents([
// Modules from the entry-point itself should be source files // Modules from the entry-point itself should be source files
'/node_modules/test/index.js', '/node_modules/test/index.js',
'/node_modules/test/public_api.js', '/node_modules/test/public_api.js',
'/node_modules/test/nested/index.js', '/node_modules/test/nested/index.js',
'/node_modules/test/root.js', '/node_modules/test/root.js',
// Modules from a secondary entry-point should be declaration files // Modules from a secondary entry-point should be declaration files
'/node_modules/test/secondary/public_api.d.ts', '/node_modules/test/secondary/public_api.d.ts',
'/node_modules/test/secondary/index.d.ts', '/node_modules/test/secondary/index.d.ts',
// Modules resolved from "other" should be declaration files // Modules resolved from "other" should be declaration files
'/node_modules/other/public_api.d.ts', '/node_modules/other/public_api.d.ts',
'/node_modules/other/index.d.ts', '/node_modules/other/index.d.ts',
].map(p => _(p).toString()))); ].map(p => absoluteFrom(p).toString())));
expect(esm5bundle.dts !.program.getSourceFiles().map(sf => sf.fileName)) expect(esm5bundle.dts !.program.getSourceFiles().map(sf => sf.fileName))
.toEqual(jasmine.arrayWithExactContents([ .toEqual(jasmine.arrayWithExactContents([
// All modules in the dts program should be declaration files // All modules in the dts program should be declaration files
'/node_modules/test/index.d.ts', '/node_modules/test/index.d.ts',
'/node_modules/test/public_api.d.ts', '/node_modules/test/public_api.d.ts',
'/node_modules/test/nested/index.d.ts', '/node_modules/test/nested/index.d.ts',
'/node_modules/test/root.d.ts', '/node_modules/test/root.d.ts',
'/node_modules/test/secondary/public_api.d.ts', '/node_modules/test/secondary/public_api.d.ts',
'/node_modules/test/secondary/index.d.ts', '/node_modules/test/secondary/index.d.ts',
'/node_modules/other/public_api.d.ts', '/node_modules/other/public_api.d.ts',
'/node_modules/other/index.d.ts', '/node_modules/other/index.d.ts',
].map(p => _(p).toString()))); ].map(p => absoluteFrom(p).toString())));
});
}); });
}); });

View File

@ -5,200 +5,206 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {DependencyResolver} from '../../src/dependencies/dependency_resolver'; import {DependencyResolver} from '../../src/dependencies/dependency_resolver';
import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
import {ModuleResolver} from '../../src/dependencies/module_resolver'; import {ModuleResolver} from '../../src/dependencies/module_resolver';
import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointFinder} from '../../src/packages/entry_point_finder'; import {EntryPointFinder} from '../../src/packages/entry_point_finder';
import {MockFileSystem, SymLink} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.from; runInEachFileSystem(() => {
describe('findEntryPoints()', () => { describe('findEntryPoints()', () => {
let resolver: DependencyResolver; let resolver: DependencyResolver;
let finder: EntryPointFinder; let finder: EntryPointFinder;
beforeEach(() => { let _: typeof absoluteFrom;
const fs = createMockFileSystem();
resolver = new DependencyResolver( beforeEach(() => {
fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))}); const fs = getFileSystem();
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { _ = absoluteFrom;
return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; 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', () => { it('should find sub-entry-points within a package', () => {
const {entryPoints} = finder.findEntryPoints(_('/sub_entry_points')); const {entryPoints} = finder.findEntryPoints(_('/sub_entry_points'));
const entryPointPaths = entryPoints.map(x => [x.package, x.path]); const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([ expect(entryPointPaths).toEqual([
[_('/sub_entry_points/common'), _('/sub_entry_points/common')], [_('/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')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/http/testing')], [_('/sub_entry_points/common'), _('/sub_entry_points/common/http/testing')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/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 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);
}

View File

@ -6,163 +6,186 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {FileSystem} from '../../src/file_system/file_system'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {getEntryPointInfo} from '../../src/packages/entry_point'; import {getEntryPointInfo} from '../../src/packages/entry_point';
import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.from; runInEachFileSystem(() => {
describe('getEntryPointInfo()', () => {
let SOME_PACKAGE: AbsoluteFsPath;
let _: typeof absoluteFrom;
let fs: FileSystem;
describe('getEntryPointInfo()', () => { beforeEach(() => {
const SOME_PACKAGE = _('/some_package'); 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', it('should return an object containing absolute paths to the formats of the specified entry-point',
() => { () => {
const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo(
const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point'));
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point')); expect(entryPoint).toEqual({
expect(entryPoint).toEqual({ name: 'some-package/valid_entry_point',
name: 'some-package/valid_entry_point', package: SOME_PACKAGE,
package: SOME_PACKAGE, path: _('/some_package/valid_entry_point'),
path: _('/some_package/valid_entry_point'), typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`),
typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`), packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'),
packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'), compiledByAngular: true,
compiledByAngular: true, });
}); });
});
it('should return null if there is no package.json at the entry-point path', () => { it('should return null if there is no package.json at the entry-point path', () => {
const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo(
const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json'));
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json')); expect(entryPoint).toBe(null);
expect(entryPoint).toBe(null); });
});
it('should return null if there is no typings or types field in the package.json', () => { it('should return null if there is no typings or types field in the package.json', () => {
const fs = createMockFileSystem(); const entryPoint =
const entryPoint = getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings'));
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings')); expect(entryPoint).toBe(null);
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', 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(
const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata'));
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata')); expect(entryPoint).toEqual({
expect(entryPoint).toEqual({ name: 'some-package/missing_metadata',
name: 'some-package/missing_metadata', package: SOME_PACKAGE,
package: SOME_PACKAGE, path: _('/some_package/missing_metadata'),
path: _('/some_package/missing_metadata'), typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`),
typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`), packageJson: loadPackageJson(fs, '/some_package/missing_metadata'),
packageJson: loadPackageJson(fs, '/some_package/missing_metadata'), compiledByAngular: false,
compiledByAngular: false, });
}); });
});
it('should work if the typings field is named `types', () => { it('should work if the typings field is named `types', () => {
const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo(
const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings'));
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings')); expect(entryPoint).toEqual({
expect(entryPoint).toEqual({ name: 'some-package/types_rather_than_typings',
name: 'some-package/types_rather_than_typings', package: SOME_PACKAGE,
package: SOME_PACKAGE, path: _('/some_package/types_rather_than_typings'),
path: _('/some_package/types_rather_than_typings'), typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`),
typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`), packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'),
packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'), compiledByAngular: true,
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', () => { function setupMockFileSystem(): void {
const fs = createMockFileSystem(); const _ = absoluteFrom;
const entryPoint = loadTestFiles([
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style')); {
expect(entryPoint).toEqual({ name: _('/some_package/valid_entry_point/package.json'),
name: 'some_package/material_style', contents: createPackageJson('valid_entry_point')
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',
}, },
'missing_package_json': { {
// no package.json! name: _('/some_package/valid_entry_point/valid_entry_point.metadata.json'),
'missing_package_json.metadata.json': 'some meta data', contents: 'some meta data'
}, },
'missing_typings': { // no package.json!
'package.json': createPackageJson('missing_typings', {excludes: ['typings']}), {
'missing_typings.metadata.json': 'some meta data', 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'), name: _('/some_package/missing_typings/package.json'),
'types_rather_than_typings.metadata.json': 'some meta data', contents: createPackageJson('missing_typings', {excludes: ['typings']})
}, },
'missing_esm2015': { {
'package.json': createPackageJson('missing_fesm2015', {excludes: ['esm2015', 'fesm2015']}), name: _('/some_package/missing_typings/missing_typings.metadata.json'),
'missing_esm2015.metadata.json': 'some meta data', contents: 'some meta data'
}, },
'missing_metadata': { {
'package.json': createPackageJson('missing_metadata'), name: _('/some_package/types_rather_than_typings/package.json'),
// no metadata.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", "name": "some_package/material_style",
"typings": "./material_style.d.ts", "typings": "./material_style.d.ts",
"main": "./bundles/material_style.umd.js", "main": "./bundles/material_style.umd.js",
"module": "./esm5/material_style.es5.js", "module": "./esm5/material_style.es5.js",
"es2015": "./esm2015/material_style.js" "es2015": "./esm2015/material_style.js"
}`, }`
'material_style.metadata.json': 'some meta data',
}, },
'unexpected_symbols': { {
// package.json might not be a valid JSON name: _('/some_package/material_style/material_style.metadata.json'),
// for example, @schematics/angular contains a package.json blueprint contents: 'some meta data'
// with unexpected symbols
'package.json':
'{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}',
}, },
} // package.json might not be a valid JSON
}); // for example, @schematics/angular contains a package.json blueprint
} // with unexpected symbols
{
function createPackageJson( name: _('/some_package/unexpected_symbols/package.json'),
packageName: string, {excludes}: {excludes?: string[]} = {}, contents: '{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}'
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);
} 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) { 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')));
} }

View File

@ -8,44 +8,30 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NoopImportRewriter} from '../../../src/ngtsc/imports'; 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 {ImportManager} from '../../../src/ngtsc/translator';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
import {CommonJsRenderingFormatter} from '../../src/rendering/commonjs_rendering_formatter'; import {CommonJsRenderingFormatter} from '../../src/rendering/commonjs_rendering_formatter';
import {makeTestEntryPointBundle, getDeclaration, createFileSystemFromProgramFiles} from '../helpers/utils'; import {makeTestEntryPointBundle} from '../helpers/utils';
import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; 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}) { beforeEach(() => {
const fs = new MockFileSystem(createFileSystemFromProgramFiles([file])); _ = absoluteFrom;
const logger = new MockLogger(); PROGRAM = {
const bundle = makeTestEntryPointBundle('module', 'commonjs', false, [file]); name: _('/some/file.js'),
const typeChecker = bundle.src.program.getTypeChecker(); contents: `
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: `
/* A copyright notice */ /* A copyright notice */
require('some-side-effect'); require('some-side-effect');
var core = require('@angular/core'); var core = require('@angular/core');
@ -105,11 +91,11 @@ exports.B = B;
exports.C = C; exports.C = C;
exports.NoIife = NoIife; exports.NoIife = NoIife;
exports.BadIife = BadIife;` exports.BadIife = BadIife;`
}; };
const PROGRAM_DECORATE_HELPER = { PROGRAM_DECORATE_HELPER = {
name: _('/some/file.js'), name: _('/some/file.js'),
contents: ` contents: `
var tslib_1 = require("tslib"); var tslib_1 = require("tslib");
/* A copyright notice */ /* A copyright notice */
var core = require('@angular/core'); var core = require('@angular/core');
@ -156,43 +142,70 @@ var D = /** @class */ (function () {
}()); }());
exports.D = D; exports.D = D;
// Some other content` // 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', () => { describe('addImports', () => {
it('should insert the given imports after existing imports of the source file', () => { it('should insert the given imports after existing imports of the source file', () => {
const {renderer, sourceFile} = setup(PROGRAM); const {renderer, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(
output, output,
[ [
{specifier: '@angular/core', qualifier: 'i0'}, {specifier: '@angular/core', qualifier: 'i0'},
{specifier: '@angular/common', qualifier: 'i1'} {specifier: '@angular/common', qualifier: 'i1'}
], ],
sourceFile); sourceFile);
expect(output.toString()).toContain(`/* A copyright notice */ expect(output.toString()).toContain(`/* A copyright notice */
require('some-side-effect'); require('some-side-effect');
var core = require('@angular/core'); var core = require('@angular/core');
var i0 = require('@angular/core'); var i0 = require('@angular/core');
var i1 = require('@angular/common');`); var i1 = require('@angular/common');`);
});
}); });
});
describe('addExports', () => { describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => { it('should insert the given exports at the end of the source file', () => {
const {importManager, renderer, sourceFile} = setup(PROGRAM); const {importManager, renderer, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addExports( renderer.addExports(
output, _(PROGRAM.name.replace(/\.js$/, '')), 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: 'ComponentA1'},
{from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, {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: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'},
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
], ],
importManager, sourceFile); importManager, sourceFile);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
// Some other content // Some other content
exports.A = A; exports.A = A;
exports.B = B; exports.B = B;
@ -203,238 +216,237 @@ exports.ComponentA1 = i0.ComponentA1;
exports.ComponentA2 = i0.ComponentA2; exports.ComponentA2 = i0.ComponentA2;
exports.ComponentB = i1.ComponentB; exports.ComponentB = i1.ComponentB;
exports.TopLevelComponent = TopLevelComponent;`); 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', () => { describe('addConstants', () => {
const {importManager, renderer, sourceFile} = setup(PROGRAM); it('should insert the given constants after imports in the source file', () => {
const output = new MagicString(PROGRAM.contents); const {renderer, program} = setup(PROGRAM);
renderer.addExports( const file = getSourceFileOrError(program, _('/some/file.js'));
output, _(PROGRAM.name.replace(/\.js$/, '')), const output = new MagicString(PROGRAM.contents);
[ renderer.addConstants(output, 'var x = 3;', file);
{from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, expect(output.toString()).toContain(`
{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(`
var core = require('@angular/core'); var core = require('@angular/core');
var x = 3; var x = 3;
var A = (function() {`); var A = (function() {`);
}); });
it('should insert constants after inserted imports', () => { it('should insert constants after inserted imports', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) { const output = new MagicString(PROGRAM.contents);
throw new Error(`Could not find source file`); renderer.addConstants(output, 'var x = 3;', file);
} renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
const output = new MagicString(PROGRAM.contents); expect(output.toString()).toContain(`
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 core = require('@angular/core');
var i0 = require('@angular/core'); var i0 = require('@angular/core');
var x = 3; var x = 3;
var A = (function() {`); var A = (function() {`);
});
}); });
});
describe('rewriteSwitchableDeclarations', () => { describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => { it('should switch marked declaration initializers', () => {
const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) { const output = new MagicString(PROGRAM.contents);
throw new Error(`Could not find source file`); renderer.rewriteSwitchableDeclarations(
} output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
const output = new MagicString(PROGRAM.contents); expect(output.toString())
renderer.rewriteSwitchableDeclarations( .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); expect(output.toString())
expect(output.toString()) .toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
.not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); expect(output.toString())
expect(output.toString()) .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
.toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); expect(output.toString())
expect(output.toString()) .toContain(
.toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); `function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); .toContain(
expect(output.toString()) `function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); });
}); });
});
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly before the return statement of the class IIFE', it('should insert the definitions directly before the return statement of the class IIFE',
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
A.prototype.ngDoCheck = function() { A.prototype.ngDoCheck = function() {
// //
}; };
SOME DEFINITION TEXT SOME DEFINITION TEXT
return A; return A;
`); `);
}); });
it('should error if the compiledClass is not valid', () => { it('should error if the compiledClass is not valid', () => {
const {renderer, sourceFile, program} = setup(PROGRAM); const {renderer, sourceFile, program} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const noIifeDeclaration = const noIifeDeclaration = getDeclaration(
getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: _('NoIife')}; const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError( .toThrowError(
'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); `Compiled class declaration is not inside an IIFE: NoIife in ${_('/some/file.js')}`);
const badIifeDeclaration = const badIifeDeclaration = getDeclaration(
getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);
const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: _('BadIife')}; const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'};
expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError( .toThrowError(
'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); `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<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString())
.not.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`);
}); });
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', describe('removeDecorators', () => {
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); it('should delete the decorator (and following comma) that was matched in the analysis',
const output = new MagicString(PROGRAM.contents); () => {
const compiledClass = const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; const output = new MagicString(PROGRAM.contents);
const decorator = compiledClass.decorators ![0]; const compiledClass =
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); const decorator = compiledClass.decorators ![0];
renderer.removeDecorators(output, decoratorsToRemove); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
expect(output.toString()) decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString())
expect(output.toString()) .not.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`);
.not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString())
expect(output.toString()) .toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`);
.toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); 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', it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators ![0]; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); expect(output.toString())
expect(output.toString()) .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`);
.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString())
expect(output.toString()) .not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`);
.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString())
expect(output.toString()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); .toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`);
expect(output.toString()).not.toContain(`C.decorators`); });
});
});
describe('[__decorate declarations]', () => { it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
it('should delete the decorator (and following comma) that was matched in the analysis', () => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`); renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`OtherA()`); expect(output.toString())
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const compiledClass = const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; const compiledClass =
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
renderer.removeDecorators(output, decoratorsToRemove); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); 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<ts.Node, ts.Node[]>();
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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function C() {\n }\n return C;`); expect(output.toString()).toContain(`function C() {\n }\n return C;`);
}); });
});
}); });
}); });

View File

@ -7,21 +7,19 @@
*/ */
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; 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 {Import, ImportManager} from '../../../src/ngtsc/translator';
import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {ModuleWithProvidersAnalyzer, ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer'; import {ModuleWithProvidersAnalyzer, ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer';
import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; 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 {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter';
import {MockFileSystem} from '../helpers/mock_file_system'; import {DtsRenderer} from '../../src/rendering/dts_renderer';
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; import {MockLogger} from '../helpers/mock_logger';
import {makeTestEntryPointBundle, getRootFiles} from '../helpers/utils';
const _ = AbsoluteFsPath.fromUnchecked;
class TestRenderingFormatter implements RenderingFormatter { class TestRenderingFormatter implements RenderingFormatter {
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) {
@ -50,13 +48,19 @@ class TestRenderingFormatter implements RenderingFormatter {
} }
function createTestRenderer( function createTestRenderer(
packageName: string, files: {name: string, contents: string}[], packageName: string, files: TestFile[], dtsFiles?: TestFile[], mappingFiles?: TestFile[]) {
dtsFiles?: {name: string, contents: string}[],
mappingFiles?: {name: string, contents: string}[]) {
const logger = new MockLogger(); 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 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 typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
@ -87,95 +91,75 @@ function createTestRenderer(
bundle}; bundle};
} }
runInEachFileSystem(() => {
describe('DtsRenderer', () => {
let _: typeof absoluteFrom;
let INPUT_PROGRAM: TestFile;
let INPUT_DTS_PROGRAM: TestFile;
describe('DtsRenderer', () => { beforeEach(() => {
const INPUT_PROGRAM = { _ = absoluteFrom;
name: '/src/file.js', INPUT_PROGRAM = {
contents: name: _('/src/file.js'),
`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` 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', INPUT_DTS_PROGRAM = {
contents: `export declare class A {\nfoo(x: number): number;\n}\n` name: _('/typings/file.d.ts'),
}; contents: `export declare class A {\nfoo(x: number): number;\n}\n`
};
});
const INPUT_PROGRAM_MAP = fromObject({ it('should render extract types into typings files', () => {
'version': 3, const {renderer, decorationAnalyses, privateDeclarationsAnalyses,
'file': '/src/file.js', moduleWithProvidersAnalyses} =
'sourceRoot': '', createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
'sources': ['/src/file.ts'], const result = renderer.renderProgram(
'names': [], decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses);
'mappings':
'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC',
'sourcesContent': [INPUT_PROGRAM.contents]
});
const RENDERED_CONTENTS = ` const typingsFile = result.find(f => f.path === _('/typings/file.d.ts')) !;
// ADD IMPORTS 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 // Add a mock export to trigger export rendering
` + INPUT_PROGRAM.contents; privateDeclarationsAnalyses.push(
{identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')});
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ const result = renderer.renderProgram(
'version': 3, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses);
'sources': ['/src/file.ts'],
'names': [],
'mappings': ';;;;;;;;;;AAAA',
'file': 'file.js',
'sourcesContent': [INPUT_PROGRAM.contents]
});
it('should render extract types into typings files', () => { const typingsFile = result.find(f => f.path === _('/typings/file.d.ts')) !;
const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`);
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') !; it('should render ModuleWithProviders type params', () => {
expect(typingsFile.contents) const {renderer, decorationAnalyses, privateDeclarationsAnalyses,
.toContain( moduleWithProvidersAnalyses} =
'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵɵDirectiveDefWithMeta'); createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
});
it('should render imports into typings files', () => { const result = renderer.renderProgram(
const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = 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') !; const typingsFile = result.find(f => f.path === _('/typings/file.d.ts')) !;
expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); expect(typingsFile.contents).toContain(`\n// ADD MODUlE WITH PROVIDERS PARAMS\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`);
}); });
}); });

View File

@ -8,32 +8,32 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NoopImportRewriter} from '../../../src/ngtsc/imports'; 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 {ImportManager} from '../../../src/ngtsc/translator';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {loadTestFiles} from '../../../test/helpers';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {IMPORT_PREFIX} from '../../src/constants'; import {IMPORT_PREFIX} from '../../src/constants';
import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {Esm5RenderingFormatter} from '../../src/rendering/esm5_rendering_formatter'; import {Esm5RenderingFormatter} from '../../src/rendering/esm5_rendering_formatter';
import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; import {makeTestEntryPointBundle} from '../helpers/utils';
import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.fromUnchecked;
function setup(file: {name: AbsoluteFsPath, contents: string}) { function setup(file: {name: AbsoluteFsPath, contents: string}) {
const fs = new MockFileSystem(); loadTestFiles([file]);
const fs = getFileSystem();
const logger = new MockLogger(); 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 typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm5ReflectionHost(logger, false, typeChecker); const host = new Esm5ReflectionHost(logger, false, typeChecker);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = const decorationAnalyses = new DecorationAnalyzer(
new DecorationAnalyzer( fs, bundle.src.program, bundle.src.options, bundle.src.host,
fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, typeChecker, host, referencesRegistry, [absoluteFrom('/')], false)
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram();
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new Esm5RenderingFormatter(host, false); const renderer = new Esm5RenderingFormatter(host, false);
const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX);
@ -44,9 +44,18 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) {
}; };
} }
const PROGRAM = { runInEachFileSystem(() => {
name: _('/some/file.js'), describe('Esm5RenderingFormatter', () => {
contents: `
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
let PROGRAM_DECORATE_HELPER: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/some/file.js'),
contents: `
/* A copyright notice */ /* A copyright notice */
import 'some-side-effect'; import 'some-side-effect';
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
@ -102,11 +111,11 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
} }
// Some other content // Some other content
export {A, B, C, NoIife, BadIife};` export {A, B, C, NoIife, BadIife};`
}; };
const PROGRAM_DECORATE_HELPER = { PROGRAM_DECORATE_HELPER = {
name: _('/some/file.js'), name: _('/some/file.js'),
contents: ` contents: `
import * as tslib_1 from "tslib"; import * as tslib_1 from "tslib";
/* A copyright notice */ /* A copyright notice */
import { Directive } from '@angular/core'; import { Directive } from '@angular/core';
@ -153,275 +162,272 @@ var D = /** @class */ (function () {
}()); }());
export { D }; export { D };
// Some other content` // Some other content`
}; };
});
describe('Esm5RenderingFormatter', () => { describe('addImports', () => {
it('should insert the given imports after existing imports of the source file', () => {
describe('addImports', () => { const {renderer, sourceFile} = setup(PROGRAM);
it('should insert the given imports after existing imports of the source file', () => { const output = new MagicString(PROGRAM.contents);
const {renderer, sourceFile} = setup(PROGRAM); renderer.addImports(
const output = new MagicString(PROGRAM.contents); output,
renderer.addImports( [
output, {specifier: '@angular/core', qualifier: 'i0'},
[ {specifier: '@angular/common', qualifier: 'i1'}
{specifier: '@angular/core', qualifier: 'i0'}, ],
{specifier: '@angular/common', qualifier: 'i1'} sourceFile);
], expect(output.toString()).toContain(`/* A copyright notice */
sourceFile);
expect(output.toString()).toContain(`/* A copyright notice */
import 'some-side-effect'; import 'some-side-effect';
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
import * as i0 from '@angular/core'; import * as i0 from '@angular/core';
import * as i1 from '@angular/common';`); import * as i1 from '@angular/common';`);
});
}); });
});
describe('addExports', () => { describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => { it('should insert the given exports at the end of the source file', () => {
const {importManager, renderer, sourceFile} = setup(PROGRAM); const {importManager, renderer, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addExports( renderer.addExports(
output, _(PROGRAM.name.replace(/\.js$/, '')), 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: 'ComponentA1'},
{from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, {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: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'},
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
], ],
importManager, sourceFile); importManager, sourceFile);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
export {A, B, C, NoIife, BadIife}; export {A, B, C, NoIife, BadIife};
export {ComponentA1} from './a'; export {ComponentA1} from './a';
export {ComponentA2} from './a'; export {ComponentA2} from './a';
export {ComponentB} from './foo/b'; export {ComponentB} from './foo/b';
export {TopLevelComponent};`); 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', () => { describe('addConstants', () => {
const {importManager, renderer, sourceFile} = setup(PROGRAM); it('should insert the given constants after imports in the source file', () => {
const output = new MagicString(PROGRAM.contents); const {renderer, program} = setup(PROGRAM);
renderer.addExports( const file = getSourceFileOrError(program, _('/some/file.js'));
output, _(PROGRAM.name.replace(/\.js$/, '')), const output = new MagicString(PROGRAM.contents);
[ renderer.addConstants(output, 'var x = 3;', file);
{from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, expect(output.toString()).toContain(`
{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(`
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
var x = 3; var x = 3;
var A = (function() {`); var A = (function() {`);
}); });
it('should insert constants after inserted imports', () => { it('should insert constants after inserted imports', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) { const output = new MagicString(PROGRAM.contents);
throw new Error(`Could not find source file`); renderer.addConstants(output, 'var x = 3;', file);
} renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
const output = new MagicString(PROGRAM.contents); expect(output.toString()).toContain(`
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 {Directive} from '@angular/core';
import * as i0 from '@angular/core'; import * as i0 from '@angular/core';
var x = 3; var x = 3;
var A = (function() {`); var A = (function() {`);
});
}); });
});
describe('rewriteSwitchableDeclarations', () => { describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => { it('should switch marked declaration initializers', () => {
const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) { const output = new MagicString(PROGRAM.contents);
throw new Error(`Could not find source file`); renderer.rewriteSwitchableDeclarations(
} output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
const output = new MagicString(PROGRAM.contents); expect(output.toString())
renderer.rewriteSwitchableDeclarations( .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); expect(output.toString())
expect(output.toString()) .toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
.not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); expect(output.toString())
expect(output.toString()) .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
.toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); expect(output.toString())
expect(output.toString()) .toContain(
.toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); `function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); .toContain(
expect(output.toString()) `function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); });
}); });
});
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly before the return statement of the class IIFE', it('should insert the definitions directly before the return statement of the class IIFE',
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
A.prototype.ngDoCheck = function() { A.prototype.ngDoCheck = function() {
// //
}; };
SOME DEFINITION TEXT SOME DEFINITION TEXT
return A; return A;
`); `);
}); });
it('should error if the compiledClass is not valid', () => { it('should error if the compiledClass is not valid', () => {
const {renderer, sourceFile, program} = setup(PROGRAM); const {renderer, sourceFile, program} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const noIifeDeclaration = const noIifeDeclaration = getDeclaration(
getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: _('NoIife')}; const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError( .toThrowError(
'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); `Compiled class declaration is not inside an IIFE: NoIife in ${_('/some/file.js')}`);
const badIifeDeclaration = const badIifeDeclaration = getDeclaration(
getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);
const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: _('BadIife')}; const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'};
expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError( .toThrowError(
'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); `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<ts.Node, ts.Node[]>();
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', describe('removeDecorators', () => {
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); it('should delete the decorator (and following comma) that was matched in the analysis',
const output = new MagicString(PROGRAM.contents); () => {
const compiledClass = const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; const output = new MagicString(PROGRAM.contents);
const decorator = compiledClass.decorators ![0]; const compiledClass =
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); const decorator = compiledClass.decorators ![0];
renderer.removeDecorators(output, decoratorsToRemove); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
expect(output.toString()).toContain(`{ type: OtherA }`); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()) expect(output.toString())
.not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); .not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); 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', it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators ![0]; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); 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: Directive, args: [{ selector: '[a]' }] },`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString())
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
expect(output.toString()).not.toContain(`C.decorators`); });
});
});
describe('[__decorate declarations]', () => { it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
it('should delete the decorator (and following comma) that was matched in the analysis', () => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`); renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const compiledClass = const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; const compiledClass =
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
renderer.removeDecorators(output, decoratorsToRemove); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); 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<ts.Node, ts.Node[]>();
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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function C() {\n }\n return C;`); expect(output.toString()).toContain(`function C() {\n }\n return C;`);
}); });
});
}); });
}); });

View File

@ -8,7 +8,9 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NoopImportRewriter} from '../../../src/ngtsc/imports'; 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 {ImportManager} from '../../../src/ngtsc/translator';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; 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 {IMPORT_PREFIX} from '../../src/constants';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {EsmRenderingFormatter} from '../../src/rendering/esm_rendering_formatter'; import {EsmRenderingFormatter} from '../../src/rendering/esm_rendering_formatter';
import {makeTestEntryPointBundle} from '../helpers/utils'; import {makeTestEntryPointBundle, getRootFiles} from '../helpers/utils';
import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
const _ = AbsoluteFsPath.fromUnchecked; function setup(files: TestFile[], dtsFiles?: TestFile[]) {
loadTestFiles(files);
function setup( if (dtsFiles) {
files: {name: string, contents: string}[], loadTestFiles(dtsFiles);
dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]) { }
const fs = new MockFileSystem(); const fs = getFileSystem();
const logger = new MockLogger(); 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 typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(logger, false, typeChecker, bundle.dts); const host = new Esm2015ReflectionHost(logger, false, typeChecker, bundle.dts);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer( const decorationAnalyses = new DecorationAnalyzer(
fs, bundle.src.program, bundle.src.options, bundle.src.host, fs, bundle.src.program, bundle.src.options, bundle.src.host,
typeChecker, host, referencesRegistry, [_('/')], false) typeChecker, host, referencesRegistry, [absoluteFrom('/')], false)
.analyzeProgram(); .analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new EsmRenderingFormatter(host, false); const renderer = new EsmRenderingFormatter(host, false);
@ -47,9 +49,18 @@ function setup(
}; };
} }
const PROGRAM = { runInEachFileSystem(() => {
name: _('/some/file.js'), describe('EsmRenderingFormatter', () => {
contents: `
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/some/file.js'),
contents: `
/* A copyright notice */ /* A copyright notice */
import 'some-side-effect'; import 'some-side-effect';
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
@ -81,209 +92,209 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
return Promise.resolve(new R3NgModuleFactory(moduleType)); return Promise.resolve(new R3NgModuleFactory(moduleType));
} }
// Some other content` // Some other content`
}; };
});
describe('EsmRenderingFormatter', () => { describe('addImports', () => {
it('should insert the given imports after existing imports of the source file', () => {
describe('addImports', () => { const {renderer, sourceFile} = setup([PROGRAM]);
it('should insert the given imports after existing imports of the source file', () => { const output = new MagicString(PROGRAM.contents);
const {renderer, sourceFile} = setup([PROGRAM]); renderer.addImports(
const output = new MagicString(PROGRAM.contents); output,
renderer.addImports( [
output, {specifier: '@angular/core', qualifier: 'i0'},
[ {specifier: '@angular/common', qualifier: 'i1'}
{specifier: '@angular/core', qualifier: 'i0'}, ],
{specifier: '@angular/common', qualifier: 'i1'} sourceFile);
], expect(output.toString()).toContain(`/* A copyright notice */
sourceFile);
expect(output.toString()).toContain(`/* A copyright notice */
import 'some-side-effect'; import 'some-side-effect';
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
import * as i0 from '@angular/core'; import * as i0 from '@angular/core';
import * as i1 from '@angular/common';`); import * as i1 from '@angular/common';`);
});
}); });
});
describe('addExports', () => { describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => { it('should insert the given exports at the end of the source file', () => {
const {importManager, renderer, sourceFile} = setup([PROGRAM]); const {importManager, renderer, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addExports( renderer.addExports(
output, _(PROGRAM.name.replace(/\.js$/, '')), 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: 'ComponentA1'},
{from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, {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: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'},
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
], ],
importManager, sourceFile); importManager, sourceFile);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
// Some other content // Some other content
export {ComponentA1} from './a'; export {ComponentA1} from './a';
export {ComponentA2} from './a'; export {ComponentA2} from './a';
export {ComponentB} from './foo/b'; export {ComponentB} from './foo/b';
export {TopLevelComponent};`); 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', () => { describe('addConstants', () => {
const {importManager, renderer, sourceFile} = setup([PROGRAM]); it('should insert the given constants after imports in the source file', () => {
const output = new MagicString(PROGRAM.contents); const {renderer, program} = setup([PROGRAM]);
renderer.addExports( const file = getSourceFileOrError(program, _('/some/file.js'));
output, _(PROGRAM.name.replace(/\.js$/, '')), const output = new MagicString(PROGRAM.contents);
[ renderer.addConstants(output, 'const x = 3;', file);
{from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, expect(output.toString()).toContain(`
{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(`
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
const x = 3; const x = 3;
export class A {}`); export class A {}`);
}); });
it('should insert constants after inserted imports', () => { it('should insert constants after inserted imports', () => {
const {renderer, program} = setup([PROGRAM]); const {renderer, program} = setup([PROGRAM]);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) { const output = new MagicString(PROGRAM.contents);
throw new Error(`Could not find source file`); renderer.addConstants(output, 'const x = 3;', file);
} renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
const output = new MagicString(PROGRAM.contents); expect(output.toString()).toContain(`
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 {Directive} from '@angular/core';
import * as i0 from '@angular/core'; import * as i0 from '@angular/core';
const x = 3; const x = 3;
export class A {`); export class A {`);
});
}); });
});
describe('rewriteSwitchableDeclarations', () => { describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => { it('should switch marked declaration initializers', () => {
const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]); const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) { const output = new MagicString(PROGRAM.contents);
throw new Error(`Could not find source file`); renderer.rewriteSwitchableDeclarations(
} output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
const output = new MagicString(PROGRAM.contents); expect(output.toString())
renderer.rewriteSwitchableDeclarations( .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); expect(output.toString())
expect(output.toString()) .toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
.not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); expect(output.toString())
expect(output.toString()) .toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
.toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); expect(output.toString())
expect(output.toString()) .toContain(
.toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); `function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); .toContain(
expect(output.toString()) `function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); });
}); });
});
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => { it('should insert the definitions directly after the class declaration', () => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]); const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
export class A {} export class A {}
SOME DEFINITION TEXT SOME DEFINITION TEXT
A.decorators = [ 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<ts.Node, ts.Node[]>();
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', () => { it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
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 {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); const compiledClass =
const output = new MagicString(PROGRAM.contents); decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const compiledClass = const decorator = compiledClass.decorators ![0];
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
const decorator = compiledClass.decorators ![0]; decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); renderer.removeDecorators(output, decoratorsToRemove);
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); expect(output.toString())
renderer.removeDecorators(output, decoratorsToRemove); .toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()) expect(output.toString()).toContain(`{ type: OtherA }`);
.not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); expect(output.toString())
expect(output.toString()).toContain(`{ type: OtherA }`); .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString())
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); .toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
}); });
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis',
() => { () => {
const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators ![0]; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); expect(output.toString())
expect(output.toString()).toContain(`{ type: OtherA }`); .toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()) expect(output.toString()).toContain(`{ type: OtherA }`);
.not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); expect(output.toString())
expect(output.toString()).toContain(`{ type: OtherB }`); .toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); 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<ts.Node, ts.Node[]>();
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 = { beforeEach(() => {
name: '/some/file.js', PROGRAM_DECORATE_HELPER = {
contents: ` name: _('/some/file.js'),
contents: `
import * as tslib_1 from "tslib"; import * as tslib_1 from "tslib";
var D_1; var D_1;
/* A copyright notice */ /* A copyright notice */
@ -317,67 +328,72 @@ D = D_1 = tslib_1.__decorate([
], D); ], D);
export { D }; export { D };
// Some other content` // Some other content`
}; };
});
it('should delete the decorator (and following comma) that was matched in the analysis', () => { 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 {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]);
const compiledClass = const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; const compiledClass =
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
renderer.removeDecorators(output, decoratorsToRemove); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); 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<ts.Node, ts.Node[]>();
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<ts.Node, ts.Node[]>();
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', describe('addModuleWithProvidersParams', () => {
() => { let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[];
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); let MODULE_WITH_PROVIDERS_DTS_PROGRAM: TestFile[];
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); beforeEach(() => {
const compiledClass = MODULE_WITH_PROVIDERS_PROGRAM = [
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; {
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; name: _('/src/index.js'),
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); contents: `
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<ts.Node, ts.Node[]>();
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: `
import {ExternalModule} from './module'; import {ExternalModule} from './module';
import {LibraryModule} from 'some-library'; import {LibraryModule} from 'some-library';
export class SomeClass {} export class SomeClass {}
@ -397,26 +413,29 @@ export { D };
export function withProviders4() { return {ngModule: ExternalModule}; } export function withProviders4() { return {ngModule: ExternalModule}; }
export function withProviders5() { return {ngModule: ExternalModule}; } export function withProviders5() { return {ngModule: ExternalModule}; }
export function withProviders6() { return {ngModule: LibraryModule}; } export function withProviders6() { return {ngModule: LibraryModule}; }
export function withProviders7() { return {ngModule: SomeModule, providers: []}; }; export function withProviders7() { return {ngModule: SomeModule, providers: []}; }
export function withProviders8() { return {ngModule: SomeModule}; }`, export function withProviders8() { return {ngModule: SomeModule}; }
}, export {ExternalModule} from './module';
{ `
name: '/src/module.js', },
contents: ` {
name: _('/src/module.js'),
contents: `
export class ExternalModule { export class ExternalModule {
static withProviders1() { return {ngModule: ExternalModule}; } static withProviders1() { return {ngModule: ExternalModule}; }
static withProviders2() { return {ngModule: ExternalModule}; } static withProviders2() { return {ngModule: ExternalModule}; }
}` }`
}, },
{ {
name: '/node_modules/some-library/index.d.ts', name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}' contents: 'export declare class LibraryModule {}'
}, },
]; ];
const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [
{ MODULE_WITH_PROVIDERS_DTS_PROGRAM = [
name: '/typings/index.d.ts', {
contents: ` name: _('/typings/index.d.ts'),
contents: `
import {ModuleWithProviders} from '@angular/core'; import {ModuleWithProviders} from '@angular/core';
export declare class SomeClass {} export declare class SomeClass {}
export interface MyModuleWithProviders extends ModuleWithProviders {} export interface MyModuleWithProviders extends ModuleWithProviders {}
@ -437,38 +456,42 @@ export { D };
export declare function withProviders5(); export declare function withProviders5();
export declare function withProviders6(): ModuleWithProviders; export declare function withProviders6(): ModuleWithProviders;
export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; export declare function withProviders7(): {ngModule: SomeModule, providers: any[]};
export declare function withProviders8(): MyModuleWithProviders;` export declare function withProviders8(): MyModuleWithProviders;
}, export {ExternalModule} from './module';
{ `
name: '/typings/module.d.ts', },
contents: ` {
name: _('/typings/module.d.ts'),
contents: `
export interface ModuleWithProviders {} export interface ModuleWithProviders {}
export declare class ExternalModule { export declare class ExternalModule {
static withProviders1(): ModuleWithProviders; static withProviders1(): ModuleWithProviders;
static withProviders2(): ModuleWithProviders; static withProviders2(): ModuleWithProviders;
}` }`
}, },
{ {
name: '/node_modules/some-library/index.d.ts', name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}' contents: 'export declare class LibraryModule {}'
}, },
]; ];
});
it('should fixup functions/methods that return ModuleWithProviders structures', () => { it('should fixup functions/methods that return ModuleWithProviders structures', () => {
const {bundle, renderer, host} = const {bundle, renderer, host} =
setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses = new ModuleWithProvidersAnalyzer(host, referencesRegistry) const moduleWithProvidersAnalyses =
.analyzeProgram(bundle.src.program); new ModuleWithProvidersAnalyzer(host, referencesRegistry)
const typingsFile = bundle.dts !.program.getSourceFile('/typings/index.d.ts') !; .analyzeProgram(bundle.src.program);
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; 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 output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[0].contents);
const importManager = new ImportManager(new NoopImportRewriter(), 'i'); const importManager = new ImportManager(new NoopImportRewriter(), 'i');
renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
static withProviders1(): ModuleWithProviders<SomeModule>; static withProviders1(): ModuleWithProviders<SomeModule>;
static withProviders2(): ModuleWithProviders<SomeModule>; static withProviders2(): ModuleWithProviders<SomeModule>;
static withProviders3(): ModuleWithProviders<SomeClass>; static withProviders3(): ModuleWithProviders<SomeClass>;
@ -477,7 +500,7 @@ export { D };
static withProviders6(): ModuleWithProviders<i2.LibraryModule>; static withProviders6(): ModuleWithProviders<i2.LibraryModule>;
static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
export declare function withProviders1(): ModuleWithProviders<SomeModule>; export declare function withProviders1(): ModuleWithProviders<SomeModule>;
export declare function withProviders2(): ModuleWithProviders<SomeModule>; export declare function withProviders2(): ModuleWithProviders<SomeModule>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>; export declare function withProviders3(): ModuleWithProviders<SomeClass>;
@ -486,26 +509,28 @@ export { D };
export declare function withProviders6(): ModuleWithProviders<i2.LibraryModule>; export declare function withProviders6(): ModuleWithProviders<i2.LibraryModule>;
export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
}); });
it('should not mistake `ModuleWithProviders` types that are not imported from `@angular/core', it('should not mistake `ModuleWithProviders` types that are not imported from `@angular/core',
() => { () => {
const {bundle, renderer, host} = const {bundle, renderer, host} =
setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses = const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(host, referencesRegistry) new ModuleWithProvidersAnalyzer(host, referencesRegistry)
.analyzeProgram(bundle.src.program); .analyzeProgram(bundle.src.program);
const typingsFile = bundle.dts !.program.getSourceFile('/typings/module.d.ts') !; const typingsFile =
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(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 output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[1].contents);
const importManager = new ImportManager(new NoopImportRewriter(), 'i'); const importManager = new ImportManager(new NoopImportRewriter(), 'i');
renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule}; static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule};
static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`); static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`);
}); });
});
}); });
}); });

View File

@ -7,8 +7,10 @@
*/ */
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {fromObject, generateMapFileComment} from 'convert-source-map'; import {fromObject, generateMapFileComment, SourceMapConverter} from 'convert-source-map';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; 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 {Import, ImportManager} from '../../../src/ngtsc/translator';
import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; 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 {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
const _ = AbsoluteFsPath.fromUnchecked;
import {Renderer} from '../../src/rendering/renderer'; import {Renderer} from '../../src/rendering/renderer';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter'; import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter';
import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; import {makeTestEntryPointBundle, getRootFiles} from '../helpers/utils';
import {MockFileSystem} from '../helpers/mock_file_system';
class TestRenderingFormatter implements RenderingFormatter { class TestRenderingFormatter implements RenderingFormatter {
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) {
@ -51,13 +50,19 @@ class TestRenderingFormatter implements RenderingFormatter {
} }
function createTestRenderer( function createTestRenderer(
packageName: string, files: {name: string, contents: string}[], packageName: string, files: TestFile[], dtsFiles?: TestFile[], mappingFiles?: TestFile[]) {
dtsFiles?: {name: string, contents: string}[],
mappingFiles?: {name: string, contents: string}[]) {
const logger = new MockLogger(); 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 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 typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
@ -87,32 +92,43 @@ function createTestRenderer(
bundle}; 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', () => { beforeEach(() => {
const INPUT_PROGRAM = { _ = absoluteFrom;
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 COMPONENT_PROGRAM = { INPUT_PROGRAM = {
name: '/src/component.js', name: _('/src/file.js'),
contents: contents:
`import { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n` `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({ COMPONENT_PROGRAM = {
'version': 3, name: _('/src/component.js'),
'file': '/src/file.js', contents:
'sourceRoot': '', `import { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n`
'sources': ['/src/file.ts'], };
'names': [],
'mappings':
'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC',
'sourcesContent': [INPUT_PROGRAM.contents]
});
const RENDERED_CONTENTS = ` 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 IMPORTS
// ADD EXPORTS // ADD EXPORTS
@ -124,47 +140,49 @@ describe('Renderer', () => {
// REMOVE DECORATORS // REMOVE DECORATORS
` + INPUT_PROGRAM.contents; ` + INPUT_PROGRAM.contents;
const OUTPUT_PROGRAM_MAP = fromObject({ OUTPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
'file': 'file.js', 'file': 'file.js',
'sources': ['/src/file.js'], 'sources': [_('/src/file.js')],
'sourcesContent': [INPUT_PROGRAM.contents], 'sourcesContent': [INPUT_PROGRAM.contents],
'names': [], 'names': [],
'mappings': ';;;;;;;;;;AAAA;;;;;;;;;' 'mappings': ';;;;;;;;;;AAAA;;;;;;;;;'
}); });
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ MERGED_OUTPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
'sources': ['/src/file.ts'], 'sources': [_('/src/file.ts')],
'names': [], 'names': [],
'mappings': ';;;;;;;;;;AAAA', 'mappings': ';;;;;;;;;;AAAA',
'file': 'file.js', 'file': 'file.js',
'sourcesContent': [INPUT_PROGRAM.contents] 'sourcesContent': [INPUT_PROGRAM.contents]
}); });
});
describe('renderProgram()', () => { describe('renderProgram()', () => {
it('should render the modified contents; and a new map file, if the original provided no map file.', it('should render the modified contents; and a new map file, if the original provided no map file.',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]); createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(result[0].path).toEqual('/src/file.js'); expect(result[0].path).toEqual(_('/src/file.js'));
expect(result[0].contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map'));
expect(result[1].path).toEqual('/src/file.js.map'); expect(result[1].path).toEqual(_('/src/file.js.map'));
expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON()); expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON());
}); });
it('should render as JavaScript', () => { it('should render as JavaScript', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
testFormatter} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); testFormatter} = createTestRenderer('test-package', [COMPONENT_PROGRAM]);
renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); renderer.renderProgram(
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(addDefinitionsSpy.calls.first().args[2]) const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
.toEqual( expect(addDefinitionsSpy.calls.first().args[2])
`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) { .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); ɵngcc0.ɵɵtext(0);
} if (rf & 2) { } if (rf & 2) {
ɵngcc0.ɵɵtextInterpolate(ctx.person.name); ɵngcc0.ɵɵtextInterpolate(ctx.person.name);
@ -173,194 +191,199 @@ describe('Renderer', () => {
type: Component, type: Component,
args: [{ selector: 'a', template: '{{ person!.name }}' }] args: [{ selector: 'a', template: '{{ person!.name }}' }]
}], null, null);`); }], null, null);`);
}); });
describe('calling RenderingFormatter methods', () => { describe('calling RenderingFormatter methods', () => {
it('should call addImports with the source code and info about the core Angular library.', it('should call addImports with the source code and info about the core Angular library.',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const addImportsSpy = testFormatter.addImports as jasmine.Spy; const addImportsSpy = testFormatter.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addImportsSpy.calls.first().args[1]).toEqual([ expect(addImportsSpy.calls.first().args[1]).toEqual([
{specifier: '@angular/core', qualifier: 'ɵngcc0'} {specifier: '@angular/core', qualifier: 'ɵngcc0'}
]); ]);
}); });
it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.', it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({ expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({
name: _('A'), name: 'A',
decorators: [jasmine.objectContaining({name: _('Directive')})] decorators: [jasmine.objectContaining({name: 'Directive'})]
})); }));
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toEqual( .toEqual(
`A.ngDirectiveDef = ɵngcc0.ɵɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } }); `A.ngDirectiveDef = ɵngcc0.ɵɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } });
/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{ /*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
type: Directive, type: Directive,
args: [{ selector: '[a]' }] args: [{ selector: '[a]' }]
}], null, { foo: [] });`); }], null, { foo: [] });`);
}); });
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const removeDecoratorsSpy = testFormatter.removeDecorators as jasmine.Spy; const removeDecoratorsSpy = testFormatter.removeDecorators as jasmine.Spy;
expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(removeDecoratorsSpy.calls.first().args[0].toString())
.toEqual(RENDERED_CONTENTS);
// Each map key is the TS node of the decorator container // 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 // Each map value is an array of TS nodes that are the decorators to remove
const map = removeDecoratorsSpy.calls.first().args[1] as Map<ts.Node, ts.Node[]>; const map = removeDecoratorsSpy.calls.first().args[1] as Map<ts.Node, ts.Node[]>;
const keys = Array.from(map.keys()); const keys = Array.from(map.keys());
expect(keys.length).toEqual(1); expect(keys.length).toEqual(1);
expect(keys[0].getText()) expect(keys[0].getText())
.toEqual(`[\n { type: Directive, args: [{ selector: '[a]' }] }\n]`); .toEqual(`[\n { type: Directive, args: [{ selector: '[a]' }] }\n]`);
const values = Array.from(map.values()); const values = Array.from(map.values());
expect(values.length).toEqual(1); expect(values.length).toEqual(1);
expect(values[0].length).toEqual(1); expect(values[0].length).toEqual(1);
expect(values[0][0].getText()) expect(values[0][0].getText())
.toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`); .toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`);
}); });
it('should render classes without decorators if handler matches', () => { it('should render classes without decorators if handler matches', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
testFormatter} = createTestRenderer('test-package', [{ testFormatter} = createTestRenderer('test-package', [{
name: '/src/file.js', name: _('/src/file.js'),
contents: ` contents: `
import { Directive, ViewChild } from '@angular/core'; import { Directive, ViewChild } from '@angular/core';
export class UndecoratedBase { test = null; } export class UndecoratedBase { test = null; }
UndecoratedBase.propDecorators = { UndecoratedBase.propDecorators = {
test: [{ test: [{
type: ViewChild, type: ViewChild,
args: ["test", {static: true}] args: ["test", {static: true}]
}], }],
}; };
` `
}]); }]);
renderer.renderProgram( renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toEqual( .toEqual(
`UndecoratedBase.ngBaseDef = ɵngcc0.ɵɵdefineBase({ viewQuery: function (rf, ctx) { if (rf & 1) { `UndecoratedBase.ngBaseDef = ɵngcc0.ɵɵdefineBase({ viewQuery: function (rf, ctx) { if (rf & 1) {
ɵngcc0.ɵɵstaticViewQuery(_c0, true, null); ɵngcc0.ɵɵstaticViewQuery(_c0, true, null);
} if (rf & 2) { } if (rf & 2) {
var _t; var _t;
ɵngcc0.ɵɵqueryRefresh(_t = ɵngcc0.ɵɵloadViewQuery()) && (ctx.test = _t.first); ɵ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', () => { describe('source map merging', () => {
// This allows the other methods to add additional imports if necessary it('should merge any inline source map from the original file and write the output as an inline source map',
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, () => {
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const {decorationAnalyses, renderer, switchMarkerAnalyses,
const addExportsSpy = testFormatter.addExports as jasmine.Spy; privateDeclarationsAnalyses} =
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; createTestRenderer(
const addConstantsSpy = testFormatter.addConstants as jasmine.Spy; 'test-package', [{
const addImportsSpy = testFormatter.addImports as jasmine.Spy; ...INPUT_PROGRAM,
renderer.renderProgram( contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); }]);
expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); const result = renderer.renderProgram(
expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); 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 external source map from the original file and write the output to an external source map',
it('should merge any inline source map from the original file and write the output as an inline source map', () => {
() => { const sourceFiles: TestFile[] = [{
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = ...INPUT_PROGRAM,
createTestRenderer( contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
'test-package', [{ }];
...INPUT_PROGRAM, const mappingFiles: TestFile[] =
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() [{name: _(INPUT_PROGRAM.name + '.map'), contents: INPUT_PROGRAM_MAP.toJSON()}];
}]); const {decorationAnalyses, renderer, switchMarkerAnalyses,
const result = renderer.renderProgram( privateDeclarationsAnalyses} =
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); createTestRenderer('test-package', sourceFiles, undefined, mappingFiles);
expect(result[0].path).toEqual('/src/file.js'); const result = renderer.renderProgram(
expect(result[0].contents) decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
.toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); expect(result[0].path).toEqual(_('/src/file.js'));
expect(result[1]).toBeUndefined(); expect(result[0].contents)
}); .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map'));
expect(result[1].path).toEqual(_('/src/file.js.map'));
it('should merge any external source map from the original file and write the output to an external source map', expect(JSON.parse(result[1].contents)).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject());
() => { });
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 render no imports in FESM bundles', () => { describe('@angular/core support', () => {
const CORE_FILE = { it('should render relative imports in ESM bundles', () => {
name: '/src/core.js', const CORE_FILE: TestFile = {
contents: `export const NgModule = () => null; 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` export class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
}; };
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]); testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]);
renderer.renderProgram( renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toContain(`/*@__PURE__*/ setClassMetadata(`); .toContain(`/*@__PURE__*/ setClassMetadata(`);
const addImportsSpy = testFormatter.addImports as jasmine.Spy; const addImportsSpy = testFormatter.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[1]).toEqual([]); expect(addImportsSpy.calls.first().args[1]).toEqual([]);
});
}); });
}); });
}); });

View File

@ -8,30 +8,31 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NoopImportRewriter} from '../../../src/ngtsc/imports'; 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 {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {UmdReflectionHost} from '../../src/host/umd_host'; import {UmdReflectionHost} from '../../src/host/umd_host';
import {ImportManager} from '../../../src/ngtsc/translator'; import {ImportManager} from '../../../src/ngtsc/translator';
import {MockFileSystem} from '../helpers/mock_file_system';
import {UmdRenderingFormatter} from '../../src/rendering/umd_rendering_formatter'; import {UmdRenderingFormatter} from '../../src/rendering/umd_rendering_formatter';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; import {makeTestEntryPointBundle} from '../helpers/utils';
const _ = AbsoluteFsPath.fromUnchecked; function setup(file: TestFile) {
loadTestFiles([file]);
function setup(file: {name: string, contents: string}) { const fs = getFileSystem();
const fs = new MockFileSystem(createFileSystemFromProgramFiles([file]));
const logger = new MockLogger(); const logger = new MockLogger();
const bundle = makeTestEntryPointBundle('esm5', 'esm5', false, [file]); const bundle = makeTestEntryPointBundle('esm5', 'esm5', false, [file.name]);
const src = bundle.src; const src = bundle.src;
const typeChecker = src.program.getTypeChecker(); const typeChecker = src.program.getTypeChecker();
const host = new UmdReflectionHost(logger, false, src.program, src.host); const host = new UmdReflectionHost(logger, false, src.program, src.host);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer( const decorationAnalyses = new DecorationAnalyzer(
fs, src.program, src.options, src.host, typeChecker, host, fs, src.program, src.options, src.host, typeChecker, host,
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) referencesRegistry, [absoluteFrom('/')], false)
.analyzeProgram(); .analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program);
const renderer = new UmdRenderingFormatter(host, false); const renderer = new UmdRenderingFormatter(host, false);
@ -45,9 +46,19 @@ function setup(file: {name: string, contents: string}) {
}; };
} }
const PROGRAM = { runInEachFileSystem(() => {
name: _('/some/file.js'), describe('UmdRenderingFormatter', () => {
contents: `
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
let PROGRAM_DECORATE_HELPER: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/some/file.js'),
contents: `
/* A copyright notice */ /* A copyright notice */
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('some-side-effect'),require('/local-dep'),require('@angular/core')) : 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.NoIife = NoIife;
exports.BadIife = BadIife; exports.BadIife = BadIife;
})));`, })));`,
}; };
const PROGRAM_DECORATE_HELPER = { PROGRAM_DECORATE_HELPER = {
name: '/some/file.js', name: _('/some/file.js'),
contents: ` contents: `
/* A copyright notice */ /* A copyright notice */
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('tslib'),require('@angular/core')) : 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; exports.D = D;
// Some other content // 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', () => { describe('addImports', () => {
const {renderer, program} = setup(PROGRAM); it('should append the given imports into the CommonJS factory call', () => {
const file = program.getSourceFile('some/file.js') !; const {renderer, program} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const file = getSourceFileOrError(program, _('/some/file.js'));
renderer.addImports( const output = new MagicString(PROGRAM.contents);
output, renderer.addImports(
[ output,
{specifier: '@angular/core', qualifier: 'i0'}, [
{specifier: '@angular/common', qualifier: 'i1'} {specifier: '@angular/core', qualifier: 'i0'},
], {specifier: '@angular/common', qualifier: 'i1'}
file); ],
expect(output.toString()) file);
.toContain( expect(output.toString())
`typeof define === 'function' && define.amd ? define('file', ['exports','some-side-effect','/local-dep','@angular/core','@angular/core','@angular/common'], factory) :`); .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', () => { describe('addExports', () => {
const {renderer, program} = setup(PROGRAM); it('should insert the given exports at the end of the source file', () => {
const file = program.getSourceFile('some/file.js') !; const {importManager, renderer, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( const generateNamedImportSpy =
output, spyOn(importManager, 'generateNamedImport').and.callThrough();
[ renderer.addExports(
{specifier: '@angular/core', qualifier: 'i0'}, output, PROGRAM.name.replace(/\.js$/, ''),
{specifier: '@angular/common', qualifier: 'i1'} [
], {from: _('/some/a.js'), identifier: 'ComponentA1'},
file); {from: _('/some/a.js'), identifier: 'ComponentA2'},
expect(output.toString()) {from: _('/some/foo/b.js'), identifier: 'ComponentB'},
.toContain( {from: PROGRAM.name, identifier: 'TopLevelComponent'},
`(factory(global.file,global.someSideEffect,global.localDep,global.ng.core,global.ng.core,global.ng.common));`); ],
}); importManager, sourceFile);
it('should append the given imports as parameters into the factory function definition', () => { expect(output.toString()).toContain(`
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js') !;
const output = new MagicString(PROGRAM.contents);
renderer.addImports(
output,
[
{specifier: '@angular/core', qualifier: 'i0'},
{specifier: '@angular/common', qualifier: 'i1'}
],
file);
expect(output.toString())
.toContain(`(function (exports,someSideEffect,localDep,core,i0,i1) {'use strict';`);
});
});
describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => {
const {importManager, renderer, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const generateNamedImportSpy = spyOn(importManager, 'generateNamedImport').and.callThrough();
renderer.addExports(
output, PROGRAM.name.replace(/\.js$/, ''),
[
{from: _('/some/a.js'), identifier: 'ComponentA1'},
{from: _('/some/a.js'), identifier: 'ComponentA2'},
{from: _('/some/foo/b.js'), identifier: 'ComponentB'},
{from: PROGRAM.name, identifier: 'TopLevelComponent'},
],
importManager, sourceFile);
expect(output.toString()).toContain(`
exports.A = A; exports.A = A;
exports.B = B; exports.B = B;
exports.C = C; exports.C = C;
@ -263,228 +275,229 @@ exports.ComponentB = i1.ComponentB;
exports.TopLevelComponent = TopLevelComponent; exports.TopLevelComponent = TopLevelComponent;
})));`); })));`);
expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA1'); expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA1');
expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA2'); expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA2');
expect(generateNamedImportSpy).toHaveBeenCalledWith('./foo/b', 'ComponentB'); 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', () => { describe('addConstants', () => {
const {importManager, renderer, sourceFile} = setup(PROGRAM); it('should insert the given constants after imports in the source file', () => {
const output = new MagicString(PROGRAM.contents); const {renderer, program} = setup(PROGRAM);
renderer.addExports( const file = getSourceFileOrError(program, _('/some/file.js'));
output, PROGRAM.name.replace(/\.js$/, ''), const output = new MagicString(PROGRAM.contents);
[ renderer.addConstants(output, 'var x = 3;', file);
{from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, expect(output.toString()).toContain(`
{from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'},
{from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'},
{from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'},
],
importManager, sourceFile);
const outputString = output.toString();
expect(outputString).not.toContain(`eComponentA1`);
expect(outputString).not.toContain(`eComponentB`);
expect(outputString).not.toContain(`eTopLevelComponent`);
});
});
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'var x = 3;', file);
expect(output.toString()).toContain(`
}(this, (function (exports,someSideEffect,localDep,core) { }(this, (function (exports,someSideEffect,localDep,core) {
var x = 3; var x = 3;
'use strict'; 'use strict';
var A = (function() {`); 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', describe('rewriteSwitchableDeclarations', () => {
() => { it('should switch marked declaration initializers', () => {
// This test (from ESM5) is not needed as constants go in the body const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM);
// of the UMD IIFE, so cannot come before imports. const file = getSourceFileOrError(program, _('/some/file.js'));
}); const output = new MagicString(PROGRAM.contents);
}); renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
describe('rewriteSwitchableDeclarations', () => { expect(output.toString())
it('should switch marked declaration initializers', () => { .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); expect(output.toString())
const file = program.getSourceFile('some/file.js'); .toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
if (file === undefined) { expect(output.toString())
throw new Error(`Could not find source file`); .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
} expect(output.toString())
const output = new MagicString(PROGRAM.contents); .toContain(
renderer.rewriteSwitchableDeclarations( `function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); expect(output.toString())
expect(output.toString()) .toContain(
.not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); `function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
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', () => { describe('addDefinitions', () => {
it('should insert the definitions directly before the return statement of the class IIFE', it('should insert the definitions directly before the return statement of the class IIFE',
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
A.prototype.ngDoCheck = function() { A.prototype.ngDoCheck = function() {
// //
}; };
SOME DEFINITION TEXT SOME DEFINITION TEXT
return A; return A;
`); `);
}); });
it('should error if the compiledClass is not valid', () => { it('should error if the compiledClass is not valid', () => {
const {renderer, sourceFile, program} = setup(PROGRAM); const {renderer, sourceFile, program} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const noIifeDeclaration = const noIifeDeclaration = getDeclaration(
getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError( .toThrowError(
'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); `Compiled class declaration is not inside an IIFE: NoIife in ${_('/some/file.js')}`);
const badIifeDeclaration = const badIifeDeclaration = getDeclaration(
getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);
const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'}; const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'};
expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError( .toThrowError(
'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); `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<ts.Node, ts.Node[]>();
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]' }] }`);
}); });
describe('removeDecorators', () => {
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', it('should delete the decorator (and following comma) that was matched in the analysis',
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators ![0]; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()) expect(output.toString())
.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); .not.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()) expect(output.toString())
.not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); .toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()) expect(output.toString())
.toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); .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', it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators ![0]; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); expect(output.toString())
expect(output.toString()) .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`);
.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString())
expect(output.toString()) .not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`);
.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString())
expect(output.toString()).not.toContain(`C.decorators`); .toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`);
}); });
});
describe('[__decorate declarations]', () => { it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
it('should delete the decorator (and following comma) that was matched in the analysis', () => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; const decorator = compiledClass.decorators ![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`core.Directive({ selector: '[a]' }),`); renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`OtherA()`); expect(output.toString())
expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`core.Directive({ selector: '[c]' })`); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const compiledClass = const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; const compiledClass =
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
renderer.removeDecorators(output, decoratorsToRemove); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
expect(output.toString()).toContain(`core.Directive({ selector: '[a]' }),`); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).not.toContain(`core.Directive({ selector: '[a]' }),`);
expect(output.toString()).not.toContain(`core.Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`core.Directive({ selector: '[c]' })`); 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<ts.Node, ts.Node[]>();
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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !; const decorator = compiledClass.decorators !.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`core.Directive({ selector: '[a]' }),`); expect(output.toString()).toContain(`core.Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`); expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`); expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`core.Directive({ selector: '[c]' })`); expect(output.toString()).not.toContain(`core.Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function C() {\n }\n return C;`); expect(output.toString()).toContain(`function C() {\n }\n return C;`);
}); });
});
}); });
}); });

View File

@ -5,80 +5,79 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer'; import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer';
import {MockFileSystem} from '../helpers/mock_file_system';
const _ = AbsoluteFsPath.from; runInEachFileSystem(() => {
describe('InPlaceFileWriter', () => {
function createMockFileSystem() { let _: typeof absoluteFrom;
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',
}
});
}
describe('InPlaceFileWriter', () => { beforeEach(() => {
it('should write all the FileInfo to the disk', () => { _ = absoluteFrom;
const fs = createMockFileSystem(); loadTestFiles([
const fileWriter = new InPlaceFileWriter(fs); {name: _('/package/path/top-level.js'), contents: 'ORIGINAL TOP LEVEL'},
fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ {name: _('/package/path/folder-1/file-1.js'), contents: 'ORIGINAL FILE 1'},
{path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, {name: _('/package/path/folder-1/file-2.js'), contents: 'ORIGINAL FILE 2'},
{path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'}, {name: _('/package/path/folder-2/file-3.js'), contents: 'ORIGINAL FILE 3'},
{path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, {name: _('/package/path/folder-2/file-4.js'), contents: 'ORIGINAL FILE 4'},
{path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, {name: _('/package/path/already-backed-up.js.__ivy_ngcc_bak'), contents: 'BACKED UP'},
]); ]);
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 create backups of all files that previously existed', () => { it('should write all the FileInfo to the disk', () => {
const fs = createMockFileSystem(); const fs = getFileSystem();
const fileWriter = new InPlaceFileWriter(fs); const fileWriter = new InPlaceFileWriter(fs);
fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [
{path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, {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-1/file-1.js'), contents: 'MODIFIED FILE 1'},
{path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'},
{path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'},
]); ]);
expect(fs.readFile(_('/package/path/top-level.js.__ivy_ngcc_bak'))) expect(fs.readFile(_('/package/path/top-level.js'))).toEqual('MODIFIED TOP LEVEL');
.toEqual('ORIGINAL TOP LEVEL'); expect(fs.readFile(_('/package/path/folder-1/file-1.js'))).toEqual('MODIFIED FILE 1');
expect(fs.readFile(_('/package/path/folder-1/file-1.js.__ivy_ngcc_bak'))) expect(fs.readFile(_('/package/path/folder-1/file-2.js'))).toEqual('ORIGINAL FILE 2');
.toEqual('ORIGINAL FILE 1'); expect(fs.readFile(_('/package/path/folder-2/file-3.js'))).toEqual('ORIGINAL FILE 3');
expect(fs.exists(_('/package/path/folder-1/file-2.js.__ivy_ngcc_bak'))).toBe(false); expect(fs.readFile(_('/package/path/folder-2/file-4.js'))).toEqual('MODIFIED FILE 4');
expect(fs.exists(_('/package/path/folder-2/file-3.js.__ivy_ngcc_bak'))).toBe(false); expect(fs.readFile(_('/package/path/folder-3/file-5.js'))).toEqual('NEW FILE 5');
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', () => { it('should create backups of all files that previously existed', () => {
const fs = createMockFileSystem(); const fs = getFileSystem();
const fileWriter = new InPlaceFileWriter(fs); const fileWriter = new InPlaceFileWriter(fs);
const absoluteBackupPath = _('/package/path/already-backed-up.js'); fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [
expect( {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'},
() => fileWriter.writeBundle( {path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'},
{} as EntryPoint, {} as EntryPointBundle, {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'},
[ {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'},
{path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}, ]);
])) expect(fs.readFile(_('/package/path/top-level.js.__ivy_ngcc_bak')))
.toThrowError( .toEqual('ORIGINAL TOP LEVEL');
`Tried to overwrite ${absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`); 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.`);
});
}); });
}); });

View File

@ -5,345 +5,354 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {FileSystem} from '../../src/file_system/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 {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo} from '../../src/packages/entry_point';
import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
import {FileWriter} from '../../src/writing/file_writer'; import {FileWriter} from '../../src/writing/file_writer';
import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_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 {MockLogger} from '../helpers/mock_logger';
import {loadPackageJson} from '../packages/entry_point_spec'; import {loadPackageJson} from '../packages/entry_point_spec';
const _ = AbsoluteFsPath.from; runInEachFileSystem(() => {
describe('NewEntryPointFileWriter', () => {
function createMockFileSystem() { let _: typeof absoluteFrom;
return new MockFileSystem({ let fs: FileSystem;
'/node_modules/test': { let fileWriter: FileWriter;
'package.json': let entryPoint: EntryPoint;
'{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', let esm5bundle: EntryPointBundle;
'index.d.ts': 'export declare class FooTop {}', let esm2015bundle: EntryPointBundle;
'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 {}',
},
});
}
describe('NewEntryPointFileWriter', () => {
let fs: FileSystem;
let fileWriter: FileWriter;
let entryPoint: EntryPoint;
let esm5bundle: EntryPointBundle;
let esm2015bundle: EntryPointBundle;
describe('writeBundle() [primary entry-point]', () => {
beforeEach(() => { beforeEach(() => {
fs = createMockFileSystem(); _ = absoluteFrom;
fileWriter = new NewEntryPointFileWriter(fs); loadTestFiles([
entryPoint = getEntryPointInfo(
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !; {
esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); name: _('/node_modules/test/package.json'),
esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); 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', () => { describe('writeBundle() [primary entry-point]', () => {
fileWriter.writeBundle(entryPoint, esm5bundle, [ beforeEach(() => {
{ fs = getFileSystem();
path: _('/node_modules/test/esm5.js'), fileWriter = new NewEntryPointFileWriter(fs);
contents: 'export function FooTop() {} // MODIFIED' entryPoint = getEntryPointInfo(
}, fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !;
{path: _('/node_modules/test/esm5.js.map'), contents: 'MODIFIED MAPPING DATA'}, esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5');
]); esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015');
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() {}'); it('should write the modified files to a new folder', () => {
expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/esm5.js.map'))) fileWriter.writeBundle(entryPoint, esm5bundle, [
.toEqual('MODIFIED MAPPING DATA'); {
expect(fs.readFile(_('/node_modules/test/esm5.js.map'))).toEqual('ORIGINAL MAPPING DATA'); 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', () => { describe('writeBundle() [secondary entry-point]', () => {
fileWriter.writeBundle(entryPoint, esm2015bundle, [ beforeEach(() => {
{ fs = getFileSystem();
path: _('/node_modules/test/es2015/foo.js'), fileWriter = new NewEntryPointFileWriter(fs);
contents: 'export class FooTop {} // MODIFIED' entryPoint = getEntryPointInfo(
}, fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !;
]); esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5');
expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/foo.js'))) esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015');
.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'))) it('should write the modified file to a new folder', () => {
.toEqual('export {FooTop} from "./foo";'); fileWriter.writeBundle(entryPoint, esm5bundle, [
expect(fs.readFile(_('/node_modules/test/es2015/index.js'))) {
.toEqual('export {FooTop} from "./foo";'); 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', () => { describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => {
fileWriter.writeBundle(entryPoint, esm5bundle, [ beforeEach(() => {
{ fs = getFileSystem();
path: _('/node_modules/test/esm5.js'), fileWriter = new NewEntryPointFileWriter(fs);
contents: 'export function FooTop() {} // MODIFIED' entryPoint = getEntryPointInfo(
}, fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !;
]); esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5');
expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015');
module_ivy_ngcc: '__ivy_ngcc__/esm5.js', });
}));
fileWriter.writeBundle(entryPoint, esm2015bundle, [ it('should write the modified file to a new folder', () => {
{ fileWriter.writeBundle(entryPoint, esm5bundle, [
path: _('/node_modules/test/es2015/foo.js'), {
contents: 'export class FooTop {} // MODIFIED' path: _('/node_modules/test/lib/esm5.js'),
}, contents: 'export function FooB() {} // MODIFIED'
]); },
expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ ]);
module_ivy_ngcc: '__ivy_ngcc__/esm5.js', expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/esm5.js')))
es2015_ivy_ngcc: '__ivy_ngcc__/es2015/index.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', () => { it('should also copy unmodified files in the program', () => {
fileWriter.writeBundle(entryPoint, esm2015bundle, [ fileWriter.writeBundle(entryPoint, esm2015bundle, [
{ {
path: _('/node_modules/test/index.d.ts'), path: _('/node_modules/test/lib/es2015/foo.js'),
contents: 'export declare class FooTop {} // MODIFIED' contents: 'export class FooB {} // MODIFIED'
}, },
{path: _('/node_modules/test/index.d.ts.map'), contents: 'MODIFIED MAPPING DATA'}, ]);
]); expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/foo.js')))
expect(fs.readFile(_('/node_modules/test/index.d.ts'))) .toEqual('export class FooB {} // MODIFIED');
.toEqual('export declare class FooTop {} // MODIFIED'); expect(fs.readFile(_('/node_modules/test/lib/es2015/foo.js')))
expect(fs.readFile(_('/node_modules/test/index.d.ts.__ivy_ngcc_bak'))) .toEqual('import {FooA} from "test/a"; import "events"; export class FooB {}');
.toEqual('export declare class FooTop {}'); expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/index.js')))
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts'))).toBe(false); .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'); it('should not copy typings files within the package (i.e. from a different entry-point)',
expect(fs.readFile(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak'))) () => {
.toEqual('ORIGINAL MAPPING DATA'); fileWriter.writeBundle(entryPoint, esm2015bundle, [
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts.map'))).toBe(false); {
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]', () => { function makeTestBundle(
beforeEach(() => { fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty,
fs = createMockFileSystem(); format: EntryPointFormat): EntryPointBundle {
fileWriter = new NewEntryPointFileWriter(fs); return makeEntryPointBundle(
entryPoint = getEntryPointInfo( fs, entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false,
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !; formatProperty, format, true) !;
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) !;
}

View File

@ -7,7 +7,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
/** /**
* Extract i18n messages from source code * Extract i18n messages from source code
*/ */
@ -16,11 +15,12 @@ import 'reflect-metadata';
import * as api from './transformers/api'; import * as api from './transformers/api';
import {ParsedConfiguration} from './perform_compile'; import {ParsedConfiguration} from './perform_compile';
import {main, readCommandLineAndConfiguration} from './main'; import {main, readCommandLineAndConfiguration} from './main';
import {setFileSystem, NodeJSFileSystem} from './ngtsc/file_system';
export function mainXi18n( export function mainXi18n(
args: string[], consoleError: (msg: string) => void = console.error): number { args: string[], consoleError: (msg: string) => void = console.error): number {
const config = readXi18nCommandLineAndConfiguration(args); const config = readXi18nCommandLineAndConfiguration(args);
return main(args, consoleError, config); return main(args, consoleError, config, undefined, undefined, undefined);
} }
function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfiguration { function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfiguration {
@ -42,5 +42,7 @@ function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfigurati
// Entry point // Entry point
if (require.main === module) { if (require.main === module) {
const args = process.argv.slice(2); 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); process.exitCode = mainXi18n(args);
} }

View File

@ -17,8 +17,9 @@ import {replaceTsWithNgInErrors} from './ngtsc/diagnostics';
import * as api from './transformers/api'; import * as api from './transformers/api';
import {GENERATED_FILES} from './transformers/util'; 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 {performWatchCompilation, createPerformWatchHost} from './perform_watch';
import {NodeJSFileSystem, setFileSystem} from './ngtsc/file_system';
export function main( export function main(
args: string[], consoleError: (s: string) => void = console.error, args: string[], consoleError: (s: string) => void = console.error,
@ -227,5 +228,7 @@ export function watchMode(
// CLI entry point // CLI entry point
if (require.main === module) { if (require.main === module) {
const args = process.argv.slice(2); 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); process.exitCode = main(args);
} }

View File

@ -25,7 +25,6 @@ import {ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {formatDiagnostics as formatDiagnosticsOrig} from './perform_compile'; import {formatDiagnostics as formatDiagnosticsOrig} from './perform_compile';
import {Program as ProgramOrig} from './transformers/api';
import {createCompilerHost as createCompilerOrig} from './transformers/compiler_host'; import {createCompilerHost as createCompilerOrig} from './transformers/compiler_host';
import {createProgram as createProgramOrig} from './transformers/program'; import {createProgram as createProgramOrig} from './transformers/program';

View File

@ -11,6 +11,7 @@ ts_library(
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",

View File

@ -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 {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 * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles'; import {CycleAnalyzer} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFrom, relative} from '../../file_system';
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {IndexingContext} from '../../indexer'; import {IndexingContext} from '../../indexer';
import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata'; 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 // Go through the root directories for this project, and select the one with the smallest
// relative path representation. // relative path representation.
const relativeContextFilePath = this.rootDirs.reduce<string|undefined>((previous, rootDir) => { const relativeContextFilePath = this.rootDirs.reduce<string|undefined>((previous, rootDir) => {
const candidate = path.posix.relative(rootDir, containingFile); const candidate = relative(absoluteFrom(rootDir), absoluteFrom(containingFile));
if (previous === undefined || candidate.length < previous.length) { if (previous === undefined || candidate.length < previous.length) {
return candidate; return candidate;
} else { } else {
@ -205,7 +205,7 @@ export class ComponentDecoratorHandler implements
/* escapedString */ false, options); /* escapedString */ false, options);
} else { } else {
// Expect an inline template to be present. // Expect an inline template to be present.
const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath); const inlineTemplate = this._extractInlineTemplate(component, containingFile);
if (inlineTemplate === null) { if (inlineTemplate === null) {
throw new FatalDiagnosticError( throw new FatalDiagnosticError(
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node,
@ -583,8 +583,7 @@ export class ComponentDecoratorHandler implements
} }
} }
private _extractInlineTemplate( private _extractInlineTemplate(component: Map<string, ts.Expression>, containingFile: string): {
component: Map<string, ts.Expression>, relativeContextFilePath: string): {
templateStr: string, templateStr: string,
templateUrl: string, templateUrl: string,
templateRange: LexerRange|undefined, templateRange: LexerRange|undefined,
@ -606,7 +605,7 @@ export class ComponentDecoratorHandler implements
// strip // strip
templateRange = getTemplateRange(templateExpr); templateRange = getTemplateRange(templateExpr);
templateStr = templateExpr.getSourceFile().text; templateStr = templateExpr.getSourceFile().text;
templateUrl = relativeContextFilePath; templateUrl = containingFile;
escapedString = true; escapedString = true;
} else { } else {
const resolvedTemplate = this.evaluator.evaluate(templateExpr); const resolvedTemplate = this.evaluator.evaluate(templateExpr);

View File

@ -8,7 +8,6 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference} from '../../imports';
import {Declaration} from '../../reflection';
/** /**
* Implement this interface if you want DecoratorHandlers to register * Implement this interface if you want DecoratorHandlers to register

View File

@ -14,15 +14,15 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics", "//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/imports",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript", "@npm//typescript",
], ],
) )

View File

@ -7,12 +7,14 @@
*/ */
import {CycleAnalyzer, ImportGraph} from '../../cycles'; import {CycleAnalyzer, ImportGraph} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; 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 {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing';
import {ResourceLoader} from '../src/api'; import {ResourceLoader} from '../src/api';
import {ComponentDecoratorHandler} from '../src/component'; import {ComponentDecoratorHandler} from '../src/component';
@ -22,62 +24,64 @@ export class NoopResourceLoader implements ResourceLoader {
load(): string { throw new Error('Not implemented'); } load(): string { throw new Error('Not implemented'); }
preload(): Promise<void>|undefined { throw new Error('Not implemented'); } preload(): Promise<void>|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', () => {
it('should produce a diagnostic when @Component has non-literal argument', () => { const {program, options, host} = makeProgram([
const {program, options, host} = makeProgram([ {
{ name: _('/node_modules/@angular/core/index.d.ts'),
name: 'node_modules/@angular/core/index.d.ts', contents: 'export const Component: any;',
contents: 'export const Component: any;', },
}, {
{ name: _('/entry.ts'),
name: 'entry.ts', contents: `
contents: `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
const TEST = ''; const TEST = '';
@Component(TEST) class TestCmp {} @Component(TEST) class TestCmp {}
` `
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker); const evaluator = new PartialEvaluator(reflectionHost, checker);
const moduleResolver = new ModuleResolver(program, options, host); const moduleResolver = new ModuleResolver(program, options, host);
const importGraph = new ImportGraph(moduleResolver); const importGraph = new ImportGraph(moduleResolver);
const cycleAnalyzer = new CycleAnalyzer(importGraph); const cycleAnalyzer = new CycleAnalyzer(importGraph);
const metaRegistry = new LocalMetadataRegistry(); const metaRegistry = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost); const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry( const scopeRegistry = new LocalModuleScopeRegistry(
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null),
null); new ReferenceEmitter([]), null);
const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]);
const refEmitter = new ReferenceEmitter([]); const refEmitter = new ReferenceEmitter([]);
const handler = new ComponentDecoratorHandler( const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, false, reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, false,
new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer, refEmitter, new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer, refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER); NOOP_DEFAULT_IMPORT_RECORDER);
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', isNamedClassDeclaration); const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) { if (detected === undefined) {
return fail('Failed to recognize @Component'); 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 diag = err.toDiagnostic(); try {
expect(diag.code).toEqual(ivyCode(ErrorCode.DECORATOR_ARG_NOT_LITERAL)); handler.analyze(TestCmp, detected.metadata);
expect(diag.file.fileName.endsWith('entry.ts')).toBe(true); return fail('Analysis should have failed');
expect(diag.start).toBe(detected.metadata.args ![0].getStart()); } 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 { function ivyCode(code: ErrorCode): number { return Number('-99' + code.valueOf()); }
return Number('-99' + code.valueOf()); });
}

View File

@ -5,25 +5,30 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {ClassDeclaration, TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing';
import {DirectiveDecoratorHandler} from '../src/directive'; import {DirectiveDecoratorHandler} from '../src/directive';
runInEachFileSystem(() => {
describe('DirectiveDecoratorHandler', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
describe('DirectiveDecoratorHandler', () => { it('should use the `ReflectionHost` to detect class inheritance', () => {
it('should use the `ReflectionHost` to detect class inheritance', () => { const {program} = makeProgram([
const {program} = makeProgram([ {
{ name: _('/node_modules/@angular/core/index.d.ts'),
name: 'node_modules/@angular/core/index.d.ts', contents: 'export const Directive: any;',
contents: 'export const Directive: any;', },
}, {
{ name: _('/entry.ts'),
name: 'entry.ts', contents: `
contents: `
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
@Directive({selector: 'test-dir-1'}) @Directive({selector: 'test-dir-1'})
@ -32,51 +37,53 @@ describe('DirectiveDecoratorHandler', () => {
@Directive({selector: 'test-dir-2'}) @Directive({selector: 'test-dir-2'})
export class TestDir2 {} export class TestDir2 {}
`, `,
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const reflectionHost = new TestReflectionHost(checker); const reflectionHost = new TestReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker); const evaluator = new PartialEvaluator(reflectionHost, checker);
const metaReader = new LocalMetadataRegistry(); const metaReader = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost); const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry( const scopeRegistry = new LocalModuleScopeRegistry(
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null); null);
const handler = new DirectiveDecoratorHandler( const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false); reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false);
const analyzeDirective = (dirName: string) => { const analyzeDirective = (dirName: string) => {
const DirNode = getDeclaration(program, 'entry.ts', dirName, isNamedClassDeclaration); const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
const detected = handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode)); const detected =
if (detected === undefined) { handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode));
throw new Error(`Failed to recognize @Directive (${dirName}).`); if (detected === undefined) {
} throw new Error(`Failed to recognize @Directive (${dirName}).`);
}
const {analysis} = handler.analyze(DirNode, detected.metadata); const {analysis} = handler.analyze(DirNode, detected.metadata);
if (analysis === undefined) { if (analysis === undefined) {
throw new Error(`Failed to analyze @Directive (${dirName}).`); throw new Error(`Failed to analyze @Directive (${dirName}).`);
} }
return analysis; return analysis;
}; };
// By default, `TestReflectionHost#hasBaseClass()` returns `false`. // By default, `TestReflectionHost#hasBaseClass()` returns `false`.
const analysis1 = analyzeDirective('TestDir1'); const analysis1 = analyzeDirective('TestDir1');
expect(analysis1.meta.usesInheritance).toBe(false); expect(analysis1.meta.usesInheritance).toBe(false);
// Tweak `TestReflectionHost#hasBaseClass()` to return true. // Tweak `TestReflectionHost#hasBaseClass()` to return true.
reflectionHost.hasBaseClassReturnValue = true; reflectionHost.hasBaseClassReturnValue = true;
const analysis2 = analyzeDirective('TestDir2'); const analysis2 = analyzeDirective('TestDir2');
expect(analysis2.meta.usesInheritance).toBe(true); 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; }
}

View File

@ -5,38 +5,29 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; 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 {NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection'; import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing';
import {ImportManager, translateStatement} from '../../translator'; import {ImportManager, translateStatement} from '../../translator';
import {generateSetClassMetadataCall} from '../src/metadata'; import {generateSetClassMetadataCall} from '../src/metadata';
const CORE = { runInEachFileSystem(() => {
name: 'node_modules/@angular/core/index.d.ts', describe('ngtsc setClassMetadata converter', () => {
contents: ` it('should convert decorated class metadata', () => {
export declare function Input(...args: any[]): any; const res = compileAndPrint(`
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(`
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component('metadata') class Target {} @Component('metadata') class Target {}
`); `);
expect(res).toEqual( expect(res).toEqual(
`/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`); `/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`);
}); });
it('should convert decorated class constructor parameter metadata', () => { it('should convert decorated class constructor parameter metadata', () => {
const res = compileAndPrint(` const res = compileAndPrint(`
import {Component, Inject, Injector} from '@angular/core'; import {Component, Inject, Injector} from '@angular/core';
const FOO = 'foo'; const FOO = 'foo';
@ -44,12 +35,12 @@ describe('ngtsc setClassMetadata converter', () => {
constructor(@Inject(FOO) foo: any, bar: Injector) {} constructor(@Inject(FOO) foo: any, bar: Injector) {}
} }
`); `);
expect(res).toContain( expect(res).toContain(
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`); `function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`);
}); });
it('should convert decorated field metadata', () => { it('should convert decorated field metadata', () => {
const res = compileAndPrint(` const res = compileAndPrint(`
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
@Component('metadata') class Target { @Component('metadata') class Target {
@ -60,35 +51,47 @@ describe('ngtsc setClassMetadata converter', () => {
notDecorated: string; 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', () => { it('should not convert non-angular decorators to metadata', () => {
const res = compileAndPrint(` const res = compileAndPrint(`
declare function NotAComponent(...args: any[]): any; declare function NotAComponent(...args: any[]): any;
@NotAComponent('metadata') class Target {} @NotAComponent('metadata') class Target {}
`); `);
expect(res).toBe(''); expect(res).toBe('');
});
}); });
});
function compileAndPrint(contents: string): string { function compileAndPrint(contents: string): string {
const {program} = makeProgram([ const _ = absoluteFrom;
CORE, { const CORE: TestFile = {
name: 'index.ts', name: _('/node_modules/@angular/core/index.d.ts'),
contents, 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 sf = getSourceFileOrError(program, _('/index.ts'));
const host = new TypeScriptReflectionHost(program.getTypeChecker()); const im = new ImportManager(new NoopImportRewriter(), 'i');
const target = getDeclaration(program, 'index.ts', 'Target', ts.isClassDeclaration); const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER);
const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false); const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
if (call === null) { return res.replace(/\s+/g, ' ');
return '';
} }
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, ' ');
}

View File

@ -5,34 +5,36 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {WrappedNodeExpr} from '@angular/compiler'; import {WrappedNodeExpr} from '@angular/compiler';
import {R3Reference} from '@angular/compiler/src/compiler'; import {R3Reference} from '@angular/compiler/src/compiler';
import * as ts from 'typescript'; 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 {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; 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 {NgModuleDecoratorHandler} from '../src/ng_module';
import {NoopReferencesRegistry} from '../src/references_registry'; import {NoopReferencesRegistry} from '../src/references_registry';
describe('NgModuleDecoratorHandler', () => { runInEachFileSystem(() => {
it('should resolve forwardRef', () => { describe('NgModuleDecoratorHandler', () => {
const {program} = makeProgram([ it('should resolve forwardRef', () => {
{ const _ = absoluteFrom;
name: 'node_modules/@angular/core/index.d.ts', const {program} = makeProgram([
contents: ` {
name: _('/node_modules/@angular/core/index.d.ts'),
contents: `
export const Component: any; export const Component: any;
export const NgModule: any; export const NgModule: any;
export declare function forwardRef(fn: () => any): any; export declare function forwardRef(fn: () => any): any;
`, `,
}, },
{ {
name: 'entry.ts', name: _('/entry.ts'),
contents: ` contents: `
import {Component, forwardRef, NgModule} from '@angular/core'; import {Component, forwardRef, NgModule} from '@angular/core';
@Component({ @Component({
@ -50,37 +52,38 @@ describe('NgModuleDecoratorHandler', () => {
}) })
export class TestModule {} export class TestModule {}
` `
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker); const evaluator = new PartialEvaluator(reflectionHost, checker);
const referencesRegistry = new NoopReferencesRegistry(); const referencesRegistry = new NoopReferencesRegistry();
const metaRegistry = new LocalMetadataRegistry(); const metaRegistry = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost); const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry( const scopeRegistry = new LocalModuleScopeRegistry(
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null),
null); new ReferenceEmitter([]), null);
const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]);
const handler = new NgModuleDecoratorHandler( const handler = new NgModuleDecoratorHandler(
reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null, reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null,
refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); refEmitter, NOOP_DEFAULT_IMPORT_RECORDER);
const TestModule = getDeclaration(program, 'entry.ts', 'TestModule', isNamedClassDeclaration); const TestModule =
const detected = getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule)); const detected =
if (detected === undefined) { handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule));
return fail('Failed to recognize @NgModule'); if (detected === undefined) {
} return fail('Failed to recognize @NgModule');
const moduleDef = handler.analyze(TestModule, detected.metadata).analysis !.ngModuleDef; }
const moduleDef = handler.analyze(TestModule, detected.metadata).analysis !.ngModuleDef;
expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']); expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']);
expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']); expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']);
expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']); expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']);
function getReferenceIdentifierTexts(references: R3Reference[]) { function getReferenceIdentifierTexts(references: R3Reference[]) {
return references.map(ref => (ref.value as WrappedNodeExpr<ts.Identifier>).node.text); return references.map(ref => (ref.value as WrappedNodeExpr<ts.Identifier>).node.text);
} }
});
}); });
}); });

View File

@ -11,6 +11,8 @@ ts_library(
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler-cli/src/ngtsc/cycles", "//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/imports",
"//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript", "@npm//typescript",

View File

@ -5,62 +5,66 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver} from '../../imports'; import {ModuleResolver} from '../../imports';
import {CycleAnalyzer} from '../src/analyzer'; import {CycleAnalyzer} from '../src/analyzer';
import {ImportGraph} from '../src/imports'; import {ImportGraph} from '../src/imports';
import {makeProgramFromGraph} from './util'; import {makeProgramFromGraph} from './util';
describe('cycle analyzer', () => { runInEachFileSystem(() => {
it('should not detect a cycle when there isn\'t one', () => { describe('cycle analyzer', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); let _: typeof absoluteFrom;
const b = program.getSourceFile('b.ts') !; beforeEach(() => _ = absoluteFrom);
const c = program.getSourceFile('c.ts') !;
expect(analyzer.wouldCreateCycle(b, c)).toBe(false); it('should not detect a cycle when there isn\'t one', () => {
expect(analyzer.wouldCreateCycle(c, b)).toBe(false); 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', () => { function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} {
const {program, analyzer} = makeAnalyzer('a:b;b'); const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph);
const a = program.getSourceFile('a.ts') !; return {
const b = program.getSourceFile('b.ts') !; program,
expect(analyzer.wouldCreateCycle(a, b)).toBe(false); analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))),
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(graph);
return {
program,
analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))),
};
}

View File

@ -5,59 +5,67 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver} from '../../imports'; import {ModuleResolver} from '../../imports';
import {ImportGraph} from '../src/imports'; import {ImportGraph} from '../src/imports';
import {makeProgramFromGraph} from './util'; import {makeProgramFromGraph} from './util';
describe('import graph', () => { runInEachFileSystem(() => {
it('should record imports of a simple program', () => { describe('import graph', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c'); let _: typeof absoluteFrom;
const a = program.getSourceFile('a.ts') !; beforeEach(() => _ = absoluteFrom);
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !; it('should record imports of a simple program', () => {
expect(importsToString(graph.importsOf(a))).toBe('b'); const {program, graph} = makeImportGraph('a:b;b:c;c');
expect(importsToString(graph.importsOf(b))).toBe('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', () => { function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} {
const {program, graph} = makeImportGraph('a:b;b:c;c'); const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph);
const a = program.getSourceFile('a.ts') !; return {
const b = program.getSourceFile('b.ts') !; program,
const c = program.getSourceFile('c.ts') !; graph: new ImportGraph(new ModuleResolver(program, options, host)),
expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c'); };
}); }
it('should calculate transitive imports in a more complex program (with a cycle)', () => { function importsToString(imports: Set<ts.SourceFile>): string {
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g'); const fs = getFileSystem();
const c = program.getSourceFile('c.ts') !; return Array.from(imports)
expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h'); .map(sf => fs.basename(sf.fileName).replace('.ts', ''))
}); .sort()
.join(',');
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 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<ts.SourceFile>): string {
return Array.from(imports).map(sf => sf.fileName.substr(1).replace('.ts', '')).sort().join(',');
}

View File

@ -5,10 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {FileSystem} from '../../file_system';
import {makeProgram} from '../../testing/in_memory_typescript'; 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 * 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. * 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, program: ts.Program,
host: ts.CompilerHost, host: ts.CompilerHost,
options: ts.CompilerOptions, options: ts.CompilerOptions,
} { } {
const files = graph.split(';').map(fileSegment => { const files: TestFile[] = graph.split(';').map(fileSegment => {
const [name, importList] = fileSegment.split(':'); const [name, importList] = fileSegment.split(':');
const contents = (importList ? importList.split(',') : []) const contents = (importList ? importList.split(',') : [])
.map(i => { .map(i => {
@ -50,7 +50,7 @@ export function makeProgramFromGraph(graph: string): {
.join('\n') + .join('\n') +
`export const ${name} = '${name}';\n`; `export const ${name} = '${name}';\n`;
return { return {
name: `${name}.ts`, name: fs.resolve(`/${name}.ts`),
contents, contents,
}; };
}); });

View File

@ -10,7 +10,7 @@ ts_library(
module_name = "@angular/compiler-cli/src/ngtsc/entry_point", module_name = "@angular/compiler-cli/src/ngtsc/entry_point",
deps = [ deps = [
"//packages/compiler-cli/src/ngtsc/diagnostics", "//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/shims",
"//packages/compiler-cli/src/ngtsc/util", "//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node", "@npm//@types/node",

View File

@ -8,9 +8,9 @@
/// <reference types="node" /> /// <reference types="node" />
import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath, dirname, join} from '../../file_system';
import {ShimGenerator} from '../../shims'; import {ShimGenerator} from '../../shims';
import {relativePathBetween} from '../../util/src/path'; import {relativePathBetween} from '../../util/src/path';
@ -18,11 +18,10 @@ export class FlatIndexGenerator implements ShimGenerator {
readonly flatIndexPath: string; readonly flatIndexPath: string;
constructor( constructor(
readonly entryPoint: string, relativeFlatIndexPath: string, readonly entryPoint: AbsoluteFsPath, relativeFlatIndexPath: string,
readonly moduleName: string|null) { readonly moduleName: string|null) {
this.flatIndexPath = path.posix.join(path.posix.dirname(entryPoint), relativeFlatIndexPath) this.flatIndexPath =
.replace(/\.js$/, '') + join(dirname(entryPoint), relativeFlatIndexPath).replace(/\.js$/, '') + '.ts';
'.ts';
} }
recognize(fileName: string): boolean { return fileName === this.flatIndexPath; } recognize(fileName: string): boolean { return fileName === this.flatIndexPath; }

View File

@ -6,15 +6,16 @@
* found in the LICENSE file at https://angular.io/license * 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'; import {isNonDeclarationTsPath} from '../../util/src/typescript';
export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray<AbsoluteFsPath>): string|null { export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray<AbsoluteFsPath>): AbsoluteFsPath|
null {
// There are two ways for a file to be recognized as the flat module index: // There are two ways for a file to be recognized as the flat module index:
// 1) if it's the only file!!!!!! // 1) if it's the only file!!!!!!
// 2) (deprecated) if it's named 'index.ts' and has the shortest path of all such files. // 2) (deprecated) if it's named 'index.ts' and has the shortest path of all such files.
const tsFiles = rootFiles.filter(file => isNonDeclarationTsPath(file)); const tsFiles = rootFiles.filter(file => isNonDeclarationTsPath(file));
let resolvedEntryPoint: string|null = null; let resolvedEntryPoint: AbsoluteFsPath|null = null;
if (tsFiles.length === 1) { if (tsFiles.length === 1) {
// There's only one file - this is the flat module index. // There's only one file - this is the flat module index.
@ -26,7 +27,7 @@ export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray<AbsoluteFsPath>
// //
// This behavior is DEPRECATED and only exists to support existing usages. // This behavior is DEPRECATED and only exists to support existing usages.
for (const tsFile of tsFiles) { for (const tsFile of tsFiles) {
if (tsFile.endsWith('/index.ts') && if (getFileSystem().basename(tsFile) === 'index.ts' &&
(resolvedEntryPoint === null || tsFile.length <= resolvedEntryPoint.length)) { (resolvedEntryPoint === null || tsFile.length <= resolvedEntryPoint.length)) {
resolvedEntryPoint = tsFile; resolvedEntryPoint = tsFile;
} }

View File

@ -11,7 +11,8 @@ ts_library(
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler-cli/src/ngtsc/entry_point", "//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", "@npm//typescript",
], ],
) )

View File

@ -6,24 +6,25 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {absoluteFrom} from '../../file_system';
import {AbsoluteFsPath} from '../../path/src/types'; import {runInEachFileSystem} from '../../file_system/testing';
import {findFlatIndexEntryPoint} from '../src/logic'; 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', () => { it('should use the only source file if only a single one is specified',
expect(findFlatIndexEntryPoint([AbsoluteFsPath.fromUnchecked('/src/index.ts')])) () => { expect(findFlatIndexEntryPoint([_('/src/index.ts')])).toBe(_('/src/index.ts')); });
.toBe('/src/index.ts');
});
it('should use the shortest source file ending with "index.ts" for multiple files', () => { it('should use the shortest source file ending with "index.ts" for multiple files', () => {
expect(findFlatIndexEntryPoint([ expect(findFlatIndexEntryPoint([
AbsoluteFsPath.fromUnchecked('/src/deep/index.ts'), _('/src/deep/index.ts'), _('/src/index.ts'), _('/index.ts')
AbsoluteFsPath.fromUnchecked('/src/index.ts'), AbsoluteFsPath.fromUnchecked('/index.ts') ])).toBe(_('/index.ts'));
])).toBe('/index.ts'); });
}); });
}); });
}); });

View File

@ -3,7 +3,7 @@ package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library") load("//tools:defaults.bzl", "ts_library")
ts_library( ts_library(
name = "path", name = "file_system",
srcs = ["index.ts"] + glob([ srcs = ["index.ts"] + glob([
"src/*.ts", "src/*.ts",
]), ]),

View File

@ -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.

View File

@ -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';

View File

@ -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
*/
/// <reference types="node" />
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<ts.SourceFile>): 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);
}
}

View File

@ -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<T extends PathString>(file: T): T {
return fs.dirname(file);
}
/**
* Static access to `join`.
*/
export function join<T extends PathString>(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<T extends PathString>(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;
}

View File

@ -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<T extends PathString>(file: T): T { throw makeError(); }
join<T extends PathString>(basePath: T, ...paths: string[]): T { throw makeError(); }
isRoot(path: AbsoluteFsPath): boolean { throw makeError(); }
isRooted(path: string): boolean { throw makeError(); }
relative<T extends PathString>(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<T extends PathString>(path: T): T { throw makeError(); }
}
function makeError() {
return new Error(
'FileSystem has not been configured. Please call `setFileSystem()` before calling this method.');
}

View File

@ -5,15 +5,14 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
/// <reference types="node" />
import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom, dirname, relative, resolve} from './helpers';
import {AbsoluteFsPath, BrandedPath, PathSegment} from './types'; import {AbsoluteFsPath, BrandedPath, PathSegment} from './types';
import {stripExtension} from './util'; import {stripExtension} from './util';
/** /**
* A path that's relative to the logical root of a TypeScript project (one of the project's * A path that's relative to the logical root of a TypeScript project (one of the project's
* rootDirs). * rootDirs).
@ -30,9 +29,9 @@ export const LogicalProjectPath = {
* importing from `to`. * importing from `to`.
*/ */
relativePathBetween: function(from: LogicalProjectPath, to: LogicalProjectPath): PathSegment { 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('../')) { if (!relativePath.startsWith('../')) {
relativePath = ('./' + relativePath); relativePath = ('./' + relativePath) as PathSegment;
} }
return 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`. * Get the logical path in the project of a `ts.SourceFile`.
* *
* This method is provided as a convenient alternative to calling * This method is provided as a convenient alternative to calling
* `logicalPathOfFile(AbsoluteFsPath.fromSourceFile(sf))`. * `logicalPathOfFile(absoluteFromSourceFile(sf))`.
*/ */
logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null { logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null {
return this.logicalPathOfFile(AbsoluteFsPath.from(sf.fileName)); return this.logicalPathOfFile(absoluteFrom(sf.fileName));
} }
/** /**

View File

@ -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
*/
/// <reference types="node" />
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<T extends string>(file: T): T { return this.normalize(p.dirname(file)) as T; }
join<T extends string>(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<T extends PathString>(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<T extends string>(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()));
}

View File

@ -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<B extends string> = 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<T extends PathString>(file: T): T;
join<T extends PathString>(basePath: T, ...paths: string[]): T;
relative<T extends PathString>(from: T, to: T): PathSegment;
basename(filePath: string, extension?: string): PathSegment;
realpath(filePath: AbsoluteFsPath): AbsoluteFsPath;
getDefaultLibLocation(): AbsoluteFsPath;
normalize<T extends PathString>(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;
}

View File

@ -5,11 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript';
// TODO(alxhub): Unify this file with `util/src/path`. import {AbsoluteFsPath} from './types';
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/; const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
const ABSOLUTE_PATH = /^([a-zA-Z]:\/|\/)/;
/** /**
* Convert Windows-style separators to POSIX separators. * Convert Windows-style separators to POSIX separators.
@ -26,10 +25,11 @@ export function stripExtension(path: string): string {
return path.replace(TS_DTS_JS_EXTENSION, ''); return path.replace(TS_DTS_JS_EXTENSION, '');
} }
/** export function getSourceFileOrError(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile {
* Returns true if the normalized path is an absolute path. const sf = program.getSourceFile(fileName);
*/ if (sf === undefined) {
export function isAbsolutePath(path: string): boolean { throw new Error(
// TODO: use regExp based on OS in the future `Program does not contain "${fileName}" - available files are ${program.getSourceFiles().map(sf => sf.fileName).join(', ')}`);
return ABSOLUTE_PATH.test(path); }
return sf;
} }

View File

@ -10,7 +10,8 @@ ts_library(
]), ]),
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
], ],
) )

View File

@ -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(); });
}
});
});

Some files were not shown because too many files have changed in this diff Show More