angular/packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts
Pete Bacon Darwin 7b55ba58b9 refactor(ivy): ngcc - remove flat-format and use AbsoluteFsPath (#29092)
Now that we are using package.json properties to indicate which
entry-point format to compile, it turns out that we don't really
need to distinguish between flat and non-flat formats, unless we
are compiling `@angular/core`.

PR Close #29092
2019-03-20 14:45:54 -04:00

263 lines
12 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'canonical-path';
import * as mockFs from 'mock-fs';
import * as ts from 'typescript';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {DependencyHost} from '../../src/packages/dependency_host';
const Module = require('module');
interface DepMap {
[path: string]: {resolved: string[], missing: string[]};
}
const _ = AbsoluteFsPath.from;
describe('DependencyHost', () => {
let host: DependencyHost;
beforeEach(() => host = new DependencyHost());
describe('getDependencies()', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
it('should not generate a TS AST if the source does not contain any imports or re-exports',
() => {
spyOn(ts, 'createSourceFile');
host.computeDependencies(
_('/no/imports/or/re-exports.js'), new Set(), new Set(), new Set());
expect(ts.createSourceFile).not.toHaveBeenCalled();
});
it('should resolve all the external imports of the source file', () => {
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies(_('/external/imports.js'), resolved, missing, deepImports);
expect(resolved.size).toBe(2);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
it('should resolve all the external re-exports of the source file', () => {
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies(_('/external/re-exports.js'), resolved, missing, deepImports);
expect(resolved.size).toBe(2);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
it('should capture missing external imports', () => {
spyOn(host, 'tryResolveEntryPoint')
.and.callFake(
(from: string, importPath: string) =>
importPath === 'missing' ? null : `RESOLVED/${importPath}`);
spyOn(host, 'tryResolve').and.callFake(() => null);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies(_('/external/imports-missing.js'), resolved, missing, deepImports);
expect(resolved.size).toBe(1);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(missing.size).toBe(1);
expect(missing.has('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 tryResolveEntryPoint = (from: string, importPath: string) =>
importPath === 'deep/import' ? null : `RESOLVED/${importPath}`;
spyOn(host, 'tryResolveEntryPoint').and.callFake(tryResolveEntryPoint);
spyOn(host, 'tryResolve')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies(_('/external/deep-import.js'), resolved, missing, deepImports);
expect(resolved.size).toBe(0);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(1);
expect(deepImports.has('deep/import')).toBe(true);
});
it('should recurse into internal dependencies', () => {
spyOn(host, 'resolveInternal')
.and.callFake(
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const getDependenciesSpy = spyOn(host, 'computeDependencies').and.callThrough();
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies(_('/internal/outer.js'), resolved, missing, deepImports);
expect(getDependenciesSpy)
.toHaveBeenCalledWith('/internal/outer.js', resolved, missing, deepImports);
expect(getDependenciesSpy)
.toHaveBeenCalledWith(
'/internal/inner.js', resolved, missing, deepImports, jasmine.any(Set));
expect(resolved.size).toBe(1);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
it('should handle circular internal dependencies', () => {
spyOn(host, 'resolveInternal')
.and.callFake(
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies(_('/internal/circular-a.js'), resolved, missing, deepImports);
expect(resolved.size).toBe(2);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
function createMockFileSystem() {
mockFs({
'/no/imports/or/re-exports.js': 'some text but no import-like statements',
'/external/imports.js': `import {X} from 'path/to/x';\nimport {Y} from 'path/to/y';`,
'/external/re-exports.js': `export {X} from 'path/to/x';\nexport {Y} from 'path/to/y';`,
'/external/imports-missing.js': `import {X} from 'path/to/x';\nimport {Y} from 'missing';`,
'/external/deep-import.js': `import {Y} from 'deep/import';`,
'/internal/outer.js': `import {X} from './inner';`,
'/internal/inner.js': `import {Y} from 'path/to/y';`,
'/internal/circular-a.js': `import {B} from './circular-b'; import {X} from 'path/to/x';`,
'/internal/circular-b.js': `import {A} from './circular-a'; import {Y} from 'path/to/y';`,
});
}
});
describe('resolveInternal', () => {
it('should resolve the dependency via `Module._resolveFilename`', () => {
spyOn(Module, '_resolveFilename').and.returnValue('/RESOLVED_PATH');
const result = host.resolveInternal(
_('/SOURCE/PATH/FILE'), PathSegment.fromFsPath('../TARGET/PATH/FILE'));
expect(result).toEqual('/RESOLVED_PATH');
});
it('should first resolve the `to` on top of the `from` directory', () => {
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('/RESOLVED_PATH');
host.resolveInternal(_('/SOURCE/PATH/FILE'), PathSegment.fromFsPath('../TARGET/PATH/FILE'));
expect(resolveSpy)
.toHaveBeenCalledWith('/SOURCE/TARGET/PATH/FILE', jasmine.any(Object), false, undefined);
});
});
describe('tryResolveExternal', () => {
it('should call `tryResolve`, appending `package.json` to the target path', () => {
const tryResolveSpy = spyOn(host, 'tryResolve').and.returnValue('/PATH/TO/RESOLVED');
host.tryResolveEntryPoint(_('/SOURCE_PATH'), PathSegment.fromFsPath('TARGET_PATH'));
expect(tryResolveSpy).toHaveBeenCalledWith('/SOURCE_PATH', 'TARGET_PATH/package.json');
});
it('should return the directory containing the result from `tryResolve', () => {
spyOn(host, 'tryResolve').and.returnValue('/PATH/TO/RESOLVED');
expect(host.tryResolveEntryPoint(_('/SOURCE_PATH'), PathSegment.fromFsPath('TARGET_PATH')))
.toEqual(_('/PATH/TO'));
});
it('should return null if `tryResolve` returns null', () => {
spyOn(host, 'tryResolve').and.returnValue(null);
expect(host.tryResolveEntryPoint(_('/SOURCE_PATH'), PathSegment.fromFsPath('TARGET_PATH')))
.toEqual(null);
});
});
describe('tryResolve()', () => {
it('should resolve the dependency via `Module._resolveFilename`, passing the `from` path to the `paths` option',
() => {
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('/RESOLVED_PATH');
const result = host.tryResolve(_('/SOURCE_PATH'), PathSegment.fromFsPath('TARGET_PATH'));
expect(resolveSpy).toHaveBeenCalledWith('TARGET_PATH', jasmine.any(Object), false, {
paths: ['/SOURCE_PATH']
});
expect(result).toEqual(_('/RESOLVED_PATH'));
});
it('should return null if `Module._resolveFilename` throws an error', () => {
const resolveSpy =
spyOn(Module, '_resolveFilename').and.throwError(`Cannot find module 'TARGET_PATH'`);
const result = host.tryResolve(_('/SOURCE_PATH'), PathSegment.fromFsPath('TARGET_PATH'));
expect(result).toBe(null);
});
});
describe('isStringImportOrReexport', () => {
it('should return true if the statement is an import', () => {
expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";')))
.toBe(true);
expect(host.isStringImportOrReexport(createStatement('import * as X from "some/x";')))
.toBe(true);
});
it('should return true if the statement is a re-export', () => {
expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";')))
.toBe(true);
expect(host.isStringImportOrReexport(createStatement('export * from "some/x";'))).toBe(true);
});
it('should return false if the statement is not an import or a re-export', () => {
expect(host.isStringImportOrReexport(createStatement('class X {}'))).toBe(false);
expect(host.isStringImportOrReexport(createStatement('export function foo() {}')))
.toBe(false);
expect(host.isStringImportOrReexport(createStatement('export const X = 10;'))).toBe(false);
});
function createStatement(source: string) {
return ts
.createSourceFile('source.js', source, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS)
.statements[0];
}
});
describe('hasImportOrReexportStatements', () => {
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);
});
});
function restoreRealFileSystem() { mockFs.restore(); }
});