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/diagnostics",
"//packages/compiler-cli/src/ngtsc/entry_point",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/routing",

View File

@ -5,6 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NodeJSFileSystem, setFileSystem} from './src/ngtsc/file_system';
export {AotCompilerHost, AotCompilerHost as StaticReflectorHost, StaticReflector, StaticSymbol} from '@angular/compiler';
export {DiagnosticTemplateInfo, getExpressionScope, getTemplateExpressionDiagnostics} from './src/diagnostics/expression_diagnostics';
export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expression_type';
@ -26,3 +28,5 @@ export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools
export {ngToTsDiagnostic} from './src/transformers/util';
export {NgTscPlugin} from './src/ngtsc/tsc_plugin';
setFileSystem(new NodeJSFileSystem());

View File

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

View File

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

View File

@ -8,7 +8,7 @@
*/
import * as yargs from 'yargs';
import {AbsoluteFsPath} from '../src/ngtsc/path';
import {resolve, setFileSystem, NodeJSFileSystem} from '../src/ngtsc/file_system';
import {mainNgcc} from './src/main';
import {ConsoleLogger, LogLevel} from './src/logging/console_logger';
@ -56,7 +56,10 @@ if (require.main === module) {
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
process.exit(1);
}
const baseSourcePath = AbsoluteFsPath.resolve(options['s'] || './node_modules');
setFileSystem(new NodeJSFileSystem());
const baseSourcePath = resolve(options['s'] || './node_modules');
const propertiesToConsider: string[] = options['p'];
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
const compileAllFormats = !options['first-only'];

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 {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
import {AbsoluteFsPath, FileSystem, LogicalFileSystem, absoluteFrom, dirname, resolve} from '../../../src/ngtsc/file_system';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path';
import {ClassDeclaration, ClassSymbol, Decorator} from '../../../src/ngtsc/reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {FileSystem} from '../file_system/file_system';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
@ -57,9 +56,9 @@ class NgccResourceLoader implements ResourceLoader {
constructor(private fs: FileSystem) {}
canPreload = false;
preload(): undefined|Promise<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 {
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 {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {AbsoluteFsPath, absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {Declaration} from '../../../src/ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host';
import {hasNameIdentifier, isDefined} from '../utils';
@ -94,12 +94,11 @@ export class PrivateDeclarationsAnalyzer {
});
return Array.from(privateDeclarations.keys()).map(id => {
const from = AbsoluteFsPath.fromSourceFile(id.getSourceFile());
const from = absoluteFromSourceFile(id.getSourceFile());
const declaration = privateDeclarations.get(id) !;
const alias = exportAliasDeclarations.has(id) ? exportAliasDeclarations.get(id) ! : null;
const dtsDeclaration = this.host.getDtsDeclaration(declaration.node);
const dtsFrom =
dtsDeclaration && AbsoluteFsPath.fromSourceFile(dtsDeclaration.getSourceFile());
const dtsFrom = dtsDeclaration && absoluteFromSourceFile(dtsDeclaration.getSourceFile());
return {identifier: id.text, from, dtsFrom, alias};
});

View File

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

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/file_system';
export interface DependencyHost {
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo;

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,74 +5,19 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as os from 'os';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {FileSystem} from '../../../src/ngtsc/file_system';
import {NgtscCompilerHost} from '../../../src/ngtsc/file_system/src/compiler_host';
import {isRelativePath} from '../utils';
export class NgccCompilerHost implements ts.CompilerHost {
private _caseSensitive = this.fs.exists(AbsoluteFsPath.fromUnchecked(__filename.toUpperCase()));
constructor(protected fs: FileSystem, protected options: ts.CompilerOptions) {}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
const text = this.readFile(fileName);
return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion) : undefined;
}
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.getDefaultLibLocation() + '/' + ts.getDefaultLibFileName(options);
}
getDefaultLibLocation(): string {
const nodeLibPath = AbsoluteFsPath.from(require.resolve('typescript'));
return AbsoluteFsPath.join(nodeLibPath, '..');
}
writeFile(fileName: string, data: string): void {
this.fs.writeFile(AbsoluteFsPath.fromUnchecked(fileName), data);
}
getCurrentDirectory(): string { return this.fs.pwd(); }
getCanonicalFileName(fileName: string): string {
return this.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
}
useCaseSensitiveFileNames(): boolean { return this._caseSensitive; }
getNewLine(): string {
switch (this.options.newLine) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return os.EOL;
}
}
fileExists(fileName: string): boolean {
return this.fs.exists(AbsoluteFsPath.fromUnchecked(fileName));
}
readFile(fileName: string): string|undefined {
if (!this.fileExists(fileName)) {
return undefined;
}
return this.fs.readFile(AbsoluteFsPath.fromUnchecked(fileName));
}
}
/**
* Represents a compiler host that resolves a module import as a JavaScript source file if
* available, instead of the .d.ts typings file that would have been resolved by TypeScript. This
* is necessary for packages that have their typings in the same directory as the sources, which
* would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file.
*/
export class NgccSourcesCompilerHost extends NgccCompilerHost {
export class NgccSourcesCompilerHost extends NgtscCompilerHost {
private cache = ts.createModuleResolutionCache(
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,238 +7,267 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {Decorator} from '../../../src/ngtsc/reflection';
import {DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Folder, MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger';
import {createFileSystemFromProgramFiles, makeTestBundleProgram} from '../helpers/utils';
const _ = AbsoluteFsPath.fromUnchecked;
const TEST_PROGRAM = [
{
name: _('/test.js'),
contents: `
import {Component, Directive, Injectable} from '@angular/core';
export class MyComponent {}
MyComponent.decorators = [{type: Component}];
export class MyDirective {}
MyDirective.decorators = [{type: Directive}];
export class MyService {}
MyService.decorators = [{type: Injectable}];
`,
},
{
name: _('/other.js'),
contents: `
import {Component} from '@angular/core';
export class MyOtherComponent {}
MyOtherComponent.decorators = [{type: Component}];
`,
},
];
const INTERNAL_COMPONENT_PROGRAM = [
{
name: _('/entrypoint.js'),
contents: `
import {Component, NgModule} from '@angular/core';
import {ImportedComponent} from './component';
export class LocalComponent {}
LocalComponent.decorators = [{type: Component}];
export class MyModule {}
MyModule.decorators = [{type: NgModule, args: [{
declarations: [ImportedComponent, LocalComponent],
exports: [ImportedComponent, LocalComponent],
},] }];
`
},
{
name: _('/component.js'),
contents: `
import {Component} from '@angular/core';
export class ImportedComponent {}
ImportedComponent.decorators = [{type: Component}];
`,
isRoot: false,
}
];
import {getRootFiles, makeTestBundleProgram} from '../helpers/utils';
type DecoratorHandlerWithResolve = DecoratorHandler<any, any>& {
resolve: NonNullable<DecoratorHandler<any, any>['resolve']>;
};
describe('DecorationAnalyzer', () => {
describe('analyzeProgram()', () => {
let logs: string[];
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
let result: DecorationAnalyses;
runInEachFileSystem(() => {
describe('DecorationAnalyzer', () => {
let _: typeof absoluteFrom;
// Helpers
const createTestHandler = () => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect',
'analyze',
'resolve',
'compile',
]);
// Only detect the Component and Directive decorators
handler.detect.and.callFake(
(node: ts.Declaration, decorators: Decorator[] | null): DetectResult<any>| undefined => {
const className = (node as any).name.text;
if (decorators === null) {
logs.push(`detect: ${className} (no decorators)`);
} else {
logs.push(`detect: ${className}@${decorators.map(d => d.name)}`);
}
if (!decorators) {
return undefined;
}
const metadata = decorators.find(d => d.name === 'Component' || d.name === 'Directive');
if (metadata === undefined) {
return undefined;
} else {
return {
metadata,
trigger: metadata.node,
};
}
});
// The "test" analysis is an object with the name of the decorator being analyzed
handler.analyze.and.callFake((decl: ts.Declaration, dec: Decorator) => {
logs.push(`analyze: ${(decl as any).name.text}@${dec.name}`);
return {analysis: {decoratorName: dec.name}, diagnostics: undefined};
});
// The "test" resolution is just setting `resolved: true` on the analysis
handler.resolve.and.callFake((decl: ts.Declaration, analysis: any) => {
logs.push(`resolve: ${(decl as any).name.text}@${analysis.decoratorName}`);
analysis.resolved = true;
});
// The "test" compilation result is just the name of the decorator being compiled
// (suffixed with `(compiled)`)
handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => {
logs.push(
`compile: ${(decl as any).name.text}@${analysis.decoratorName} (resolved: ${analysis.resolved})`);
return `@${analysis.decoratorName} (compiled)`;
});
return handler;
};
beforeEach(() => { _ = absoluteFrom; });
const setUpAndAnalyzeProgram = (...progArgs: Parameters<typeof makeTestBundleProgram>) => {
logs = [];
describe('analyzeProgram()', () => {
let logs: string[];
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
let result: DecorationAnalyses;
const {options, host, ...bundle} = makeTestBundleProgram(...progArgs);
program = bundle.program;
// Helpers
const createTestHandler = () => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect',
'analyze',
'resolve',
'compile',
]);
// Only detect the Component and Directive decorators
handler.detect.and.callFake(
(node: ts.Declaration, decorators: Decorator[] | null): DetectResult<any>|
undefined => {
const className = (node as any).name.text;
if (decorators === null) {
logs.push(`detect: ${className} (no decorators)`);
} else {
logs.push(`detect: ${className}@${decorators.map(d => d.name)}`);
}
if (!decorators) {
return undefined;
}
const metadata =
decorators.find(d => d.name === 'Component' || d.name === 'Directive');
if (metadata === undefined) {
return undefined;
} else {
return {
metadata,
trigger: metadata.node,
};
}
});
// The "test" analysis is an object with the name of the decorator being analyzed
handler.analyze.and.callFake((decl: ts.Declaration, dec: Decorator) => {
logs.push(`analyze: ${(decl as any).name.text}@${dec.name}`);
return {analysis: {decoratorName: dec.name}, diagnostics: undefined};
});
// The "test" resolution is just setting `resolved: true` on the analysis
handler.resolve.and.callFake((decl: ts.Declaration, analysis: any) => {
logs.push(`resolve: ${(decl as any).name.text}@${analysis.decoratorName}`);
analysis.resolved = true;
});
// The "test" compilation result is just the name of the decorator being compiled
// (suffixed with `(compiled)`)
handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => {
logs.push(
`compile: ${(decl as any).name.text}@${analysis.decoratorName} (resolved: ${analysis.resolved})`);
return `@${analysis.decoratorName} (compiled)`;
});
return handler;
};
const reflectionHost =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const fs = new MockFileSystem(createFileSystemFromProgramFiles(...progArgs));
const analyzer = new DecorationAnalyzer(
fs, program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry,
[AbsoluteFsPath.fromUnchecked('/')], false);
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram();
};
function setUpAndAnalyzeProgram(testFiles: TestFile[]) {
logs = [];
loadTestFiles(testFiles);
loadFakeCore(getFileSystem());
const rootFiles = getRootFiles(testFiles);
const {options, host, ...bundle} = makeTestBundleProgram(rootFiles[0]);
program = bundle.program;
describe('basic usage', () => {
beforeEach(() => setUpAndAnalyzeProgram(TEST_PROGRAM));
const reflectionHost =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer = new DecorationAnalyzer(
getFileSystem(), program, options, host, program.getTypeChecker(), reflectionHost,
referencesRegistry, [absoluteFrom('/')], false);
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram();
}
it('should return an object containing a reference to the original source file', () => {
TEST_PROGRAM.forEach(({name}) => {
const file = program.getSourceFile(name) !;
expect(result.get(file) !.sourceFile).toBe(file);
describe('basic usage', () => {
beforeEach(() => {
const TEST_PROGRAM = [
{
name: _('/index.js'),
contents: `
import * as test from './test';
import * as other from './other';
`,
},
{
name: _('/test.js'),
contents: `
import {Component, Directive, Injectable} from '@angular/core';
export class MyComponent {}
MyComponent.decorators = [{type: Component}];
export class MyDirective {}
MyDirective.decorators = [{type: Directive}];
export class MyService {}
MyService.decorators = [{type: Injectable}];
`,
},
{
name: _('/other.js'),
contents: `
import {Component} from '@angular/core';
export class MyOtherComponent {}
MyOtherComponent.decorators = [{type: Component}];
`,
},
];
setUpAndAnalyzeProgram(TEST_PROGRAM);
});
it('should return an object containing a reference to the original source file', () => {
const testFile = getSourceFileOrError(program, _('/test.js'));
expect(result.get(testFile) !.sourceFile).toBe(testFile);
const otherFile = getSourceFileOrError(program, _('/other.js'));
expect(result.get(otherFile) !.sourceFile).toBe(otherFile);
});
it('should call detect on the decorator handlers with each class from the parsed file',
() => {
expect(testHandler.detect).toHaveBeenCalledTimes(11);
expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([
null,
null,
null,
null,
null,
null,
null,
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Injectable'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]),
]);
});
it('should return an object containing the classes that were analyzed', () => {
const file1 = getSourceFileOrError(program, _('/test.js'));
const compiledFile1 = result.get(file1) !;
expect(compiledFile1.compiledClasses.length).toEqual(2);
expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({
name: 'MyComponent', compilation: ['@Component (compiled)'],
} as unknown as CompiledClass));
expect(compiledFile1.compiledClasses[1]).toEqual(jasmine.objectContaining({
name: 'MyDirective', compilation: ['@Directive (compiled)'],
} as unknown as CompiledClass));
const file2 = getSourceFileOrError(program, _('/other.js'));
const compiledFile2 = result.get(file2) !;
expect(compiledFile2.compiledClasses.length).toEqual(1);
expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({
name: 'MyOtherComponent', compilation: ['@Component (compiled)'],
} as unknown as CompiledClass));
});
it('should analyze, resolve and compile the classes that are detected', () => {
expect(logs).toEqual([
// Classes without decorators should also be detected.
'detect: ChangeDetectorRef (no decorators)',
'detect: ElementRef (no decorators)',
'detect: Injector (no decorators)',
'detect: TemplateRef (no decorators)',
'detect: ViewContainerRef (no decorators)',
'detect: Renderer2 (no decorators)',
'detect: ɵNgModuleFactory (no decorators)',
// First detect and (potentially) analyze.
'detect: MyComponent@Component',
'analyze: MyComponent@Component',
'detect: MyDirective@Directive',
'analyze: MyDirective@Directive',
'detect: MyService@Injectable',
'detect: MyOtherComponent@Component',
'analyze: MyOtherComponent@Component',
// The resolve.
'resolve: MyComponent@Component',
'resolve: MyDirective@Directive',
'resolve: MyOtherComponent@Component',
// Finally compile.
'compile: MyComponent@Component (resolved: true)',
'compile: MyDirective@Directive (resolved: true)',
'compile: MyOtherComponent@Component (resolved: true)',
]);
});
});
it('should call detect on the decorator handlers with each class from the parsed file',
() => {
expect(testHandler.detect).toHaveBeenCalledTimes(5);
expect(testHandler.detect.calls.allArgs().map(args => args[1])).toEqual([
null,
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Directive'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Injectable'})]),
jasmine.arrayContaining([jasmine.objectContaining({name: 'Component'})]),
]);
});
describe('internal components', () => {
beforeEach(() => {
const INTERNAL_COMPONENT_PROGRAM = [
{
name: _('/entrypoint.js'),
contents: `
import {Component, NgModule} from '@angular/core';
import {ImportedComponent} from './component';
it('should return an object containing the classes that were analyzed', () => {
const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !;
const compiledFile1 = result.get(file1) !;
expect(compiledFile1.compiledClasses.length).toEqual(2);
expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({
name: 'MyComponent', compilation: ['@Component (compiled)'],
} as unknown as CompiledClass));
expect(compiledFile1.compiledClasses[1]).toEqual(jasmine.objectContaining({
name: 'MyDirective', compilation: ['@Directive (compiled)'],
} as unknown as CompiledClass));
export class LocalComponent {}
LocalComponent.decorators = [{type: Component}];
const file2 = program.getSourceFile(TEST_PROGRAM[1].name) !;
const compiledFile2 = result.get(file2) !;
expect(compiledFile2.compiledClasses.length).toEqual(1);
expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({
name: 'MyOtherComponent', compilation: ['@Component (compiled)'],
} as unknown as CompiledClass));
});
export class MyModule {}
MyModule.decorators = [{type: NgModule, args: [{
declarations: [ImportedComponent, LocalComponent],
exports: [ImportedComponent, LocalComponent],
},] }];
`
},
{
name: _('/component.js'),
contents: `
import {Component} from '@angular/core';
export class ImportedComponent {}
ImportedComponent.decorators = [{type: Component}];
`,
isRoot: false,
}
];
setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM);
});
it('should analyze, resolve and compile the classes that are detected', () => {
expect(logs).toEqual([
// Classes without decorators should also be detected.
'detect: InjectionToken (no decorators)',
// First detect and (potentially) analyze.
'detect: MyComponent@Component',
'analyze: MyComponent@Component',
'detect: MyDirective@Directive',
'analyze: MyDirective@Directive',
'detect: MyService@Injectable',
'detect: MyOtherComponent@Component',
'analyze: MyOtherComponent@Component',
// The resolve.
'resolve: MyComponent@Component',
'resolve: MyDirective@Directive',
'resolve: MyOtherComponent@Component',
// Finally compile.
'compile: MyComponent@Component (resolved: true)',
'compile: MyDirective@Directive (resolved: true)',
'compile: MyOtherComponent@Component (resolved: true)',
]);
});
});
// The problem of exposing the type of these internal components in the .d.ts typing
// files is not yet solved.
it('should analyze an internally imported component, which is not publicly exported from the entry-point',
() => {
const file = getSourceFileOrError(program, _('/component.js'));
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const ImportedComponent =
analysis.compiledClasses.find(f => f.name === 'ImportedComponent') !;
expect(ImportedComponent).toBeDefined();
});
describe('internal components', () => {
beforeEach(() => setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM));
// The problem of exposing the type of these internal components in the .d.ts typing files
// is not yet solved.
it('should analyze an internally imported component, which is not publicly exported from the entry-point',
() => {
const file = program.getSourceFile('component.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const ImportedComponent =
analysis.compiledClasses.find(f => f.name === 'ImportedComponent') !;
expect(ImportedComponent).toBeDefined();
});
it('should analyze an internally defined component, which is not exported at all', () => {
const file = program.getSourceFile('entrypoint.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !;
expect(LocalComponent).toBeDefined();
it('should analyze an internally defined component, which is not exported at all', () => {
const file = getSourceFileOrError(program, _('/entrypoint.js'));
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !;
expect(LocalComponent).toBeDefined();
});
});
});
});

View File

@ -7,397 +7,415 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadTestFiles} from '../../../test/helpers';
import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {BundleProgram} from '../../src/packages/bundle_program';
import {MockLogger} from '../helpers/mock_logger';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils';
const TEST_PROGRAM = [
{
name: '/src/entry-point.js',
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: '/src/explicit.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ExplicitInternalModule {}
export function explicitInternalFunction() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
export function explicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function explicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class ExplicitClass {
static explicitInternalMethod() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
static explicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static explicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: '/src/any.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class AnyInternalModule {}
export function anyInternalFunction() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
export function anyExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function anyLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class AnyClass {
static anyInternalMethod() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
static anyExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static anyLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: '/src/implicit.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ImplicitInternalModule {}
export function implicitInternalFunction() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
export function implicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: [],
};
}
export function implicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: [],
};
}
export class ImplicitClass {
static implicitInternalMethod() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
static implicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: [],
};
}
static implicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: [],
};
}
}
`
},
{
name: '/src/no-providers.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class NoProvidersInternalModule {}
export function noProvExplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvExplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvExplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvAnyInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvAnyExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvAnyLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvImplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvImplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvImplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
`
},
{
name: '/src/module.js',
contents: `
export class ExternalModule {}
`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
const TEST_DTS_PROGRAM = [
{
name: '/typings/entry-point.d.ts',
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: '/typings/explicit.d.ts',
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ExplicitInternalModule {}
export declare function explicitInternalFunction(): ModuleWithProviders<ExplicitInternalModule>;
export declare function explicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function explicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare class ExplicitClass {
static explicitInternalMethod(): ModuleWithProviders<ExplicitInternalModule>;
static explicitExternalMethod(): ModuleWithProviders<ExternalModule>;
static explicitLibraryMethod(): ModuleWithProviders<LibraryModule>;
}
`
},
{
name: '/typings/any.d.ts',
contents: `
import {ModuleWithProviders} from './core';
export declare class AnyInternalModule {}
export declare function anyInternalFunction(): ModuleWithProviders<any>;
export declare function anyExternalFunction(): ModuleWithProviders<any>;
export declare function anyLibraryFunction(): ModuleWithProviders<any>;
export declare class AnyClass {
static anyInternalMethod(): ModuleWithProviders<any>;
static anyExternalMethod(): ModuleWithProviders<any>;
static anyLibraryMethod(): ModuleWithProviders<any>;
}
`
},
{
name: '/typings/implicit.d.ts',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ImplicitInternalModule {}
export declare function implicitInternalFunction(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
export declare function implicitExternalFunction(): { ngModule: typeof ExternalModule; providers: never[]; };
export declare function implicitLibraryFunction(): { ngModule: typeof LibraryModule; providers: never[]; };
export declare class ImplicitClass {
static implicitInternalMethod(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
static implicitExternalMethod(): { ngModule: typeof ExternalModule; providers: never[]; };
static implicitLibraryMethod(): { ngModule: typeof LibraryModule; providers: never[]; };
}
`
},
{
name: '/typings/no-providers.d.ts',
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class NoProvidersInternalModule {}
export declare function noProvExplicitInternalFunction(): ModuleWithProviders<NoProvidersInternalModule>;
export declare function noProvExplicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function noProvExplicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare function noProvAnyInternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyExternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyLibraryFunction(): ModuleWithProviders<any>;
export declare function noProvImplicitInternalFunction(): { ngModule: typeof NoProvidersInternalModule; };
export declare function noProvImplicitExternalFunction(): { ngModule: typeof ExternalModule; };
export declare function noProvImplicitLibraryFunction(): { ngModule: typeof LibraryModule; };
`
},
{
name: '/typings/module.d.ts',
contents: `
export declare class ExternalModule {}
`
},
{
name: '/typings/core.d.ts',
contents: `
runInEachFileSystem(() => {
describe('ModuleWithProvidersAnalyzer', () => {
describe('analyzeProgram()', () => {
let _: typeof absoluteFrom;
let analyses: ModuleWithProvidersAnalyses;
let program: ts.Program;
let dtsProgram: BundleProgram|null;
let referencesRegistry: NgccReferencesRegistry;
export declare interface Type<T> {
new (...args: any[]): T
}
export declare type Provider = any;
export declare interface ModuleWithProviders<T> {
ngModule: Type<T>
providers?: Provider[]
}
`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
beforeEach(() => {
_ = absoluteFrom;
describe('ModuleWithProvidersAnalyzer', () => {
describe('analyzeProgram()', () => {
let analyses: ModuleWithProvidersAnalyses;
let program: ts.Program;
let dtsProgram: BundleProgram;
let referencesRegistry: NgccReferencesRegistry;
const TEST_PROGRAM: TestFile[] = [
{
name: _('/src/entry-point.js'),
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: _('/src/explicit.js'),
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ExplicitInternalModule {}
export function explicitInternalFunction() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
export function explicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function explicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class ExplicitClass {
static explicitInternalMethod() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
static explicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static explicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: _('/src/any.js'),
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class AnyInternalModule {}
export function anyInternalFunction() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
export function anyExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function anyLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class AnyClass {
static anyInternalMethod() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
static anyExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static anyLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: _('/src/implicit.js'),
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ImplicitInternalModule {}
export function implicitInternalFunction() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
export function implicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: [],
};
}
export function implicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: [],
};
}
export class ImplicitClass {
static implicitInternalMethod() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
static implicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: [],
};
}
static implicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: [],
};
}
}
`
},
{
name: _('/src/no-providers.js'),
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class NoProvidersInternalModule {}
export function noProvExplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvExplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvExplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvAnyInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvAnyExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvAnyLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvImplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvImplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvImplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
`
},
{
name: _('/src/module.js'),
contents: `
export class ExternalModule {}
`
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
},
];
const TEST_DTS_PROGRAM: TestFile[] = [
{
name: _('/typings/entry-point.d.ts'),
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: _('/typings/explicit.d.ts'),
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ExplicitInternalModule {}
export declare function explicitInternalFunction(): ModuleWithProviders<ExplicitInternalModule>;
export declare function explicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function explicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare class ExplicitClass {
static explicitInternalMethod(): ModuleWithProviders<ExplicitInternalModule>;
static explicitExternalMethod(): ModuleWithProviders<ExternalModule>;
static explicitLibraryMethod(): ModuleWithProviders<LibraryModule>;
}
`
},
{
name: _('/typings/any.d.ts'),
contents: `
import {ModuleWithProviders} from './core';
export declare class AnyInternalModule {}
export declare function anyInternalFunction(): ModuleWithProviders<any>;
export declare function anyExternalFunction(): ModuleWithProviders<any>;
export declare function anyLibraryFunction(): ModuleWithProviders<any>;
export declare class AnyClass {
static anyInternalMethod(): ModuleWithProviders<any>;
static anyExternalMethod(): ModuleWithProviders<any>;
static anyLibraryMethod(): ModuleWithProviders<any>;
}
`
},
{
name: _('/typings/implicit.d.ts'),
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ImplicitInternalModule {}
export declare function implicitInternalFunction(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
export declare function implicitExternalFunction(): { ngModule: typeof ExternalModule; providers: never[]; };
export declare function implicitLibraryFunction(): { ngModule: typeof LibraryModule; providers: never[]; };
export declare class ImplicitClass {
static implicitInternalMethod(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
static implicitExternalMethod(): { ngModule: typeof ExternalModule; providers: never[]; };
static implicitLibraryMethod(): { ngModule: typeof LibraryModule; providers: never[]; };
}
`
},
{
name: _('/typings/no-providers.d.ts'),
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class NoProvidersInternalModule {}
export declare function noProvExplicitInternalFunction(): ModuleWithProviders<NoProvidersInternalModule>;
export declare function noProvExplicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function noProvExplicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare function noProvAnyInternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyExternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyLibraryFunction(): ModuleWithProviders<any>;
export declare function noProvImplicitInternalFunction(): { ngModule: typeof NoProvidersInternalModule; };
export declare function noProvImplicitExternalFunction(): { ngModule: typeof ExternalModule; };
export declare function noProvImplicitLibraryFunction(): { ngModule: typeof LibraryModule; };
`
},
{
name: _('/typings/module.d.ts'),
contents: `
export declare class ExternalModule {}
`
},
{
name: _('/typings/core.d.ts'),
contents: `
beforeAll(() => {
program = makeTestProgram(...TEST_PROGRAM);
dtsProgram = makeTestBundleProgram(TEST_DTS_PROGRAM);
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dtsProgram);
referencesRegistry = new NgccReferencesRegistry(host);
export declare interface Type<T> {
new (...args: any[]): T
}
export declare type Provider = any;
export declare interface ModuleWithProviders<T> {
ngModule: Type<T>
providers?: Provider[]
}
`
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
},
];
loadTestFiles(TEST_PROGRAM);
loadTestFiles(TEST_DTS_PROGRAM);
const bundle = makeTestEntryPointBundle(
'esm2015', 'esm2015', false, getRootFiles(TEST_PROGRAM),
getRootFiles(TEST_DTS_PROGRAM));
program = bundle.src.program;
dtsProgram = bundle.dts;
const host = new Esm2015ReflectionHost(
new MockLogger(), false, program.getTypeChecker(), dtsProgram);
referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry);
analyses = analyzer.analyzeProgram(program);
const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry);
analyses = analyzer.analyzeProgram(program);
});
it('should ignore declarations that already have explicit NgModule type params', () => {
expect(getAnalysisDescription(analyses, _('/typings/explicit.d.ts'))).toEqual([]);
});
it('should find declarations that use `any` for the NgModule type param', () => {
const anyAnalysis = getAnalysisDescription(analyses, _('/typings/any.d.ts'));
expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['anyInternalMethod', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should track internal module references in the references registry', () => {
const declarations = referencesRegistry.getDeclarationMap();
const externalModuleDeclaration = getDeclaration(
program, absoluteFrom('/src/module.js'), 'ExternalModule', ts.isClassDeclaration);
const libraryModuleDeclaration = getDeclaration(
program, absoluteFrom('/node_modules/some-library/index.d.ts'), 'LibraryModule',
ts.isClassDeclaration);
expect(declarations.has(externalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false);
});
it('should find declarations that have implicit return types', () => {
const anyAnalysis = getAnalysisDescription(analyses, _('/typings/implicit.d.ts'));
expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['implicitInternalMethod', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should find declarations that do not specify a `providers` property in the return type',
() => {
const anyAnalysis = getAnalysisDescription(analyses, _('/typings/no-providers.d.ts'));
expect(anyAnalysis).not.toContain([
'noProvExplicitInternalFunction', 'NoProvidersInternalModule'
]);
expect(anyAnalysis).not.toContain([
'noProvExplicitExternalFunction', 'ExternalModule', null
]);
expect(anyAnalysis).toContain([
'noProvAnyInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain(['noProvAnyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain([
'noProvAnyLibraryFunction', 'LibraryModule', 'some-library'
]);
expect(anyAnalysis).toContain([
'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain([
'noProvImplicitExternalFunction', 'ExternalModule', null
]);
expect(anyAnalysis).toContain([
'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library'
]);
});
function getAnalysisDescription(
analyses: ModuleWithProvidersAnalyses, fileName: AbsoluteFsPath) {
const file = getSourceFileOrError(dtsProgram !.program, fileName);
const analysis = analyses.get(file);
return analysis ?
analysis.map(
info =>
[info.declaration.name !.getText(),
(info.ngModule.node as ts.ClassDeclaration).name !.getText(),
info.ngModule.viaModule]) :
[];
}
});
it('should ignore declarations that already have explicit NgModule type params',
() => { expect(getAnalysisDescription(analyses, '/typings/explicit.d.ts')).toEqual([]); });
it('should find declarations that use `any` for the NgModule type param', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/any.d.ts');
expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['anyInternalMethod', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should track internal module references in the references registry', () => {
const declarations = referencesRegistry.getDeclarationMap();
const externalModuleDeclaration =
getDeclaration(program, '/src/module.js', 'ExternalModule', ts.isClassDeclaration);
const libraryModuleDeclaration = getDeclaration(
program, '/node_modules/some-library/index.d.ts', 'LibraryModule', ts.isClassDeclaration);
expect(declarations.has(externalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false);
});
it('should find declarations that have implicit return types', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/implicit.d.ts');
expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['implicitInternalMethod', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should find declarations that do not specify a `providers` property in the return type',
() => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/no-providers.d.ts');
expect(anyAnalysis).not.toContain([
'noProvExplicitInternalFunction', 'NoProvidersInternalModule'
]);
expect(anyAnalysis).not.toContain([
'noProvExplicitExternalFunction', 'ExternalModule', null
]);
expect(anyAnalysis).toContain([
'noProvAnyInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain(['noProvAnyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain([
'noProvAnyLibraryFunction', 'LibraryModule', 'some-library'
]);
expect(anyAnalysis).toContain([
'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain(['noProvImplicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain([
'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library'
]);
});
function getAnalysisDescription(analyses: ModuleWithProvidersAnalyses, fileName: string) {
const file = dtsProgram.program.getSourceFile(fileName) !;
const analysis = analyses.get(file);
return analysis ?
analysis.map(
info =>
[info.declaration.name !.getText(),
(info.ngModule.node as ts.ClassDeclaration).name !.getText(),
info.ngModule.viaModule]) :
[];
}
});
});

View File

@ -5,241 +5,242 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom} from '../../../src/ngtsc/file_system';
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {Reference} from '../../../src/ngtsc/imports';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadTestFiles} from '../../../test/helpers/src/mock_file_loading';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {MockLogger} from '../helpers/mock_logger';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils';
const _ = AbsoluteFsPath.fromUnchecked;
runInEachFileSystem(() => {
describe('PrivateDeclarationsAnalyzer', () => {
describe('analyzeProgram()', () => {
it('should find all NgModule declarations that were not publicly exported from the entry-point',
() => {
const _ = absoluteFrom;
describe('PrivateDeclarationsAnalyzer', () => {
describe('analyzeProgram()', () => {
const TEST_PROGRAM = [
{
name: '/src/entry_point.js',
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: '/src/a.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class PublicComponent {}
PublicComponent.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
`
},
{
name: '/src/b.js',
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
class PrivateComponent1 {}
PrivateComponent1.decorators = [
{type: Component, args: [{selectors: 'b', template: ''}]}
];
class PrivateComponent2 {}
PrivateComponent2.decorators = [
{type: Component, args: [{selectors: 'c', template: ''}]}
];
export class ModuleB {}
ModuleB.decorators = [
{type: NgModule, args: [{declarations: [PrivateComponent1]}]}
];
`
},
{
name: '/src/c.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class InternalComponent1 {}
InternalComponent1.decorators = [
{type: Component, args: [{selectors: 'd', template: ''}]}
];
export class InternalComponent2 {}
InternalComponent2.decorators = [
{type: Component, args: [{selectors: 'e', template: ''}]}
];
`
},
{
name: '/src/mod.js',
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent1} from './c';
export class ModuleA {}
ModuleA.decorators = [
{type: NgModule, args: [{
declarations: [PublicComponent, InternalComponent1],
imports: [ModuleB]
}]}
];
`
}
];
const TEST_DTS_PROGRAM = [
{
name: '/typings/entry_point.d.ts',
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: '/typings/a.d.ts',
isRoot: false,
contents: `
export declare class PublicComponent {}
`
},
{
name: '/typings/b.d.ts',
isRoot: false,
contents: `
export declare class ModuleB {}
`
},
{
name: '/typings/c.d.ts',
isRoot: false,
contents: `
export declare class InternalComponent1 {}
`
},
{
name: '/typings/mod.d.ts',
isRoot: false,
contents: `
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent1} from './c';
export declare class ModuleA {}
`
},
];
it('should find all NgModule declarations that were not publicly exported from the entry-point',
() => {
const {program, referencesRegistry, analyzer} = setup(TEST_PROGRAM, TEST_DTS_PROGRAM);
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'PublicComponent');
addToReferencesRegistry(program, referencesRegistry, '/src/b.js', 'PrivateComponent1');
addToReferencesRegistry(program, referencesRegistry, '/src/c.js', 'InternalComponent1');
const analyses = analyzer.analyzeProgram(program);
// Note that `PrivateComponent2` and `InternalComponent2` are not found because they are
// not added to the ReferencesRegistry (i.e. they were not declared in an NgModule).
expect(analyses.length).toEqual(2);
expect(analyses).toEqual([
{identifier: 'PrivateComponent1', from: _('/src/b.js'), dtsFrom: null, alias: null},
{
identifier: 'InternalComponent1',
from: _('/src/c.js'),
dtsFrom: _('/typings/c.d.ts'),
alias: null
},
]);
});
const ALIASED_EXPORTS_PROGRAM = [
{
name: '/src/entry_point.js',
isRoot: true,
contents: `
// This component is only exported as an alias.
export {ComponentOne as aliasedComponentOne} from './a';
// This component is exported both as itself and an alias.
export {ComponentTwo as aliasedComponentTwo, ComponentTwo} from './a';
const TEST_PROGRAM: TestFile[] = [
{
name: _('/src/entry_point.js'),
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: '/src/a.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class ComponentOne {}
ComponentOne.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
export class ComponentTwo {}
Component.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
`
}
];
const ALIASED_EXPORTS_DTS_PROGRAM = [
{
name: '/typings/entry_point.d.ts',
isRoot: true,
contents: `
export declare class aliasedComponentOne {}
export declare class ComponentTwo {}
export {ComponentTwo as aliasedComponentTwo}
},
{
name: _('/src/a.js'),
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class PublicComponent {}
PublicComponent.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
`
},
];
},
{
name: _('/src/b.js'),
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
class PrivateComponent1 {}
PrivateComponent1.decorators = [
{type: Component, args: [{selectors: 'b', template: ''}]}
];
class PrivateComponent2 {}
PrivateComponent2.decorators = [
{type: Component, args: [{selectors: 'c', template: ''}]}
];
export class ModuleB {}
ModuleB.decorators = [
{type: NgModule, args: [{declarations: [PrivateComponent1]}]}
];
`
},
{
name: _('/src/c.js'),
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class InternalComponent1 {}
InternalComponent1.decorators = [
{type: Component, args: [{selectors: 'd', template: ''}]}
];
export class InternalComponent2 {}
InternalComponent2.decorators = [
{type: Component, args: [{selectors: 'e', template: ''}]}
];
`
},
{
name: _('/src/mod.js'),
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent1} from './c';
export class ModuleA {}
ModuleA.decorators = [
{type: NgModule, args: [{
declarations: [PublicComponent, InternalComponent1],
imports: [ModuleB]
}]}
];
`
}
];
const TEST_DTS_PROGRAM = [
{
name: _('/typings/entry_point.d.ts'),
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: _('/typings/a.d.ts'),
isRoot: false,
contents: `
export declare class PublicComponent {}
`
},
{
name: _('/typings/b.d.ts'),
isRoot: false,
contents: `
export declare class ModuleB {}
`
},
{
name: _('/typings/c.d.ts'),
isRoot: false,
contents: `
export declare class InternalComponent1 {}
`
},
{
name: _('/typings/mod.d.ts'),
isRoot: false,
contents: `
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent1} from './c';
export declare class ModuleA {}
`
},
];
const {program, referencesRegistry, analyzer} = setup(TEST_PROGRAM, TEST_DTS_PROGRAM);
it('should find all non-public declarations that were aliased', () => {
const {program, referencesRegistry, analyzer} =
setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM);
addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'PublicComponent');
addToReferencesRegistry(
program, referencesRegistry, _('/src/b.js'), 'PrivateComponent1');
addToReferencesRegistry(
program, referencesRegistry, _('/src/c.js'), 'InternalComponent1');
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentOne');
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentTwo');
const analyses = analyzer.analyzeProgram(program);
// Note that `PrivateComponent2` and `InternalComponent2` are not found because they are
// not added to the ReferencesRegistry (i.e. they were not declared in an NgModule).
expect(analyses.length).toEqual(2);
expect(analyses).toEqual([
{identifier: 'PrivateComponent1', from: _('/src/b.js'), dtsFrom: null, alias: null},
{
identifier: 'InternalComponent1',
from: _('/src/c.js'),
dtsFrom: _('/typings/c.d.ts'),
alias: null
},
]);
});
const analyses = analyzer.analyzeProgram(program);
expect(analyses).toEqual([{
identifier: 'ComponentOne',
from: _('/src/a.js'),
dtsFrom: null,
alias: 'aliasedComponentOne',
}]);
it('should find all non-public declarations that were aliased', () => {
const _ = absoluteFrom;
const ALIASED_EXPORTS_PROGRAM = [
{
name: _('/src/entry_point.js'),
isRoot: true,
contents: `
// This component is only exported as an alias.
export {ComponentOne as aliasedComponentOne} from './a';
// This component is exported both as itself and an alias.
export {ComponentTwo as aliasedComponentTwo, ComponentTwo} from './a';
`
},
{
name: _('/src/a.js'),
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class ComponentOne {}
ComponentOne.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
export class ComponentTwo {}
Component.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
`
}
];
const ALIASED_EXPORTS_DTS_PROGRAM = [
{
name: _('/typings/entry_point.d.ts'),
isRoot: true,
contents: `
export declare class aliasedComponentOne {}
export declare class ComponentTwo {}
export {ComponentTwo as aliasedComponentTwo}
`
},
];
const {program, referencesRegistry, analyzer} =
setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM);
addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'ComponentOne');
addToReferencesRegistry(program, referencesRegistry, _('/src/a.js'), 'ComponentTwo');
const analyses = analyzer.analyzeProgram(program);
expect(analyses).toEqual([{
identifier: 'ComponentOne',
from: _('/src/a.js'),
dtsFrom: null,
alias: 'aliasedComponentOne',
}]);
});
});
});
function setup(jsProgram: TestFile[], dtsProgram: TestFile[]) {
loadTestFiles(jsProgram);
loadTestFiles(dtsProgram);
const {src: {program}, dts} = makeTestEntryPointBundle(
'esm2015', 'esm2015', false, getRootFiles(jsProgram), getRootFiles(dtsProgram));
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts);
const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry);
return {program, referencesRegistry, analyzer};
}
/**
* Add up the named component to the references registry.
*
* This would normally be done by the decoration handlers in the `DecorationAnalyzer`.
*/
function addToReferencesRegistry(
program: ts.Program, registry: NgccReferencesRegistry, fileName: AbsoluteFsPath,
componentName: string) {
const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration);
registry.add(null !, new Reference(declaration));
}
});
type Files = {
name: string,
contents: string, isRoot?: boolean | undefined
}[];
function setup(jsProgram: Files, dtsProgram: Files) {
const program = makeTestProgram(...jsProgram);
const dts = makeTestBundleProgram(dtsProgram);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts);
const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry);
return {program, referencesRegistry, analyzer};
}
/**
* Add up the named component to the references registry.
*
* This would normally be done by the decoration handlers in the `DecorationAnalyzer`.
*/
function addToReferencesRegistry(
program: ts.Program, registry: NgccReferencesRegistry, fileName: string,
componentName: string) {
const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration);
registry.add(null !, new Reference(declaration));
}

