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:
parent
1e7e065423
commit
7186f9c016
|
@ -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",
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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};
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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); }
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.`);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]) :
|
||||
[];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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__',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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')));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
})));
|
||||
`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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())));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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')));
|
||||
}
|
||||
|
|
|
@ -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;`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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};`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) !;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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()); }
|
||||
});
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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, ' ');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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))),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(',');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
]),
|
|
@ -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.
|
|
@ -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';
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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.');
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
|
@ -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()));
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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
Loading…
Reference in New Issue