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,16 +49,15 @@ 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;

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,78 +7,27 @@
*/ */
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('DecorationAnalyzer', () => {
let _: typeof absoluteFrom;
beforeEach(() => { _ = absoluteFrom; });
describe('analyzeProgram()', () => { describe('analyzeProgram()', () => {
let logs: string[]; let logs: string[];
let program: ts.Program; let program: ts.Program;
@ -95,7 +44,8 @@ describe('DecorationAnalyzer', () => {
]); ]);
// Only detect the Component and Directive decorators // Only detect the Component and Directive decorators
handler.detect.and.callFake( handler.detect.and.callFake(
(node: ts.Declaration, decorators: Decorator[] | null): DetectResult<any>| undefined => { (node: ts.Declaration, decorators: Decorator[] | null): DetectResult<any>|
undefined => {
const className = (node as any).name.text; const className = (node as any).name.text;
if (decorators === null) { if (decorators === null) {
logs.push(`detect: ${className} (no decorators)`); logs.push(`detect: ${className} (no decorators)`);
@ -105,7 +55,8 @@ describe('DecorationAnalyzer', () => {
if (!decorators) { if (!decorators) {
return undefined; return undefined;
} }
const metadata = decorators.find(d => d.name === 'Component' || d.name === 'Directive'); const metadata =
decorators.find(d => d.name === 'Component' || d.name === 'Directive');
if (metadata === undefined) { if (metadata === undefined) {
return undefined; return undefined;
} else { } else {
@ -135,38 +86,80 @@ describe('DecorationAnalyzer', () => {
return handler; return handler;
}; };
const setUpAndAnalyzeProgram = (...progArgs: Parameters<typeof makeTestBundleProgram>) => { function setUpAndAnalyzeProgram(testFiles: TestFile[]) {
logs = []; logs = [];
loadTestFiles(testFiles);
const {options, host, ...bundle} = makeTestBundleProgram(...progArgs); loadFakeCore(getFileSystem());
const rootFiles = getRootFiles(testFiles);
const {options, host, ...bundle} = makeTestBundleProgram(rootFiles[0]);
program = bundle.program; program = bundle.program;
const reflectionHost = const reflectionHost =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const fs = new MockFileSystem(createFileSystemFromProgramFiles(...progArgs));
const analyzer = new DecorationAnalyzer( const analyzer = new DecorationAnalyzer(
fs, program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, getFileSystem(), program, options, host, program.getTypeChecker(), reflectionHost,
[AbsoluteFsPath.fromUnchecked('/')], false); referencesRegistry, [absoluteFrom('/')], false);
testHandler = createTestHandler(); testHandler = createTestHandler();
analyzer.handlers = [testHandler]; analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram(); result = analyzer.analyzeProgram();
}; }
describe('basic usage', () => { describe('basic usage', () => {
beforeEach(() => setUpAndAnalyzeProgram(TEST_PROGRAM)); beforeEach(() => {
const TEST_PROGRAM = [
{
name: _('/index.js'),
contents: `
import * as test from './test';
import * as other from './other';
`,
},
{
name: _('/test.js'),
contents: `
import {Component, Directive, Injectable} from '@angular/core';
export class MyComponent {}
MyComponent.decorators = [{type: Component}];
export class MyDirective {}
MyDirective.decorators = [{type: Directive}];
export class MyService {}
MyService.decorators = [{type: Injectable}];
`,
},
{
name: _('/other.js'),
contents: `
import {Component} from '@angular/core';
export class MyOtherComponent {}
MyOtherComponent.decorators = [{type: Component}];
`,
},
];
setUpAndAnalyzeProgram(TEST_PROGRAM);
});
it('should return an object containing a reference to the original source file', () => { it('should return an object containing a reference to the original source file', () => {
TEST_PROGRAM.forEach(({name}) => { const testFile = getSourceFileOrError(program, _('/test.js'));
const file = program.getSourceFile(name) !; expect(result.get(testFile) !.sourceFile).toBe(testFile);
expect(result.get(file) !.sourceFile).toBe(file); 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', it('should call detect on the decorator handlers with each class from the parsed file',
() => { () => {
expect(testHandler.detect).toHaveBeenCalledTimes(5); expect(testHandler.detect).toHaveBeenCalledTimes(11);
expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([ expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([
null,
null,
null,
null,
null,
null,
null, null,
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]), jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]), jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]),
@ -176,7 +169,7 @@ describe('DecorationAnalyzer', () => {
}); });
it('should return an object containing the classes that were analyzed', () => { it('should return an object containing the classes that were analyzed', () => {
const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !; const file1 = getSourceFileOrError(program, _('/test.js'));
const compiledFile1 = result.get(file1) !; const compiledFile1 = result.get(file1) !;
expect(compiledFile1.compiledClasses.length).toEqual(2); expect(compiledFile1.compiledClasses.length).toEqual(2);
expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({ expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({
@ -186,7 +179,7 @@ describe('DecorationAnalyzer', () => {
name: 'MyDirective', compilation: ['@Directive (compiled)'], name: 'MyDirective', compilation: ['@Directive (compiled)'],
} as unknown as CompiledClass)); } as unknown as CompiledClass));
const file2 = program.getSourceFile(TEST_PROGRAM[1].name) !; const file2 = getSourceFileOrError(program, _('/other.js'));
const compiledFile2 = result.get(file2) !; const compiledFile2 = result.get(file2) !;
expect(compiledFile2.compiledClasses.length).toEqual(1); expect(compiledFile2.compiledClasses.length).toEqual(1);
expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({ expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({
@ -197,7 +190,13 @@ describe('DecorationAnalyzer', () => {
it('should analyze, resolve and compile the classes that are detected', () => { it('should analyze, resolve and compile the classes that are detected', () => {
expect(logs).toEqual([ expect(logs).toEqual([
// Classes without decorators should also be detected. // Classes without decorators should also be detected.
'detect: InjectionToken (no decorators)', '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. // First detect and (potentially) analyze.
'detect: MyComponent@Component', 'detect: MyComponent@Component',
'analyze: MyComponent@Component', 'analyze: MyComponent@Component',
@ -219,13 +218,42 @@ describe('DecorationAnalyzer', () => {
}); });
describe('internal components', () => { describe('internal components', () => {
beforeEach(() => setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM)); beforeEach(() => {
const INTERNAL_COMPONENT_PROGRAM = [
{
name: _('/entrypoint.js'),
contents: `
import {Component, NgModule} from '@angular/core';
import {ImportedComponent} from './component';
// The problem of exposing the type of these internal components in the .d.ts typing files export class LocalComponent {}
// is not yet solved. 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,
}
];
setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM);
});
// The problem of exposing the type of these internal components in the .d.ts typing
// files is not yet solved.
it('should analyze an internally imported component, which is not publicly exported from the entry-point', it('should analyze an internally imported component, which is not publicly exported from the entry-point',
() => { () => {
const file = program.getSourceFile('component.js') !; const file = getSourceFileOrError(program, _('/component.js'));
const analysis = result.get(file) !; const analysis = result.get(file) !;
expect(analysis).toBeDefined(); expect(analysis).toBeDefined();
const ImportedComponent = const ImportedComponent =
@ -234,7 +262,7 @@ describe('DecorationAnalyzer', () => {
}); });
it('should analyze an internally defined component, which is not exported at all', () => { it('should analyze an internally defined component, which is not exported at all', () => {
const file = program.getSourceFile('entrypoint.js') !; const file = getSourceFileOrError(program, _('/entrypoint.js'));
const analysis = result.get(file) !; const analysis = result.get(file) !;
expect(analysis).toBeDefined(); expect(analysis).toBeDefined();
const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !; const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !;
@ -242,4 +270,5 @@ describe('DecorationAnalyzer', () => {
}); });
}); });
}); });
});
}); });

View File

@ -7,16 +7,32 @@
*/ */
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', () => {
describe('analyzeProgram()', () => {
let _: typeof absoluteFrom;
let analyses: ModuleWithProvidersAnalyses;
let program: ts.Program;
let dtsProgram: BundleProgram|null;
let referencesRegistry: NgccReferencesRegistry;
beforeEach(() => {
_ = absoluteFrom;
const TEST_PROGRAM: TestFile[] = [
{ {
name: '/src/entry-point.js', name: _('/src/entry-point.js'),
contents: ` contents: `
export * from './explicit'; export * from './explicit';
export * from './any'; export * from './any';
@ -26,7 +42,7 @@ const TEST_PROGRAM = [
` `
}, },
{ {
name: '/src/explicit.js', name: _('/src/explicit.js'),
contents: ` contents: `
import {ExternalModule} from './module'; import {ExternalModule} from './module';
import {LibraryModule} from 'some-library'; import {LibraryModule} from 'some-library';
@ -72,7 +88,7 @@ const TEST_PROGRAM = [
` `
}, },
{ {
name: '/src/any.js', name: _('/src/any.js'),
contents: ` contents: `
import {ExternalModule} from './module'; import {ExternalModule} from './module';
import {LibraryModule} from 'some-library'; import {LibraryModule} from 'some-library';
@ -118,7 +134,7 @@ const TEST_PROGRAM = [
` `
}, },
{ {
name: '/src/implicit.js', name: _('/src/implicit.js'),
contents: ` contents: `
import {ExternalModule} from './module'; import {ExternalModule} from './module';
import {LibraryModule} from 'some-library'; import {LibraryModule} from 'some-library';
@ -164,7 +180,7 @@ const TEST_PROGRAM = [
` `
}, },
{ {
name: '/src/no-providers.js', name: _('/src/no-providers.js'),
contents: ` contents: `
import {ExternalModule} from './module'; import {ExternalModule} from './module';
import {LibraryModule} from 'some-library'; import {LibraryModule} from 'some-library';
@ -199,19 +215,19 @@ const TEST_PROGRAM = [
` `
}, },
{ {
name: '/src/module.js', name: _('/src/module.js'),
contents: ` contents: `
export class ExternalModule {} export class 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 TEST_DTS_PROGRAM = [ const TEST_DTS_PROGRAM: TestFile[] = [
{ {
name: '/typings/entry-point.d.ts', name: _('/typings/entry-point.d.ts'),
contents: ` contents: `
export * from './explicit'; export * from './explicit';
export * from './any'; export * from './any';
@ -221,7 +237,7 @@ const TEST_DTS_PROGRAM = [
` `
}, },
{ {
name: '/typings/explicit.d.ts', name: _('/typings/explicit.d.ts'),
contents: ` contents: `
import {ModuleWithProviders} from './core'; import {ModuleWithProviders} from './core';
import {ExternalModule} from './module'; import {ExternalModule} from './module';
@ -238,7 +254,7 @@ const TEST_DTS_PROGRAM = [
` `
}, },
{ {
name: '/typings/any.d.ts', name: _('/typings/any.d.ts'),
contents: ` contents: `
import {ModuleWithProviders} from './core'; import {ModuleWithProviders} from './core';
export declare class AnyInternalModule {} export declare class AnyInternalModule {}
@ -253,7 +269,7 @@ const TEST_DTS_PROGRAM = [
` `
}, },
{ {
name: '/typings/implicit.d.ts', name: _('/typings/implicit.d.ts'),
contents: ` contents: `
import {ExternalModule} from './module'; import {ExternalModule} from './module';
import {LibraryModule} from 'some-library'; import {LibraryModule} from 'some-library';
@ -269,7 +285,7 @@ const TEST_DTS_PROGRAM = [
` `
}, },
{ {
name: '/typings/no-providers.d.ts', name: _('/typings/no-providers.d.ts'),
contents: ` contents: `
import {ModuleWithProviders} from './core'; import {ModuleWithProviders} from './core';
import {ExternalModule} from './module'; import {ExternalModule} from './module';
@ -287,13 +303,13 @@ const TEST_DTS_PROGRAM = [
` `
}, },
{ {
name: '/typings/module.d.ts', name: _('/typings/module.d.ts'),
contents: ` contents: `
export declare class ExternalModule {} export declare class ExternalModule {}
` `
}, },
{ {
name: '/typings/core.d.ts', name: _('/typings/core.d.ts'),
contents: ` contents: `
export declare interface Type<T> { export declare interface Type<T> {
@ -307,34 +323,31 @@ const TEST_DTS_PROGRAM = [
` `
}, },
{ {
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 {}'
}, },
]; ];
loadTestFiles(TEST_PROGRAM);
describe('ModuleWithProvidersAnalyzer', () => { loadTestFiles(TEST_DTS_PROGRAM);
describe('analyzeProgram()', () => { const bundle = makeTestEntryPointBundle(
let analyses: ModuleWithProvidersAnalyses; 'esm2015', 'esm2015', false, getRootFiles(TEST_PROGRAM),
let program: ts.Program; getRootFiles(TEST_DTS_PROGRAM));
let dtsProgram: BundleProgram; program = bundle.src.program;
let referencesRegistry: NgccReferencesRegistry; dtsProgram = bundle.dts;
const host = new Esm2015ReflectionHost(
beforeAll(() => { new MockLogger(), false, program.getTypeChecker(), dtsProgram);
program = makeTestProgram(...TEST_PROGRAM);
dtsProgram = makeTestBundleProgram(TEST_DTS_PROGRAM);
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dtsProgram);
referencesRegistry = new NgccReferencesRegistry(host); 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', it('should ignore declarations that already have explicit NgModule type params', () => {
() => { expect(getAnalysisDescription(analyses, '/typings/explicit.d.ts')).toEqual([]); }); expect(getAnalysisDescription(analyses, _('/typings/explicit.d.ts'))).toEqual([]);
});
it('should find declarations that use `any` for the NgModule type param', () => { it('should find declarations that use `any` for the NgModule type param', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/any.d.ts'); const anyAnalysis = getAnalysisDescription(analyses, _('/typings/any.d.ts'));
expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]); expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]); expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']); expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']);
@ -345,16 +358,17 @@ describe('ModuleWithProvidersAnalyzer', () => {
it('should track internal module references in the references registry', () => { it('should track internal module references in the references registry', () => {
const declarations = referencesRegistry.getDeclarationMap(); const declarations = referencesRegistry.getDeclarationMap();
const externalModuleDeclaration = const externalModuleDeclaration = getDeclaration(
getDeclaration(program, '/src/module.js', 'ExternalModule', ts.isClassDeclaration); program, absoluteFrom('/src/module.js'), 'ExternalModule', ts.isClassDeclaration);
const libraryModuleDeclaration = getDeclaration( const libraryModuleDeclaration = getDeclaration(
program, '/node_modules/some-library/index.d.ts', 'LibraryModule', ts.isClassDeclaration); program, absoluteFrom('/node_modules/some-library/index.d.ts'), 'LibraryModule',
ts.isClassDeclaration);
expect(declarations.has(externalModuleDeclaration.name !)).toBe(true); expect(declarations.has(externalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false); expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false);
}); });
it('should find declarations that have implicit return types', () => { it('should find declarations that have implicit return types', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/implicit.d.ts'); const anyAnalysis = getAnalysisDescription(analyses, _('/typings/implicit.d.ts'));
expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]); expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]); expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']); expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']);
@ -365,7 +379,7 @@ describe('ModuleWithProvidersAnalyzer', () => {
it('should find declarations that do not specify a `providers` property in the return type', it('should find declarations that do not specify a `providers` property in the return type',
() => { () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/no-providers.d.ts'); const anyAnalysis = getAnalysisDescription(analyses, _('/typings/no-providers.d.ts'));
expect(anyAnalysis).not.toContain([ expect(anyAnalysis).not.toContain([
'noProvExplicitInternalFunction', 'NoProvidersInternalModule' 'noProvExplicitInternalFunction', 'NoProvidersInternalModule'
]); ]);
@ -382,14 +396,17 @@ describe('ModuleWithProvidersAnalyzer', () => {
expect(anyAnalysis).toContain([ expect(anyAnalysis).toContain([
'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null 'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null
]); ]);
expect(anyAnalysis).toContain(['noProvImplicitExternalFunction', 'ExternalModule', null]); expect(anyAnalysis).toContain([
'noProvImplicitExternalFunction', 'ExternalModule', null
]);
expect(anyAnalysis).toContain([ expect(anyAnalysis).toContain([
'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library' 'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library'
]); ]);
}); });
function getAnalysisDescription(analyses: ModuleWithProvidersAnalyses, fileName: string) { function getAnalysisDescription(
const file = dtsProgram.program.getSourceFile(fileName) !; analyses: ModuleWithProvidersAnalyses, fileName: AbsoluteFsPath) {
const file = getSourceFileOrError(dtsProgram !.program, fileName);
const analysis = analyses.get(file); const analysis = analyses.get(file);
return analysis ? return analysis ?
analysis.map( analysis.map(
@ -400,4 +417,5 @@ describe('ModuleWithProvidersAnalyzer', () => {
[]; [];
} }
}); });
});
}); });

View File

@ -5,25 +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 {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('PrivateDeclarationsAnalyzer', () => {
describe('analyzeProgram()', () => { describe('analyzeProgram()', () => {
it('should find all NgModule declarations that were not publicly exported from the entry-point',
() => {
const _ = absoluteFrom;
const TEST_PROGRAM = [ const TEST_PROGRAM: TestFile[] = [
{ {
name: '/src/entry_point.js', name: _('/src/entry_point.js'),
isRoot: true, isRoot: true,
contents: ` contents: `
export {PublicComponent} from './a'; export {PublicComponent} from './a';
@ -32,7 +36,7 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
{ {
name: '/src/a.js', name: _('/src/a.js'),
isRoot: false, isRoot: false,
contents: ` contents: `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@ -43,7 +47,7 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
{ {
name: '/src/b.js', name: _('/src/b.js'),
isRoot: false, isRoot: false,
contents: ` contents: `
import {Component, NgModule} from '@angular/core'; import {Component, NgModule} from '@angular/core';
@ -62,7 +66,7 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
{ {
name: '/src/c.js', name: _('/src/c.js'),
isRoot: false, isRoot: false,
contents: ` contents: `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@ -77,7 +81,7 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
{ {
name: '/src/mod.js', name: _('/src/mod.js'),
isRoot: false, isRoot: false,
contents: ` contents: `
import {Component, NgModule} from '@angular/core'; import {Component, NgModule} from '@angular/core';
@ -96,7 +100,7 @@ describe('PrivateDeclarationsAnalyzer', () => {
]; ];
const TEST_DTS_PROGRAM = [ const TEST_DTS_PROGRAM = [
{ {
name: '/typings/entry_point.d.ts', name: _('/typings/entry_point.d.ts'),
isRoot: true, isRoot: true,
contents: ` contents: `
export {PublicComponent} from './a'; export {PublicComponent} from './a';
@ -105,28 +109,28 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
{ {
name: '/typings/a.d.ts', name: _('/typings/a.d.ts'),
isRoot: false, isRoot: false,
contents: ` contents: `
export declare class PublicComponent {} export declare class PublicComponent {}
` `
}, },
{ {
name: '/typings/b.d.ts', name: _('/typings/b.d.ts'),
isRoot: false, isRoot: false,
contents: ` contents: `
export declare class ModuleB {} export declare class ModuleB {}
` `
}, },
{ {
name: '/typings/c.d.ts', name: _('/typings/c.d.ts'),
isRoot: false, isRoot: false,
contents: ` contents: `
export declare class InternalComponent1 {} export declare class InternalComponent1 {}
` `
}, },
{ {
name: '/typings/mod.d.ts', name: _('/typings/mod.d.ts'),
isRoot: false, isRoot: false,
contents: ` contents: `
import {PublicComponent} from './a'; import {PublicComponent} from './a';
@ -136,14 +140,13 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
]; ];
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); const {program, referencesRegistry, analyzer} = setup(TEST_PROGRAM, TEST_DTS_PROGRAM);
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'PublicComponent'); addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'PublicComponent');
addToReferencesRegistry(program, referencesRegistry, '/src/b.js', 'PrivateComponent1'); addToReferencesRegistry(
addToReferencesRegistry(program, referencesRegistry, '/src/c.js', 'InternalComponent1'); program, referencesRegistry, _('/src/b.js'), 'PrivateComponent1');
addToReferencesRegistry(
program, referencesRegistry, _('/src/c.js'), 'InternalComponent1');
const analyses = analyzer.analyzeProgram(program); const analyses = analyzer.analyzeProgram(program);
// Note that `PrivateComponent2` and `InternalComponent2` are not found because they are // Note that `PrivateComponent2` and `InternalComponent2` are not found because they are
@ -160,9 +163,11 @@ describe('PrivateDeclarationsAnalyzer', () => {
]); ]);
}); });
it('should find all non-public declarations that were aliased', () => {
const _ = absoluteFrom;
const ALIASED_EXPORTS_PROGRAM = [ const ALIASED_EXPORTS_PROGRAM = [
{ {
name: '/src/entry_point.js', name: _('/src/entry_point.js'),
isRoot: true, isRoot: true,
contents: ` contents: `
// This component is only exported as an alias. // This component is only exported as an alias.
@ -172,7 +177,7 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
{ {
name: '/src/a.js', name: _('/src/a.js'),
isRoot: false, isRoot: false,
contents: ` contents: `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@ -190,7 +195,7 @@ describe('PrivateDeclarationsAnalyzer', () => {
]; ];
const ALIASED_EXPORTS_DTS_PROGRAM = [ const ALIASED_EXPORTS_DTS_PROGRAM = [
{ {
name: '/typings/entry_point.d.ts', name: _('/typings/entry_point.d.ts'),
isRoot: true, isRoot: true,
contents: ` contents: `
export declare class aliasedComponentOne {} export declare class aliasedComponentOne {}
@ -199,13 +204,11 @@ describe('PrivateDeclarationsAnalyzer', () => {
` `
}, },
]; ];
it('should find all non-public declarations that were aliased', () => {
const {program, referencesRegistry, analyzer} = const {program, referencesRegistry, analyzer} =
setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM); setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM);
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentOne'); addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'ComponentOne');
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentTwo'); addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'ComponentTwo');
const analyses = analyzer.analyzeProgram(program); const analyses = analyzer.analyzeProgram(program);
expect(analyses).toEqual([{ expect(analyses).toEqual([{
@ -216,30 +219,28 @@ describe('PrivateDeclarationsAnalyzer', () => {
}]); }]);
}); });
}); });
}); });
type Files = { function setup(jsProgram: TestFile[], dtsProgram: TestFile[]) {
name: string, loadTestFiles(jsProgram);
contents: string, isRoot?: boolean | undefined loadTestFiles(dtsProgram);
}[]; const {src: {program}, dts} = makeTestEntryPointBundle(
'esm2015', 'esm2015', false, getRootFiles(jsProgram), getRootFiles(dtsProgram));
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 host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry); const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry);
return {program, referencesRegistry, analyzer}; return {program, referencesRegistry, analyzer};
} }
/** /**
* Add up the named component to the references registry. * Add up the named component to the references registry.
* *
* This would normally be done by the decoration handlers in the `DecorationAnalyzer`. * This would normally be done by the decoration handlers in the `DecorationAnalyzer`.
*/ */
function addToReferencesRegistry( function addToReferencesRegistry(
program: ts.Program, registry: NgccReferencesRegistry, fileName: string, program: ts.Program, registry: NgccReferencesRegistry, fileName: AbsoluteFsPath,
componentName: string) { componentName: string) {
const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration); const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration);
registry.add(null !, new Reference(declaration)); registry.add(null !, new Reference(declaration));
} }
});

View File

@ -5,19 +5,24 @@
* 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(() => {
describe('NgccReferencesRegistry', () => {
it('should return a mapping from resolved reference identifiers to their declarations', () => { it('should return a mapping from resolved reference identifiers to their declarations', () => {
const {program, options, host} = makeProgram([{ const _ = absoluteFrom;
name: 'index.ts', const TEST_FILES: TestFile[] = [{
name: _('/index.ts'),
contents: ` contents: `
export class SomeClass {} export class SomeClass {}
export function someFunction() {} export function someFunction() {}
@ -25,25 +30,27 @@ describe('NgccReferencesRegistry', () => {
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 indexPath = _('/index.ts');
const testArrayDeclaration = const testArrayDeclaration =
getDeclaration(program, 'index.ts', 'testArray', ts.isVariableDeclaration); getDeclaration(program, indexPath, 'testArray', ts.isVariableDeclaration);
const someClassDecl = getDeclaration(program, 'index.ts', 'SomeClass', ts.isClassDeclaration); const someClassDecl = getDeclaration(program, indexPath, 'SomeClass', ts.isClassDeclaration);
const someFunctionDecl = const someFunctionDecl =
getDeclaration(program, 'index.ts', 'someFunction', ts.isFunctionDeclaration); getDeclaration(program, indexPath, 'someFunction', ts.isFunctionDeclaration);
const someVariableDecl = const someVariableDecl =
getDeclaration(program, 'index.ts', 'someVariable', ts.isVariableDeclaration); getDeclaration(program, indexPath, 'someVariable', ts.isVariableDeclaration);
const testArrayExpression = testArrayDeclaration.initializer !; 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();
@ -52,4 +59,9 @@ describe('NgccReferencesRegistry', () => {
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,55 +5,60 @@
* 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', () => {
describe('analyzeProgram()', () => {
it('should check for switchable markers in all the files of the program', () => {
const _ = absoluteFrom;
const TEST_PROGRAM: TestFile[] = [
{ {
name: 'entrypoint.js', name: _('/entrypoint.js'),
contents: ` contents: `
import {a} from './a'; import {a} from './a';
import {b} from './b'; import {b} from './b';
` `
}, },
{ {
name: 'a.js', name: _('/a.js'),
contents: ` contents: `
import {c} from './c'; import {c} from './c';
export const a = 1; export const a = 1;
` `
}, },
{ {
name: 'b.js', name: _('/b.js'),
contents: ` contents: `
export const b = 42; export const b = 42;
var factoryB = factory__PRE_R3__; var factoryB = factory__PRE_R3__;
` `
}, },
{ {
name: 'c.js', name: _('/c.js'),
contents: ` contents: `
export const c = 'So long, and thanks for all the fish!'; export const c = 'So long, and thanks for all the fish!';
var factoryC = factory__PRE_R3__; var factoryC = factory__PRE_R3__;
var factoryD = factory__PRE_R3__; var factoryD = factory__PRE_R3__;
` `
}, },
]; ];
loadTestFiles(TEST_PROGRAM);
describe('SwitchMarkerAnalyzer', () => { const {program} = makeTestBundleProgram(getRootFiles(TEST_PROGRAM)[0]);
describe('analyzeProgram()', () => {
it('should check for switchable markers in all the files of the program', () => {
const program = makeTestProgram(...TEST_PROGRAM);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const analyzer = new SwitchMarkerAnalyzer(host); const analyzer = new SwitchMarkerAnalyzer(host);
const analysis = analyzer.analyzeProgram(program); const analysis = analyzer.analyzeProgram(program);
const entrypoint = program.getSourceFile('entrypoint.js') !; const entrypoint = getSourceFileOrError(program, _('/entrypoint.js'));
const a = program.getSourceFile('a.js') !; const a = getSourceFileOrError(program, _('/a.js'));
const b = program.getSourceFile('b.js') !; const b = getSourceFileOrError(program, _('/b.js'));
const c = program.getSourceFile('c.js') !; const c = getSourceFileOrError(program, _('/c.js'));
expect(analysis.size).toEqual(2); expect(analysis.size).toEqual(2);
expect(analysis.has(entrypoint)).toBe(false); expect(analysis.has(entrypoint)).toBe(false);
@ -72,4 +77,5 @@ describe('SwitchMarkerAnalyzer', () => {
]); ]);
}); });
}); });
});
}); });

View File

@ -6,18 +6,110 @@
* 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', () => {
describe('CommonJsDependencyHost', () => { let _: typeof absoluteFrom;
let host: CommonJsDependencyHost; let host: CommonJsDependencyHost;
beforeEach(() => { beforeEach(() => {
const fs = createMockFileSystem(); _ = absoluteFrom;
loadTestFiles([
{
name: _('/no/imports/or/re-exports/index.js'),
contents: '// some text but no import-like statements'
},
{name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/external/imports/index.js'), contents: commonJs(['lib_1', 'lib_1/sub_1'])},
{name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/imports/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/external/re-exports/index.js'),
contents: commonJs(['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y'])
},
{name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/external/imports-missing/index.js'), contents: commonJs(['lib_1', 'missing'])},
{name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/external/deep-import/index.js'), contents: commonJs(['lib_1/deep/import'])},
{name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/deep-import/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/internal/outer/index.js'), contents: commonJs(['../inner'])},
{name: _('/internal/outer/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/internal/inner/index.js'), contents: commonJs(['lib_1/sub_1'], ['X'])},
{
name: _('/internal/circular_a/index.js'),
contents: commonJs(['../circular_b', 'lib_1/sub_1'], ['Y'])
},
{
name: _('/internal/circular_b/index.js'),
contents: commonJs(['../circular_a', 'lib_1'], ['X'])
},
{name: _('/internal/circular_a/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/internal/circular_a/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/re-directed/index.js'), contents: commonJs(['lib_1/sub_2'])},
{name: _('/re-directed/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/re-directed/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/path-alias/index.js'),
contents: commonJs(['@app/components', '@app/shared', '@lib/shared/test', 'lib_1'])
},
{name: _('/path-alias/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/path-alias/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/node_modules/lib_1/index.d.ts'), contents: 'export declare class X {}'},
{
name: _('/node_modules/lib_1/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/node_modules/lib_1/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/node_modules/lib_1/deep/import/index.js'),
contents: 'export class DeepImport {}'
},
{name: _('/node_modules/lib_1/sub_1/index.d.ts'), contents: 'export declare class Y {}'},
{
name: _('/node_modules/lib_1/sub_1/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/node_modules/lib_1/sub_1/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/node_modules/lib_1/sub_2.d.ts'), contents: `export * from './sub_2/sub_2';`},
{name: _('/node_modules/lib_1/sub_2/sub_2.d.ts'), contents: `export declare class Z {}';`},
{
name: _('/node_modules/lib_1/sub_2/package.json'),
contents: '{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}'
},
{name: _('/node_modules/lib_1/sub_2/sub_2.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/dist/components/index.d.ts'), contents: `export declare class MyComponent {};`},
{
name: _('/dist/components/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/components/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/dist/shared/index.d.ts'),
contents: `import {X} from 'lib_1';\nexport declare class Service {}`
},
{
name: _('/dist/shared/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/shared/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/dist/lib/shared/test/index.d.ts'), contents: `export class TestHelper {}`},
{
name: _('/dist/lib/shared/test/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/lib/shared/test/index.metadata.json'), contents: 'MOCK METADATA'},
]);
const fs = getFileSystem();
host = new CommonJsDependencyHost(fs, new ModuleResolver(fs)); host = new CommonJsDependencyHost(fs, new ModuleResolver(fs));
}); });
@ -55,14 +147,14 @@ describe('CommonJsDependencyHost', () => {
expect(dependencies.size).toBe(1); expect(dependencies.size).toBe(1);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(missing.size).toBe(1); expect(missing.size).toBe(1);
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); expect(missing.has(relativeFrom('missing'))).toBe(true);
expect(deepImports.size).toBe(0); expect(deepImports.size).toBe(0);
}); });
it('should not register deep imports as missing', () => { it('should not register deep imports as missing', () => {
// This scenario verifies the behavior of the dependency analysis when an external import // 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. // is found that does not map to an entry-point but still exists on disk, i.e. a deep
// Such deep imports are captured for diagnostics purposes. // import. Such deep imports are captured for diagnostics purposes.
const {dependencies, missing, deepImports} = const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/deep-import/index.js')); host.findDependencies(_('/external/deep-import/index.js'));
@ -93,7 +185,7 @@ describe('CommonJsDependencyHost', () => {
}); });
it('should support `paths` alias mappings when resolving modules', () => { it('should support `paths` alias mappings when resolving modules', () => {
const fs = createMockFileSystem(); const fs = getFileSystem();
host = new CommonJsDependencyHost(fs, new ModuleResolver(fs, { host = new CommonJsDependencyHost(fs, new ModuleResolver(fs, {
baseUrl: '/dist', baseUrl: '/dist',
paths: { paths: {
@ -101,7 +193,8 @@ describe('CommonJsDependencyHost', () => {
'@lib/*/test': ['lib/*/test'], '@lib/*/test': ['lib/*/test'],
} }
})); }));
const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); const {dependencies, missing, deepImports} =
host.findDependencies(_('/path-alias/index.js'));
expect(dependencies.size).toBe(4); expect(dependencies.size).toBe(4);
expect(dependencies.has(_('/dist/components'))).toBe(true); expect(dependencies.has(_('/dist/components'))).toBe(true);
expect(dependencies.has(_('/dist/shared'))).toBe(true); expect(dependencies.has(_('/dist/shared'))).toBe(true);
@ -111,67 +204,9 @@ describe('CommonJsDependencyHost', () => {
expect(deepImports.size).toBe(0); expect(deepImports.size).toBe(0);
}); });
}); });
function createMockFileSystem() {
return new MockFileSystem({
'/no/imports/or/re-exports/index.js': '// some text but no import-like statements',
'/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}',
'/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA',
'/external/imports/index.js': commonJs(['lib_1', 'lib_1/sub_1']),
'/external/imports/package.json': '{"esm2015": "./index.js"}',
'/external/imports/index.metadata.json': 'MOCK METADATA',
'/external/re-exports/index.js':
commonJs(['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']),
'/external/re-exports/package.json': '{"esm2015": "./index.js"}',
'/external/re-exports/index.metadata.json': 'MOCK METADATA',
'/external/imports-missing/index.js': commonJs(['lib_1', 'missing']),
'/external/imports-missing/package.json': '{"esm2015": "./index.js"}',
'/external/imports-missing/index.metadata.json': 'MOCK METADATA',
'/external/deep-import/index.js': commonJs(['lib_1/deep/import']),
'/external/deep-import/package.json': '{"esm2015": "./index.js"}',
'/external/deep-import/index.metadata.json': 'MOCK METADATA',
'/internal/outer/index.js': commonJs(['../inner']),
'/internal/outer/package.json': '{"esm2015": "./index.js"}',
'/internal/outer/index.metadata.json': 'MOCK METADATA',
'/internal/inner/index.js': commonJs(['lib_1/sub_1'], ['X']),
'/internal/circular_a/index.js': commonJs(['../circular_b', 'lib_1/sub_1'], ['Y']),
'/internal/circular_b/index.js': commonJs(['../circular_a', 'lib_1'], ['X']),
'/internal/circular_a/package.json': '{"esm2015": "./index.js"}',
'/internal/circular_a/index.metadata.json': 'MOCK METADATA',
'/re-directed/index.js': commonJs(['lib_1/sub_2']),
'/re-directed/package.json': '{"esm2015": "./index.js"}',
'/re-directed/index.metadata.json': 'MOCK METADATA',
'/path-alias/index.js':
commonJs(['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']),
'/path-alias/package.json': '{"esm2015": "./index.js"}',
'/path-alias/index.metadata.json': 'MOCK METADATA',
'/node_modules/lib_1/index.d.ts': 'export declare class X {}',
'/node_modules/lib_1/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/node_modules/lib_1/index.metadata.json': 'MOCK METADATA',
'/node_modules/lib_1/deep/import/index.js': 'export class DeepImport {}',
'/node_modules/lib_1/sub_1/index.d.ts': 'export declare class Y {}',
'/node_modules/lib_1/sub_1/package.json':
'{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/node_modules/lib_1/sub_1/index.metadata.json': 'MOCK METADATA',
'/node_modules/lib_1/sub_2.d.ts': `export * from './sub_2/sub_2';`,
'/node_modules/lib_1/sub_2/sub_2.d.ts': `export declare class Z {}';`,
'/node_modules/lib_1/sub_2/package.json':
'{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}',
'/node_modules/lib_1/sub_2/sub_2.metadata.json': 'MOCK METADATA',
'/dist/components/index.d.ts': `export declare class MyComponent {};`,
'/dist/components/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/dist/components/index.metadata.json': 'MOCK METADATA',
'/dist/shared/index.d.ts': `import {X} from 'lib_1';\nexport declare class Service {}`,
'/dist/shared/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/dist/shared/index.metadata.json': 'MOCK METADATA',
'/dist/lib/shared/test/index.d.ts': `export class TestHelper {}`,
'/dist/lib/shared/test/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA',
}); });
}
});
function commonJs(importPaths: string[], exportNames: string[] = []) { function commonJs(importPaths: string[], exportNames: string[] = []) {
const commonJsRequires = const commonJsRequires =
importPaths importPaths
.map( .map(
@ -182,4 +217,5 @@ function commonJs(importPaths: string[], exportNames: string[] = []) {
exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n'); exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
return `${commonJsRequires} return `${commonJsRequires}
${exportStatements}`; ${exportStatements}`;
} }
});

View File

@ -5,62 +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 {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(() => {
describe('DependencyResolver', () => {
let _: typeof absoluteFrom;
let host: EsmDependencyHost; let host: EsmDependencyHost;
let resolver: DependencyResolver; let resolver: DependencyResolver;
let fs: FileSystem; let fs: FileSystem;
let moduleResolver: ModuleResolver; let moduleResolver: ModuleResolver;
beforeEach(() => { beforeEach(() => {
fs = new MockFileSystem(); _ = absoluteFrom;
fs = getFileSystem();
moduleResolver = new ModuleResolver(fs); moduleResolver = new ModuleResolver(fs);
host = new EsmDependencyHost(fs, moduleResolver); host = new EsmDependencyHost(fs, moduleResolver);
resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host}); resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host});
}); });
describe('sortEntryPointsByDependency()', () => { describe('sortEntryPointsByDependency()', () => {
const first = { let first: EntryPoint;
let second: EntryPoint;
let third: EntryPoint;
let fourth: EntryPoint;
let fifth: EntryPoint;
let dependencies: DepMap;
beforeEach(() => {
first = {
path: _('/first'), path: _('/first'),
packageJson: {esm5: './index.js'}, packageJson: {esm5: './index.js'},
compiledByAngular: true compiledByAngular: true
} as EntryPoint; } as EntryPoint;
const second = { second = {
path: _('/second'), path: _('/second'),
packageJson: {esm2015: './sub/index.js'}, packageJson: {esm2015: './sub/index.js'},
compiledByAngular: true compiledByAngular: true
} as EntryPoint; } as EntryPoint;
const third = { third = {
path: _('/third'), path: _('/third'),
packageJson: {fesm5: './index.js'}, packageJson: {fesm5: './index.js'},
compiledByAngular: true compiledByAngular: true
} as EntryPoint; } as EntryPoint;
const fourth = { fourth = {
path: _('/fourth'), path: _('/fourth'),
packageJson: {fesm2015: './sub2/index.js'}, packageJson: {fesm2015: './sub2/index.js'},
compiledByAngular: true compiledByAngular: true
} as EntryPoint; } as EntryPoint;
const fifth = { fifth = {
path: _('/fifth'), path: _('/fifth'),
packageJson: {module: './index.js'}, packageJson: {module: './index.js'},
compiledByAngular: true compiledByAngular: true
} as EntryPoint; } as EntryPoint;
const dependencies = { dependencies = {
[_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []}, [_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []},
[_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []}, [_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []},
[_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []}, [_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []},
[_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []}, [_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []},
[_('/fifth/index.js')]: {resolved: [], missing: []}, [_('/fifth/index.js')]: {resolved: [], missing: []},
}; };
});
it('should order the entry points by their dependency on each other', () => { it('should order the entry points by their dependency on each other', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
@ -154,29 +169,33 @@ describe('DependencyResolver', () => {
const esm2015Host = new EsmDependencyHost(fs, moduleResolver); const esm2015Host = new EsmDependencyHost(fs, moduleResolver);
resolver = resolver =
new DependencyResolver(fs, new MockLogger(), {esm5: esm5Host, esm2015: esm2015Host}); new DependencyResolver(fs, new MockLogger(), {esm5: esm5Host, esm2015: esm2015Host});
spyOn(esm5Host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); spyOn(esm5Host, 'findDependencies')
.and.callFake(createFakeComputeDependencies(dependencies));
spyOn(esm2015Host, 'findDependencies') spyOn(esm2015Host, 'findDependencies')
.and.callFake(createFakeComputeDependencies(dependencies)); .and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]); expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${first.path}/index.js`); expect(esm5Host.findDependencies).toHaveBeenCalledWith(fs.resolve(first.path, 'index.js'));
expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${second.path}/sub/index.js`); expect(esm5Host.findDependencies)
expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${third.path}/index.js`); .not.toHaveBeenCalledWith(fs.resolve(second.path, 'sub/index.js'));
expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`); expect(esm5Host.findDependencies).toHaveBeenCalledWith(fs.resolve(third.path, 'index.js'));
expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${fifth.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(`${first.path}/index.js`); expect(esm2015Host.findDependencies)
expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${second.path}/sub/index.js`); .not.toHaveBeenCalledWith(fs.resolve(first.path, 'index.js'));
expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${third.path}/index.js`); expect(esm2015Host.findDependencies)
expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`); .toHaveBeenCalledWith(fs.resolve(second.path, 'sub/index.js'));
expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${fifth.path}/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'));
}); });
interface DepMap {
[path: string]: {resolved: string[], missing: string[]};
}
function createFakeComputeDependencies(deps: DepMap) { function createFakeComputeDependencies(deps: DepMap) {
return (entryPoint: string) => { return (entryPoint: string) => {
const dependencies = new Set(); const dependencies = new Set();
@ -188,4 +207,5 @@ describe('DependencyResolver', () => {
}; };
} }
}); });
});
}); });

View File

@ -7,17 +7,21 @@
*/ */
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 _: typeof absoluteFrom;
let host: EsmDependencyHost; let host: EsmDependencyHost;
beforeEach(() => { beforeEach(() => {
const fs = createMockFileSystem(); _ = absoluteFrom;
setupMockFileSystem();
const fs = getFileSystem();
host = new EsmDependencyHost(fs, new ModuleResolver(fs)); host = new EsmDependencyHost(fs, new ModuleResolver(fs));
}); });
@ -56,14 +60,14 @@ describe('EsmDependencyHost', () => {
expect(dependencies.size).toBe(1); expect(dependencies.size).toBe(1);
expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true);
expect(missing.size).toBe(1); expect(missing.size).toBe(1);
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); expect(missing.has(relativeFrom('missing'))).toBe(true);
expect(deepImports.size).toBe(0); expect(deepImports.size).toBe(0);
}); });
it('should not register deep imports as missing', () => { it('should not register deep imports as missing', () => {
// This scenario verifies the behavior of the dependency analysis when an external import // 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. // is found that does not map to an entry-point but still exists on disk, i.e. a deep
// Such deep imports are captured for diagnostics purposes. // import. Such deep imports are captured for diagnostics purposes.
const {dependencies, missing, deepImports} = const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/deep-import/index.js')); host.findDependencies(_('/external/deep-import/index.js'));
@ -94,7 +98,7 @@ describe('EsmDependencyHost', () => {
}); });
it('should support `paths` alias mappings when resolving modules', () => { it('should support `paths` alias mappings when resolving modules', () => {
const fs = createMockFileSystem(); const fs = getFileSystem();
host = new EsmDependencyHost(fs, new ModuleResolver(fs, { host = new EsmDependencyHost(fs, new ModuleResolver(fs, {
baseUrl: '/dist', baseUrl: '/dist',
paths: { paths: {
@ -102,7 +106,8 @@ describe('EsmDependencyHost', () => {
'@lib/*/test': ['lib/*/test'], '@lib/*/test': ['lib/*/test'],
} }
})); }));
const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); const {dependencies, missing, deepImports} =
host.findDependencies(_('/path-alias/index.js'));
expect(dependencies.size).toBe(4); expect(dependencies.size).toBe(4);
expect(dependencies.has(_('/dist/components'))).toBe(true); expect(dependencies.has(_('/dist/components'))).toBe(true);
expect(dependencies.has(_('/dist/shared'))).toBe(true); expect(dependencies.has(_('/dist/shared'))).toBe(true);
@ -113,61 +118,94 @@ describe('EsmDependencyHost', () => {
}); });
}); });
function createMockFileSystem() { function setupMockFileSystem(): void {
return new MockFileSystem({ loadTestFiles([
'/no/imports/or/re-exports/index.js': '// some text but no import-like statements', {
'/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', name: _('/no/imports/or/re-exports/index.js'),
'/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', contents: '// some text but no import-like statements'
'/external/imports/index.js': `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`, },
'/external/imports/package.json': '{"esm2015": "./index.js"}', {name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
'/external/imports/index.metadata.json': 'MOCK METADATA', {name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
'/external/re-exports/index.js': `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`, {
'/external/re-exports/package.json': '{"esm2015": "./index.js"}', name: _('/external/imports/index.js'),
'/external/re-exports/index.metadata.json': 'MOCK METADATA', contents: `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`
'/external/imports-missing/index.js': `import {X} from 'lib-1';\nimport {Y} from 'missing';`, },
'/external/imports-missing/package.json': '{"esm2015": "./index.js"}', {name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'},
'/external/imports-missing/index.metadata.json': 'MOCK METADATA', {name: _('/external/imports/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: _('/external/re-exports/index.js'),
'/external/deep-import/index.metadata.json': 'MOCK METADATA', contents: `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`
'/internal/outer/index.js': `import {X} from '../inner';`, },
'/internal/outer/package.json': '{"esm2015": "./index.js"}', {name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
'/internal/outer/index.metadata.json': 'MOCK METADATA', {name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
'/internal/inner/index.js': `import {Y} from 'lib-1/sub-1'; export declare class X {}`, {
'/internal/circular-a/index.js': name: _('/external/imports-missing/index.js'),
`import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`, contents: `import {X} from 'lib-1';\nimport {Y} from 'missing';`
'/internal/circular-b/index.js': },
`import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';`, {name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'},
'/internal/circular-a/package.json': '{"esm2015": "./index.js"}', {name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'},
'/internal/circular-a/index.metadata.json': 'MOCK METADATA', {
'/re-directed/index.js': `import {Z} from 'lib-1/sub-2';`, name: _('/external/deep-import/index.js'),
'/re-directed/package.json': '{"esm2015": "./index.js"}', contents: `import {Y} from 'lib-1/deep/import';`
'/re-directed/index.metadata.json': 'MOCK METADATA', },
'/path-alias/index.js': {name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'},
`import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';`, {name: _('/external/deep-import/index.metadata.json'), contents: 'MOCK METADATA'},
'/path-alias/package.json': '{"esm2015": "./index.js"}', {name: _('/internal/outer/index.js'), contents: `import {X} from '../inner';`},
'/path-alias/index.metadata.json': 'MOCK METADATA', {name: _('/internal/outer/package.json'), contents: '{"esm2015": "./index.js"}'},
'/node_modules/lib-1/index.js': 'export declare class X {}', {name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'},
'/node_modules/lib-1/package.json': '{"esm2015": "./index.js"}', {
'/node_modules/lib-1/index.metadata.json': 'MOCK METADATA', name: _('/internal/inner/index.js'),
'/node_modules/lib-1/deep/import/index.js': 'export declare class DeepImport {}', contents: `import {Y} from 'lib-1/sub-1'; export declare class X {}`
'/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', name: _('/internal/circular-a/index.js'),
'/node_modules/lib-1/sub-2.js': `export * from './sub-2/sub-2';`, contents:
'/node_modules/lib-1/sub-2/sub-2.js': `export declare class Z {}';`, `import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`
'/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 {};`, name: _('/internal/circular-b/index.js'),
'/dist/components/package.json': '{"esm2015": "./index.js"}', contents:
'/dist/components/index.metadata.json': 'MOCK METADATA', `import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';`
'/dist/shared/index.js': `import {X} from 'lib-1';\nexport class Service {}`, },
'/dist/shared/package.json': '{"esm2015": "./index.js"}', {name: _('/internal/circular-a/package.json'), contents: '{"esm2015": "./index.js"}'},
'/dist/shared/index.metadata.json': 'MOCK METADATA', {name: _('/internal/circular-a/index.metadata.json'), contents: 'MOCK METADATA'},
'/dist/lib/shared/test/index.js': `export class TestHelper {}`, {name: _('/re-directed/index.js'), contents: `import {Z} from 'lib-1/sub-2';`},
'/dist/lib/shared/test/package.json': '{"esm2015": "./index.js"}', {name: _('/re-directed/package.json'), contents: '{"esm2015": "./index.js"}'},
'/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', {name: _('/re-directed/index.metadata.json'), contents: 'MOCK METADATA'},
}); {
name: _('/path-alias/index.js'),
contents:
`import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';`
},
{name: _('/path-alias/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/path-alias/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/node_modules/lib-1/index.js'), contents: 'export declare class X {}'},
{name: _('/node_modules/lib-1/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/node_modules/lib-1/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/node_modules/lib-1/deep/import/index.js'),
contents: 'export declare class DeepImport {}'
},
{name: _('/node_modules/lib-1/sub-1/index.js'), contents: 'export declare class Y {}'},
{name: _('/node_modules/lib-1/sub-1/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/node_modules/lib-1/sub-1/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/node_modules/lib-1/sub-2.js'), contents: `export * from './sub-2/sub-2';`},
{name: _('/node_modules/lib-1/sub-2/sub-2.js'), contents: `export declare class Z {}';`},
{name: _('/node_modules/lib-1/sub-2/package.json'), contents: '{"esm2015": "./sub-2.js"}'},
{name: _('/node_modules/lib-1/sub-2/sub-2.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/dist/components/index.js'), contents: `class MyComponent {};`},
{name: _('/dist/components/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/dist/components/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/dist/shared/index.js'),
contents: `import {X} from 'lib-1';\nexport class Service {}`
},
{name: _('/dist/shared/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/dist/shared/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/dist/lib/shared/test/index.js'), contents: `export class TestHelper {}`},
{name: _('/dist/lib/shared/test/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/dist/lib/shared/test/index.metadata.json'), contents: 'MOCK METADATA'},
]);
} }
describe('isStringImportOrReexport', () => { describe('isStringImportOrReexport', () => {
@ -181,7 +219,8 @@ describe('EsmDependencyHost', () => {
it('should return true if the statement is a re-export', () => { it('should return true if the statement is a re-export', () => {
expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";'))) expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";')))
.toBe(true); .toBe(true);
expect(host.isStringImportOrReexport(createStatement('export * 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', () => { it('should return false if the statement is not an import or a re-export', () => {
@ -202,15 +241,15 @@ describe('EsmDependencyHost', () => {
it('should return true if there is an import statement', () => { it('should return true if there is an import statement', () => {
expect(host.hasImportOrReexportStatements('import {X} from "some/x";')).toBe(true); expect(host.hasImportOrReexportStatements('import {X} from "some/x";')).toBe(true);
expect(host.hasImportOrReexportStatements('import * as X from "some/x";')).toBe(true); expect(host.hasImportOrReexportStatements('import * as X from "some/x";')).toBe(true);
expect( expect(host.hasImportOrReexportStatements(
host.hasImportOrReexportStatements('blah blah\n\n import {X} from "some/x";\nblah blah')) '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); expect(host.hasImportOrReexportStatements('\t\timport {X} from "some/x";')).toBe(true);
}); });
it('should return true if there is a re-export statement', () => { it('should return true if there is a re-export statement', () => {
expect(host.hasImportOrReexportStatements('export {X} from "some/x";')).toBe(true); expect(host.hasImportOrReexportStatements('export {X} from "some/x";')).toBe(true);
expect( expect(host.hasImportOrReexportStatements(
host.hasImportOrReexportStatements('blah blah\n\n export {X} from "some/x";\nblah blah')) 'blah blah\n\n export {X} from "some/x";\nblah blah'))
.toBe(true); .toBe(true);
expect(host.hasImportOrReexportStatements('\t\texport {X} from "some/x";')).toBe(true); expect(host.hasImportOrReexportStatements('\t\texport {X} from "some/x";')).toBe(true);
expect(host.hasImportOrReexportStatements( expect(host.hasImportOrReexportStatements(
@ -225,4 +264,5 @@ describe('EsmDependencyHost', () => {
.toBe(false); .toBe(false);
}); });
}); });
});
}); });

View File

@ -5,72 +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 {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',
}, },
{
name: _('/libs/node_modules/package-2/package.json'),
contents: 'PACKAGE.JSON for package-2'
}, },
{
name: _('/libs/node_modules/package-2/node_modules/package-3/package.json'),
contents: 'PACKAGE.JSON for package-3'
}, },
'node_modules': { {name: _('/dist/package-4/x.js'), contents: `export class X {}`},
'package-2': { {name: _('/dist/package-4/package.json'), contents: 'PACKAGE.JSON for package-4'},
'package.json': 'PACKAGE.JSON for package-2', {
'node_modules': { name: _('/dist/package-4/sub-folder/index.js'),
'package-3': { contents: `import {X} from '@shared/package-4/x';`
'package.json': 'PACKAGE.JSON for package-3',
}, },
{
name: _('/dist/sub-folder/package-4/package.json'),
contents: 'PACKAGE.JSON for package-4'
}, },
{
name: _('/dist/sub-folder/package-5/package.json'),
contents: 'PACKAGE.JSON for package-5'
}, },
{
name: _('/dist/sub-folder/package-5/post-fix/package.json'),
contents: 'PACKAGE.JSON for package-5/post-fix'
}, },
{
name: _('/node_modules/top-package/package.json'),
contents: 'PACKAGE.JSON for top-package'
}, },
'/dist': { ]);
'package-4': {
'x.js': `export class X {}`,
'package.json': 'PACKAGE.JSON for package-4',
'sub-folder': {'index.js': `import {X} from '@shared/package-4/x';`},
},
'sub-folder': {
'package-4': {
'package.json': 'PACKAGE.JSON for package-4',
},
'package-5': {
'package.json': 'PACKAGE.JSON for package-5',
'post-fix': {
'package.json': 'PACKAGE.JSON for package-5/post-fix',
}
},
}
},
'/node_modules': {
'top-package': {
'package.json': 'PACKAGE.JSON for top-package',
}
}
}); });
}
describe('ModuleResolver', () => {
describe('resolveModule()', () => { describe('resolveModule()', () => {
describe('with relative paths', () => { describe('with relative paths', () => {
it('should resolve sibling, child and aunt modules', () => { it('should resolve sibling, child and aunt modules', () => {
const resolver = new ModuleResolver(createMockFileSystem()); const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js')))
.toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js')));
expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js')))
@ -80,25 +75,25 @@ describe('ModuleResolver', () => {
}); });
it('should return `null` if the resolved module relative module does not exist', () => { it('should return `null` if the resolved module relative module does not exist', () => {
const resolver = new ModuleResolver(createMockFileSystem()); const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null); expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null);
}); });
}); });
describe('with non-mapped external paths', () => { describe('with non-mapped external paths', () => {
it('should resolve to the package.json of a local node_modules package', () => { it('should resolve to the package.json of a local node_modules package', () => {
const resolver = new ModuleResolver(createMockFileSystem()); const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
expect( expect(resolver.resolveModuleImport(
resolver.resolveModuleImport('package-1', _('/libs/local-package/sub-folder/index.js'))) 'package-1', _('/libs/local-package/sub-folder/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js'))) expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
}); });
it('should resolve to the package.json of a higher node_modules package', () => { it('should resolve to the package.json of a higher node_modules package', () => {
const resolver = new ModuleResolver(createMockFileSystem()); const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2'))); .toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2')));
expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js')))
@ -106,31 +101,31 @@ describe('ModuleResolver', () => {
}); });
it('should return `null` if the package cannot be found', () => { it('should return `null` if the package cannot be found', () => {
const resolver = new ModuleResolver(createMockFileSystem()); const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js')))
.toBe(null); .toBe(null);
}); });
it('should return `null` if the package is not accessible because it is in a inner node_modules package', it('should return `null` if the package is not accessible because it is in a inner node_modules package',
() => { () => {
const resolver = new ModuleResolver(createMockFileSystem()); const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js')))
.toBe(null); .toBe(null);
}); });
it('should identify deep imports into an external module', () => { it('should identify deep imports into an external module', () => {
const resolver = new ModuleResolver(createMockFileSystem()); const resolver = new ModuleResolver(getFileSystem());
expect( expect(resolver.resolveModuleImport(
resolver.resolveModuleImport('package-1/sub-folder', _('/libs/local-package/index.js'))) 'package-1/sub-folder', _('/libs/local-package/index.js')))
.toEqual( .toEqual(new ResolvedDeepImport(
new ResolvedDeepImport(_('/libs/local-package/node_modules/package-1/sub-folder'))); _('/libs/local-package/node_modules/package-1/sub-folder')));
}); });
}); });
describe('with mapped path external modules', () => { describe('with mapped path external modules', () => {
it('should resolve to the package.json of simple mapped packages', () => { it('should resolve to the package.json of simple mapped packages', () => {
const resolver = new ModuleResolver( const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); getFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4'))); .toEqual(new ResolvedExternalModule(_('/dist/package-4')));
@ -140,7 +135,7 @@ describe('ModuleResolver', () => {
}); });
it('should select the best match by the length of prefix before the *', () => { it('should select the best match by the length of prefix before the *', () => {
const resolver = new ModuleResolver(createMockFileSystem(), { const resolver = new ModuleResolver(getFileSystem(), {
baseUrl: '/dist', baseUrl: '/dist',
paths: { paths: {
'@lib/*': ['*'], '@lib/*': ['*'],
@ -148,8 +143,8 @@ describe('ModuleResolver', () => {
} }
}); });
// We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map to // We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map
// `*` and so the final resolved path will not include the `sub-folder` segment. // to `*` and so the final resolved path will not include the `sub-folder` segment.
expect(resolver.resolveModuleImport( expect(resolver.resolveModuleImport(
'@lib/sub-folder/package-4', _('/libs/local-package/index.js'))) '@lib/sub-folder/package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4'))); .toEqual(new ResolvedExternalModule(_('/dist/package-4')));
@ -158,27 +153,30 @@ describe('ModuleResolver', () => {
it('should follow the ordering of `paths` when matching mapped packages', () => { it('should follow the ordering of `paths` when matching mapped packages', () => {
let resolver: ModuleResolver; let resolver: ModuleResolver;
const fs = createMockFileSystem(); const fs = getFileSystem();
resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); resolver =
new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4'))); .toEqual(new ResolvedExternalModule(_('/dist/package-4')));
resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}}); resolver =
new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4')));
}); });
it('should resolve packages when the path mappings have post-fixes', () => { it('should resolve packages when the path mappings have post-fixes', () => {
const resolver = new ModuleResolver( const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); getFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}});
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix')));
}); });
it('should match paths against complex path matchers', () => { it('should match paths against complex path matchers', () => {
const resolver = new ModuleResolver( const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}});
expect(resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js'))) expect(
resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4')));
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toBe(null); .toBe(null);
@ -187,7 +185,7 @@ describe('ModuleResolver', () => {
it('should resolve path as "relative" if the mapped path is inside the current package', it('should resolve path as "relative" if the mapped path is inside the current package',
() => { () => {
const resolver = new ModuleResolver( const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['*']}}); getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['*']}});
expect(resolver.resolveModuleImport( expect(resolver.resolveModuleImport(
'@shared/package-4/x', _('/dist/package-4/sub-folder/index.js'))) '@shared/package-4/x', _('/dist/package-4/sub-folder/index.js')))
.toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js'))); .toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js')));
@ -195,13 +193,13 @@ describe('ModuleResolver', () => {
it('should resolve paths where the wildcard matches more than one path segment', () => { it('should resolve paths where the wildcard matches more than one path segment', () => {
const resolver = new ModuleResolver( const resolver = new ModuleResolver(
createMockFileSystem(), getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}});
{baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); expect(resolver.resolveModuleImport(
expect( '@shared/sub-folder/package-5/post-fix',
resolver.resolveModuleImport( _('/dist/package-4/sub-folder/index.js')))
'@shared/sub-folder/package-5/post-fix', _('/dist/package-4/sub-folder/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix')));
}); });
}); });
}); });
});
}); });

View File

@ -7,17 +7,21 @@
*/ */
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', () => {
describe('UmdDependencyHost', () => { let _: typeof absoluteFrom;
let host: UmdDependencyHost; let host: UmdDependencyHost;
beforeEach(() => { beforeEach(() => {
const fs = createMockFileSystem(); _ = absoluteFrom;
setupMockFileSystem();
const fs = getFileSystem();
host = new UmdDependencyHost(fs, new ModuleResolver(fs)); host = new UmdDependencyHost(fs, new ModuleResolver(fs));
}); });
@ -55,14 +59,14 @@ describe('UmdDependencyHost', () => {
expect(dependencies.size).toBe(1); expect(dependencies.size).toBe(1);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(missing.size).toBe(1); expect(missing.size).toBe(1);
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); expect(missing.has(relativeFrom('missing'))).toBe(true);
expect(deepImports.size).toBe(0); expect(deepImports.size).toBe(0);
}); });
it('should not register deep imports as missing', () => { it('should not register deep imports as missing', () => {
// This scenario verifies the behavior of the dependency analysis when an external import // 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. // is found that does not map to an entry-point but still exists on disk, i.e. a deep
// Such deep imports are captured for diagnostics purposes. // import. Such deep imports are captured for diagnostics purposes.
const {dependencies, missing, deepImports} = const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/deep-import/index.js')); host.findDependencies(_('/external/deep-import/index.js'));
@ -93,7 +97,7 @@ describe('UmdDependencyHost', () => {
}); });
it('should support `paths` alias mappings when resolving modules', () => { it('should support `paths` alias mappings when resolving modules', () => {
const fs = createMockFileSystem(); const fs = getFileSystem();
host = new UmdDependencyHost(fs, new ModuleResolver(fs, { host = new UmdDependencyHost(fs, new ModuleResolver(fs, {
baseUrl: '/dist', baseUrl: '/dist',
paths: { paths: {
@ -101,7 +105,8 @@ describe('UmdDependencyHost', () => {
'@lib/*/test': ['lib/*/test'], '@lib/*/test': ['lib/*/test'],
} }
})); }));
const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); const {dependencies, missing, deepImports} =
host.findDependencies(_('/path-alias/index.js'));
expect(dependencies.size).toBe(4); expect(dependencies.size).toBe(4);
expect(dependencies.has(_('/dist/components'))).toBe(true); expect(dependencies.has(_('/dist/components'))).toBe(true);
expect(dependencies.has(_('/dist/shared'))).toBe(true); expect(dependencies.has(_('/dist/shared'))).toBe(true);
@ -112,70 +117,116 @@ describe('UmdDependencyHost', () => {
}); });
}); });
function createMockFileSystem() { function setupMockFileSystem(): void {
return new MockFileSystem({ loadTestFiles([
'/no/imports/or/re-exports/index.js': '// some text but no import-like statements', {
'/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', name: _('/no/imports/or/re-exports/index.js'),
'/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', contents: '// some text but no import-like statements'
'/external/imports/index.js': umd('imports_index', ['lib_1', 'lib_1/sub_1']), },
'/external/imports/package.json': '{"esm2015": "./index.js"}', {name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
'/external/imports/index.metadata.json': 'MOCK METADATA', {name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
'/external/re-exports/index.js': {
umd('imports_index', ['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']), name: _('/external/imports/index.js'),
'/external/re-exports/package.json': '{"esm2015": "./index.js"}', contents: umd('imports_index', ['lib_1', 'lib_1/sub_1'])
'/external/re-exports/index.metadata.json': 'MOCK METADATA', },
'/external/imports-missing/index.js': umd('imports_missing', ['lib_1', 'missing']), {name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'},
'/external/imports-missing/package.json': '{"esm2015": "./index.js"}', {name: _('/external/imports/index.metadata.json'), contents: 'MOCK METADATA'},
'/external/imports-missing/index.metadata.json': 'MOCK METADATA', {
'/external/deep-import/index.js': umd('deep_import', ['lib_1/deep/import']), name: _('/external/re-exports/index.js'),
'/external/deep-import/package.json': '{"esm2015": "./index.js"}', contents: umd('imports_index', ['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y'])
'/external/deep-import/index.metadata.json': 'MOCK METADATA', },
'/internal/outer/index.js': umd('outer', ['../inner']), {name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
'/internal/outer/package.json': '{"esm2015": "./index.js"}', {name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
'/internal/outer/index.metadata.json': 'MOCK METADATA', {
'/internal/inner/index.js': umd('inner', ['lib_1/sub_1'], ['X']), name: _('/external/imports-missing/index.js'),
'/internal/circular_a/index.js': umd('circular_a', ['../circular_b', 'lib_1/sub_1'], ['Y']), contents: umd('imports_missing', ['lib_1', 'missing'])
'/internal/circular_b/index.js': umd('circular_b', ['../circular_a', 'lib_1'], ['X']), },
'/internal/circular_a/package.json': '{"esm2015": "./index.js"}', {name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'},
'/internal/circular_a/index.metadata.json': 'MOCK METADATA', {name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'},
'/re-directed/index.js': umd('re_directed', ['lib_1/sub_2']), {
'/re-directed/package.json': '{"esm2015": "./index.js"}', name: _('/external/deep-import/index.js'),
'/re-directed/index.metadata.json': 'MOCK METADATA', contents: umd('deep_import', ['lib_1/deep/import'])
'/path-alias/index.js': },
umd('path_alias', ['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']), {name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'},
'/path-alias/package.json': '{"esm2015": "./index.js"}', {name: _('/external/deep-import/index.metadata.json'), contents: 'MOCK METADATA'},
'/path-alias/index.metadata.json': 'MOCK METADATA', {name: _('/internal/outer/index.js'), contents: umd('outer', ['../inner'])},
'/node_modules/lib_1/index.d.ts': 'export declare class X {}', {name: _('/internal/outer/package.json'), contents: '{"esm2015": "./index.js"}'},
'/node_modules/lib_1/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', {name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'},
'/node_modules/lib_1/index.metadata.json': 'MOCK METADATA', {name: _('/internal/inner/index.js'), contents: umd('inner', ['lib_1/sub_1'], ['X'])},
'/node_modules/lib_1/deep/import/index.js': 'export class DeepImport {}', {
'/node_modules/lib_1/sub_1/index.d.ts': 'export declare class Y {}', name: _('/internal/circular_a/index.js'),
'/node_modules/lib_1/sub_1/package.json': contents: umd('circular_a', ['../circular_b', 'lib_1/sub_1'], ['Y'])
'{"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';`, name: _('/internal/circular_b/index.js'),
'/node_modules/lib_1/sub_2/sub_2.d.ts': `export declare class Z {}';`, contents: umd('circular_b', ['../circular_a', 'lib_1'], ['X'])
'/node_modules/lib_1/sub_2/package.json': },
'{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}', {name: _('/internal/circular_a/package.json'), contents: '{"esm2015": "./index.js"}'},
'/node_modules/lib_1/sub_2/sub_2.metadata.json': 'MOCK METADATA', {name: _('/internal/circular_a/index.metadata.json'), contents: 'MOCK METADATA'},
'/dist/components/index.d.ts': `export declare class MyComponent {};`, {name: _('/re-directed/index.js'), contents: umd('re_directed', ['lib_1/sub_2'])},
'/dist/components/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', {name: _('/re-directed/package.json'), contents: '{"esm2015": "./index.js"}'},
'/dist/components/index.metadata.json': 'MOCK METADATA', {name: _('/re-directed/index.metadata.json'), contents: '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"}', name: _('/path-alias/index.js'),
'/dist/shared/index.metadata.json': 'MOCK METADATA', contents:
'/dist/lib/shared/test/index.d.ts': `export class TestHelper {}`, umd('path_alias', ['@app/components', '@app/shared', '@lib/shared/test', 'lib_1'])
'/dist/lib/shared/test/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', },
'/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', {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'},
]);
} }
}); });
function umd(moduleName: string, importPaths: string[], exportNames: string[] = []) { function umd(moduleName: string, importPaths: string[], exportNames: string[] = []) {
const commonJsRequires = importPaths.map(p => `,require('${p}')`).join(''); const commonJsRequires = importPaths.map(p => `,require('${p}')`).join('');
const amdDeps = importPaths.map(p => `,'${p}'`).join(''); const amdDeps = importPaths.map(p => `,'${p}'`).join('');
const globalParams = const globalParams =
importPaths.map(p => `,global.${p.replace('@angular/', 'ng.').replace(/\//g, '')}`).join(''); importPaths.map(p => `,global.${p.replace('@angular/', 'ng.').replace(/\//g, '')}`)
.join('');
const params = const params =
importPaths.map(p => `,${p.replace('@angular/', '').replace(/\.?\.?\//g, '')}`).join(''); importPaths.map(p => `,${p.replace('@angular/', '').replace(/\.?\.?\//g, '')}`).join('');
const exportStatements = const exportStatements =
@ -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,16 +8,27 @@
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]', () => {
let _: typeof absoluteFrom;
let FILES: {[label: string]: TestFile[]};
beforeEach(() => {
_ = absoluteFrom;
const NAMESPACED_IMPORT_FILES = [
{ {
name: '/some_directive.js', name: _('/some_directive.js'),
contents: ` 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';
@ -54,7 +65,7 @@ const FILES = [
`, `,
}, },
{ {
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';
@ -72,7 +83,7 @@ const FILES = [
`, `,
}, },
{ {
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';
@ -97,20 +108,31 @@ const FILES = [
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'}, FILES = {
].forEach(fileSystem => { 'namespaced': NAMESPACED_IMPORT_FILES,
describe(`[${fileSystem.label}]`, () => { 'direct import': DIRECT_IMPORT_FILES,
};
});
['namespaced', 'direct import'].forEach(label => {
describe(`[${label}]`, () => {
beforeEach(() => {
const fs = getFileSystem();
loadFakeCore(fs);
loadTestFiles(FILES[label]);
});
describe('getDecoratorsOfDeclaration()', () => { describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => { it('should find the decorators on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined(); expect(decorators).toBeDefined();
@ -125,16 +147,18 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake( .and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ? (identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} : {from: '@angular/core', name: 'Directive'} :
{}); {});
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
@ -146,10 +170,12 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should support decorators being used inside @angular/core', () => { it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]); const {program} =
const host = new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker()); makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration( const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
@ -167,10 +193,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
describe('getMembersOfClass()', () => { describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => { it('should find decorated members on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !; const input1 = members.find(member => member.name === 'input1') !;
@ -185,10 +212,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should find non decorated properties on a class', () => { it('should find non decorated properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !; const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
@ -199,10 +227,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should find static methods on a class', () => { it('should find static methods on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !; const staticMethod = members.find(member => member.name === 'staticMethod') !;
@ -212,10 +241,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should find static properties on a class', () => { it('should find static properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !; const staticProperty = members.find(member => member.name === 'staticProperty') !;
@ -227,11 +257,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
it('should find static properties on a class that has an intermediate variable assignment', it('should find static properties on a class that has an intermediate variable assignment',
() => { () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
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, '/ngmodule.js', 'HttpClientXsrfModule', isNamedVariableDeclaration); program, _('/ngmodule.js'), 'HttpClientXsrfModule', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !; const staticProperty = members.find(member => member.name === 'staticProperty') !;
@ -245,10 +275,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
const spy = const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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);
host.getMembersOfClass(classNode); host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
@ -256,10 +287,12 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should support decorators being used inside @angular/core', () => { it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]); const {program} =
const host = new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker()); makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration( const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
@ -272,10 +305,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
describe('getConstructorParameters', () => { describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => { it('should find the decorated constructor parameters', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined(); expect(parameters).toBeDefined();
@ -295,11 +329,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo); .and.returnValue(mockImportInfo);
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !; const decorators = parameters ![2].decorators !;
@ -314,10 +348,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
describe('getDeclarationOfIdentifier', () => { describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => { it('should return the declaration of a locally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 ctrDecorators = host.getConstructorParameters(classNode) !; const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true, local: true,
@ -326,7 +361,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}).expression; }).expression;
const expectedDeclarationNode = getDeclaration( const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', ts.isClassDeclaration); program, _('/some_directive.js'), 'ViewContainerRef', ts.isClassDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null); expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode); expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
@ -334,10 +369,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should return the declaration of an externally defined identifier', () => { it('should return the declaration of an externally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
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 classDecorators = host.getDecoratorsOfDeclaration(classNode) !; const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node; const decoratorNode = classDecorators[0].node;
const identifierOfDirective = const identifierOfDirective =
@ -346,7 +382,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
null; null;
const expectedDeclarationNode = getDeclaration( const expectedDeclarationNode = getDeclaration(
program, 'node_modules/@angular/core/index.d.ts', 'Directive', program, _('/node_modules/@angular/core/index.d.ts'), 'Directive',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null); expect(actualDeclaration).not.toBe(null);
@ -357,10 +393,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
describe('getVariableValue', () => { describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => { it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration( const ngModuleRef = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1'); getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !); const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null); expect(value).not.toBe(null);
@ -372,19 +409,21 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
}); });
it('should return null if the variable has no assignment', () => { it('should return null if the variable has no assignment', () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const missingValue = findVariableDeclaration( const missingValue = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue'); getSourceFileOrError(program, _('/ngmodule.js')), 'missingValue');
const value = host.getVariableValue(missingValue !); const value = host.getVariableValue(missingValue !);
expect(value).toBe(null); expect(value).toBe(null);
}); });
it('should return null if the variable is not assigned from a call to __decorate', () => { it('should return null if the variable is not assigned from a call to __decorate', () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration( const nonDecoratedVar = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar'); getSourceFileOrError(program, _('/ngmodule.js')), 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !); const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null); expect(value).toBe(null);
}); });
@ -403,4 +442,5 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
} }
return node.forEachChild(node => findVariableDeclaration(node, variableName)); return node.forEachChild(node => findVariableDeclaration(node, variableName));
} }
});
}); });

File diff suppressed because it is too large Load Diff

View File

@ -5,19 +5,37 @@
* 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]', () => {
let _: typeof absoluteFrom;
let FILES: {[label: string]: TestFile[]};
beforeEach(() => {
_ = absoluteFrom;
const NAMESPACED_IMPORT_FILES = [
{ {
name: '/some_directive.js', 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: ` 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';
@ -61,7 +79,7 @@ const FILES = [
`, `,
}, },
{ {
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';
@ -82,7 +100,7 @@ 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';
@ -111,20 +129,30 @@ export { SomeDirective };
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'}, FILES = {
].forEach(fileSystem => { 'namespaced': NAMESPACED_IMPORT_FILES,
describe(`[${fileSystem.label}]`, () => { 'direct import': DIRECT_IMPORT_FILES,
};
});
['namespaced', 'direct import'].forEach(label => {
describe(`[${label}]`, () => {
beforeEach(() => {
const fs = getFileSystem();
loadFakeCore(fs);
loadTestFiles(FILES[label]);
});
describe('getDecoratorsOfDeclaration()', () => { describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => { it('should find the decorators on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined(); expect(decorators).toBeDefined();
@ -139,16 +167,17 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake( .and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ? (identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} : {from: '@angular/core', name: 'Directive'} :
{}); {});
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
@ -160,10 +189,11 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should support decorators being used inside @angular/core', () => { it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]); const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration( const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
@ -181,10 +211,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
describe('getMembersOfClass()', () => { describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => { it('should find decorated members on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !; const input1 = members.find(member => member.name === 'input1') !;
@ -199,10 +229,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should find non decorated properties on a class', () => { it('should find non decorated properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !; const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
@ -213,10 +243,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should find static methods on a class', () => { it('should find static methods on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !; const staticMethod = members.find(member => member.name === 'staticMethod') !;
@ -226,10 +256,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should find static properties on a class', () => { it('should find static properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !; const staticProperty = members.find(member => member.name === 'staticProperty') !;
@ -243,10 +273,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
const spy = const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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);
host.getMembersOfClass(classNode); host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
@ -254,10 +284,11 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should support decorators being used inside @angular/core', () => { it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]); const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration( const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode); const members = host.getMembersOfClass(classNode);
@ -267,13 +298,12 @@ describe('Esm5ReflectionHost [import helper style]', () => {
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
}); });
}); });
describe('getConstructorParameters', () => { describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => { it('should find the decorated constructor parameters', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined(); expect(parameters).toBeDefined();
@ -293,10 +323,12 @@ describe('Esm5ReflectionHost [import helper style]', () => {
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo); .and.returnValue(mockImportInfo);
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !; const decorators = parameters ![2].decorators !;
@ -311,15 +343,15 @@ describe('Esm5ReflectionHost [import helper style]', () => {
describe('findClassSymbols()', () => { describe('findClassSymbols()', () => {
it('should return an array of all classes in the given source file', () => { it('should return an array of all classes in the given source file', () => {
const program = makeTestProgram(...fileSystem.files); const {program} = makeTestBundleProgram(_('/index.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleFile = program.getSourceFile('/ngmodule.js') !; const ngModuleFile = getSourceFileOrError(program, _('/ngmodule.js'));
const ngModuleClasses = host.findClassSymbols(ngModuleFile); const ngModuleClasses = host.findClassSymbols(ngModuleFile);
expect(ngModuleClasses.length).toEqual(1); expect(ngModuleClasses.length).toEqual(1);
expect(ngModuleClasses[0].name).toBe('HttpClientXsrfModule'); expect(ngModuleClasses[0].name).toBe('HttpClientXsrfModule');
const someDirectiveFile = program.getSourceFile('/some_directive.js') !; const someDirectiveFile = getSourceFileOrError(program, _('/some_directive.js'));
const someDirectiveClasses = host.findClassSymbols(someDirectiveFile); const someDirectiveClasses = host.findClassSymbols(someDirectiveFile);
expect(someDirectiveClasses.length).toEqual(3); expect(someDirectiveClasses.length).toEqual(3);
expect(someDirectiveClasses[0].name).toBe('ViewContainerRef'); expect(someDirectiveClasses[0].name).toBe('ViewContainerRef');
@ -330,17 +362,17 @@ describe('Esm5ReflectionHost [import helper style]', () => {
describe('getDecoratorsOfSymbol()', () => { describe('getDecoratorsOfSymbol()', () => {
it('should return decorators of class symbol', () => { it('should return decorators of class symbol', () => {
const program = makeTestProgram(...fileSystem.files); const {program} = makeTestBundleProgram(_('/index.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleFile = program.getSourceFile('/ngmodule.js') !; const ngModuleFile = getSourceFileOrError(program, _('/ngmodule.js'));
const ngModuleClasses = host.findClassSymbols(ngModuleFile); const ngModuleClasses = host.findClassSymbols(ngModuleFile);
const ngModuleDecorators = ngModuleClasses.map(s => host.getDecoratorsOfSymbol(s)); const ngModuleDecorators = ngModuleClasses.map(s => host.getDecoratorsOfSymbol(s));
expect(ngModuleClasses.length).toEqual(1); expect(ngModuleClasses.length).toEqual(1);
expect(ngModuleDecorators[0] !.map(d => d.name)).toEqual(['NgModule']); expect(ngModuleDecorators[0] !.map(d => d.name)).toEqual(['NgModule']);
const someDirectiveFile = program.getSourceFile('/some_directive.js') !; const someDirectiveFile = getSourceFileOrError(program, _('/some_directive.js'));
const someDirectiveClasses = host.findClassSymbols(someDirectiveFile); const someDirectiveClasses = host.findClassSymbols(someDirectiveFile);
const someDirectiveDecorators = const someDirectiveDecorators =
someDirectiveClasses.map(s => host.getDecoratorsOfSymbol(s)); someDirectiveClasses.map(s => host.getDecoratorsOfSymbol(s));
@ -354,10 +386,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
describe('getDeclarationOfIdentifier', () => { describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => { it('should return the declaration of a locally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 ctrDecorators = host.getConstructorParameters(classNode) !; const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true, local: true,
@ -366,7 +398,7 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}).expression; }).expression;
const expectedDeclarationNode = getDeclaration( const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', isNamedVariableDeclaration); program, _('/some_directive.js'), 'ViewContainerRef', isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null); expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode); expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
@ -374,10 +406,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should return the declaration of an externally defined identifier', () => { it('should return the declaration of an externally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]); const {program} = makeTestBundleProgram(_('/some_directive.js'));
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 classDecorators = host.getDecoratorsOfDeclaration(classNode) !; const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node; const decoratorNode = classDecorators[0].node;
@ -387,7 +419,7 @@ describe('Esm5ReflectionHost [import helper style]', () => {
null; null;
const expectedDeclarationNode = getDeclaration( const expectedDeclarationNode = getDeclaration(
program, 'node_modules/@angular/core/index.d.ts', 'Directive', program, _('/node_modules/@angular/core/index.d.ts'), 'Directive',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null); expect(actualDeclaration).not.toBe(null);
@ -396,10 +428,10 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should find the "actual" declaration of an aliased variable identifier', () => { it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findIdentifier( const ngModuleRef = findIdentifier(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1', getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1',
isNgModulePropertyAssignment); isNgModulePropertyAssignment);
const declaration = host.getDeclarationOfIdentifier(ngModuleRef !); const declaration = host.getDeclarationOfIdentifier(ngModuleRef !);
@ -407,14 +439,12 @@ describe('Esm5ReflectionHost [import helper style]', () => {
expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()'); expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()');
}); });
}); });
});
describe('getVariableValue', () => { describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => { it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration( const ngModuleRef = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1'); getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !); const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null); expect(value).not.toBe(null);
@ -426,19 +456,19 @@ describe('Esm5ReflectionHost [import helper style]', () => {
}); });
it('should return undefined if the variable has no assignment', () => { it('should return undefined if the variable has no assignment', () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const missingValue = findVariableDeclaration( const missingValue = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue'); getSourceFileOrError(program, _('/ngmodule.js')), 'missingValue');
const value = host.getVariableValue(missingValue !); const value = host.getVariableValue(missingValue !);
expect(value).toBe(null); expect(value).toBe(null);
}); });
it('should return null if the variable is not assigned from a call to __decorate', () => { it('should return null if the variable is not assigned from a call to __decorate', () => {
const program = makeTestProgram(fileSystem.files[2]); const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration( const nonDecoratedVar = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar'); getSourceFileOrError(program, _('/ngmodule.js')), 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !); const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null); expect(value).toBe(null);
}); });
@ -455,9 +485,9 @@ describe('Esm5ReflectionHost [import helper style]', () => {
} }
return node.forEachChild(node => findVariableDeclaration(node, variableName)); return node.forEachChild(node => findVariableDeclaration(node, variableName));
} }
}); });
function findIdentifier( function findIdentifier(
node: ts.Node | undefined, identifierName: string, node: ts.Node | undefined, identifierName: string,
requireFn: (node: ts.Identifier) => boolean): ts.Identifier|undefined { requireFn: (node: ts.Identifier) => boolean): ts.Identifier|undefined {
if (!node) { if (!node) {
@ -467,9 +497,11 @@ function findIdentifier(
return node; return node;
} }
return node.forEachChild(node => findIdentifier(node, identifierName, requireFn)); return node.forEachChild(node => findIdentifier(node, identifierName, requireFn));
} }
function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean { function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean {
return ts.isPropertyAssignment(identifier.parent) && return ts.isPropertyAssignment(identifier.parent) &&
identifier.parent.name.getText() === 'ngModule'; 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,24 +5,26 @@
* 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;
beforeEach(() => {
_ = absoluteFrom;
fs = getFileSystem();
initMockFileSystem(fs, testFiles);
});
it('should run ngcc without errors for esm2015', () => { it('should run ngcc without errors for esm2015', () => {
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']})) expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']}))
@ -45,7 +47,7 @@ describe('ngcc main()', () => {
logger: new MockLogger(), logger: new MockLogger(),
}); });
expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({ expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER',
}); });
@ -101,10 +103,13 @@ describe('ngcc main()', () => {
basePath: '/node_modules', basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing', logger, targetEntryPointPath: '@angular/common/http/testing', logger,
}); });
expect(logger.logs.debug).toContain(['The target entry-point has already been processed']); 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', () => { it('should process the target if any `propertyToConsider` is not marked as processed',
() => {
const logger = new MockLogger(); const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']); markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']);
mainNgcc({ mainNgcc({
@ -157,9 +162,8 @@ describe('ngcc main()', () => {
function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) { function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) {
const basePath = _('/node_modules'); const basePath = _('/node_modules');
const targetPackageJsonPath = AbsoluteFsPath.join(basePath, packagePath, 'package.json'); const targetPackageJsonPath = join(basePath, packagePath, 'package.json');
const targetPackage = loadPackage(packagePath); const targetPackage = loadPackage(packagePath);
const fs = new NodeJSFileSystem();
markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings'); markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings');
properties.forEach( properties.forEach(
property => markAsProcessed(fs, targetPackage, targetPackageJsonPath, property)); property => markAsProcessed(fs, targetPackage, targetPackageJsonPath, property));
@ -287,26 +291,27 @@ describe('ngcc main()', () => {
.toEqual('__ivy_ngcc__/esm5/common.js'); .toEqual('__ivy_ngcc__/esm5/common.js');
// Doesn't touch original files // Doesn't touch original files
expect(readFileSync(`/node_modules/@angular/common/esm5/src/common_module.js`, 'utf8')) expect(fs.readFile(_(`/node_modules/@angular/common/esm5/src/common_module.js`)))
.not.toMatch(ANGULAR_CORE_IMPORT_REGEX); .not.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Or create a backup of the original // Or create a backup of the original
expect(existsSync(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`)) expect(
fs.exists(_(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`)))
.toBe(false); .toBe(false);
// Creates new files // Creates new files
expect(readFileSync( expect(
`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`, 'utf8')) fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`)))
.toMatch(ANGULAR_CORE_IMPORT_REGEX); .toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Copies over files (unchanged) that did not need compiling // Copies over files (unchanged) that did not need compiling
expect(existsSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)); expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)));
expect(readFileSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`, 'utf8')) expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)))
.toEqual(readFileSync(`/node_modules/@angular/common/esm5/src/version.js`, 'utf8')); .toEqual(fs.readFile(_(`/node_modules/@angular/common/esm5/src/version.js`)));
// Overwrites .d.ts files (as usual) // Overwrites .d.ts files (as usual)
expect(readFileSync(`/node_modules/@angular/common/common.d.ts`, 'utf8')) expect(fs.readFile(_(`/node_modules/@angular/common/common.d.ts`)))
.toMatch(ANGULAR_CORE_IMPORT_REGEX); .toMatch(ANGULAR_CORE_IMPORT_REGEX);
expect(existsSync(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`)).toBe(true); expect(fs.exists(_(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`))).toBe(true);
}); });
}); });
@ -339,88 +344,58 @@ describe('ngcc main()', () => {
es2015: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER',
}); });
expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({ expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER',
}); });
}); });
}); });
function loadPackage(
packageName: string, basePath: AbsoluteFsPath = _('/node_modules')): EntryPointPackageJson {
return JSON.parse(fs.readFile(fs.resolve(basePath, packageName, 'package.json')));
}
function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
if (fs instanceof MockFileSystem) {
fs.init(testFiles);
}
// a random test package that no metadata.json file so not compiled by Angular.
loadTestFiles([
{
name: _('/node_modules/test-package/package.json'),
contents: '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/test-package/index.js'),
contents:
'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};'
},
{
name: _('/node_modules/test-package/index.d.ts'),
contents:
'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;'
},
]);
// An Angular package that has been built locally and stored in the `dist` directory.
loadTestFiles([
{
name: _('/dist/local-package/package.json'),
contents: '{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/local-package/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/dist/local-package/index.js'),
contents:
`import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '<h2>Hello</h2>'}] }\n];`
},
{
name: _('/dist/local-package/index.d.ts'),
contents: `export declare class AppComponent {};`
},
]);
}
});
}); });
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,94 +5,83 @@
* 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': { {name: _('/node_modules/@angular/common/fesm2015/common.js'), contents: 'DUMMY CONTENT'},
'package.json': `{ {name: _('/node_modules/@angular/common/fesm2015/http.js'), contents: 'DUMMY CONTENT'},
"fesm2015": "../fesm2015/http.js", {
"fesm5": "../fesm5/http.js", name: _('/node_modules/@angular/common/fesm2015/http/testing.js'),
"typings": "./http.d.ts" contents: 'DUMMY CONTENT'
}`,
'testing': {
'package.json': `{
"fesm2015": "../../fesm2015/http/testing.js",
"fesm5": "../../fesm5/http/testing.js",
"typings": "../http/testing.d.ts"
}`,
}, },
{name: _('/node_modules/@angular/common/fesm2015/testing.js'), contents: 'DUMMY CONTENT'},
{
name: _('/node_modules/@angular/common/http/package.json'),
contents:
`{"fesm2015": "../fesm2015/http.js", "fesm5": "../fesm5/http.js", "typings": "./http.d.ts"}`
}, },
'other': { {
'package.json': '{ }', name: _('/node_modules/@angular/common/http/testing/package.json'),
contents:
`{"fesm2015": "../../fesm2015/http/testing.js", "fesm5": "../../fesm5/http/testing.js", "typings": "../http/testing.d.ts" }`
}, },
'testing': { {name: _('/node_modules/@angular/common/other/package.json'), contents: '{ }'},
'package.json': `{ {
"fesm2015": "../fesm2015/testing.js", name: _('/node_modules/@angular/common/testing/package.json'),
"fesm5": "../fesm5/testing.js", contents:
"typings": "../testing.d.ts" `{"fesm2015": "../fesm2015/testing.js", "fesm5": "../fesm5/testing.js", "typings": "../testing.d.ts"}`
}`,
}, },
'node_modules': { {name: _('/node_modules/@angular/common/node_modules/tslib/package.json'), contents: '{ }'},
'tslib': { {
'package.json': '{ }', name: _(
'node_modules': { '/node_modules/@angular/common/node_modules/tslib/node_modules/other-lib/package.json'),
'other-lib': { contents: '{ }'
'package.json': '{ }',
}, },
{
name: _('/node_modules/@angular/no-typings/package.json'),
contents: `{ "fesm2015": "./fesm2015/index.js" }`
}, },
{name: _('/node_modules/@angular/no-typings/fesm2015/index.js'), contents: 'DUMMY CONTENT'},
{
name: _('/node_modules/@angular/no-typings/fesm2015/index.d.ts'),
contents: 'DUMMY CONTENT'
}, },
{
name: _('/node_modules/@angular/other/not-package.json'),
contents: '{ "fesm2015": "./fesm2015/other.js" }'
}, },
{
name: _('/node_modules/@angular/other/package.jsonot'),
contents: '{ "fesm5": "./fesm5/other.js" }'
}, },
'/node_modules/@angular/no-typings': { {
'package.json': `{ name: _('/node_modules/@angular/other2/node_modules_not/lib1/package.json'),
"fesm2015": "./fesm2015/index.js" contents: '{ }'
}`,
'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': {
'lib2': {
'package.json': '{ }',
},
}, },
{
name: _('/node_modules/@angular/other2/not_node_modules/lib2/package.json'),
contents: '{ }'
}, },
]);
}); });
}
describe('Marker files', () => {
const COMMON_PACKAGE_PATH = AbsoluteFsPath.from('/node_modules/@angular/common/package.json');
describe('markAsProcessed', () => { describe('markAsProcessed', () => {
it('should write a property in the package.json containing the version placeholder', () => { it('should write a property in the package.json containing the version placeholder', () => {
const fs = createMockFileSystem(); const COMMON_PACKAGE_PATH = _('/node_modules/@angular/common/package.json');
const fs = getFileSystem();
let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH));
expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined();
expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined();
@ -109,7 +98,8 @@ describe('Marker files', () => {
}); });
it('should update the packageJson object in-place', () => { it('should update the packageJson object in-place', () => {
const fs = createMockFileSystem(); const COMMON_PACKAGE_PATH = _('/node_modules/@angular/common/package.json');
const fs = getFileSystem();
let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH));
expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined();
markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015');
@ -169,4 +159,5 @@ describe('Marker files', () => {
'Please completely remove `node_modules` and try again.'); 'Please completely remove `node_modules` and try again.');
}); });
}); });
});
}); });

View File

@ -5,87 +5,129 @@
* 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";',
'index.js': 'export * from "../root";',
}, },
'es2015': { {name: _('/node_modules/test/nested/index.d.ts'), contents: 'export * from "../root";'},
'index.js': 'export * from "./public_api";', {name: _('/node_modules/test/nested/index.js'), contents: 'export * from "../root";'},
'public_api.js': 'export class TestClass {};', {name: _('/node_modules/test/es2015/index.js'), contents: 'export * from "./public_api";'},
'root.js': ` {
name: _('/node_modules/test/es2015/public_api.js'),
contents: 'export class TestClass {};'
},
{
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";',
}, },
{
name: _('/node_modules/test/es2015/nested/index.js'),
contents: 'export * from "../root";'
}, },
'secondary': { {
'package.json': name: _('/node_modules/test/secondary/package.json'),
'{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', contents:
'index.d.ts': 'export * from "./public_api";', '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}'
'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/index.d.ts'),
contents: 'export * from "./public_api";'
}, },
{
name: _('/node_modules/test/secondary/index.js'),
contents: 'export * from "./public_api";'
}, },
'/node_modules/other': { {name: _('/node_modules/test/secondary/index.metadata.json'), contents: '...'},
'package.json': {
'{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', name: _('/node_modules/test/secondary/public_api.d.ts'),
'index.d.ts': 'export * from "./public_api";', contents: 'export declare class SecondaryClass {};'
'index.js': 'export * from "./public_api";',
'index.metadata.json': '...',
'public_api.d.ts': 'export declare class OtherClass {};',
'public_api.js': 'export class OtherClass {};',
'es2015': {
'index.js': 'export * from "./public_api";',
'public_api.js': 'export class OtherClass {};',
}, },
{
name: _('/node_modules/test/secondary/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 fs = getFileSystem();
const esm5bundle = makeEntryPointBundle( const esm5bundle = makeEntryPointBundle(
fs, '/node_modules/test', './index.js', './index.d.ts', false, 'esm5', 'esm5', true) !; fs, '/node_modules/test', './index.js', './index.d.ts', false, 'esm5', 'esm5', true) !;
@ -104,7 +146,7 @@ describe('entry point bundle', () => {
// 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([
@ -117,6 +159,7 @@ describe('entry point bundle', () => {
'/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,23 +5,27 @@
* 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;
let _: typeof absoluteFrom;
beforeEach(() => { beforeEach(() => {
const fs = createMockFileSystem(); const fs = getFileSystem();
_ = absoluteFrom;
setupMockFileSystem();
resolver = new DependencyResolver( resolver = new DependencyResolver(
fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))}); fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))});
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => {
@ -100,98 +104,99 @@ describe('findEntryPoints()', () => {
]); ]);
}); });
function createMockFileSystem() { function setupMockFileSystem(): void {
return new MockFileSystem({ loadTestFiles([
'/sub_entry_points': { {name: _('/sub_entry_points/common/package.json'), contents: createPackageJson('common')},
'common': { {name: _('/sub_entry_points/common/common.metadata.json'), contents: 'metadata info'},
'package.json': createPackageJson('common'), {
'common.metadata.json': 'metadata info', name: _('/sub_entry_points/common/http/package.json'),
'http': { contents: createPackageJson('http')
'package.json': createPackageJson('http'),
'http.metadata.json': 'metadata info',
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
}, },
{name: _('/sub_entry_points/common/http/http.metadata.json'), contents: 'metadata info'},
{
name: _('/sub_entry_points/common/http/testing/package.json'),
contents: createPackageJson('testing')
}, },
'testing': { {
'package.json': createPackageJson('testing'), name: _('/sub_entry_points/common/http/testing/testing.metadata.json'),
'testing.metadata.json': 'metadata info', 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'
}, },
'/pathMappings': { {name: _('/pathMappings/dist/my-lib/package.json'), contents: createPackageJson('my-lib')},
'dist': { {name: _('/pathMappings/dist/my-lib/my-lib.metadata.json'), contents: 'metadata info'},
'my-lib': { {
'package.json': createPackageJson('my-lib'), name: _('/pathMappings/dist/my-lib/sub-lib/package.json'),
'my-lib.metadata.json': 'metadata info', contents: createPackageJson('sub-lib')
'sub-lib': {
'package.json': createPackageJson('sub-lib'),
'sub-lib.metadata.json': 'metadata info',
}, },
{
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')
}, },
'node_modules': { {
'@angular': { name: _('/pathMappings/node_modules/@angular/common/common.metadata.json'),
'common': { contents: 'metadata info'
'package.json': createPackageJson('common'),
'common.metadata.json': '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')
}, },
'/namespaced': { {name: _('/namespaced/@angular/common/http/http.metadata.json'), contents: 'metadata info'},
'@angular': { {
'common': { name: _('/namespaced/@angular/common/http/testing/package.json'),
'package.json': createPackageJson('common'), contents: createPackageJson('testing')
'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',
}, },
{
name: _('/namespaced/@angular/common/http/testing/testing.metadata.json'),
contents: 'metadata info'
}, },
'testing': { {
'package.json': createPackageJson('testing'), name: _('/namespaced/@angular/common/testing/package.json'),
'testing.metadata.json': 'metadata info', 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'
}, },
'/no_packages': {'should_not_be_found': {}}, ]);
'/no_valid_entry_points': { const fs = getFileSystem();
'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',
},
},
},
},
});
}
});
function createPackageJson(packageName: string): string { 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 = { const packageJson: any = {
typings: `./${packageName}.d.ts`, typings: `./${packageName}.d.ts`,
fesm2015: `./fesm2015/${packageName}.js`, fesm2015: `./fesm2015/${packageName}.js`,
@ -201,4 +206,5 @@ function createPackageJson(packageName: string): string {
main: `./bundles/${packageName}.umd.js`, main: `./bundles/${packageName}.umd.js`,
}; };
return JSON.stringify(packageJson); return JSON.stringify(packageJson);
} }
});

View File

@ -6,20 +6,27 @@
* 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({
@ -33,14 +40,12 @@ describe('getEntryPointInfo()', () => {
}); });
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);
@ -48,7 +53,6 @@ describe('getEntryPointInfo()', () => {
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({
@ -62,7 +66,6 @@ describe('getEntryPointInfo()', () => {
}); });
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({
@ -76,7 +79,6 @@ describe('getEntryPointInfo()', () => {
}); });
it('should work with Angular Material style package.json', () => { it('should work with Angular Material style package.json', () => {
const fs = createMockFileSystem();
const entryPoint = const entryPoint =
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style')); getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style'));
expect(entryPoint).toEqual({ expect(entryPoint).toEqual({
@ -90,62 +92,82 @@ describe('getEntryPointInfo()', () => {
}); });
it('should return null if the package.json is not valid JSON', () => { it('should return null if the package.json is not valid JSON', () => {
const fs = createMockFileSystem();
const entryPoint = getEntryPointInfo( const entryPoint = getEntryPointInfo(
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols')); fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols'));
expect(entryPoint).toBe(null); expect(entryPoint).toBe(null);
}); });
}); });
function createMockFileSystem() { function setupMockFileSystem(): void {
return new MockFileSystem({ const _ = absoluteFrom;
'/some_package': { loadTestFiles([
'valid_entry_point': { {
'package.json': createPackageJson('valid_entry_point'), name: _('/some_package/valid_entry_point/package.json'),
'valid_entry_point.metadata.json': 'some meta data', contents: createPackageJson('valid_entry_point')
},
{
name: _('/some_package/valid_entry_point/valid_entry_point.metadata.json'),
contents: 'some meta data'
}, },
'missing_package_json': {
// no package.json! // no package.json!
'missing_package_json.metadata.json': 'some meta data', {
name: _('/some_package/missing_package_json/missing_package_json.metadata.json'),
contents: 'some meta data'
}, },
'missing_typings': { {
'package.json': createPackageJson('missing_typings', {excludes: ['typings']}), name: _('/some_package/missing_typings/package.json'),
'missing_typings.metadata.json': 'some meta data', contents: createPackageJson('missing_typings', {excludes: ['typings']})
}, },
'types_rather_than_typings': { {
'package.json': createPackageJson('types_rather_than_typings', {}, 'types'), name: _('/some_package/missing_typings/missing_typings.metadata.json'),
'types_rather_than_typings.metadata.json': 'some meta data', contents: 'some meta data'
}, },
'missing_esm2015': { {
'package.json': createPackageJson('missing_fesm2015', {excludes: ['esm2015', 'fesm2015']}), name: _('/some_package/types_rather_than_typings/package.json'),
'missing_esm2015.metadata.json': 'some meta data', contents: createPackageJson('types_rather_than_typings', {}, 'types')
},
{
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'
}, },
'missing_metadata': {
'package.json': createPackageJson('missing_metadata'),
// no metadata.json! // no metadata.json!
{
name: _('/some_package/missing_metadata/package.json'),
contents: createPackageJson('missing_metadata')
}, },
'material_style': { {
'package.json': `{ 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', },
{
name: _('/some_package/material_style/material_style.metadata.json'),
contents: 'some meta data'
}, },
'unexpected_symbols': {
// package.json might not be a valid JSON // package.json might not be a valid JSON
// for example, @schematics/angular contains a package.json blueprint // for example, @schematics/angular contains a package.json blueprint
// with unexpected symbols // with unexpected symbols
'package.json': {
'{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}', name: _('/some_package/unexpected_symbols/package.json'),
contents: '{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}'
}, },
]);
} }
});
}
function createPackageJson( function createPackageJson(
packageName: string, {excludes}: {excludes?: string[]} = {}, packageName: string, {excludes}: {excludes?: string[]} = {},
typingsProp: string = 'typings'): string { typingsProp: string = 'typings'): string {
const packageJson: any = { const packageJson: any = {
@ -161,8 +183,9 @@ function createPackageJson(
excludes.forEach(exclude => delete packageJson[exclude]); excludes.forEach(exclude => delete packageJson[exclude]);
} }
return JSON.stringify(packageJson); 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,42 +8,28 @@
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]);
const typeChecker = bundle.src.program.getTypeChecker();
const host = new CommonJsReflectionHost(logger, false, bundle.src.program, bundle.src.host);
const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses =
new DecorationAnalyzer(
fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new CommonJsRenderingFormatter(host, false);
const importManager = new ImportManager(new NoopImportRewriter(), 'i');
return {
host,
program: bundle.src.program,
sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager
};
}
const PROGRAM = {
name: _('/some/file.js'), name: _('/some/file.js'),
contents: ` contents: `
/* A copyright notice */ /* A copyright notice */
@ -105,9 +91,9 @@ 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");
@ -156,9 +142,36 @@ 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', () => {
@ -211,9 +224,9 @@ exports.TopLevelComponent = TopLevelComponent;`);
renderer.addExports( renderer.addExports(
output, _(PROGRAM.name.replace(/\.js$/, '')), output, _(PROGRAM.name.replace(/\.js$/, '')),
[ [
{from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'},
{from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'},
{from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'},
{from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'},
], ],
importManager, sourceFile); importManager, sourceFile);
@ -227,10 +240,7 @@ exports.TopLevelComponent = TopLevelComponent;`);
describe('addConstants', () => { describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => { it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'var x = 3;', file); renderer.addConstants(output, 'var x = 3;', file);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
@ -242,10 +252,7 @@ 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) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'var x = 3;', file); renderer.addConstants(output, 'var x = 3;', file);
renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
@ -261,10 +268,7 @@ 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) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations( renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
@ -275,9 +279,11 @@ var A = (function() {`);
expect(output.toString()) expect(output.toString())
.toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
}); });
}); });
@ -302,26 +308,27 @@ SOME DEFINITION TEXT
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', () => { describe('removeDecorators', () => {
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); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
@ -333,9 +340,11 @@ SOME DEFINITION TEXT
expect(output.toString()) expect(output.toString())
.not.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()).toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); expect(output.toString())
.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); expect(output.toString())
.toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`);
}); });
@ -377,14 +386,16 @@ SOME DEFINITION TEXT
expect(output.toString()) expect(output.toString())
.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()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); expect(output.toString())
.toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`);
expect(output.toString()).not.toContain(`C.decorators`); expect(output.toString()).not.toContain(`C.decorators`);
}); });
}); });
describe('[__decorate declarations]', () => { describe('[__decorate declarations]', () => {
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 {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 =
@ -437,4 +448,5 @@ SOME DEFINITION TEXT
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,74 +91,52 @@ 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 = {
name: _('/src/file.js'),
contents: 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` `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 = { INPUT_DTS_PROGRAM = {
name: '/typings/file.d.ts', name: _('/typings/file.d.ts'),
contents: `export declare class A {\nfoo(x: number): number;\n}\n` contents: `export declare class A {\nfoo(x: number): number;\n}\n`
}; };
const INPUT_PROGRAM_MAP = fromObject({
'version': 3,
'file': '/src/file.js',
'sourceRoot': '',
'sources': ['/src/file.ts'],
'names': [],
'mappings':
'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC',
'sourcesContent': [INPUT_PROGRAM.contents]
});
const RENDERED_CONTENTS = `
// ADD IMPORTS
// ADD EXPORTS
// ADD CONSTANTS
// ADD DEFINITIONS
// REMOVE DECORATORS
` + INPUT_PROGRAM.contents;
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({
'version': 3,
'sources': ['/src/file.ts'],
'names': [],
'mappings': ';;;;;;;;;;AAAA',
'file': 'file.js',
'sourcesContent': [INPUT_PROGRAM.contents]
}); });
it('should render extract types into typings files', () => { it('should render extract types into typings files', () => {
const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = const {renderer, decorationAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); 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) expect(typingsFile.contents)
.toContain( .toContain(
'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵɵDirectiveDefWithMeta'); 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵɵDirectiveDefWithMeta');
}); });
it('should render imports into typings files', () => { it('should render imports into typings files', () => {
const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = const {renderer, decorationAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); 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 IMPORTS\n`);
}); });
it('should render exports into typings files', () => { it('should render exports into typings files', () => {
const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = const {renderer, decorationAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
// Add a mock export to trigger export rendering // Add a mock export to trigger export rendering
@ -164,18 +146,20 @@ describe('DtsRenderer', () => {
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); 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 EXPORTS\n`); expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`);
}); });
it('should render ModuleWithProviders type params', () => { it('should render ModuleWithProviders type params', () => {
const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = const {renderer, decorationAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); 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 MODUlE WITH PROVIDERS PARAMS\n`); expect(typingsFile.contents).toContain(`\n// ADD MODUlE WITH PROVIDERS PARAMS\n`);
}); });
});
}); });

View File

@ -8,31 +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 {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);
@ -44,7 +44,16 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) {
}; };
} }
const PROGRAM = { runInEachFileSystem(() => {
describe('Esm5RenderingFormatter', () => {
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
let PROGRAM_DECORATE_HELPER: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/some/file.js'), name: _('/some/file.js'),
contents: ` contents: `
/* A copyright notice */ /* A copyright notice */
@ -102,9 +111,9 @@ 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";
@ -153,9 +162,8 @@ var D = /** @class */ (function () {
}()); }());
export { D }; export { D };
// Some other content` // Some other content`
}; };
});
describe('Esm5RenderingFormatter', () => {
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', () => {
@ -203,9 +211,9 @@ export {TopLevelComponent};`);
renderer.addExports( renderer.addExports(
output, _(PROGRAM.name.replace(/\.js$/, '')), output, _(PROGRAM.name.replace(/\.js$/, '')),
[ [
{from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'},
{from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'},
{from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'},
{from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'},
], ],
importManager, sourceFile); importManager, sourceFile);
@ -219,10 +227,7 @@ export {TopLevelComponent};`);
describe('addConstants', () => { describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => { it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'var x = 3;', file); renderer.addConstants(output, 'var x = 3;', file);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
@ -234,10 +239,7 @@ 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) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'var x = 3;', file); renderer.addConstants(output, 'var x = 3;', file);
renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
@ -253,10 +255,7 @@ 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) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations( renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
@ -267,9 +266,11 @@ var A = (function() {`);
expect(output.toString()) expect(output.toString())
.toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
}); });
}); });
@ -294,26 +295,27 @@ SOME DEFINITION TEXT
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', () => { describe('removeDecorators', () => {
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); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
@ -322,7 +324,8 @@ SOME DEFINITION TEXT
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(`{ type: Directive, args: [{ selector: '[a]' }] },`); expect(output.toString())
.not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).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 }`);
@ -364,14 +367,16 @@ SOME DEFINITION TEXT
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).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()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); expect(output.toString())
.toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`);
expect(output.toString()).not.toContain(`C.decorators`); expect(output.toString()).not.toContain(`C.decorators`);
}); });
}); });
describe('[__decorate declarations]', () => { describe('[__decorate declarations]', () => {
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 {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 =
@ -424,4 +429,5 @@ SOME DEFINITION TEXT
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,7 +49,16 @@ function setup(
}; };
} }
const PROGRAM = { runInEachFileSystem(() => {
describe('EsmRenderingFormatter', () => {
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/some/file.js'), name: _('/some/file.js'),
contents: ` contents: `
/* A copyright notice */ /* A copyright notice */
@ -81,9 +92,8 @@ 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', () => { 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', () => {
@ -147,10 +157,7 @@ export {TopLevelComponent};`);
describe('addConstants', () => { describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => { it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup([PROGRAM]); const {renderer, program} = setup([PROGRAM]);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file); renderer.addConstants(output, 'const x = 3;', file);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
@ -162,10 +169,7 @@ 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) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file); renderer.addConstants(output, 'const x = 3;', file);
renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
@ -181,10 +185,7 @@ 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) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations( renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
@ -195,9 +196,11 @@ export class A {`);
expect(output.toString()) expect(output.toString())
.toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); .toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
}); });
}); });
@ -233,9 +236,11 @@ A.decorators = [
expect(output.toString()) expect(output.toString())
.not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); .not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).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()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); expect(output.toString())
.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
}); });
@ -249,12 +254,14 @@ A.decorators = [
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())
.toContain(`{ type: 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: 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(`{ type: Directive, args: [{ selector: '[c]' }] }`); expect(output.toString())
.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
}); });
@ -268,9 +275,11 @@ A.decorators = [
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())
.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`); expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).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())
.not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
@ -280,9 +289,11 @@ A.decorators = [
}); });
describe('[__decorate declarations]', () => { describe('[__decorate declarations]', () => {
let PROGRAM_DECORATE_HELPER: TestFile;
const PROGRAM_DECORATE_HELPER = { beforeEach(() => {
name: '/some/file.js', PROGRAM_DECORATE_HELPER = {
name: _('/some/file.js'),
contents: ` contents: `
import * as tslib_1 from "tslib"; import * as tslib_1 from "tslib";
var D_1; var D_1;
@ -318,8 +329,10 @@ D = D_1 = tslib_1.__decorate([
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 {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 =
@ -374,9 +387,12 @@ export { D };
}); });
describe('addModuleWithProvidersParams', () => { describe('addModuleWithProvidersParams', () => {
const MODULE_WITH_PROVIDERS_PROGRAM = [ let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[];
let MODULE_WITH_PROVIDERS_DTS_PROGRAM: TestFile[];
beforeEach(() => {
MODULE_WITH_PROVIDERS_PROGRAM = [
{ {
name: '/src/index.js', name: _('/src/index.js'),
contents: ` contents: `
import {ExternalModule} from './module'; import {ExternalModule} from './module';
import {LibraryModule} from 'some-library'; import {LibraryModule} from 'some-library';
@ -397,11 +413,13 @@ 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', name: _('/src/module.js'),
contents: ` contents: `
export class ExternalModule { export class ExternalModule {
static withProviders1() { return {ngModule: ExternalModule}; } static withProviders1() { return {ngModule: ExternalModule}; }
@ -409,13 +427,14 @@ export { D };
}` }`
}, },
{ {
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', name: _('/typings/index.d.ts'),
contents: ` contents: `
import {ModuleWithProviders} from '@angular/core'; import {ModuleWithProviders} from '@angular/core';
export declare class SomeClass {} export declare class SomeClass {}
@ -437,10 +456,12 @@ 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', name: _('/typings/module.d.ts'),
contents: ` contents: `
export interface ModuleWithProviders {} export interface ModuleWithProviders {}
export declare class ExternalModule { export declare class ExternalModule {
@ -449,19 +470,21 @@ export { D };
}` }`
}, },
{ {
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 =
new ModuleWithProvidersAnalyzer(host, referencesRegistry)
.analyzeProgram(bundle.src.program); .analyzeProgram(bundle.src.program);
const typingsFile = bundle.dts !.program.getSourceFile('/typings/index.d.ts') !; const typingsFile = getSourceFileOrError(bundle.dts !.program, _('/typings/index.d.ts'));
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; 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);
@ -497,7 +520,8 @@ export { D };
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 =
getSourceFileOrError(bundle.dts !.program, _('/typings/module.d.ts'));
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; 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);
@ -508,4 +532,5 @@ export { D };
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',
INPUT_PROGRAM = {
name: _('/src/file.js'),
contents: 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` `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 = { COMPONENT_PROGRAM = {
name: '/src/component.js', name: _('/src/component.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 { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n`
}; };
const INPUT_PROGRAM_MAP = fromObject({ INPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
'file': '/src/file.js', 'file': _('/src/file.js'),
'sourceRoot': '', 'sourceRoot': '',
'sources': ['/src/file.ts'], 'sources': [_('/src/file.ts')],
'names': [], 'names': [],
'mappings': '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', '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] 'sourcesContent': [INPUT_PROGRAM.contents]
}); });
const RENDERED_CONTENTS = ` RENDERED_CONTENTS = `
// ADD IMPORTS // ADD IMPORTS
// ADD EXPORTS // ADD EXPORTS
@ -124,23 +140,24 @@ 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.',
@ -149,10 +166,10 @@ describe('Renderer', () => {
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());
}); });
@ -160,7 +177,8 @@ describe('Renderer', () => {
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(
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(
@ -199,8 +217,8 @@ describe('Renderer', () => {
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(
@ -218,7 +236,8 @@ describe('Renderer', () => {
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
@ -237,7 +256,7 @@ describe('Renderer', () => {
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';
@ -285,7 +304,8 @@ describe('Renderer', () => {
describe('source map merging', () => { describe('source map merging', () => {
it('should merge any inline source map from the original file and write the output as an inline source map', it('should merge any inline source map from the original file and write the output as an inline source map',
() => { () => {
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {decorationAnalyses, renderer, switchMarkerAnalyses,
privateDeclarationsAnalyses} =
createTestRenderer( createTestRenderer(
'test-package', [{ 'test-package', [{
...INPUT_PROGRAM, ...INPUT_PROGRAM,
@ -293,7 +313,7 @@ describe('Renderer', () => {
}]); }]);
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' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment());
expect(result[1]).toBeUndefined(); expect(result[1]).toBeUndefined();
@ -301,34 +321,36 @@ describe('Renderer', () => {
it('should merge any external source map from the original file and write the output to an external source map', it('should merge any external source map from the original file and write the output to an external source map',
() => { () => {
const sourceFiles = [{ const sourceFiles: TestFile[] = [{
...INPUT_PROGRAM, ...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
}]; }];
const mappingFiles = const mappingFiles: TestFile[] =
[{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}]; [{name: _(INPUT_PROGRAM.name + '.map'), contents: INPUT_PROGRAM_MAP.toJSON()}];
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {decorationAnalyses, renderer, switchMarkerAnalyses,
privateDeclarationsAnalyses} =
createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); createTestRenderer('test-package', sourceFiles, undefined, mappingFiles);
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(JSON.parse(result[1].contents)).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject()); expect(JSON.parse(result[1].contents)).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject());
}); });
}); });
describe('@angular/core support', () => { describe('@angular/core support', () => {
it('should render relative imports in ESM bundles', () => { it('should render relative imports in ESM bundles', () => {
const CORE_FILE = { const CORE_FILE: TestFile = {
name: '/src/core.js', name: _('/src/core.js'),
contents: contents:
`import { NgModule } from './ng_module';\nexport class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n` `import { NgModule } from './ng_module';\nexport class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
}; };
const R3_SYMBOLS_FILE = { const R3_SYMBOLS_FILE: TestFile = {
// r3_symbols in the file name indicates that this is the path to rewrite core imports to // r3_symbols in the file name indicates that this is the path to rewrite core imports
name: '/src/r3_symbols.js', // to
name: _('/src/r3_symbols.js'),
contents: `export const NgModule = () => null;` contents: `export const NgModule = () => null;`
}; };
// The package name of `@angular/core` indicates that we are compiling the core library. // The package name of `@angular/core` indicates that we are compiling the core library.
@ -346,8 +368,8 @@ describe('Renderer', () => {
}); });
it('should render no imports in FESM bundles', () => { it('should render no imports in FESM bundles', () => {
const CORE_FILE = { const CORE_FILE: TestFile = {
name: '/src/core.js', name: _('/src/core.js'),
contents: `export const NgModule = () => null; 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`
}; };
@ -364,4 +386,5 @@ describe('Renderer', () => {
}); });
}); });
}); });
});
}); });

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,7 +46,17 @@ function setup(file: {name: string, contents: string}) {
}; };
} }
const PROGRAM = { runInEachFileSystem(() => {
describe('UmdRenderingFormatter', () => {
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
let PROGRAM_DECORATE_HELPER: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/some/file.js'), name: _('/some/file.js'),
contents: ` contents: `
/* A copyright notice */ /* A copyright notice */
@ -111,11 +122,11 @@ 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) {
@ -167,14 +178,13 @@ 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', () => { describe('addImports', () => {
it('should append the given imports into the CommonJS factory call', () => { it('should append the given imports into the CommonJS factory call', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js') !; const file = getSourceFileOrError(program, _('/some/file.js'));
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(
output, output,
@ -190,7 +200,7 @@ describe('UmdRenderingFormatter', () => {
it('should append the given imports into the AMD initialization', () => { it('should append the given imports into the AMD initialization', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js') !; const file = getSourceFileOrError(program, _('/some/file.js'));
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(
output, output,
@ -206,7 +216,7 @@ describe('UmdRenderingFormatter', () => {
it('should append the given imports into the global initialization', () => { it('should append the given imports into the global initialization', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js') !; const file = getSourceFileOrError(program, _('/some/file.js'));
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(
output, output,
@ -220,9 +230,10 @@ describe('UmdRenderingFormatter', () => {
`(factory(global.file,global.someSideEffect,global.localDep,global.ng.core,global.ng.core,global.ng.common));`); `(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', () => { it('should append the given imports as parameters into the factory function definition',
() => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js') !; const file = getSourceFileOrError(program, _('/some/file.js'));
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(
output, output,
@ -240,7 +251,8 @@ describe('UmdRenderingFormatter', () => {
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);
const generateNamedImportSpy = spyOn(importManager, 'generateNamedImport').and.callThrough(); const generateNamedImportSpy =
spyOn(importManager, 'generateNamedImport').and.callThrough();
renderer.addExports( renderer.addExports(
output, PROGRAM.name.replace(/\.js$/, ''), output, PROGRAM.name.replace(/\.js$/, ''),
[ [
@ -290,10 +302,7 @@ exports.TopLevelComponent = TopLevelComponent;
describe('addConstants', () => { describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => { it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup(PROGRAM); const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js'); const file = getSourceFileOrError(program, _('/some/file.js'));
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'var x = 3;', file); renderer.addConstants(output, 'var x = 3;', file);
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
@ -313,10 +322,7 @@ 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) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations( renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
@ -327,9 +333,11 @@ var A = (function() {`);
expect(output.toString()) expect(output.toString())
.toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString()) expect(output.toString())
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); .toContain(
`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
}); });
}); });
@ -354,25 +362,26 @@ SOME DEFINITION TEXT
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', () => { describe('removeDecorators', () => {
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); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const compiledClass = const compiledClass =
@ -384,9 +393,11 @@ SOME DEFINITION TEXT
expect(output.toString()) expect(output.toString())
.not.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()).toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); expect(output.toString())
.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); expect(output.toString())
.toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`);
}); });
@ -434,7 +445,8 @@ SOME DEFINITION TEXT
}); });
describe('[__decorate declarations]', () => { describe('[__decorate declarations]', () => {
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 {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 =
@ -487,4 +499,5 @@ SOME DEFINITION TEXT
expect(output.toString()).toContain(`function C() {\n }\n return C;`); expect(output.toString()).toContain(`function C() {\n }\n return C;`);
}); });
}); });
});
}); });

View File

@ -5,34 +5,32 @@
* 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': { beforeEach(() => {
'top-level.js': 'ORIGINAL TOP LEVEL', _ = absoluteFrom;
'folder-1': { loadTestFiles([
'file-1.js': 'ORIGINAL FILE 1', {name: _('/package/path/top-level.js'), contents: 'ORIGINAL TOP LEVEL'},
'file-2.js': 'ORIGINAL FILE 2', {name: _('/package/path/folder-1/file-1.js'), contents: 'ORIGINAL FILE 1'},
}, {name: _('/package/path/folder-1/file-2.js'), contents: 'ORIGINAL FILE 2'},
'folder-2': { {name: _('/package/path/folder-2/file-3.js'), contents: 'ORIGINAL FILE 3'},
'file-3.js': 'ORIGINAL FILE 3', {name: _('/package/path/folder-2/file-4.js'), contents: 'ORIGINAL FILE 4'},
'file-4.js': 'ORIGINAL FILE 4', {name: _('/package/path/already-backed-up.js.__ivy_ngcc_bak'), contents: 'BACKED UP'},
}, ]);
'already-backed-up.js.__ivy_ngcc_bak': 'BACKED UP',
}
}); });
}
describe('InPlaceFileWriter', () => {
it('should write all the FileInfo to the disk', () => { 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'},
@ -49,7 +47,7 @@ describe('InPlaceFileWriter', () => {
}); });
it('should create backups of all files that previously existed', () => { 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);
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'},
@ -69,7 +67,7 @@ describe('InPlaceFileWriter', () => {
}); });
it('should error if the backup file already exists', () => { it('should error if the backup file already exists', () => {
const fs = createMockFileSystem(); const fs = getFileSystem();
const fileWriter = new InPlaceFileWriter(fs); const fileWriter = new InPlaceFileWriter(fs);
const absoluteBackupPath = _('/package/path/already-backed-up.js'); const absoluteBackupPath = _('/package/path/already-backed-up.js');
expect( expect(
@ -81,4 +79,5 @@ describe('InPlaceFileWriter', () => {
.toThrowError( .toThrowError(
`Tried to overwrite ${absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`); `Tried to overwrite ${absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`);
}); });
});
}); });

View File

@ -5,82 +5,86 @@
* 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({
'/node_modules/test': {
'package.json':
'{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}',
'index.d.ts': 'export declare class FooTop {}',
'index.d.ts.map': 'ORIGINAL MAPPING DATA',
'index.metadata.json': '...',
'esm5.js': 'export function FooTop() {}',
'esm5.js.map': 'ORIGINAL MAPPING DATA',
'es2015': {
'index.js': 'export {FooTop} from "./foo";',
'foo.js': 'export class FooTop {}',
},
'a': {
'package.json':
'{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}',
'index.d.ts': 'export declare class FooA {}',
'index.metadata.json': '...',
'esm5.js': 'export function FooA() {}',
'es2015': {
'index.js': 'export {FooA} from "./foo";',
'foo.js': 'export class FooA {}',
},
},
'b': {
// This entry-point points to files outside its folder
'package.json':
'{"module": "../lib/esm5.js", "es2015": "../lib/es2015/index.js", "typings": "../typings/index.d.ts"}',
},
'lib': {
'esm5.js': 'export function FooB() {}',
'es2015': {
'index.js': 'export {FooB} from "./foo"; import * from "other";',
'foo.js': 'import {FooA} from "test/a"; import "events"; export class FooB {}',
},
},
'typings': {
'index.d.ts': 'export declare class FooB {}',
'index.metadata.json': '...',
}
},
'/node_modules/other': {
'package.json': '{"module": "./esm5.js", "typings": "./index.d.ts"}',
'index.d.ts': 'export declare class OtherClass {}',
'esm5.js': 'export class OtherClass {}',
},
'/node_modules/events': {
'package.json': '{"main": "./events.js"}',
'events.js': 'export class OtherClass {}',
},
});
}
describe('NewEntryPointFileWriter', () => {
let fs: FileSystem; let fs: FileSystem;
let fileWriter: FileWriter; let fileWriter: FileWriter;
let entryPoint: EntryPoint; let entryPoint: EntryPoint;
let esm5bundle: EntryPointBundle; let esm5bundle: EntryPointBundle;
let esm2015bundle: EntryPointBundle; let esm2015bundle: EntryPointBundle;
beforeEach(() => {
_ = absoluteFrom;
loadTestFiles([
{
name: _('/node_modules/test/package.json'),
contents:
'{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}'
},
{name: _('/node_modules/test/index.d.ts'), contents: 'export declare class FooTop {}'},
{name: _('/node_modules/test/index.d.ts.map'), contents: 'ORIGINAL MAPPING DATA'},
{name: _('/node_modules/test/index.metadata.json'), contents: '...'},
{name: _('/node_modules/test/esm5.js'), contents: 'export function FooTop() {}'},
{name: _('/node_modules/test/esm5.js.map'), contents: 'ORIGINAL MAPPING DATA'},
{name: _('/node_modules/test/es2015/index.js'), contents: 'export {FooTop} from "./foo";'},
{name: _('/node_modules/test/es2015/foo.js'), contents: 'export class FooTop {}'},
{
name: _('/node_modules/test/a/package.json'),
contents:
`{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}`
},
{name: _('/node_modules/test/a/index.d.ts'), contents: 'export declare class FooA {}'},
{name: _('/node_modules/test/a/index.metadata.json'), contents: '...'},
{name: _('/node_modules/test/a/esm5.js'), contents: 'export function FooA() {}'},
{name: _('/node_modules/test/a/es2015/index.js'), contents: 'export {FooA} from "./foo";'},
{name: _('/node_modules/test/a/es2015/foo.js'), contents: 'export class FooA {}'},
{
name: _('/node_modules/test/b/package.json'),
// This entry-point points to files outside its folder
contents:
`{"module": "../lib/esm5.js", "es2015": "../lib/es2015/index.js", "typings": "../typings/index.d.ts"}`
},
{name: _('/node_modules/test/lib/esm5.js'), contents: 'export function FooB() {}'},
{
name: _('/node_modules/test/lib/es2015/index.js'),
contents: 'export {FooB} from "./foo"; import * from "other";'
},
{
name: _('/node_modules/test/lib/es2015/foo.js'),
contents: 'import {FooA} from "test/a"; import "events"; export class FooB {}'
},
{
name: _('/node_modules/test/typings/index.d.ts'),
contents: 'export declare class FooB {}'
},
{name: _('/node_modules/test/typings/index.metadata.json'), contents: '...'},
{
name: _('/node_modules/other/package.json'),
contents: '{"module": "./esm5.js", "typings": "./index.d.ts"}'
},
{name: _('/node_modules/other/index.d.ts'), contents: 'export declare class OtherClass {}'},
{name: _('/node_modules/other/esm5.js'), contents: 'export class OtherClass {}'},
{name: _('/node_modules/events/package.json'), contents: '{"main": "./events.js"}'},
{name: _('/node_modules/events/events.js'), contents: 'export class OtherClass {}'},
]);
});
describe('writeBundle() [primary entry-point]', () => { describe('writeBundle() [primary entry-point]', () => {
beforeEach(() => { beforeEach(() => {
fs = createMockFileSystem(); fs = getFileSystem();
fileWriter = new NewEntryPointFileWriter(fs); fileWriter = new NewEntryPointFileWriter(fs);
entryPoint = getEntryPointInfo( entryPoint = getEntryPointInfo(
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !; fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !;
@ -113,7 +117,8 @@ describe('NewEntryPointFileWriter', () => {
]); ]);
expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/foo.js'))) expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/foo.js')))
.toEqual('export class FooTop {} // MODIFIED'); .toEqual('export class FooTop {} // MODIFIED');
expect(fs.readFile(_('/node_modules/test/es2015/foo.js'))).toEqual('export class FooTop {}'); expect(fs.readFile(_('/node_modules/test/es2015/foo.js')))
.toEqual('export class FooTop {}');
expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/index.js'))) expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/index.js')))
.toEqual('export {FooTop} from "./foo";'); .toEqual('export {FooTop} from "./foo";');
expect(fs.readFile(_('/node_modules/test/es2015/index.js'))) expect(fs.readFile(_('/node_modules/test/es2015/index.js')))
@ -157,7 +162,8 @@ describe('NewEntryPointFileWriter', () => {
.toEqual('export declare class FooTop {}'); .toEqual('export declare class FooTop {}');
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts'))).toBe(false); 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')))
.toEqual('MODIFIED MAPPING DATA');
expect(fs.readFile(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak'))) expect(fs.readFile(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak')))
.toEqual('ORIGINAL MAPPING DATA'); .toEqual('ORIGINAL MAPPING DATA');
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts.map'))).toBe(false); expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts.map'))).toBe(false);
@ -166,7 +172,7 @@ describe('NewEntryPointFileWriter', () => {
describe('writeBundle() [secondary entry-point]', () => { describe('writeBundle() [secondary entry-point]', () => {
beforeEach(() => { beforeEach(() => {
fs = createMockFileSystem(); fs = getFileSystem();
fileWriter = new NewEntryPointFileWriter(fs); fileWriter = new NewEntryPointFileWriter(fs);
entryPoint = getEntryPointInfo( entryPoint = getEntryPointInfo(
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !; fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !;
@ -195,7 +201,8 @@ describe('NewEntryPointFileWriter', () => {
]); ]);
expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js'))) expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js')))
.toEqual('export class FooA {} // MODIFIED'); .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/a/es2015/foo.js')))
.toEqual('export class FooA {}');
expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/index.js'))) expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/index.js')))
.toEqual('export {FooA} from "./foo";'); .toEqual('export {FooA} from "./foo";');
expect(fs.readFile(_('/node_modules/test/a/es2015/index.js'))) expect(fs.readFile(_('/node_modules/test/a/es2015/index.js')))
@ -242,7 +249,7 @@ describe('NewEntryPointFileWriter', () => {
describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => { describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => {
beforeEach(() => { beforeEach(() => {
fs = createMockFileSystem(); fs = getFileSystem();
fileWriter = new NewEntryPointFileWriter(fs); fileWriter = new NewEntryPointFileWriter(fs);
entryPoint = getEntryPointInfo( entryPoint = getEntryPointInfo(
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !; fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !;
@ -259,7 +266,8 @@ describe('NewEntryPointFileWriter', () => {
]); ]);
expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/esm5.js'))) expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/esm5.js')))
.toEqual('export function FooB() {} // MODIFIED'); .toEqual('export function FooB() {} // MODIFIED');
expect(fs.readFile(_('/node_modules/test/lib/esm5.js'))).toEqual('export function FooB() {}'); expect(fs.readFile(_('/node_modules/test/lib/esm5.js')))
.toEqual('export function FooB() {}');
}); });
it('should also copy unmodified files in the program', () => { it('should also copy unmodified files in the program', () => {
@ -338,12 +346,13 @@ describe('NewEntryPointFileWriter', () => {
expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/typings/index.d.ts'))).toBe(false); expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/typings/index.d.ts'))).toBe(false);
}); });
}); });
}); });
function makeTestBundle( function makeTestBundle(
fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty,
format: EntryPointFormat): EntryPointBundle { format: EntryPointFormat): EntryPointBundle {
return makeEntryPointBundle( return makeEntryPointBundle(
fs, entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false, fs, entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false,
formatProperty, format, true) !; 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,16 +24,19 @@ 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';
@ -49,8 +54,8 @@ describe('ComponentDecoratorHandler', () => {
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([]);
@ -58,7 +63,7 @@ describe('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');
@ -76,8 +81,7 @@ describe('ComponentDecoratorHandler', () => {
expect(diag.start).toBe(detected.metadata.args ![0].getStart()); 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,24 +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 {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';
@ -47,9 +52,10 @@ describe('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 =
handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode));
if (detected === undefined) { if (detected === undefined) {
throw new Error(`Failed to recognize @Directive (${dirName}).`); throw new Error(`Failed to recognize @Directive (${dirName}).`);
} }
@ -72,11 +78,12 @@ describe('DirectiveDecoratorHandler', () => {
const analysis2 = analyzeDirective('TestDir2'); const analysis2 = analyzeDirective('TestDir2');
expect(analysis2.meta.usesInheritance).toBe(true); expect(analysis2.meta.usesInheritance).toBe(true);
}); });
}); });
// Helpers // Helpers
class TestReflectionHost extends TypeScriptReflectionHost { class TestReflectionHost extends TypeScriptReflectionHost {
hasBaseClassReturnValue = false; hasBaseClassReturnValue = false;
hasBaseClass(clazz: ClassDeclaration): boolean { return this.hasBaseClassReturnValue; } hasBaseClass(clazz: ClassDeclaration): boolean { return this.hasBaseClassReturnValue; }
} }
});

View File

@ -5,26 +5,17 @@
* 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: `
export declare function Input(...args: any[]): any;
export declare function Inject(...args: any[]): any;
export declare function Component(...args: any[]): any;
export declare class Injector {}
`
};
describe('ngtsc setClassMetadata converter', () => {
it('should convert decorated class metadata', () => { it('should convert decorated class metadata', () => {
const res = compileAndPrint(` const res = compileAndPrint(`
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@ -71,24 +62,36 @@ describe('ngtsc setClassMetadata converter', () => {
`); `);
expect(res).toBe(''); expect(res).toBe('');
}); });
}); });
function compileAndPrint(contents: string): string {
const _ = absoluteFrom;
const CORE: TestFile = {
name: _('/node_modules/@angular/core/index.d.ts'),
contents: `
export declare function Input(...args: any[]): any;
export declare function Inject(...args: any[]): any;
export declare function Component(...args: any[]): any;
export declare class Injector {}
`
};
function compileAndPrint(contents: string): string {
const {program} = makeProgram([ const {program} = makeProgram([
CORE, { CORE, {
name: 'index.ts', name: _('/index.ts'),
contents, contents,
} }
]); ]);
const host = new TypeScriptReflectionHost(program.getTypeChecker()); const host = new TypeScriptReflectionHost(program.getTypeChecker());
const target = getDeclaration(program, 'index.ts', 'Target', ts.isClassDeclaration); const target = getDeclaration(program, _('/index.ts'), 'Target', ts.isClassDeclaration);
const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false); const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false);
if (call === null) { if (call === null) {
return ''; return '';
} }
const sf = program.getSourceFile('index.ts') !; const sf = getSourceFileOrError(program, _('/index.ts'));
const im = new ImportManager(new NoopImportRewriter(), 'i'); const im = new ImportManager(new NoopImportRewriter(), 'i');
const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER); const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf); const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' '); return res.replace(/\s+/g, ' ');
} }
});

View File

@ -5,25 +5,27 @@
* 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(() => {
describe('NgModuleDecoratorHandler', () => {
it('should resolve forwardRef', () => { it('should resolve forwardRef', () => {
const _ = absoluteFrom;
const {program} = makeProgram([ const {program} = makeProgram([
{ {
name: 'node_modules/@angular/core/index.d.ts', name: _('/node_modules/@angular/core/index.d.ts'),
contents: ` contents: `
export const Component: any; export const Component: any;
export const NgModule: any; export const NgModule: any;
@ -31,7 +33,7 @@ describe('NgModuleDecoratorHandler', () => {
`, `,
}, },
{ {
name: 'entry.ts', name: _('/entry.ts'),
contents: ` contents: `
import {Component, forwardRef, NgModule} from '@angular/core'; import {Component, forwardRef, NgModule} from '@angular/core';
@ -59,14 +61,15 @@ describe('NgModuleDecoratorHandler', () => {
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 =
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
const detected = const detected =
handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule)); handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule));
if (detected === undefined) { if (detected === undefined) {
@ -82,5 +85,5 @@ describe('NgModuleDecoratorHandler', () => {
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(() => {
describe('cycle analyzer', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('should not detect a cycle when there isn\'t one', () => { it('should not detect a cycle when there isn\'t one', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = program.getSourceFile('b.ts') !; const b = getSourceFileOrError(program, (_('/b.ts')));
const c = program.getSourceFile('c.ts') !; const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(b, c)).toBe(false); expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, b)).toBe(false); expect(analyzer.wouldCreateCycle(c, b)).toBe(false);
}); });
it('should detect a simple cycle between two files', () => { it('should detect a simple cycle between two files', () => {
const {program, analyzer} = makeAnalyzer('a:b;b'); const {program, analyzer} = makeAnalyzer('a:b;b');
const a = program.getSourceFile('a.ts') !; const a = getSourceFileOrError(program, (_('/a.ts')));
const b = program.getSourceFile('b.ts') !; const b = getSourceFileOrError(program, (_('/b.ts')));
expect(analyzer.wouldCreateCycle(a, b)).toBe(false); expect(analyzer.wouldCreateCycle(a, b)).toBe(false);
expect(analyzer.wouldCreateCycle(b, a)).toBe(true); expect(analyzer.wouldCreateCycle(b, a)).toBe(true);
}); });
it('should detect a cycle with a re-export in the chain', () => { it('should detect a cycle with a re-export in the chain', () => {
const {program, analyzer} = makeAnalyzer('a:*b;b:c;c'); const {program, analyzer} = makeAnalyzer('a:*b;b:c;c');
const a = program.getSourceFile('a.ts') !; const a = getSourceFileOrError(program, (_('/a.ts')));
const c = program.getSourceFile('c.ts') !; const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(a, c)).toBe(false); expect(analyzer.wouldCreateCycle(a, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, a)).toBe(true); expect(analyzer.wouldCreateCycle(c, a)).toBe(true);
}); });
it('should detect a cycle in a more complex program', () => { 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 {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 b = getSourceFileOrError(program, (_('/b.ts')));
const g = program.getSourceFile('g.ts') !; const g = getSourceFileOrError(program, (_('/g.ts')));
expect(analyzer.wouldCreateCycle(b, g)).toBe(false); expect(analyzer.wouldCreateCycle(b, g)).toBe(false);
expect(analyzer.wouldCreateCycle(g, b)).toBe(true); expect(analyzer.wouldCreateCycle(g, b)).toBe(true);
}); });
it('should detect a cycle caused by a synthetic edge', () => { it('should detect a cycle caused by a synthetic edge', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = program.getSourceFile('b.ts') !; const b = getSourceFileOrError(program, (_('/b.ts')));
const c = program.getSourceFile('c.ts') !; const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(b, c)).toBe(false); expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
analyzer.recordSyntheticImport(c, b); analyzer.recordSyntheticImport(c, b);
expect(analyzer.wouldCreateCycle(b, c)).toBe(true); expect(analyzer.wouldCreateCycle(b, c)).toBe(true);
}); });
}); });
function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} { function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} {
const {program, options, host} = makeProgramFromGraph(graph); const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph);
return { return {
program, program,
analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))), 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(() => {
describe('import graph', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('should record imports of a simple program', () => { it('should record imports of a simple program', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c'); const {program, graph} = makeImportGraph('a:b;b:c;c');
const a = program.getSourceFile('a.ts') !; const a = getSourceFileOrError(program, (_('/a.ts')));
const b = program.getSourceFile('b.ts') !; const b = getSourceFileOrError(program, (_('/b.ts')));
const c = program.getSourceFile('c.ts') !; const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.importsOf(a))).toBe('b'); expect(importsToString(graph.importsOf(a))).toBe('b');
expect(importsToString(graph.importsOf(b))).toBe('c'); expect(importsToString(graph.importsOf(b))).toBe('c');
}); });
it('should calculate transitive imports of a simple program', () => { it('should calculate transitive imports of a simple program', () => {
const {program, graph} = makeImportGraph('a:b;b:c;c'); const {program, graph} = makeImportGraph('a:b;b:c;c');
const a = program.getSourceFile('a.ts') !; const a = getSourceFileOrError(program, (_('/a.ts')));
const b = program.getSourceFile('b.ts') !; const b = getSourceFileOrError(program, (_('/b.ts')));
const c = program.getSourceFile('c.ts') !; const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c'); expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c');
}); });
it('should calculate transitive imports in a more complex program (with a cycle)', () => { 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 {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
const c = program.getSourceFile('c.ts') !; const c = getSourceFileOrError(program, (_('/c.ts')));
expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h'); expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h');
}); });
it('should reflect the addition of a synthetic import', () => { it('should reflect the addition of a synthetic import', () => {
const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b'); const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b');
const b = program.getSourceFile('b.ts') !; const b = getSourceFileOrError(program, (_('/b.ts')));
const c = program.getSourceFile('c.ts') !; const c = getSourceFileOrError(program, (_('/c.ts')));
const d = program.getSourceFile('d.ts') !; const d = getSourceFileOrError(program, (_('/d.ts')));
expect(importsToString(graph.importsOf(b))).toEqual(''); expect(importsToString(graph.importsOf(b))).toEqual('');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d'); expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d');
graph.addSyntheticImport(b, c); graph.addSyntheticImport(b, c);
expect(importsToString(graph.importsOf(b))).toEqual('c'); expect(importsToString(graph.importsOf(b))).toEqual('c');
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d'); expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d');
}); });
}); });
function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} { function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} {
const {program, options, host} = makeProgramFromGraph(graph); const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph);
return { return {
program, program,
graph: new ImportGraph(new ModuleResolver(program, options, host)), graph: new ImportGraph(new ModuleResolver(program, options, host)),
}; };
} }
function importsToString(imports: Set<ts.SourceFile>): string { function importsToString(imports: Set<ts.SourceFile>): string {
return Array.from(imports).map(sf => sf.fileName.substr(1).replace('.ts', '')).sort().join(','); const fs = getFileSystem();
} return Array.from(imports)
.map(sf => fs.basename(sf.fileName).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