View File

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

View File

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

View File

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

View File

@ -5,187 +5,207 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver';
import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
import {ModuleResolver} from '../../src/dependencies/module_resolver';
import {FileSystem} from '../../src/file_system/file_system';
import {EntryPoint} from '../../src/packages/entry_point';
import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.from;
interface DepMap {
[path: string]: {resolved: string[], missing: string[]};
}
describe('DependencyResolver', () => {
let host: EsmDependencyHost;
let resolver: DependencyResolver;
let fs: FileSystem;
let moduleResolver: ModuleResolver;
beforeEach(() => {
fs = new MockFileSystem();
moduleResolver = new ModuleResolver(fs);
host = new EsmDependencyHost(fs, moduleResolver);
resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host});
});
describe('sortEntryPointsByDependency()', () => {
const first = {
path: _('/first'),
packageJson: {esm5: './index.js'},
compiledByAngular: true
} as EntryPoint;
const second = {
path: _('/second'),
packageJson: {esm2015: './sub/index.js'},
compiledByAngular: true
} as EntryPoint;
const third = {
path: _('/third'),
packageJson: {fesm5: './index.js'},
compiledByAngular: true
} as EntryPoint;
const fourth = {
path: _('/fourth'),
packageJson: {fesm2015: './sub2/index.js'},
compiledByAngular: true
} as EntryPoint;
const fifth = {
path: _('/fifth'),
packageJson: {module: './index.js'},
compiledByAngular: true
} as EntryPoint;
runInEachFileSystem(() => {
describe('DependencyResolver', () => {
let _: typeof absoluteFrom;
let host: EsmDependencyHost;
let resolver: DependencyResolver;
let fs: FileSystem;
let moduleResolver: ModuleResolver;
const dependencies = {
[_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []},
[_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []},
[_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []},
[_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []},
[_('/fifth/index.js')]: {resolved: [], missing: []},
};
it('should order the entry points by their dependency on each other', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
beforeEach(() => {
_ = absoluteFrom;
fs = getFileSystem();
moduleResolver = new ModuleResolver(fs);
host = new EsmDependencyHost(fs, moduleResolver);
resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host});
});
it('should remove entry-points that have missing direct dependencies', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.js')]: {resolved: [], missing: ['/missing']},
[_('/second/sub/index.js')]: {resolved: [], missing: []},
}));
const result = resolver.sortEntryPointsByDependency([first, second]);
expect(result.entryPoints).toEqual([second]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: first, missingDependencies: ['/missing']},
]);
describe('sortEntryPointsByDependency()', () => {
let first: EntryPoint;
let second: EntryPoint;
let third: EntryPoint;
let fourth: EntryPoint;
let fifth: EntryPoint;
let dependencies: DepMap;
beforeEach(() => {
first = {
path: _('/first'),
packageJson: {esm5: './index.js'},
compiledByAngular: true
} as EntryPoint;
second = {
path: _('/second'),
packageJson: {esm2015: './sub/index.js'},
compiledByAngular: true
} as EntryPoint;
third = {
path: _('/third'),
packageJson: {fesm5: './index.js'},
compiledByAngular: true
} as EntryPoint;
fourth = {
path: _('/fourth'),
packageJson: {fesm2015: './sub2/index.js'},
compiledByAngular: true
} as EntryPoint;
fifth = {
path: _('/fifth'),
packageJson: {module: './index.js'},
compiledByAngular: true
} as EntryPoint;
dependencies = {
[_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []},
[_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []},
[_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []},
[_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []},
[_('/fifth/index.js')]: {resolved: [], missing: []},
};
});
it('should order the entry points by their dependency on each other', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
});
it('should remove entry-points that have missing direct dependencies', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.js')]: {resolved: [], missing: ['/missing']},
[_('/second/sub/index.js')]: {resolved: [], missing: []},
}));
const result = resolver.sortEntryPointsByDependency([first, second]);
expect(result.entryPoints).toEqual([second]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: first, missingDependencies: ['/missing']},
]);
});
it('should remove entry points that depended upon an invalid entry-point', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.js')]: {resolved: [second.path, third.path], missing: []},
[_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']},
[_('/third/index.js')]: {resolved: [], missing: []},
}));
// Note that we will process `first` before `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([first, second, third]);
expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: ['/missing']},
{entryPoint: first, missingDependencies: ['/missing']},
]);
});
it('should remove entry points that will depend upon an invalid entry-point', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.js')]: {resolved: [second.path, third.path], missing: []},
[_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']},
[_('/third/index.js')]: {resolved: [], missing: []},
}));
// Note that we will process `first` after `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([second, first, third]);
expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: ['/missing']},
{entryPoint: first, missingDependencies: [second.path]},
]);
});
it('should error if the entry point does not have a suitable format', () => {
expect(() => resolver.sortEntryPointsByDependency([
{ path: '/first', packageJson: {}, compiledByAngular: true } as EntryPoint
])).toThrowError(`There is no appropriate source code format in '/first' entry-point.`);
});
it('should error if there is no appropriate DependencyHost for the given formats', () => {
resolver = new DependencyResolver(fs, new MockLogger(), {esm2015: host});
expect(() => resolver.sortEntryPointsByDependency([first]))
.toThrowError(
`Could not find a suitable format for computing dependencies of entry-point: '${first.path}'.`);
});
it('should capture any dependencies that were ignored', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.ignoredDependencies).toEqual([
{entryPoint: first, dependencyPath: '/ignored-1'},
{entryPoint: third, dependencyPath: '/ignored-2'},
]);
});
it('should only return dependencies of the target, if provided', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const entryPoints = [fifth, first, fourth, second, third];
let sorted: SortedEntryPointsInfo;
sorted = resolver.sortEntryPointsByDependency(entryPoints, first);
expect(sorted.entryPoints).toEqual([fifth, fourth, third, second, first]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, second);
expect(sorted.entryPoints).toEqual([fifth, fourth, third, second]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, third);
expect(sorted.entryPoints).toEqual([fifth, fourth, third]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, fourth);
expect(sorted.entryPoints).toEqual([fifth, fourth]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, fifth);
expect(sorted.entryPoints).toEqual([fifth]);
});
it('should use the appropriate DependencyHost for each entry-point', () => {
const esm5Host = new EsmDependencyHost(fs, moduleResolver);
const esm2015Host = new EsmDependencyHost(fs, moduleResolver);
resolver =
new DependencyResolver(fs, new MockLogger(), {esm5: esm5Host, esm2015: esm2015Host});
spyOn(esm5Host, 'findDependencies')
.and.callFake(createFakeComputeDependencies(dependencies));
spyOn(esm2015Host, 'findDependencies')
.and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
expect(esm5Host.findDependencies).toHaveBeenCalledWith(fs.resolve(first.path, 'index.js'));
expect(esm5Host.findDependencies)
.not.toHaveBeenCalledWith(fs.resolve(second.path, 'sub/index.js'));
expect(esm5Host.findDependencies).toHaveBeenCalledWith(fs.resolve(third.path, 'index.js'));
expect(esm5Host.findDependencies)
.not.toHaveBeenCalledWith(fs.resolve(fourth.path, 'sub2/index.js'));
expect(esm5Host.findDependencies).toHaveBeenCalledWith(fs.resolve(fifth.path, 'index.js'));
expect(esm2015Host.findDependencies)
.not.toHaveBeenCalledWith(fs.resolve(first.path, 'index.js'));
expect(esm2015Host.findDependencies)
.toHaveBeenCalledWith(fs.resolve(second.path, 'sub/index.js'));
expect(esm2015Host.findDependencies)
.not.toHaveBeenCalledWith(fs.resolve(third.path, 'index.js'));
expect(esm2015Host.findDependencies)
.toHaveBeenCalledWith(fs.resolve(fourth.path, 'sub2/index.js'));
expect(esm2015Host.findDependencies)
.not.toHaveBeenCalledWith(fs.resolve(fifth.path, 'index.js'));
});
function createFakeComputeDependencies(deps: DepMap) {
return (entryPoint: string) => {
const dependencies = new Set();
const missing = new Set();
const deepImports = new Set();
deps[entryPoint].resolved.forEach(dep => dependencies.add(dep));
deps[entryPoint].missing.forEach(dep => missing.add(dep));
return {dependencies, missing, deepImports};
};
}
});
it('should remove entry points that depended upon an invalid entry-point', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.js')]: {resolved: [second.path, third.path], missing: []},
[_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']},
[_('/third/index.js')]: {resolved: [], missing: []},
}));
// Note that we will process `first` before `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([first, second, third]);
expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: ['/missing']},
{entryPoint: first, missingDependencies: ['/missing']},
]);
});
it('should remove entry points that will depend upon an invalid entry-point', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.js')]: {resolved: [second.path, third.path], missing: []},
[_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']},
[_('/third/index.js')]: {resolved: [], missing: []},
}));
// Note that we will process `first` after `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([second, first, third]);
expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: ['/missing']},
{entryPoint: first, missingDependencies: [second.path]},
]);
});
it('should error if the entry point does not have a suitable format', () => {
expect(() => resolver.sortEntryPointsByDependency([
{ path: '/first', packageJson: {}, compiledByAngular: true } as EntryPoint
])).toThrowError(`There is no appropriate source code format in '/first' entry-point.`);
});
it('should error if there is no appropriate DependencyHost for the given formats', () => {
resolver = new DependencyResolver(fs, new MockLogger(), {esm2015: host});
expect(() => resolver.sortEntryPointsByDependency([first]))
.toThrowError(
`Could not find a suitable format for computing dependencies of entry-point: '${first.path}'.`);
});
it('should capture any dependencies that were ignored', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.ignoredDependencies).toEqual([
{entryPoint: first, dependencyPath: '/ignored-1'},
{entryPoint: third, dependencyPath: '/ignored-2'},
]);
});
it('should only return dependencies of the target, if provided', () => {
spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const entryPoints = [fifth, first, fourth, second, third];
let sorted: SortedEntryPointsInfo;
sorted = resolver.sortEntryPointsByDependency(entryPoints, first);
expect(sorted.entryPoints).toEqual([fifth, fourth, third, second, first]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, second);
expect(sorted.entryPoints).toEqual([fifth, fourth, third, second]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, third);
expect(sorted.entryPoints).toEqual([fifth, fourth, third]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, fourth);
expect(sorted.entryPoints).toEqual([fifth, fourth]);
sorted = resolver.sortEntryPointsByDependency(entryPoints, fifth);
expect(sorted.entryPoints).toEqual([fifth]);
});
it('should use the appropriate DependencyHost for each entry-point', () => {
const esm5Host = new EsmDependencyHost(fs, moduleResolver);
const esm2015Host = new EsmDependencyHost(fs, moduleResolver);
resolver =
new DependencyResolver(fs, new MockLogger(), {esm5: esm5Host, esm2015: esm2015Host});
spyOn(esm5Host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
spyOn(esm2015Host, 'findDependencies')
.and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${first.path}/index.js`);
expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${second.path}/sub/index.js`);
expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${third.path}/index.js`);
expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`);
expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${fifth.path}/index.js`);
expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${first.path}/index.js`);
expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${second.path}/sub/index.js`);
expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${third.path}/index.js`);
expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`);
expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${fifth.path}/index.js`);
});
interface DepMap {
[path: string]: {resolved: string[], missing: string[]};
}
function createFakeComputeDependencies(deps: DepMap) {
return (entryPoint: string) => {
const dependencies = new Set();
const missing = new Set();
const deepImports = new Set();
deps[entryPoint].resolved.forEach(dep => dependencies.add(dep));
deps[entryPoint].missing.forEach(dep => missing.add(dep));
return {dependencies, missing, deepImports};
};
}
});
});

View File

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

View File

@ -5,202 +5,200 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {ModuleResolver, ResolvedDeepImport, ResolvedExternalModule, ResolvedRelativeModule} from '../../src/dependencies/module_resolver';
import {MockFileSystem} from '../helpers/mock_file_system';
const _ = AbsoluteFsPath.from;
runInEachFileSystem(() => {
describe('ModuleResolver', () => {
let _: typeof absoluteFrom;
function createMockFileSystem() {
return new MockFileSystem({
'/libs': {
'local-package': {
'package.json': 'PACKAGE.JSON for local-package',
'index.js': `import {X} from './x';`,
'x.js': `export class X {}`,
'sub-folder': {
'index.js': `import {X} from '../x';`,
beforeEach(() => {
_ = absoluteFrom;
loadTestFiles([
{name: _('/libs/local-package/package.json'), contents: 'PACKAGE.JSON for local-package'},
{name: _('/libs/local-package/index.js'), contents: `import {X} from './x';`},
{name: _('/libs/local-package/x.js'), contents: `export class X {}`},
{name: _('/libs/local-package/sub-folder/index.js'), contents: `import {X} from '../x';`},
{
name: _('/libs/local-package/node_modules/package-1/sub-folder/index.js'),
contents: `export class Z {}`
},
'node_modules': {
'package-1': {
'sub-folder': {'index.js': `export class Z {}`},
'package.json': 'PACKAGE.JSON for package-1',
},
{
name: _('/libs/local-package/node_modules/package-1/package.json'),
contents: 'PACKAGE.JSON for package-1'
},
},
'node_modules': {
'package-2': {
'package.json': 'PACKAGE.JSON for package-2',
'node_modules': {
'package-3': {
'package.json': 'PACKAGE.JSON for package-3',
},
},
{
name: _('/libs/node_modules/package-2/package.json'),
contents: 'PACKAGE.JSON for package-2'
},
},
},
'/dist': {
'package-4': {
'x.js': `export class X {}`,
'package.json': 'PACKAGE.JSON for package-4',
'sub-folder': {'index.js': `import {X} from '@shared/package-4/x';`},
},
'sub-folder': {
'package-4': {
'package.json': 'PACKAGE.JSON for package-4',
{
name: _('/libs/node_modules/package-2/node_modules/package-3/package.json'),
contents: 'PACKAGE.JSON for package-3'
},
'package-5': {
'package.json': 'PACKAGE.JSON for package-5',
'post-fix': {
'package.json': 'PACKAGE.JSON for package-5/post-fix',
}
{name: _('/dist/package-4/x.js'), contents: `export class X {}`},
{name: _('/dist/package-4/package.json'), contents: 'PACKAGE.JSON for package-4'},
{
name: _('/dist/package-4/sub-folder/index.js'),
contents: `import {X} from '@shared/package-4/x';`
},
}
},
'/node_modules': {
'top-package': {
'package.json': 'PACKAGE.JSON for top-package',
}
}
});
}
describe('ModuleResolver', () => {
describe('resolveModule()', () => {
describe('with relative paths', () => {
it('should resolve sibling, child and aunt modules', () => {
const resolver = new ModuleResolver(createMockFileSystem());
expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js')))
.toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js')));
expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js')))
.toEqual(new ResolvedRelativeModule(_('/libs/local-package/sub-folder/index.js')));
expect(resolver.resolveModuleImport('../x', _('/libs/local-package/sub-folder/index.js')))
.toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js')));
});
it('should return `null` if the resolved module relative module does not exist', () => {
const resolver = new ModuleResolver(createMockFileSystem());
expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null);
});
{
name: _('/dist/sub-folder/package-4/package.json'),
contents: 'PACKAGE.JSON for package-4'
},
{
name: _('/dist/sub-folder/package-5/package.json'),
contents: 'PACKAGE.JSON for package-5'
},
{
name: _('/dist/sub-folder/package-5/post-fix/package.json'),
contents: 'PACKAGE.JSON for package-5/post-fix'
},
{
name: _('/node_modules/top-package/package.json'),
contents: 'PACKAGE.JSON for top-package'
},
]);
});
describe('with non-mapped external paths', () => {
it('should resolve to the package.json of a local node_modules package', () => {
const resolver = new ModuleResolver(createMockFileSystem());
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
expect(
resolver.resolveModuleImport('package-1', _('/libs/local-package/sub-folder/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
});
it('should resolve to the package.json of a higher node_modules package', () => {
const resolver = new ModuleResolver(createMockFileSystem());
expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2')));
expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/node_modules/top-package')));
});
it('should return `null` if the package cannot be found', () => {
const resolver = new ModuleResolver(createMockFileSystem());
expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js')))
.toBe(null);
});
it('should return `null` if the package is not accessible because it is in a inner node_modules package',
() => {
const resolver = new ModuleResolver(createMockFileSystem());
expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js')))
.toBe(null);
});
it('should identify deep imports into an external module', () => {
const resolver = new ModuleResolver(createMockFileSystem());
expect(
resolver.resolveModuleImport('package-1/sub-folder', _('/libs/local-package/index.js')))
.toEqual(
new ResolvedDeepImport(_('/libs/local-package/node_modules/package-1/sub-folder')));
});
});
describe('with mapped path external modules', () => {
it('should resolve to the package.json of simple mapped packages', () => {
const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4')));
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5')));
});
it('should select the best match by the length of prefix before the *', () => {
const resolver = new ModuleResolver(createMockFileSystem(), {
baseUrl: '/dist',
paths: {
'@lib/*': ['*'],
'@lib/sub-folder/*': ['*'],
}
describe('resolveModule()', () => {
describe('with relative paths', () => {
it('should resolve sibling, child and aunt modules', () => {
const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js')))
.toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js')));
expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js')))
.toEqual(new ResolvedRelativeModule(_('/libs/local-package/sub-folder/index.js')));
expect(resolver.resolveModuleImport('../x', _('/libs/local-package/sub-folder/index.js')))
.toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js')));
});
// We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map to
// `*` and so the final resolved path will not include the `sub-folder` segment.
expect(resolver.resolveModuleImport(
'@lib/sub-folder/package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4')));
it('should return `null` if the resolved module relative module does not exist', () => {
const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null);
});
});
it('should follow the ordering of `paths` when matching mapped packages', () => {
let resolver: ModuleResolver;
describe('with non-mapped external paths', () => {
it('should resolve to the package.json of a local node_modules package', () => {
const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
expect(resolver.resolveModuleImport(
'package-1', _('/libs/local-package/sub-folder/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js')))
.toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1')));
});
const fs = createMockFileSystem();
resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4')));
it('should resolve to the package.json of a higher node_modules package', () => {
const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2')));
expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/node_modules/top-package')));
});
resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4')));
it('should return `null` if the package cannot be found', () => {
const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js')))
.toBe(null);
});
it('should return `null` if the package is not accessible because it is in a inner node_modules package',
() => {
const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js')))
.toBe(null);
});
it('should identify deep imports into an external module', () => {
const resolver = new ModuleResolver(getFileSystem());
expect(resolver.resolveModuleImport(
'package-1/sub-folder', _('/libs/local-package/index.js')))
.toEqual(new ResolvedDeepImport(
_('/libs/local-package/node_modules/package-1/sub-folder')));
});
});
it('should resolve packages when the path mappings have post-fixes', () => {
const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}});
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix')));
});
describe('with mapped path external modules', () => {
it('should resolve to the package.json of simple mapped packages', () => {
const resolver = new ModuleResolver(
getFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}});
it('should match paths against complex path matchers', () => {
const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}});
expect(resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4')));
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toBe(null);
});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4')));
it('should resolve path as "relative" if the mapped path is inside the current package',
() => {
const resolver = new ModuleResolver(
createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['*']}});
expect(resolver.resolveModuleImport(
'@shared/package-4/x', _('/dist/package-4/sub-folder/index.js')))
.toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js')));
});
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5')));
});
it('should resolve paths where the wildcard matches more than one path segment', () => {
const resolver = new ModuleResolver(
createMockFileSystem(),
{baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}});
expect(
resolver.resolveModuleImport(
'@shared/sub-folder/package-5/post-fix', _('/dist/package-4/sub-folder/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix')));
it('should select the best match by the length of prefix before the *', () => {
const resolver = new ModuleResolver(getFileSystem(), {
baseUrl: '/dist',
paths: {
'@lib/*': ['*'],
'@lib/sub-folder/*': ['*'],
}
});
// We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map
// to `*` and so the final resolved path will not include the `sub-folder` segment.
expect(resolver.resolveModuleImport(
'@lib/sub-folder/package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4')));
});
it('should follow the ordering of `paths` when matching mapped packages', () => {
let resolver: ModuleResolver;
const fs = getFileSystem();
resolver =
new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/package-4')));
resolver =
new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}});
expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4')));
});
it('should resolve packages when the path mappings have post-fixes', () => {
const resolver = new ModuleResolver(
getFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}});
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix')));
});
it('should match paths against complex path matchers', () => {
const resolver = new ModuleResolver(
getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}});
expect(
resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4')));
expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js')))
.toBe(null);
});
it('should resolve path as "relative" if the mapped path is inside the current package',
() => {
const resolver = new ModuleResolver(
getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['*']}});
expect(resolver.resolveModuleImport(
'@shared/package-4/x', _('/dist/package-4/sub-folder/index.js')))
.toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js')));
});
it('should resolve paths where the wildcard matches more than one path segment', () => {
const resolver = new ModuleResolver(
getFileSystem(), {baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}});
expect(resolver.resolveModuleImport(
'@shared/sub-folder/package-5/post-fix',
_('/dist/package-4/sub-folder/index.js')))
.toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix')));
});
});
});
});

View File

@ -7,180 +7,231 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {absoluteFrom, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {ModuleResolver} from '../../src/dependencies/module_resolver';
import {UmdDependencyHost} from '../../src/dependencies/umd_dependency_host';
import {MockFileSystem} from '../helpers/mock_file_system';
const _ = AbsoluteFsPath.from;
runInEachFileSystem(() => {
describe('UmdDependencyHost', () => {
let _: typeof absoluteFrom;
let host: UmdDependencyHost;
describe('UmdDependencyHost', () => {
let host: UmdDependencyHost;
beforeEach(() => {
const fs = createMockFileSystem();
host = new UmdDependencyHost(fs, new ModuleResolver(fs));
beforeEach(() => {
_ = absoluteFrom;
setupMockFileSystem();
const fs = getFileSystem();
host = new UmdDependencyHost(fs, new ModuleResolver(fs));
});
describe('getDependencies()', () => {
it('should not generate a TS AST if the source does not contain any require calls', () => {
spyOn(ts, 'createSourceFile');
host.findDependencies(_('/no/imports/or/re-exports/index.js'));
expect(ts.createSourceFile).not.toHaveBeenCalled();
});
it('should resolve all the external imports of the source file', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/imports/index.js'));
expect(dependencies.size).toBe(2);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
});
it('should resolve all the external re-exports of the source file', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/re-exports/index.js'));
expect(dependencies.size).toBe(2);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
});
it('should capture missing external imports', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/imports-missing/index.js'));
expect(dependencies.size).toBe(1);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(missing.size).toBe(1);
expect(missing.has(relativeFrom('missing'))).toBe(true);
expect(deepImports.size).toBe(0);
});
it('should not register deep imports as missing', () => {
// This scenario verifies the behavior of the dependency analysis when an external import
// is found that does not map to an entry-point but still exists on disk, i.e. a deep
// import. Such deep imports are captured for diagnostics purposes.
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/deep-import/index.js'));
expect(dependencies.size).toBe(0);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(1);
expect(deepImports.has(_('/node_modules/lib_1/deep/import'))).toBe(true);
});
it('should recurse into internal dependencies', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/internal/outer/index.js'));
expect(dependencies.size).toBe(1);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
});
it('should handle circular internal dependencies', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/internal/circular_a/index.js'));
expect(dependencies.size).toBe(2);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
});
it('should support `paths` alias mappings when resolving modules', () => {
const fs = getFileSystem();
host = new UmdDependencyHost(fs, new ModuleResolver(fs, {
baseUrl: '/dist',
paths: {
'@app/*': ['*'],
'@lib/*/test': ['lib/*/test'],
}
}));
const {dependencies, missing, deepImports} =
host.findDependencies(_('/path-alias/index.js'));
expect(dependencies.size).toBe(4);
expect(dependencies.has(_('/dist/components'))).toBe(true);
expect(dependencies.has(_('/dist/shared'))).toBe(true);
expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
});
});
function setupMockFileSystem(): void {
loadTestFiles([
{
name: _('/no/imports/or/re-exports/index.js'),
contents: '// some text but no import-like statements'
},
{name: _('/no/imports/or/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/no/imports/or/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/external/imports/index.js'),
contents: umd('imports_index', ['lib_1', 'lib_1/sub_1'])
},
{name: _('/external/imports/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/imports/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/external/re-exports/index.js'),
contents: umd('imports_index', ['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y'])
},
{name: _('/external/re-exports/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/re-exports/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/external/imports-missing/index.js'),
contents: umd('imports_missing', ['lib_1', 'missing'])
},
{name: _('/external/imports-missing/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/imports-missing/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/external/deep-import/index.js'),
contents: umd('deep_import', ['lib_1/deep/import'])
},
{name: _('/external/deep-import/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/external/deep-import/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/internal/outer/index.js'), contents: umd('outer', ['../inner'])},
{name: _('/internal/outer/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/internal/outer/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/internal/inner/index.js'), contents: umd('inner', ['lib_1/sub_1'], ['X'])},
{
name: _('/internal/circular_a/index.js'),
contents: umd('circular_a', ['../circular_b', 'lib_1/sub_1'], ['Y'])
},
{
name: _('/internal/circular_b/index.js'),
contents: umd('circular_b', ['../circular_a', 'lib_1'], ['X'])
},
{name: _('/internal/circular_a/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/internal/circular_a/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/re-directed/index.js'), contents: umd('re_directed', ['lib_1/sub_2'])},
{name: _('/re-directed/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/re-directed/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/path-alias/index.js'),
contents:
umd('path_alias', ['@app/components', '@app/shared', '@lib/shared/test', 'lib_1'])
},
{name: _('/path-alias/package.json'), contents: '{"esm2015": "./index.js"}'},
{name: _('/path-alias/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/node_modules/lib_1/index.d.ts'), contents: 'export declare class X {}'},
{
name: _('/node_modules/lib_1/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/node_modules/lib_1/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/node_modules/lib_1/deep/import/index.js'),
contents: 'export class DeepImport {}'
},
{name: _('/node_modules/lib_1/sub_1/index.d.ts'), contents: 'export declare class Y {}'},
{
name: _('/node_modules/lib_1/sub_1/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/node_modules/lib_1/sub_1/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/node_modules/lib_1/sub_2.d.ts'), contents: `export * from './sub_2/sub_2';`},
{name: _('/node_modules/lib_1/sub_2/sub_2.d.ts'), contents: `export declare class Z {}';`},
{
name: _('/node_modules/lib_1/sub_2/package.json'),
contents: '{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}'
},
{name: _('/node_modules/lib_1/sub_2/sub_2.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/dist/components/index.d.ts'), contents: `export declare class MyComponent {};`},
{
name: _('/dist/components/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/components/index.metadata.json'), contents: 'MOCK METADATA'},
{
name: _('/dist/shared/index.d.ts'),
contents: `import {X} from 'lib_1';\nexport declare class Service {}`
},
{
name: _('/dist/shared/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/shared/index.metadata.json'), contents: 'MOCK METADATA'},
{name: _('/dist/lib/shared/test/index.d.ts'), contents: `export class TestHelper {}`},
{
name: _('/dist/lib/shared/test/package.json'),
contents: '{"esm2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/lib/shared/test/index.metadata.json'), contents: 'MOCK METADATA'},
]);
}
});
describe('getDependencies()', () => {
it('should not generate a TS AST if the source does not contain any require calls', () => {
spyOn(ts, 'createSourceFile');
host.findDependencies(_('/no/imports/or/re-exports/index.js'));
expect(ts.createSourceFile).not.toHaveBeenCalled();
});
it('should resolve all the external imports of the source file', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/imports/index.js'));
expect(dependencies.size).toBe(2);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
});
it('should resolve all the external re-exports of the source file', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/re-exports/index.js'));
expect(dependencies.size).toBe(2);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
});
it('should capture missing external imports', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/imports-missing/index.js'));
expect(dependencies.size).toBe(1);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(missing.size).toBe(1);
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true);
expect(deepImports.size).toBe(0);
});
it('should not register deep imports as missing', () => {
// This scenario verifies the behavior of the dependency analysis when an external import
// is found that does not map to an entry-point but still exists on disk, i.e. a deep import.
// Such deep imports are captured for diagnostics purposes.
const {dependencies, missing, deepImports} =
host.findDependencies(_('/external/deep-import/index.js'));
expect(dependencies.size).toBe(0);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(1);
expect(deepImports.has(_('/node_modules/lib_1/deep/import'))).toBe(true);
});
it('should recurse into internal dependencies', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/internal/outer/index.js'));
expect(dependencies.size).toBe(1);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
});
it('should handle circular internal dependencies', () => {
const {dependencies, missing, deepImports} =
host.findDependencies(_('/internal/circular_a/index.js'));
expect(dependencies.size).toBe(2);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
});
it('should support `paths` alias mappings when resolving modules', () => {
const fs = createMockFileSystem();
host = new UmdDependencyHost(fs, new ModuleResolver(fs, {
baseUrl: '/dist',
paths: {
'@app/*': ['*'],
'@lib/*/test': ['lib/*/test'],
}
}));
const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js'));
expect(dependencies.size).toBe(4);
expect(dependencies.has(_('/dist/components'))).toBe(true);
expect(dependencies.has(_('/dist/shared'))).toBe(true);
expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true);
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(0);
});
});
function createMockFileSystem() {
return new MockFileSystem({
'/no/imports/or/re-exports/index.js': '// some text but no import-like statements',
'/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}',
'/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA',
'/external/imports/index.js': umd('imports_index', ['lib_1', 'lib_1/sub_1']),
'/external/imports/package.json': '{"esm2015": "./index.js"}',
'/external/imports/index.metadata.json': 'MOCK METADATA',
'/external/re-exports/index.js':
umd('imports_index', ['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']),
'/external/re-exports/package.json': '{"esm2015": "./index.js"}',
'/external/re-exports/index.metadata.json': 'MOCK METADATA',
'/external/imports-missing/index.js': umd('imports_missing', ['lib_1', 'missing']),
'/external/imports-missing/package.json': '{"esm2015": "./index.js"}',
'/external/imports-missing/index.metadata.json': 'MOCK METADATA',
'/external/deep-import/index.js': umd('deep_import', ['lib_1/deep/import']),
'/external/deep-import/package.json': '{"esm2015": "./index.js"}',
'/external/deep-import/index.metadata.json': 'MOCK METADATA',
'/internal/outer/index.js': umd('outer', ['../inner']),
'/internal/outer/package.json': '{"esm2015": "./index.js"}',
'/internal/outer/index.metadata.json': 'MOCK METADATA',
'/internal/inner/index.js': umd('inner', ['lib_1/sub_1'], ['X']),
'/internal/circular_a/index.js': umd('circular_a', ['../circular_b', 'lib_1/sub_1'], ['Y']),
'/internal/circular_b/index.js': umd('circular_b', ['../circular_a', 'lib_1'], ['X']),
'/internal/circular_a/package.json': '{"esm2015": "./index.js"}',
'/internal/circular_a/index.metadata.json': 'MOCK METADATA',
'/re-directed/index.js': umd('re_directed', ['lib_1/sub_2']),
'/re-directed/package.json': '{"esm2015": "./index.js"}',
'/re-directed/index.metadata.json': 'MOCK METADATA',
'/path-alias/index.js':
umd('path_alias', ['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']),
'/path-alias/package.json': '{"esm2015": "./index.js"}',
'/path-alias/index.metadata.json': 'MOCK METADATA',
'/node_modules/lib_1/index.d.ts': 'export declare class X {}',
'/node_modules/lib_1/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/node_modules/lib_1/index.metadata.json': 'MOCK METADATA',
'/node_modules/lib_1/deep/import/index.js': 'export class DeepImport {}',
'/node_modules/lib_1/sub_1/index.d.ts': 'export declare class Y {}',
'/node_modules/lib_1/sub_1/package.json':
'{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/node_modules/lib_1/sub_1/index.metadata.json': 'MOCK METADATA',
'/node_modules/lib_1/sub_2.d.ts': `export * from './sub_2/sub_2';`,
'/node_modules/lib_1/sub_2/sub_2.d.ts': `export declare class Z {}';`,
'/node_modules/lib_1/sub_2/package.json':
'{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}',
'/node_modules/lib_1/sub_2/sub_2.metadata.json': 'MOCK METADATA',
'/dist/components/index.d.ts': `export declare class MyComponent {};`,
'/dist/components/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/dist/components/index.metadata.json': 'MOCK METADATA',
'/dist/shared/index.d.ts': `import {X} from 'lib_1';\nexport declare class Service {}`,
'/dist/shared/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/dist/shared/index.metadata.json': 'MOCK METADATA',
'/dist/lib/shared/test/index.d.ts': `export class TestHelper {}`,
'/dist/lib/shared/test/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
'/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA',
});
}
});
function umd(moduleName: string, importPaths: string[], exportNames: string[] = []) {
const commonJsRequires = importPaths.map(p => `,require('${p}')`).join('');
const amdDeps = importPaths.map(p => `,'${p}'`).join('');
const globalParams =
importPaths.map(p => `,global.${p.replace('@angular/', 'ng.').replace(/\//g, '')}`).join('');
const params =
importPaths.map(p => `,${p.replace('@angular/', '').replace(/\.?\.?\//g, '')}`).join('');
const exportStatements =
exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
return `
function umd(moduleName: string, importPaths: string[], exportNames: string[] = []) {
const commonJsRequires = importPaths.map(p => `,require('${p}')`).join('');
const amdDeps = importPaths.map(p => `,'${p}'`).join('');
const globalParams =
importPaths.map(p => `,global.${p.replace('@angular/', 'ng.').replace(/\//g, '')}`)
.join('');
const params =
importPaths.map(p => `,${p.replace('@angular/', '').replace(/\.?\.?\//g, '')}`).join('');
const exportStatements =
exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
return `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports${commonJsRequires}) :
typeof define === 'function' && define.amd ? define('${moduleName}', ['exports'${amdDeps}], factory) :
@ -189,4 +240,5 @@ function umd(moduleName: string, importPaths: string[], exportNames: string[] =
${exportStatements}
})));
`;
}
}
});

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@ -8,17 +8,28 @@
import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {ClassMemberKind, Import, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {MockLogger} from '../helpers/mock_logger';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils';
import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const FILES = [
{
name: '/some_directive.js',
contents: `
runInEachFileSystem(() => {
describe('Fesm2015ReflectionHost [import helper style]', () => {
let _: typeof absoluteFrom;
let FILES: {[label: string]: TestFile[]};
beforeEach(() => {
_ = absoluteFrom;
const NAMESPACED_IMPORT_FILES = [
{
name: _('/some_directive.js'),
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Inject, InjectionToken, Input } from '@angular/core';
const INJECTED_TOKEN = new InjectionToken('injected');
@ -52,10 +63,10 @@ const FILES = [
], SomeDirective);
export { SomeDirective };
`,
},
{
name: '/node_modules/@angular/core/some_directive.js',
contents: `
},
{
name: _('/node_modules/@angular/core/some_directive.js'),
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Input } from './directives';
let SomeDirective = class SomeDirective {
@ -70,10 +81,10 @@ const FILES = [
], SomeDirective);
export { SomeDirective };
`,
},
{
name: 'ngmodule.js',
contents: `
},
{
name: _('/ngmodule.js'),
contents: `
import * as tslib_1 from 'tslib';
import { NgModule } from './directives';
var HttpClientXsrfModule_1;
@ -96,311 +107,340 @@ const FILES = [
nonDecoratedVar = 43;
export { HttpClientXsrfModule };
`
},
];
},
];
describe('Fesm2015ReflectionHost [import helper style]', () => {
[{files: FILES, label: 'namespaced'},
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
].forEach(fileSystem => {
describe(`[${fileSystem.label}]`, () => {
const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES);
describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
FILES = {
'namespaced': NAMESPACED_IMPORT_FILES,
'direct import': DIRECT_IMPORT_FILES,
};
});
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
['namespaced', 'direct import'].forEach(label => {
describe(`[${label}]`, () => {
beforeEach(() => {
const fs = getFileSystem();
loadFakeCore(fs);
loadTestFiles(FILES[label]);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
});
describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
it('should find non decorated properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
expect(instanceProperty.isStatic).toEqual(false);
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
});
it('should find static methods on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !;
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
expect(staticMethod.isStatic).toEqual(true);
expect(ts.isMethodDeclaration(staticMethod.implementation !)).toEqual(true);
});
it('should find static properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should find static properties on a class that has an intermediate variable assignment',
() => {
const program = makeTestProgram(fileSystem.files[2]);
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/ngmodule.js', 'HttpClientXsrfModule', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
});
describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined();
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
describe('(returned parameters `decorators`)', () => {
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const mockImportInfo = {} as Import;
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const program = makeTestProgram(fileSystem.files[0]);
describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !;
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo);
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
});
describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
it('should find non decorated properties on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
expect(instanceProperty.isStatic).toEqual(false);
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
});
it('should find static methods on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !;
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
expect(staticMethod.isStatic).toEqual(true);
expect(ts.isMethodDeclaration(staticMethod.implementation !)).toEqual(true);
});
it('should find static properties on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should find static properties on a class that has an intermediate variable assignment',
() => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/ngmodule.js'), 'HttpClientXsrfModule', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
});
describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined();
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
describe('(returned parameters `decorators`)', () => {
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const mockImportInfo = {} as Import;
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo);
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
});
});
});
describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;
const expectedDeclarationNode = getDeclaration(
program, _('/some_directive.js'), 'ViewContainerRef', ts.isClassDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe(null);
});
it('should return the declaration of an externally defined identifier', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node;
const identifierOfDirective =
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
decoratorNode.expression :
null;
const expectedDeclarationNode = getDeclaration(
program, _('/node_modules/@angular/core/index.d.ts'), 'Directive',
isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
});
describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration(
getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null);
if (!value || !ts.isClassExpression(value)) {
throw new Error(
`Expected value to be a class expression: ${value && value.getText()}.`);
}
expect(value.name !.text).toBe('HttpClientXsrfModule');
});
it('should return null if the variable has no assignment', () => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const missingValue = findVariableDeclaration(
getSourceFileOrError(program, _('/ngmodule.js')), 'missingValue');
const value = host.getVariableValue(missingValue !);
expect(value).toBe(null);
});
it('should return null if the variable is not assigned from a call to __decorate', () => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration(
getSourceFileOrError(program, _('/ngmodule.js')), 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null);
});
});
});
describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;
const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', ts.isClassDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe(null);
});
it('should return the declaration of an externally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node;
const identifierOfDirective =
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
decoratorNode.expression :
null;
const expectedDeclarationNode = getDeclaration(
program, 'node_modules/@angular/core/index.d.ts', 'Directive',
isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
});
describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null);
if (!value || !ts.isClassExpression(value)) {
throw new Error(
`Expected value to be a class expression: ${value && value.getText()}.`);
}
expect(value.name !.text).toBe('HttpClientXsrfModule');
});
it('should return null if the variable has no assignment', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const missingValue = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue');
const value = host.getVariableValue(missingValue !);
expect(value).toBe(null);
});
it('should return null if the variable is not assigned from a call to __decorate', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null);
});
});
});
});
function findVariableDeclaration(
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
if (!node) {
return;
function findVariableDeclaration(
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
if (!node) {
return;
}
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) &&
node.name.text === variableName) {
return node;
}
return node.forEachChild(node => findVariableDeclaration(node, variableName));
}
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) &&
node.name.text === variableName) {
return node;
}
return node.forEachChild(node => findVariableDeclaration(node, variableName));
}
});
});

File diff suppressed because it is too large Load Diff

View File

@ -5,20 +5,38 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {ClassMemberKind, Import, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {MockLogger} from '../helpers/mock_logger';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils';
import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const FILES = [
{
name: '/some_directive.js',
contents: `
runInEachFileSystem(() => {
describe('Esm5ReflectionHost [import helper style]', () => {
let _: typeof absoluteFrom;
let FILES: {[label: string]: TestFile[]};
beforeEach(() => {
_ = absoluteFrom;
const NAMESPACED_IMPORT_FILES = [
{
name: _('/index.js'),
contents: `
import * as some_directive from './some_directive';
import * as some_directive2 from '/node_modules/@angular/core/some_directive';
import * as ngmodule from './ngmodule';
`
},
{
name: _('/some_directive.js'),
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Inject, InjectionToken, Input } from '@angular/core';
var INJECTED_TOKEN = new InjectionToken('injected');
@ -59,10 +77,10 @@ const FILES = [
}());
export { SomeDirective };
`,
},
{
name: '/node_modules/@angular/core/some_directive.js',
contents: `
},
{
name: _('/node_modules/@angular/core/some_directive.js'),
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Input } from './directives';
var SomeDirective = /** @class */ (function () {
@ -80,10 +98,10 @@ const FILES = [
}());
export { SomeDirective };
`,
},
{
name: '/ngmodule.js',
contents: `
},
{
name: _('/ngmodule.js'),
contents: `
import * as tslib_1 from 'tslib';
import { NgModule } from '@angular/core';
var HttpClientXsrfModule = /** @class */ (function () {
@ -110,366 +128,380 @@ export { SomeDirective };
nonDecoratedVar = 43;
export { HttpClientXsrfModule };
`
},
];
},
];
describe('Esm5ReflectionHost [import helper style]', () => {
[{files: FILES, label: 'namespaced'},
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
].forEach(fileSystem => {
describe(`[${fileSystem.label}]`, () => {
const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES);
describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
FILES = {
'namespaced': NAMESPACED_IMPORT_FILES,
'direct import': DIRECT_IMPORT_FILES,
};
});
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
['namespaced', 'direct import'].forEach(label => {
describe(`[${label}]`, () => {
beforeEach(() => {
const fs = getFileSystem();
loadFakeCore(fs);
loadTestFiles(FILES[label]);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
});
describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
it('should find non decorated properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
expect(instanceProperty.isStatic).toEqual(false);
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
});
it('should find static methods on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !;
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
expect(staticMethod.isStatic).toEqual(true);
expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true);
});
it('should find static properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
});
describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined();
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
describe('(returned parameters `decorators`)', () => {
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const mockImportInfo = {} as Import;
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const program = makeTestProgram(fileSystem.files[0]);
describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !;
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo);
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
});
describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
it('should find non decorated properties on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
expect(instanceProperty.isStatic).toEqual(false);
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
});
it('should find static methods on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !;
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
expect(staticMethod.isStatic).toEqual(true);
expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true);
});
it('should find static properties on a class', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), true, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/node_modules/@angular/core/some_directive.js'), 'SomeDirective',
isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
});
describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined();
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
describe('(returned parameters `decorators`)', () => {
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const mockImportInfo = {} as Import;
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo);
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
});
});
});
describe('findClassSymbols()', () => {
it('should return an array of all classes in the given source file', () => {
const {program} = makeTestBundleProgram(_('/index.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleFile = getSourceFileOrError(program, _('/ngmodule.js'));
const ngModuleClasses = host.findClassSymbols(ngModuleFile);
expect(ngModuleClasses.length).toEqual(1);
expect(ngModuleClasses[0].name).toBe('HttpClientXsrfModule');
const someDirectiveFile = getSourceFileOrError(program, _('/some_directive.js'));
const someDirectiveClasses = host.findClassSymbols(someDirectiveFile);
expect(someDirectiveClasses.length).toEqual(3);
expect(someDirectiveClasses[0].name).toBe('ViewContainerRef');
expect(someDirectiveClasses[1].name).toBe('TemplateRef');
expect(someDirectiveClasses[2].name).toBe('SomeDirective');
});
});
describe('getDecoratorsOfSymbol()', () => {
it('should return decorators of class symbol', () => {
const {program} = makeTestBundleProgram(_('/index.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleFile = getSourceFileOrError(program, _('/ngmodule.js'));
const ngModuleClasses = host.findClassSymbols(ngModuleFile);
const ngModuleDecorators = ngModuleClasses.map(s => host.getDecoratorsOfSymbol(s));
expect(ngModuleClasses.length).toEqual(1);
expect(ngModuleDecorators[0] !.map(d => d.name)).toEqual(['NgModule']);
const someDirectiveFile = getSourceFileOrError(program, _('/some_directive.js'));
const someDirectiveClasses = host.findClassSymbols(someDirectiveFile);
const someDirectiveDecorators =
someDirectiveClasses.map(s => host.getDecoratorsOfSymbol(s));
expect(someDirectiveDecorators.length).toEqual(3);
expect(someDirectiveDecorators[0]).toBe(null);
expect(someDirectiveDecorators[1]).toBe(null);
expect(someDirectiveDecorators[2] !.map(d => d.name)).toEqual(['Directive']);
});
});
describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;
const expectedDeclarationNode = getDeclaration(
program, _('/some_directive.js'), 'ViewContainerRef', isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe(null);
});
it('should return the declaration of an externally defined identifier', () => {
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node;
const identifierOfDirective =
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
decoratorNode.expression :
null;
const expectedDeclarationNode = getDeclaration(
program, _('/node_modules/@angular/core/index.d.ts'), 'Directive',
isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
it('should find the "actual" declaration of an aliased variable identifier', () => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findIdentifier(
getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1',
isNgModulePropertyAssignment);
const declaration = host.getDeclarationOfIdentifier(ngModuleRef !);
expect(declaration).not.toBe(null);
expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()');
});
});
describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration(
getSourceFileOrError(program, _('/ngmodule.js')), 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null);
if (!value || !ts.isFunctionDeclaration(value.parent)) {
throw new Error(
`Expected result to be a function declaration: ${value && value.getText()}.`);
}
expect(value.getText()).toBe('HttpClientXsrfModule');
});
it('should return undefined if the variable has no assignment', () => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const missingValue = findVariableDeclaration(
getSourceFileOrError(program, _('/ngmodule.js')), 'missingValue');
const value = host.getVariableValue(missingValue !);
expect(value).toBe(null);
});
it('should return null if the variable is not assigned from a call to __decorate', () => {
const {program} = makeTestBundleProgram(_('/ngmodule.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration(
getSourceFileOrError(program, _('/ngmodule.js')), 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null);
});
});
});
describe('findClassSymbols()', () => {
it('should return an array of all classes in the given source file', () => {
const program = makeTestProgram(...fileSystem.files);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleFile = program.getSourceFile('/ngmodule.js') !;
const ngModuleClasses = host.findClassSymbols(ngModuleFile);
expect(ngModuleClasses.length).toEqual(1);
expect(ngModuleClasses[0].name).toBe('HttpClientXsrfModule');
const someDirectiveFile = program.getSourceFile('/some_directive.js') !;
const someDirectiveClasses = host.findClassSymbols(someDirectiveFile);
expect(someDirectiveClasses.length).toEqual(3);
expect(someDirectiveClasses[0].name).toBe('ViewContainerRef');
expect(someDirectiveClasses[1].name).toBe('TemplateRef');
expect(someDirectiveClasses[2].name).toBe('SomeDirective');
});
});
describe('getDecoratorsOfSymbol()', () => {
it('should return decorators of class symbol', () => {
const program = makeTestProgram(...fileSystem.files);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleFile = program.getSourceFile('/ngmodule.js') !;
const ngModuleClasses = host.findClassSymbols(ngModuleFile);
const ngModuleDecorators = ngModuleClasses.map(s => host.getDecoratorsOfSymbol(s));
expect(ngModuleClasses.length).toEqual(1);
expect(ngModuleDecorators[0] !.map(d => d.name)).toEqual(['NgModule']);
const someDirectiveFile = program.getSourceFile('/some_directive.js') !;
const someDirectiveClasses = host.findClassSymbols(someDirectiveFile);
const someDirectiveDecorators =
someDirectiveClasses.map(s => host.getDecoratorsOfSymbol(s));
expect(someDirectiveDecorators.length).toEqual(3);
expect(someDirectiveDecorators[0]).toBe(null);
expect(someDirectiveDecorators[1]).toBe(null);
expect(someDirectiveDecorators[2] !.map(d => d.name)).toEqual(['Directive']);
});
});
describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;
const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe(null);
});
it('should return the declaration of an externally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', isNamedVariableDeclaration);
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node;
const identifierOfDirective =
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
decoratorNode.expression :
null;
const expectedDeclarationNode = getDeclaration(
program, 'node_modules/@angular/core/index.d.ts', 'Directive',
isNamedVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findIdentifier(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1',
isNgModulePropertyAssignment);
const declaration = host.getDeclarationOfIdentifier(ngModuleRef !);
expect(declaration).not.toBe(null);
expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()');
});
});
});
describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null);
if (!value || !ts.isFunctionDeclaration(value.parent)) {
throw new Error(
`Expected result to be a function declaration: ${value && value.getText()}.`);
function findVariableDeclaration(
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
if (!node) {
return;
}
expect(value.getText()).toBe('HttpClientXsrfModule');
});
it('should return undefined if the variable has no assignment', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const missingValue = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue');
const value = host.getVariableValue(missingValue !);
expect(value).toBe(null);
});
it('should return null if the variable is not assigned from a call to __decorate', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null);
});
if (isNamedVariableDeclaration(node) && node.name.text === variableName) {
return node;
}
return node.forEachChild(node => findVariableDeclaration(node, variableName));
}
});
function findIdentifier(
node: ts.Node | undefined, identifierName: string,
requireFn: (node: ts.Identifier) => boolean): ts.Identifier|undefined {
if (!node) {
return undefined;
}
if (ts.isIdentifier(node) && node.text === identifierName && requireFn(node)) {
return node;
}
return node.forEachChild(node => findIdentifier(node, identifierName, requireFn));
}
function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean {
return ts.isPropertyAssignment(identifier.parent) &&
identifier.parent.name.getText() === 'ngModule';
}
});
function findVariableDeclaration(
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
if (!node) {
return;
}
if (isNamedVariableDeclaration(node) && node.name.text === variableName) {
return node;
}
return node.forEachChild(node => findVariableDeclaration(node, variableName));
}
});
function findIdentifier(
node: ts.Node | undefined, identifierName: string,
requireFn: (node: ts.Identifier) => boolean): ts.Identifier|undefined {
if (!node) {
return undefined;
}
if (ts.isIdentifier(node) && node.text === identifierName && requireFn(node)) {
return node;
}
return node.forEachChild(node => findIdentifier(node, identifierName, requireFn));
}
function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean {
return ts.isPropertyAssignment(identifier.parent) &&
identifier.parent.name.getText() === 'ngModule';
}

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
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {CtorParameter} from '../../../src/ngtsc/reflection';
/**

View File

@ -5,422 +5,397 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path';
import {existsSync, readFileSync, readdirSync, statSync, symlinkSync} from 'fs';
import * as mockFs from 'mock-fs';
import * as path from 'path';
import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers';
import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system';
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
import {Folder, MockFileSystem, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
import {mainNgcc} from '../../src/main';
import {markAsProcessed} from '../../src/packages/build_marker';
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.from;
const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true});
describe('ngcc main()', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
runInEachFileSystem(() => {
describe('ngcc main()', () => {
let _: typeof absoluteFrom;
let fs: FileSystem;
it('should run ngcc without errors for esm2015', () => {
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']}))
.not.toThrow();
});
beforeEach(() => {
_ = absoluteFrom;
fs = getFileSystem();
initMockFileSystem(fs, testFiles);
});
it('should run ngcc without errors for esm5', () => {
expect(() => mainNgcc({
it('should run ngcc without errors for esm2015', () => {
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']}))
.not.toThrow();
});
it('should run ngcc without errors for esm5', () => {
expect(() => mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm5'],
logger: new MockLogger(),
}))
.not.toThrow();
});
it('should run ngcc without errors when "main" property is not present', () => {
mainNgcc({
basePath: '/dist',
propertiesToConsider: ['main', 'es2015'],
logger: new MockLogger(),
});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
describe('with targetEntryPointPath', () => {
it('should only compile the given package entry-point (and its dependencies).', () => {
const STANDARD_MARKERS = {
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
esm5: '0.0.0-PLACEHOLDER',
esm2015: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
};
mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing'});
expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `common/http` is a dependency of `common/http/testing`, so is compiled.
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `core` is a dependency of `common/http`, so is compiled.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS);
// * `common` is a private (only in .js not .d.ts) dependency so is compiled.
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS);
// * `common/testing` is not a dependency so is not compiled.
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toBeUndefined();
});
it('should mark a non-Angular package target as processed', () => {
mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'test-package'});
// `test-package` has no Angular but is marked as processed.
expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
});
// * `core` is a dependency of `test-package`, but it is not processed, since test-package
// was not processed.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined();
});
});
describe('early skipping of target entry-point', () => {
describe('[compileAllFormats === true]', () => {
it('should skip all processing if all the properties are marked as processed', () => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', SUPPORTED_FORMAT_PROPERTIES);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing', logger,
});
expect(logger.logs.debug).toContain([
'The target entry-point has already been processed'
]);
});
it('should process the target if any `propertyToConsider` is not marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
propertiesToConsider: ['fesm2015', 'esm5', 'esm2015'], logger,
});
expect(logger.logs.debug).not.toContain([
'The target entry-point has already been processed'
]);
});
});
describe('[compileAllFormats === false]', () => {
it('should process the target if the first matching `propertyToConsider` is not marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
propertiesToConsider: ['esm5', 'esm2015'],
compileAllFormats: false, logger,
});
expect(logger.logs.debug).not.toContain([
'The target entry-point has already been processed'
]);
});
it('should skip all processing if the first matching `propertyToConsider` is marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
// Simulate a property that does not exist on the package.json and will be ignored.
propertiesToConsider: ['missing', 'esm2015', 'esm5'],
compileAllFormats: false, logger,
});
expect(logger.logs.debug).toContain([
'The target entry-point has already been processed'
]);
});
});
});
function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) {
const basePath = _('/node_modules');
const targetPackageJsonPath = join(basePath, packagePath, 'package.json');
const targetPackage = loadPackage(packagePath);
markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings');
properties.forEach(
property => markAsProcessed(fs, targetPackage, targetPackageJsonPath, property));
}
describe('with propertiesToConsider', () => {
it('should only compile the entry-point formats given in the `propertiesToConsider` list',
() => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['main', 'esm5', 'module', 'fesm5'],
logger: new MockLogger(),
});
// The ES2015 formats are not compiled as they are not in `propertiesToConsider`.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
describe('with compileAllFormats set to false', () => {
it('should only compile the first matching format', () => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['module', 'fesm5', 'esm5'],
compileAllFormats: false,
logger: new MockLogger(),
});
// * In the Angular packages fesm5 and module have the same underlying format,
// so both are marked as compiled.
// * The `esm5` is not compiled because we stopped after the `fesm5` format.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
it('should cope with compiling the same entry-point multiple times with different formats',
() => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['module'],
compileAllFormats: false,
logger: new MockLogger(),
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// If ngcc tries to write out the typings files again, this will throw an exception.
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm5'],
compileAllFormats: false,
logger: new MockLogger(),
}))
.not.toThrow();
});
it('should run ngcc without errors when "main" property is not present', () => {
mainNgcc({
basePath: '/dist',
propertiesToConsider: ['main', 'es2015'],
logger: new MockLogger(),
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
describe('with targetEntryPointPath', () => {
it('should only compile the given package entry-point (and its dependencies).', () => {
const STANDARD_MARKERS = {
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
esm5: '0.0.0-PLACEHOLDER',
esm2015: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
};
mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing'});
expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `common/http` is a dependency of `common/http/testing`, so is compiled.
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `core` is a dependency of `common/http`, so is compiled.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS);
// * `common` is a private (only in .js not .d.ts) dependency so is compiled.
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS);
// * `common/testing` is not a dependency so is not compiled.
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toBeUndefined();
});
it('should mark a non-Angular package target as processed', () => {
mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'test-package'});
// `test-package` has no Angular but is marked as processed.
expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
});
// * `core` is a dependency of `test-package`, but it is not processed, since test-package
// was not processed.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined();
});
});
describe('early skipping of target entry-point', () => {
describe('[compileAllFormats === true]', () => {
it('should skip all processing if all the properties are marked as processed', () => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', SUPPORTED_FORMAT_PROPERTIES);
describe('with createNewEntryPointFormats', () => {
it('should create new files rather than overwriting the originals', () => {
const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/;
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing', logger,
createNewEntryPointFormats: true,
propertiesToConsider: ['esm5'],
logger: new MockLogger(),
});
expect(logger.logs.debug).toContain(['The target entry-point has already been processed']);
// Updates the package.json
expect(loadPackage('@angular/common').esm5).toEqual('./esm5/common.js');
expect((loadPackage('@angular/common') as any).esm5_ivy_ngcc)
.toEqual('__ivy_ngcc__/esm5/common.js');
// Doesn't touch original files
expect(fs.readFile(_(`/node_modules/@angular/common/esm5/src/common_module.js`)))
.not.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Or create a backup of the original
expect(
fs.exists(_(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`)))
.toBe(false);
// Creates new files
expect(
fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`)))
.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Copies over files (unchanged) that did not need compiling
expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)));
expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)))
.toEqual(fs.readFile(_(`/node_modules/@angular/common/esm5/src/version.js`)));
// Overwrites .d.ts files (as usual)
expect(fs.readFile(_(`/node_modules/@angular/common/common.d.ts`)))
.toMatch(ANGULAR_CORE_IMPORT_REGEX);
expect(fs.exists(_(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`))).toBe(true);
});
});
describe('logger', () => {
it('should log info message to the console by default', () => {
const consoleInfoSpy = spyOn(console, 'info');
mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']});
expect(consoleInfoSpy)
.toHaveBeenCalledWith('Compiling @angular/common/http : esm2015 as esm2015');
});
it('should process the target if any `propertyToConsider` is not marked as processed', () => {
it('should use a custom logger if provided', () => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
propertiesToConsider: ['fesm2015', 'esm5', 'esm2015'], logger,
propertiesToConsider: ['esm2015'], logger,
});
expect(logger.logs.debug).not.toContain([
'The target entry-point has already been processed'
]);
expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']);
});
});
describe('[compileAllFormats === false]', () => {
it('should process the target if the first matching `propertyToConsider` is not marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
propertiesToConsider: ['esm5', 'esm2015'],
compileAllFormats: false, logger,
});
expect(logger.logs.debug).not.toContain([
'The target entry-point has already been processed'
]);
});
it('should skip all processing if the first matching `propertyToConsider` is marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
// Simulate a property that does not exist on the package.json and will be ignored.
propertiesToConsider: ['missing', 'esm2015', 'esm5'],
compileAllFormats: false, logger,
});
expect(logger.logs.debug).toContain([
'The target entry-point has already been processed'
]);
});
});
});
function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) {
const basePath = _('/node_modules');
const targetPackageJsonPath = AbsoluteFsPath.join(basePath, packagePath, 'package.json');
const targetPackage = loadPackage(packagePath);
const fs = new NodeJSFileSystem();
markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings');
properties.forEach(
property => markAsProcessed(fs, targetPackage, targetPackageJsonPath, property));
}
describe('with propertiesToConsider', () => {
it('should only compile the entry-point formats given in the `propertiesToConsider` list',
() => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['main', 'esm5', 'module', 'fesm5'],
logger: new MockLogger(),
});
// The ES2015 formats are not compiled as they are not in `propertiesToConsider`.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
describe('with compileAllFormats set to false', () => {
it('should only compile the first matching format', () => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['module', 'fesm5', 'esm5'],
compileAllFormats: false,
logger: new MockLogger(),
});
// * In the Angular packages fesm5 and module have the same underlying format,
// so both are marked as compiled.
// * The `esm5` is not compiled because we stopped after the `fesm5` format.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
describe('with pathMappings', () => {
it('should find and compile packages accessible via the pathMappings', () => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['es2015'],
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'},
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
it('should cope with compiling the same entry-point multiple times with different formats',
() => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['module'],
compileAllFormats: false,
logger: new MockLogger(),
function loadPackage(
packageName: string, basePath: AbsoluteFsPath = _('/node_modules')): EntryPointPackageJson {
return JSON.parse(fs.readFile(fs.resolve(basePath, packageName, 'package.json')));
}
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// If ngcc tries to write out the typings files again, this will throw an exception.
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm5'],
compileAllFormats: false,
logger: new MockLogger(),
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
if (fs instanceof MockFileSystem) {
fs.init(testFiles);
}
describe('with createNewEntryPointFormats', () => {
it('should create new files rather than overwriting the originals', () => {
const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/;
mainNgcc({
basePath: '/node_modules',
createNewEntryPointFormats: true,
propertiesToConsider: ['esm5'],
logger: new MockLogger(),
// a random test package that no metadata.json file so not compiled by Angular.
loadTestFiles([
{
name: _('/node_modules/test-package/package.json'),
contents: '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/test-package/index.js'),
contents:
'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};'
},
{
name: _('/node_modules/test-package/index.d.ts'),
contents:
'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;'
},
]);
});
// Updates the package.json
expect(loadPackage('@angular/common').esm5).toEqual('./esm5/common.js');
expect((loadPackage('@angular/common') as any).esm5_ivy_ngcc)
.toEqual('__ivy_ngcc__/esm5/common.js');
// Doesn't touch original files
expect(readFileSync(`/node_modules/@angular/common/esm5/src/common_module.js`, 'utf8'))
.not.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Or create a backup of the original
expect(existsSync(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`))
.toBe(false);
// Creates new files
expect(readFileSync(
`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`, 'utf8'))
.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Copies over files (unchanged) that did not need compiling
expect(existsSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`));
expect(readFileSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`, 'utf8'))
.toEqual(readFileSync(`/node_modules/@angular/common/esm5/src/version.js`, 'utf8'));
// Overwrites .d.ts files (as usual)
expect(readFileSync(`/node_modules/@angular/common/common.d.ts`, 'utf8'))
.toMatch(ANGULAR_CORE_IMPORT_REGEX);
expect(existsSync(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`)).toBe(true);
});
});
describe('logger', () => {
it('should log info message to the console by default', () => {
const consoleInfoSpy = spyOn(console, 'info');
mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']});
expect(consoleInfoSpy)
.toHaveBeenCalledWith('Compiling @angular/common/http : esm2015 as esm2015');
});
it('should use a custom logger if provided', () => {
const logger = new MockLogger();
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm2015'], logger,
});
expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']);
});
});
describe('with pathMappings', () => {
it('should find and compile packages accessible via the pathMappings', () => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['es2015'],
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'},
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
});
function createMockFileSystem() {
const typeScriptPath = path.join(process.env.RUNFILES !, 'typescript');
if (!existsSync(typeScriptPath)) {
symlinkSync(resolveNpmTreeArtifact('typescript'), typeScriptPath, 'junction');
}
mockFs({
'/node_modules/@angular': loadAngularPackages(),
'/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs')),
'/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib')),
'/node_modules/test-package': {
'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}',
// no metadata.json file so not compiled by Angular.
'index.js':
'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};',
'index.d.ts':
'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;',
},
'/dist/local-package': {
'package.json':
'{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}',
'index.metadata.json': 'DUMMY DATA',
'index.js': `
import {Component} from '@angular/core';
export class AppComponent {};
AppComponent.decorators = [
{ type: Component, args: [{selector: 'app', template: '<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');
// 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 {};`
},
]);
}
});
return directory;
}
interface Directory {
[pathSegment: string]: string|Directory;
}
function loadPackage(packageName: string, basePath = '/node_modules'): EntryPointPackageJson {
return JSON.parse(readFileSync(`${basePath}/${packageName}/package.json`, 'utf8'));
}
});

View File

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

View File

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

View File

@ -5,200 +5,206 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {DependencyResolver} from '../../src/dependencies/dependency_resolver';
import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
import {ModuleResolver} from '../../src/dependencies/module_resolver';
import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointFinder} from '../../src/packages/entry_point_finder';
import {MockFileSystem, SymLink} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger';
const _ = AbsoluteFsPath.from;
runInEachFileSystem(() => {
describe('findEntryPoints()', () => {
let resolver: DependencyResolver;
let finder: EntryPointFinder;
beforeEach(() => {
const fs = createMockFileSystem();
resolver = new DependencyResolver(
fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))});
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => {
return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []};
describe('findEntryPoints()', () => {
let resolver: DependencyResolver;
let finder: EntryPointFinder;
let _: typeof absoluteFrom;
beforeEach(() => {
const fs = getFileSystem();
_ = absoluteFrom;
setupMockFileSystem();
resolver = new DependencyResolver(
fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))});
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => {
return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []};
});
finder = new EntryPointFinder(fs, new MockLogger(), resolver);
});
finder = new EntryPointFinder(fs, new MockLogger(), resolver);
});
it('should find sub-entry-points within a package', () => {
const {entryPoints} = finder.findEntryPoints(_('/sub_entry_points'));
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/sub_entry_points/common'), _('/sub_entry_points/common')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/http')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/http/testing')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/testing')],
]);
});
it('should find packages inside a namespace', () => {
const {entryPoints} = finder.findEntryPoints(_('/namespaced'));
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common')],
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http')],
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http/testing')],
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common/testing')],
]);
});
it('should find entry-points via `pathMappings', () => {
const {entryPoints} = finder.findEntryPoints(
_('/pathMappings/node_modules'), undefined,
{baseUrl: _('/pathMappings'), paths: {'my-lib': ['dist/my-lib']}});
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib')],
[_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib/sub-lib')],
[
_('/pathMappings/node_modules/@angular/common'),
_('/pathMappings/node_modules/@angular/common')
],
]);
});
it('should return an empty array if there are no packages', () => {
const {entryPoints} = finder.findEntryPoints(_('/no_packages'));
expect(entryPoints).toEqual([]);
});
it('should return an empty array if there are no valid entry-points', () => {
const {entryPoints} = finder.findEntryPoints(_('/no_valid_entry_points'));
expect(entryPoints).toEqual([]);
});
it('should ignore folders starting with .', () => {
const {entryPoints} = finder.findEntryPoints(_('/dotted_folders'));
expect(entryPoints).toEqual([]);
});
it('should ignore folders that are symlinked', () => {
const {entryPoints} = finder.findEntryPoints(_('/symlinked_folders'));
expect(entryPoints).toEqual([]);
});
it('should handle nested node_modules folders', () => {
const {entryPoints} = finder.findEntryPoints(_('/nested_node_modules'));
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/nested_node_modules/outer'), _('/nested_node_modules/outer')],
// Note that the inner entry point does not get included as part of the outer package
[
_('/nested_node_modules/outer/node_modules/inner'),
_('/nested_node_modules/outer/node_modules/inner'),
],
]);
});
function createMockFileSystem() {
return new MockFileSystem({
'/sub_entry_points': {
'common': {
'package.json': createPackageJson('common'),
'common.metadata.json': 'metadata info',
'http': {
'package.json': createPackageJson('http'),
'http.metadata.json': 'metadata info',
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
},
'/pathMappings': {
'dist': {
'my-lib': {
'package.json': createPackageJson('my-lib'),
'my-lib.metadata.json': 'metadata info',
'sub-lib': {
'package.json': createPackageJson('sub-lib'),
'sub-lib.metadata.json': 'metadata info',
},
},
},
'node_modules': {
'@angular': {
'common': {
'package.json': createPackageJson('common'),
'common.metadata.json': 'metadata info',
},
},
}
},
'/namespaced': {
'@angular': {
'common': {
'package.json': createPackageJson('common'),
'common.metadata.json': 'metadata info',
'http': {
'package.json': createPackageJson('http'),
'http.metadata.json': 'metadata info',
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
},
},
'/no_packages': {'should_not_be_found': {}},
'/no_valid_entry_points': {
'some_package': {
'package.json': '{}',
},
},
'/dotted_folders': {
'.common': {
'package.json': createPackageJson('common'),
'common.metadata.json': 'metadata info',
},
},
'/symlinked_folders': {
'common': new SymLink(_('/sub_entry_points/common')),
},
'/nested_node_modules': {
'outer': {
'package.json': createPackageJson('outer'),
'outer.metadata.json': 'metadata info',
'node_modules': {
'inner': {
'package.json': createPackageJson('inner'),
'inner.metadata.json': 'metadata info',
},
},
},
},
it('should find sub-entry-points within a package', () => {
const {entryPoints} = finder.findEntryPoints(_('/sub_entry_points'));
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/sub_entry_points/common'), _('/sub_entry_points/common')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/http')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/http/testing')],
[_('/sub_entry_points/common'), _('/sub_entry_points/common/testing')],
]);
});
it('should find packages inside a namespace', () => {
const {entryPoints} = finder.findEntryPoints(_('/namespaced'));
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common')],
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http')],
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common/http/testing')],
[_('/namespaced/@angular/common'), _('/namespaced/@angular/common/testing')],
]);
});
it('should find entry-points via `pathMappings', () => {
const {entryPoints} = finder.findEntryPoints(
_('/pathMappings/node_modules'), undefined,
{baseUrl: _('/pathMappings'), paths: {'my-lib': ['dist/my-lib']}});
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib')],
[_('/pathMappings/dist/my-lib'), _('/pathMappings/dist/my-lib/sub-lib')],
[
_('/pathMappings/node_modules/@angular/common'),
_('/pathMappings/node_modules/@angular/common')
],
]);
});
it('should return an empty array if there are no packages', () => {
const {entryPoints} = finder.findEntryPoints(_('/no_packages'));
expect(entryPoints).toEqual([]);
});
it('should return an empty array if there are no valid entry-points', () => {
const {entryPoints} = finder.findEntryPoints(_('/no_valid_entry_points'));
expect(entryPoints).toEqual([]);
});
it('should ignore folders starting with .', () => {
const {entryPoints} = finder.findEntryPoints(_('/dotted_folders'));
expect(entryPoints).toEqual([]);
});
it('should ignore folders that are symlinked', () => {
const {entryPoints} = finder.findEntryPoints(_('/symlinked_folders'));
expect(entryPoints).toEqual([]);
});
it('should handle nested node_modules folders', () => {
const {entryPoints} = finder.findEntryPoints(_('/nested_node_modules'));
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
[_('/nested_node_modules/outer'), _('/nested_node_modules/outer')],
// Note that the inner entry point does not get included as part of the outer package
[
_('/nested_node_modules/outer/node_modules/inner'),
_('/nested_node_modules/outer/node_modules/inner'),
],
]);
});
function setupMockFileSystem(): void {
loadTestFiles([
{name: _('/sub_entry_points/common/package.json'), contents: createPackageJson('common')},
{name: _('/sub_entry_points/common/common.metadata.json'), contents: 'metadata info'},
{
name: _('/sub_entry_points/common/http/package.json'),
contents: createPackageJson('http')
},
{name: _('/sub_entry_points/common/http/http.metadata.json'), contents: 'metadata info'},
{
name: _('/sub_entry_points/common/http/testing/package.json'),
contents: createPackageJson('testing')
},
{
name: _('/sub_entry_points/common/http/testing/testing.metadata.json'),
contents: 'metadata info'
},
{
name: _('/sub_entry_points/common/testing/package.json'),
contents: createPackageJson('testing')
},
{
name: _('/sub_entry_points/common/testing/testing.metadata.json'),
contents: 'metadata info'
},
{name: _('/pathMappings/dist/my-lib/package.json'), contents: createPackageJson('my-lib')},
{name: _('/pathMappings/dist/my-lib/my-lib.metadata.json'), contents: 'metadata info'},
{
name: _('/pathMappings/dist/my-lib/sub-lib/package.json'),
contents: createPackageJson('sub-lib')
},
{
name: _('/pathMappings/dist/my-lib/sub-lib/sub-lib.metadata.json'),
contents: 'metadata info'
},
{
name: _('/pathMappings/node_modules/@angular/common/package.json'),
contents: createPackageJson('common')
},
{
name: _('/pathMappings/node_modules/@angular/common/common.metadata.json'),
contents: 'metadata info'
},
{
name: _('/namespaced/@angular/common/package.json'),
contents: createPackageJson('common')
},
{name: _('/namespaced/@angular/common/common.metadata.json'), contents: 'metadata info'},
{
name: _('/namespaced/@angular/common/http/package.json'),
contents: createPackageJson('http')
},
{name: _('/namespaced/@angular/common/http/http.metadata.json'), contents: 'metadata info'},
{
name: _('/namespaced/@angular/common/http/testing/package.json'),
contents: createPackageJson('testing')
},
{
name: _('/namespaced/@angular/common/http/testing/testing.metadata.json'),
contents: 'metadata info'
},
{
name: _('/namespaced/@angular/common/testing/package.json'),
contents: createPackageJson('testing')
},
{
name: _('/namespaced/@angular/common/testing/testing.metadata.json'),
contents: 'metadata info'
},
{name: _('/no_valid_entry_points/some_package/package.json'), contents: '{}'},
{name: _('/dotted_folders/.common/package.json'), contents: createPackageJson('common')},
{name: _('/dotted_folders/.common/common.metadata.json'), contents: 'metadata info'},
{name: _('/nested_node_modules/outer/package.json'), contents: createPackageJson('outer')},
{name: _('/nested_node_modules/outer/outer.metadata.json'), contents: 'metadata info'},
{
name: _('/nested_node_modules/outer/node_modules/inner/package.json'),
contents: createPackageJson('inner')
},
{
name: _('/nested_node_modules/outer/node_modules/inner/inner.metadata.json'),
contents: 'metadata info'
},
]);
const fs = getFileSystem();
fs.ensureDir(_('/no_packages/should_not_be_found'));
fs.ensureDir(_('/symlinked_folders'));
fs.symlink(_('/sub_entry_points/common'), _('/symlinked_folders/common'));
}
});
function createPackageJson(packageName: string): string {
const packageJson: any = {
typings: `./${packageName}.d.ts`,
fesm2015: `./fesm2015/${packageName}.js`,
esm2015: `./esm2015/${packageName}.js`,
fesm5: `./fesm2015/${packageName}.js`,
esm5: `./esm2015/${packageName}.js`,
main: `./bundles/${packageName}.umd.js`,
};
return JSON.stringify(packageJson);
}
});
function createPackageJson(packageName: string): string {
const packageJson: any = {
typings: `./${packageName}.d.ts`,
fesm2015: `./fesm2015/${packageName}.js`,
esm2015: `./esm2015/${packageName}.js`,
fesm5: `./fesm2015/${packageName}.js`,
esm5: `./esm2015/${packageName}.js`,
main: `./bundles/${packageName}.umd.js`,
};
return JSON.stringify(packageJson);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,9 @@ import {replaceTsWithNgInErrors} from './ngtsc/diagnostics';
import * as api from './transformers/api';
import {GENERATED_FILES} from './transformers/util';
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult, filterErrorsAndWarnings} from './perform_compile';
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, filterErrorsAndWarnings} from './perform_compile';
import {performWatchCompilation, createPerformWatchHost} from './perform_watch';
import {NodeJSFileSystem, setFileSystem} from './ngtsc/file_system';
export function main(
args: string[], consoleError: (s: string) => void = console.error,
@ -227,5 +228,7 @@ export function watchMode(
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
// We are running the real compiler so run against the real file-system
setFileSystem(new NodeJSFileSystem());
process.exitCode = main(args);
}

View File

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

View File

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

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

View File

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

View File

@ -14,15 +14,15 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",
],
)

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript",

View File

@ -5,62 +5,66 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver} from '../../imports';
import {CycleAnalyzer} from '../src/analyzer';
import {ImportGraph} from '../src/imports';
import {makeProgramFromGraph} from './util';
describe('cycle analyzer', () => {
it('should not detect a cycle when there isn\'t one', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, b)).toBe(false);
runInEachFileSystem(() => {
describe('cycle analyzer', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('should not detect a cycle when there isn\'t one', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, b)).toBe(false);
});
it('should detect a simple cycle between two files', () => {
const {program, analyzer} = makeAnalyzer('a:b;b');
const a = getSourceFileOrError(program, (_('/a.ts')));
const b = getSourceFileOrError(program, (_('/b.ts')));
expect(analyzer.wouldCreateCycle(a, b)).toBe(false);
expect(analyzer.wouldCreateCycle(b, a)).toBe(true);
});
it('should detect a cycle with a re-export in the chain', () => {
const {program, analyzer} = makeAnalyzer('a:*b;b:c;c');
const a = getSourceFileOrError(program, (_('/a.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(a, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, a)).toBe(true);
});
it('should detect a cycle in a more complex program', () => {
const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g');
const b = getSourceFileOrError(program, (_('/b.ts')));
const g = getSourceFileOrError(program, (_('/g.ts')));
expect(analyzer.wouldCreateCycle(b, g)).toBe(false);
expect(analyzer.wouldCreateCycle(g, b)).toBe(true);
});
it('should detect a cycle caused by a synthetic edge', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = getSourceFileOrError(program, (_('/b.ts')));
const c = getSourceFileOrError(program, (_('/c.ts')));
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
analyzer.recordSyntheticImport(c, b);
expect(analyzer.wouldCreateCycle(b, c)).toBe(true);
});
});
it('should detect a simple cycle between two files', () => {
const {program, analyzer} = makeAnalyzer('a:b;b');
const a = program.getSourceFile('a.ts') !;
const b = program.getSourceFile('b.ts') !;
expect(analyzer.wouldCreateCycle(a, b)).toBe(false);
expect(analyzer.wouldCreateCycle(b, a)).toBe(true);
});
it('should detect a cycle with a re-export in the chain', () => {
const {program, analyzer} = makeAnalyzer('a:*b;b:c;c');
const a = program.getSourceFile('a.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(analyzer.wouldCreateCycle(a, c)).toBe(false);
expect(analyzer.wouldCreateCycle(c, a)).toBe(true);
});
it('should detect a cycle in a more complex program', () => {
const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g');
const b = program.getSourceFile('b.ts') !;
const g = program.getSourceFile('g.ts') !;
expect(analyzer.wouldCreateCycle(b, g)).toBe(false);
expect(analyzer.wouldCreateCycle(g, b)).toBe(true);
});
it('should detect a cycle caused by a synthetic edge', () => {
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
const b = program.getSourceFile('b.ts') !;
const c = program.getSourceFile('c.ts') !;
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
analyzer.recordSyntheticImport(c, b);
expect(analyzer.wouldCreateCycle(b, c)).toBe(true);
});
function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} {
const {program, options, host} = makeProgramFromGraph(getFileSystem(), graph);
return {
program,
analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))),
};
}
});
function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} {
const {program, options, host} = makeProgramFromGraph(graph);
return {
program,
analyzer: new CycleAnalyzer(new ImportGraph(new ModuleResolver(program, options, host))),
};
}

View File

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

View File

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

View File

@ -10,7 +10,7 @@ ts_library(
module_name = "@angular/compiler-cli/src/ngtsc/entry_point",
deps = [
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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