perf(ngcc): read dependencies from entry-point manifest (#36486)

Previously, even if an entry-point did not need to be processed,
ngcc would always parse the files of the entry-point to compute
its dependencies. This can take a lot of time for large node_modules.

Now these dependencies are cached in the entry-point manifest,
and read from there rather than computing them every time.

See https://github.com/angular/angular/issues/36414\#issuecomment-608401834
FW-2047

PR Close #36486
This commit is contained in:
Pete Bacon Darwin 2020-04-07 17:47:46 +01:00 committed by atscott
parent 4aa4e6fd03
commit a185efbd60
8 changed files with 203 additions and 89 deletions

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system';
import {EntryPoint} from '../packages/entry_point';
import {resolveFileWithPostfixes} from '../utils'; import {resolveFileWithPostfixes} from '../utils';
import {ModuleResolver} from './module_resolver'; import {ModuleResolver} from './module_resolver';
@ -21,6 +22,11 @@ export interface DependencyInfo {
deepImports: Set<AbsoluteFsPath>; deepImports: Set<AbsoluteFsPath>;
} }
export interface EntryPointWithDependencies {
entryPoint: EntryPoint;
depInfo: DependencyInfo;
}
export function createDependencyInfo(): DependencyInfo { export function createDependencyInfo(): DependencyInfo {
return {dependencies: new Set(), missing: new Set(), deepImports: new Set()}; return {dependencies: new Set(), missing: new Set(), deepImports: new Set()};
} }

View File

@ -14,7 +14,7 @@ import {NgccConfiguration} from '../packages/configuration';
import {EntryPoint, EntryPointFormat, getEntryPointFormat, SUPPORTED_FORMAT_PROPERTIES} from '../packages/entry_point'; import {EntryPoint, EntryPointFormat, getEntryPointFormat, SUPPORTED_FORMAT_PROPERTIES} from '../packages/entry_point';
import {PartiallyOrderedList} from '../utils'; import {PartiallyOrderedList} from '../utils';
import {createDependencyInfo, DependencyHost, DependencyInfo} from './dependency_host'; import {createDependencyInfo, DependencyHost, EntryPointWithDependencies} from './dependency_host';
const builtinNodeJsModules = new Set<string>(require('module').builtinModules); const builtinNodeJsModules = new Set<string>(require('module').builtinModules);
@ -94,7 +94,7 @@ export class DependencyResolver {
* @param target If provided, only return entry-points depended on by this entry-point. * @param target If provided, only return entry-points depended on by this entry-point.
* @returns the result of sorting the entry points by dependency. * @returns the result of sorting the entry points by dependency.
*/ */
sortEntryPointsByDependency(entryPoints: EntryPoint[], target?: EntryPoint): sortEntryPointsByDependency(entryPoints: EntryPointWithDependencies[], target?: EntryPoint):
SortedEntryPointsInfo { SortedEntryPointsInfo {
const {invalidEntryPoints, ignoredDependencies, graph} = const {invalidEntryPoints, ignoredDependencies, graph} =
this.computeDependencyGraph(entryPoints); this.computeDependencyGraph(entryPoints);
@ -120,18 +120,21 @@ export class DependencyResolver {
}; };
} }
getEntryPointDependencies(entryPoint: EntryPoint): DependencyInfo { getEntryPointWithDependencies(entryPoint: EntryPoint): EntryPointWithDependencies {
const formatInfo = this.getEntryPointFormatInfo(entryPoint); const dependencies = createDependencyInfo();
const host = this.hosts[formatInfo.format]; if (entryPoint.compiledByAngular) {
if (!host) { // Only bother to compute dependencies of entry-points that have been compiled by Angular
throw new Error( const formatInfo = this.getEntryPointFormatInfo(entryPoint);
`Could not find a suitable format for computing dependencies of entry-point: '${ const host = this.hosts[formatInfo.format];
entryPoint.path}'.`); if (!host) {
throw new Error(
`Could not find a suitable format for computing dependencies of entry-point: '${
entryPoint.path}'.`);
}
host.collectDependencies(formatInfo.path, dependencies);
this.typingsHost.collectDependencies(entryPoint.typings, dependencies);
} }
const depInfo = createDependencyInfo(); return {entryPoint, depInfo: dependencies};
host.collectDependencies(formatInfo.path, depInfo);
this.typingsHost.collectDependencies(entryPoint.typings, depInfo);
return depInfo;
} }
/** /**
@ -140,20 +143,18 @@ export class DependencyResolver {
* The graph only holds entry-points that ngcc cares about and whose dependencies * The graph only holds entry-points that ngcc cares about and whose dependencies
* (direct and transitive) all exist. * (direct and transitive) all exist.
*/ */
private computeDependencyGraph(entryPoints: EntryPoint[]): DependencyGraph { private computeDependencyGraph(entryPoints: EntryPointWithDependencies[]): DependencyGraph {
const invalidEntryPoints: InvalidEntryPoint[] = []; const invalidEntryPoints: InvalidEntryPoint[] = [];
const ignoredDependencies: IgnoredDependency[] = []; const ignoredDependencies: IgnoredDependency[] = [];
const graph = new DepGraph<EntryPoint>(); const graph = new DepGraph<EntryPoint>();
const angularEntryPoints = entryPoints.filter(entryPoint => entryPoint.compiledByAngular); const angularEntryPoints = entryPoints.filter(e => e.entryPoint.compiledByAngular);
// Add the Angular compiled entry points to the graph as nodes // Add the Angular compiled entry points to the graph as nodes
angularEntryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint)); angularEntryPoints.forEach(e => graph.addNode(e.entryPoint.path, e.entryPoint));
// Now add the dependencies between them // Now add the dependencies between them
angularEntryPoints.forEach(entryPoint => { angularEntryPoints.forEach(({entryPoint, depInfo: {dependencies, missing, deepImports}}) => {
const {dependencies, missing, deepImports} = this.getEntryPointDependencies(entryPoint);
const missingDependencies = Array.from(missing).filter(dep => !builtinNodeJsModules.has(dep)); const missingDependencies = Array.from(missing).filter(dep => !builtinNodeJsModules.has(dep));
if (missingDependencies.length > 0 && !entryPoint.ignoreMissingDependencies) { if (missingDependencies.length > 0 && !entryPoint.ignoreMissingDependencies) {

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system';
import {EntryPointWithDependencies} from '../dependencies/dependency_host';
import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver'; import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {NgccConfiguration} from '../packages/configuration'; import {NgccConfiguration} from '../packages/configuration';
import {EntryPoint, getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT} from '../packages/entry_point'; import {getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT} from '../packages/entry_point';
import {EntryPointManifest} from '../packages/entry_point_manifest'; import {EntryPointManifest} from '../packages/entry_point_manifest';
import {PathMappings} from '../utils'; import {PathMappings} from '../utils';
import {NGCC_DIRECTORY} from '../writing/new_entry_point_file_writer'; import {NGCC_DIRECTORY} from '../writing/new_entry_point_file_writer';
@ -32,11 +33,11 @@ export class DirectoryWalkerEntryPointFinder implements EntryPointFinder {
* all package entry-points. * all package entry-points.
*/ */
findEntryPoints(): SortedEntryPointsInfo { findEntryPoints(): SortedEntryPointsInfo {
const unsortedEntryPoints: EntryPoint[] = []; const unsortedEntryPoints: EntryPointWithDependencies[] = [];
for (const basePath of this.basePaths) { for (const basePath of this.basePaths) {
const entryPoints = this.entryPointManifest.readEntryPointsUsingManifest(basePath) || const entryPoints = this.entryPointManifest.readEntryPointsUsingManifest(basePath) ||
this.walkBasePathForPackages(basePath); this.walkBasePathForPackages(basePath);
unsortedEntryPoints.push(...entryPoints); entryPoints.forEach(e => unsortedEntryPoints.push(e));
} }
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints); return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints);
} }
@ -47,10 +48,10 @@ export class DirectoryWalkerEntryPointFinder implements EntryPointFinder {
* @param basePath The path at which to start the search * @param basePath The path at which to start the search
* @returns an array of `EntryPoint`s that were found within `basePath`. * @returns an array of `EntryPoint`s that were found within `basePath`.
*/ */
walkBasePathForPackages(basePath: AbsoluteFsPath): EntryPoint[] { walkBasePathForPackages(basePath: AbsoluteFsPath): EntryPointWithDependencies[] {
this.logger.debug( this.logger.debug(
`No manifest found for ${basePath} so walking the directories for entry-points.`); `No manifest found for ${basePath} so walking the directories for entry-points.`);
const entryPoints: EntryPoint[] = trackDuration( const entryPoints = trackDuration(
() => this.walkDirectoryForPackages(basePath), () => this.walkDirectoryForPackages(basePath),
duration => this.logger.debug(`Walking ${basePath} for entry-points took ${duration}s.`)); duration => this.logger.debug(`Walking ${basePath} for entry-points took ${duration}s.`));
this.entryPointManifest.writeEntryPointManifest(basePath, entryPoints); this.entryPointManifest.writeEntryPointManifest(basePath, entryPoints);
@ -64,7 +65,7 @@ export class DirectoryWalkerEntryPointFinder implements EntryPointFinder {
* @param sourceDirectory An absolute path to the root directory where searching begins. * @param sourceDirectory An absolute path to the root directory where searching begins.
* @returns an array of `EntryPoint`s that were found within `sourceDirectory`. * @returns an array of `EntryPoint`s that were found within `sourceDirectory`.
*/ */
walkDirectoryForPackages(sourceDirectory: AbsoluteFsPath): EntryPoint[] { walkDirectoryForPackages(sourceDirectory: AbsoluteFsPath): EntryPointWithDependencies[] {
// Try to get a primary entry point from this directory // Try to get a primary entry point from this directory
const primaryEntryPoint = const primaryEntryPoint =
getEntryPointInfo(this.fs, this.config, this.logger, sourceDirectory, sourceDirectory); getEntryPointInfo(this.fs, this.config, this.logger, sourceDirectory, sourceDirectory);
@ -76,15 +77,15 @@ export class DirectoryWalkerEntryPointFinder implements EntryPointFinder {
return []; return [];
} }
const entryPoints: EntryPoint[] = []; const entryPoints: EntryPointWithDependencies[] = [];
if (primaryEntryPoint !== NO_ENTRY_POINT) { if (primaryEntryPoint !== NO_ENTRY_POINT) {
entryPoints.push(primaryEntryPoint); entryPoints.push(this.resolver.getEntryPointWithDependencies(primaryEntryPoint));
this.collectSecondaryEntryPoints( this.collectSecondaryEntryPoints(
entryPoints, sourceDirectory, sourceDirectory, this.fs.readdir(sourceDirectory)); entryPoints, sourceDirectory, sourceDirectory, this.fs.readdir(sourceDirectory));
// Also check for any nested node_modules in this package but only if at least one of the // Also check for any nested node_modules in this package but only if at least one of the
// entry-points was compiled by Angular. // entry-points was compiled by Angular.
if (entryPoints.some(e => e.compiledByAngular)) { if (entryPoints.some(e => e.entryPoint.compiledByAngular)) {
const nestedNodeModulesPath = this.fs.join(sourceDirectory, 'node_modules'); const nestedNodeModulesPath = this.fs.join(sourceDirectory, 'node_modules');
if (this.fs.exists(nestedNodeModulesPath)) { if (this.fs.exists(nestedNodeModulesPath)) {
entryPoints.push(...this.walkDirectoryForPackages(nestedNodeModulesPath)); entryPoints.push(...this.walkDirectoryForPackages(nestedNodeModulesPath));
@ -125,8 +126,8 @@ export class DirectoryWalkerEntryPointFinder implements EntryPointFinder {
* @param paths The paths contained in the current `directory`. * @param paths The paths contained in the current `directory`.
*/ */
private collectSecondaryEntryPoints( private collectSecondaryEntryPoints(
entryPoints: EntryPoint[], packagePath: AbsoluteFsPath, directory: AbsoluteFsPath, entryPoints: EntryPointWithDependencies[], packagePath: AbsoluteFsPath,
paths: PathSegment[]): void { directory: AbsoluteFsPath, paths: PathSegment[]): void {
for (const path of paths) { for (const path of paths) {
if (isIgnorablePath(path)) { if (isIgnorablePath(path)) {
// Ignore hidden files, node_modules and ngcc directory // Ignore hidden files, node_modules and ngcc directory
@ -153,7 +154,7 @@ export class DirectoryWalkerEntryPointFinder implements EntryPointFinder {
const subEntryPoint = const subEntryPoint =
getEntryPointInfo(this.fs, this.config, this.logger, packagePath, possibleEntryPointPath); getEntryPointInfo(this.fs, this.config, this.logger, packagePath, possibleEntryPointPath);
if (subEntryPoint !== NO_ENTRY_POINT && subEntryPoint !== INCOMPATIBLE_ENTRY_POINT) { if (subEntryPoint !== NO_ENTRY_POINT && subEntryPoint !== INCOMPATIBLE_ENTRY_POINT) {
entryPoints.push(subEntryPoint); entryPoints.push(this.resolver.getEntryPointWithDependencies(subEntryPoint));
isEntryPoint = true; isEntryPoint = true;
} }

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteFsPath, FileSystem, join, PathSegment, relative, relativeFrom} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath, FileSystem, join, PathSegment, relative, relativeFrom} from '../../../src/ngtsc/file_system';
import {EntryPointWithDependencies} from '../dependencies/dependency_host';
import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver'; import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {hasBeenProcessed} from '../packages/build_marker'; import {hasBeenProcessed} from '../packages/build_marker';
@ -25,7 +26,7 @@ import {getBasePaths} from './utils';
*/ */
export class TargetedEntryPointFinder implements EntryPointFinder { export class TargetedEntryPointFinder implements EntryPointFinder {
private unprocessedPaths: AbsoluteFsPath[] = []; private unprocessedPaths: AbsoluteFsPath[] = [];
private unsortedEntryPoints = new Map<AbsoluteFsPath, EntryPoint>(); private unsortedEntryPoints = new Map<AbsoluteFsPath, EntryPointWithDependencies>();
private basePaths = getBasePaths(this.logger, this.basePath, this.pathMappings); private basePaths = getBasePaths(this.logger, this.basePath, this.pathMappings);
constructor( constructor(
@ -40,7 +41,7 @@ export class TargetedEntryPointFinder implements EntryPointFinder {
} }
const targetEntryPoint = this.unsortedEntryPoints.get(this.targetPath); const targetEntryPoint = this.unsortedEntryPoints.get(this.targetPath);
const entryPoints = this.resolver.sortEntryPointsByDependency( const entryPoints = this.resolver.sortEntryPointsByDependency(
Array.from(this.unsortedEntryPoints.values()), targetEntryPoint); Array.from(this.unsortedEntryPoints.values()), targetEntryPoint?.entryPoint);
const invalidTarget = const invalidTarget =
entryPoints.invalidEntryPoints.find(i => i.entryPoint.path === this.targetPath); entryPoints.invalidEntryPoints.find(i => i.entryPoint.path === this.targetPath);
@ -82,9 +83,9 @@ export class TargetedEntryPointFinder implements EntryPointFinder {
if (entryPoint === null || !entryPoint.compiledByAngular) { if (entryPoint === null || !entryPoint.compiledByAngular) {
return; return;
} }
this.unsortedEntryPoints.set(entryPoint.path, entryPoint); const entryPointWithDeps = this.resolver.getEntryPointWithDependencies(entryPoint);
const deps = this.resolver.getEntryPointDependencies(entryPoint); this.unsortedEntryPoints.set(entryPoint.path, entryPointWithDeps);
deps.dependencies.forEach(dep => { entryPointWithDeps.depInfo.dependencies.forEach(dep => {
if (!this.unsortedEntryPoints.has(dep)) { if (!this.unsortedEntryPoints.has(dep)) {
this.unprocessedPaths.push(dep); this.unprocessedPaths.push(dep);
} }

View File

@ -7,12 +7,13 @@
*/ */
import {createHash} from 'crypto'; import {createHash} from 'crypto';
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_system';
import {EntryPointWithDependencies} from '../dependencies/dependency_host';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {NGCC_VERSION} from './build_marker'; import {NGCC_VERSION} from './build_marker';
import {NgccConfiguration} from './configuration'; import {NgccConfiguration} from './configuration';
import {EntryPoint, getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT} from './entry_point'; import {getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT} from './entry_point';
/** /**
* Manages reading and writing a manifest file that contains a list of all the entry-points that * Manages reading and writing a manifest file that contains a list of all the entry-points that
@ -40,7 +41,7 @@ export class EntryPointManifest {
* @returns an array of entry-point information for all entry-points found below the given * @returns an array of entry-point information for all entry-points found below the given
* `basePath` or `null` if the manifest was out of date. * `basePath` or `null` if the manifest was out of date.
*/ */
readEntryPointsUsingManifest(basePath: AbsoluteFsPath): EntryPoint[]|null { readEntryPointsUsingManifest(basePath: AbsoluteFsPath): EntryPointWithDependencies[]|null {
try { try {
if (this.fs.basename(basePath) !== 'node_modules') { if (this.fs.basename(basePath) !== 'node_modules') {
return null; return null;
@ -67,8 +68,9 @@ export class EntryPointManifest {
basePath} so loading entry-point information directly.`); basePath} so loading entry-point information directly.`);
const startTime = Date.now(); const startTime = Date.now();
const entryPoints: EntryPoint[] = []; const entryPoints: EntryPointWithDependencies[] = [];
for (const [packagePath, entryPointPath] of entryPointPaths) { for (const [packagePath, entryPointPath, dependencyPaths, missingPaths, deepImportPaths] of
entryPointPaths) {
const result = const result =
getEntryPointInfo(this.fs, this.config, this.logger, packagePath, entryPointPath); getEntryPointInfo(this.fs, this.config, this.logger, packagePath, entryPointPath);
if (result === NO_ENTRY_POINT || result === INCOMPATIBLE_ENTRY_POINT) { if (result === NO_ENTRY_POINT || result === INCOMPATIBLE_ENTRY_POINT) {
@ -76,7 +78,14 @@ export class EntryPointManifest {
manifestPath} contained an invalid pair of package paths: [${packagePath}, ${ manifestPath} contained an invalid pair of package paths: [${packagePath}, ${
entryPointPath}]`); entryPointPath}]`);
} else { } else {
entryPoints.push(result); entryPoints.push({
entryPoint: result,
depInfo: {
dependencies: new Set(dependencyPaths),
missing: new Set(missingPaths),
deepImports: new Set(deepImportPaths),
}
});
} }
} }
const duration = Math.round((Date.now() - startTime) / 100) / 10; const duration = Math.round((Date.now() - startTime) / 100) / 10;
@ -99,7 +108,8 @@ export class EntryPointManifest {
* @param basePath The path where the manifest file is to be written. * @param basePath The path where the manifest file is to be written.
* @param entryPoints A collection of entry-points to record in the manifest. * @param entryPoints A collection of entry-points to record in the manifest.
*/ */
writeEntryPointManifest(basePath: AbsoluteFsPath, entryPoints: EntryPoint[]): void { writeEntryPointManifest(basePath: AbsoluteFsPath, entryPoints: EntryPointWithDependencies[]):
void {
if (this.fs.basename(basePath) !== 'node_modules') { if (this.fs.basename(basePath) !== 'node_modules') {
return; return;
} }
@ -112,7 +122,14 @@ export class EntryPointManifest {
ngccVersion: NGCC_VERSION, ngccVersion: NGCC_VERSION,
configFileHash: this.config.hash, configFileHash: this.config.hash,
lockFileHash: lockFileHash, lockFileHash: lockFileHash,
entryPointPaths: entryPoints.map(entryPoint => [entryPoint.package, entryPoint.path]), entryPointPaths: entryPoints.map(
e =>
[e.entryPoint.package,
e.entryPoint.path,
Array.from(e.depInfo.dependencies),
Array.from(e.depInfo.missing),
Array.from(e.depInfo.deepImports),
]),
}; };
this.fs.writeFile(this.getEntryPointManifestPath(basePath), JSON.stringify(manifest)); this.fs.writeFile(this.getEntryPointManifestPath(basePath), JSON.stringify(manifest));
} }
@ -143,7 +160,7 @@ export class EntryPointManifest {
* called. * called.
*/ */
export class InvalidatingEntryPointManifest extends EntryPointManifest { export class InvalidatingEntryPointManifest extends EntryPointManifest {
readEntryPointsUsingManifest(basePath: AbsoluteFsPath): EntryPoint[]|null { readEntryPointsUsingManifest(_basePath: AbsoluteFsPath): EntryPointWithDependencies[]|null {
return null; return null;
} }
} }
@ -155,5 +172,8 @@ export interface EntryPointManifestFile {
ngccVersion: string; ngccVersion: string;
configFileHash: string; configFileHash: string;
lockFileHash: string; lockFileHash: string;
entryPointPaths: Array<[AbsoluteFsPath, AbsoluteFsPath]>; entryPointPaths: Array<[
AbsoluteFsPath, AbsoluteFsPath, AbsoluteFsPath[], (AbsoluteFsPath | PathSegment)[],
AbsoluteFsPath[]
]>;
} }

View File

@ -10,7 +10,7 @@ import {DepGraph} from 'dependency-graph';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system'; import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {DependencyInfo} from '../../src/dependencies/dependency_host'; import {DependencyInfo, EntryPointWithDependencies} from '../../src/dependencies/dependency_host';
import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver'; import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver';
import {DtsDependencyHost} from '../../src/dependencies/dts_dependency_host'; import {DtsDependencyHost} from '../../src/dependencies/dts_dependency_host';
import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
@ -131,7 +131,8 @@ runInEachFileSystem(() => {
.and.callFake(createFakeComputeDependencies(dependencies)); .and.callFake(createFakeComputeDependencies(dependencies));
spyOn(dtsHost, 'collectDependencies') spyOn(dtsHost, 'collectDependencies')
.and.callFake(createFakeComputeDependencies(dtsDependencies)); .and.callFake(createFakeComputeDependencies(dtsDependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [fifth, first, fourth, second, third]));
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]); expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
}); });
@ -144,7 +145,8 @@ runInEachFileSystem(() => {
[_('/first/index.d.ts')]: {resolved: [], missing: [_('/missing')]}, [_('/first/index.d.ts')]: {resolved: [], missing: [_('/missing')]},
[_('/second/sub/index.d.ts')]: {resolved: [], missing: []}, [_('/second/sub/index.d.ts')]: {resolved: [], missing: []},
})); }));
const result = resolver.sortEntryPointsByDependency([first, second]); const result =
resolver.sortEntryPointsByDependency(getEntryPointsWithDeps(resolver, [first, second]));
expect(result.entryPoints).toEqual([second]); expect(result.entryPoints).toEqual([second]);
expect(result.invalidEntryPoints).toEqual([ expect(result.invalidEntryPoints).toEqual([
{entryPoint: first, missingDependencies: [_('/missing')]}, {entryPoint: first, missingDependencies: [_('/missing')]},
@ -163,7 +165,8 @@ runInEachFileSystem(() => {
[_('/third/index.d.ts')]: {resolved: [], missing: []}, [_('/third/index.d.ts')]: {resolved: [], missing: []},
})); }));
// Note that we will process `first` before `second`, which has the missing dependency. // Note that we will process `first` before `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([first, second, third]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [first, second, third]));
expect(result.entryPoints).toEqual([third]); expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([ expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: [_('/missing')]}, {entryPoint: second, missingDependencies: [_('/missing')]},
@ -183,7 +186,8 @@ runInEachFileSystem(() => {
[_('/third/index.d.ts')]: {resolved: [], missing: []}, [_('/third/index.d.ts')]: {resolved: [], missing: []},
})); }));
// Note that we will process `first` after `second`, which has the missing dependency. // Note that we will process `first` after `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([second, first, third]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [second, first, third]));
expect(result.entryPoints).toEqual([third]); expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([ expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: [_('/missing')]}, {entryPoint: second, missingDependencies: [_('/missing')]},
@ -202,7 +206,8 @@ runInEachFileSystem(() => {
[_('/sixth/index.d.ts')]: {resolved: [], missing: [_('/missing')]}, [_('/sixth/index.d.ts')]: {resolved: [], missing: [_('/missing')]},
})); }));
// Note that we will process `first` after `second`, which has the missing dependency. // Note that we will process `first` after `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([sixthIgnoreMissing, first]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [sixthIgnoreMissing, first]));
expect(result.entryPoints).toEqual([sixthIgnoreMissing, first]); expect(result.entryPoints).toEqual([sixthIgnoreMissing, first]);
expect(result.invalidEntryPoints).toEqual([]); expect(result.invalidEntryPoints).toEqual([]);
}); });
@ -218,7 +223,8 @@ runInEachFileSystem(() => {
[_('/second/sub/index.d.ts')]: {resolved: [first.path], missing: []}, [_('/second/sub/index.d.ts')]: {resolved: [first.path], missing: []},
[_('/sixth/index.d.ts')]: {resolved: [second.path], missing: []}, [_('/sixth/index.d.ts')]: {resolved: [second.path], missing: []},
})); }));
const result = resolver.sortEntryPointsByDependency([first, second, sixthIgnoreMissing]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [first, second, sixthIgnoreMissing]));
// sixth has no missing dependencies, but it has _invalid_ dependencies, so it's not // sixth has no missing dependencies, but it has _invalid_ dependencies, so it's not
// compiled. // compiled.
expect(result.entryPoints).toEqual([]); expect(result.entryPoints).toEqual([]);
@ -235,7 +241,8 @@ runInEachFileSystem(() => {
[_('/second/sub/index.d.ts')]: {resolved: [], missing: [_('/missing2')]}, [_('/second/sub/index.d.ts')]: {resolved: [], missing: [_('/missing2')]},
[_('/third/index.d.ts')]: {resolved: [first.path, second.path], missing: []}, [_('/third/index.d.ts')]: {resolved: [first.path, second.path], missing: []},
})); }));
const result = resolver.sortEntryPointsByDependency([first, second, third]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [first, second, third]));
expect(result.entryPoints).toEqual([]); expect(result.entryPoints).toEqual([]);
expect(result.invalidEntryPoints).toEqual([ expect(result.invalidEntryPoints).toEqual([
{entryPoint: first, missingDependencies: [_('/missing1')]}, {entryPoint: first, missingDependencies: [_('/missing1')]},
@ -251,7 +258,8 @@ runInEachFileSystem(() => {
spyOn(dtsHost, 'collectDependencies').and.callFake(createFakeComputeDependencies({ spyOn(dtsHost, 'collectDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.d.ts')]: {resolved: [], missing: []}, [_('/first/index.d.ts')]: {resolved: [], missing: []},
})); }));
const result = resolver.sortEntryPointsByDependency([first]); const result =
resolver.sortEntryPointsByDependency(getEntryPointsWithDeps(resolver, [first]));
expect(result.entryPoints).toEqual([first]); expect(result.entryPoints).toEqual([first]);
expect(logger.logs.warn).toEqual([[ expect(logger.logs.warn).toEqual([[
`Entry point 'first' contains deep imports into '${ `Entry point 'first' contains deep imports into '${
@ -290,7 +298,8 @@ runInEachFileSystem(() => {
typings: _('/project/node_modules/test-package/index.d.ts'), typings: _('/project/node_modules/test-package/index.d.ts'),
} as EntryPoint; } as EntryPoint;
const result = resolver.sortEntryPointsByDependency([testEntryPoint]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [testEntryPoint]));
expect(result.entryPoints).toEqual([testEntryPoint]); expect(result.entryPoints).toEqual([testEntryPoint]);
expect(logger.logs.warn).toEqual([[ expect(logger.logs.warn).toEqual([[
`Entry point 'test-package' contains deep imports into '${ `Entry point 'test-package' contains deep imports into '${
@ -299,14 +308,15 @@ runInEachFileSystem(() => {
}); });
it('should error if the entry point does not have a suitable format', () => { it('should error if the entry point does not have a suitable format', () => {
expect(() => resolver.sortEntryPointsByDependency([ expect(() => resolver.sortEntryPointsByDependency(getEntryPointsWithDeps(resolver, [
{path: '/first', packageJson: {}, compiledByAngular: true} as EntryPoint {path: '/first', packageJson: {}, compiledByAngular: true} as EntryPoint
])).toThrowError(`There is no appropriate source code format in '/first' entry-point.`); ]))).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', () => { it('should error if there is no appropriate DependencyHost for the given formats', () => {
resolver = new DependencyResolver(fs, new MockLogger(), config, {esm2015: host}, host); resolver = new DependencyResolver(fs, new MockLogger(), config, {esm2015: host}, host);
expect(() => resolver.sortEntryPointsByDependency([first])) expect(
() => resolver.sortEntryPointsByDependency(getEntryPointsWithDeps(resolver, [first])))
.toThrowError( .toThrowError(
`Could not find a suitable format for computing dependencies of entry-point: '${ `Could not find a suitable format for computing dependencies of entry-point: '${
first.path}'.`); first.path}'.`);
@ -317,7 +327,8 @@ runInEachFileSystem(() => {
.and.callFake(createFakeComputeDependencies(dependencies)); .and.callFake(createFakeComputeDependencies(dependencies));
spyOn(dtsHost, 'collectDependencies') spyOn(dtsHost, 'collectDependencies')
.and.callFake(createFakeComputeDependencies(dtsDependencies)); .and.callFake(createFakeComputeDependencies(dtsDependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [fifth, first, fourth, second, third]));
expect(result.ignoredDependencies).toEqual([ expect(result.ignoredDependencies).toEqual([
{entryPoint: first, dependencyPath: _('/ignored-1')}, {entryPoint: first, dependencyPath: _('/ignored-1')},
{entryPoint: third, dependencyPath: _('/ignored-2')}, {entryPoint: third, dependencyPath: _('/ignored-2')},
@ -329,7 +340,8 @@ runInEachFileSystem(() => {
.and.callFake(createFakeComputeDependencies(dependencies)); .and.callFake(createFakeComputeDependencies(dependencies));
spyOn(dtsHost, 'collectDependencies') spyOn(dtsHost, 'collectDependencies')
.and.callFake(createFakeComputeDependencies(dtsDependencies)); .and.callFake(createFakeComputeDependencies(dtsDependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [fifth, first, fourth, second, third]));
expect(result.graph).toEqual(jasmine.any(DepGraph)); expect(result.graph).toEqual(jasmine.any(DepGraph));
expect(result.graph.size()).toBe(5); expect(result.graph.size()).toBe(5);
@ -341,7 +353,7 @@ runInEachFileSystem(() => {
.and.callFake(createFakeComputeDependencies(dependencies)); .and.callFake(createFakeComputeDependencies(dependencies));
spyOn(dtsHost, 'collectDependencies') spyOn(dtsHost, 'collectDependencies')
.and.callFake(createFakeComputeDependencies(dtsDependencies)); .and.callFake(createFakeComputeDependencies(dtsDependencies));
const entryPoints = [fifth, first, fourth, second, third]; const entryPoints = getEntryPointsWithDeps(resolver, [fifth, first, fourth, second, third]);
let sorted: SortedEntryPointsInfo; let sorted: SortedEntryPointsInfo;
sorted = resolver.sortEntryPointsByDependency(entryPoints, first); sorted = resolver.sortEntryPointsByDependency(entryPoints, first);
@ -363,7 +375,7 @@ runInEachFileSystem(() => {
spyOn(dtsHost, 'collectDependencies').and.callFake(createFakeComputeDependencies({ spyOn(dtsHost, 'collectDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.d.ts')]: {resolved: [], missing: [_('/missing')]}, [_('/first/index.d.ts')]: {resolved: [], missing: [_('/missing')]},
})); }));
const entryPoints = [first]; const entryPoints = getEntryPointsWithDeps(resolver, [first]);
let sorted: SortedEntryPointsInfo; let sorted: SortedEntryPointsInfo;
sorted = resolver.sortEntryPointsByDependency(entryPoints, first); sorted = resolver.sortEntryPointsByDependency(entryPoints, first);
@ -379,7 +391,7 @@ runInEachFileSystem(() => {
spyOn(dtsHost, 'collectDependencies').and.callFake(createFakeComputeDependencies({ spyOn(dtsHost, 'collectDependencies').and.callFake(createFakeComputeDependencies({
[_('/first/index.d.ts')]: {resolved: [], missing: ['fs']}, [_('/first/index.d.ts')]: {resolved: [], missing: ['fs']},
})); }));
const entryPoints = [first]; const entryPoints = getEntryPointsWithDeps(resolver, [first]);
let sorted: SortedEntryPointsInfo; let sorted: SortedEntryPointsInfo;
sorted = resolver.sortEntryPointsByDependency(entryPoints, first); sorted = resolver.sortEntryPointsByDependency(entryPoints, first);
@ -400,7 +412,8 @@ runInEachFileSystem(() => {
.and.callFake(createFakeComputeDependencies(dependencies)); .and.callFake(createFakeComputeDependencies(dependencies));
spyOn(dtsHost, 'collectDependencies') spyOn(dtsHost, 'collectDependencies')
.and.callFake(createFakeComputeDependencies(dtsDependencies)); .and.callFake(createFakeComputeDependencies(dtsDependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); const result = resolver.sortEntryPointsByDependency(
getEntryPointsWithDeps(resolver, [fifth, first, fourth, second, third]));
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]); expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
expect(esm5Host.collectDependencies) expect(esm5Host.collectDependencies)
@ -449,7 +462,7 @@ runInEachFileSystem(() => {
[_('/second/sub/index.d.ts')]: {resolved: [], missing: [_('/missing2')]}, [_('/second/sub/index.d.ts')]: {resolved: [], missing: [_('/missing2')]},
[_('/third/index.d.ts')]: {resolved: [second.path], missing: []}, [_('/third/index.d.ts')]: {resolved: [second.path], missing: []},
})); }));
const entryPoints = [first, second, third]; const entryPoints = getEntryPointsWithDeps(resolver, [first, second, third]);
const sorted = resolver.sortEntryPointsByDependency(entryPoints); const sorted = resolver.sortEntryPointsByDependency(entryPoints);
expect(sorted.entryPoints).toEqual([first]); expect(sorted.entryPoints).toEqual([first]);
expect(sorted.invalidEntryPoints).toEqual([ expect(sorted.invalidEntryPoints).toEqual([
@ -469,6 +482,11 @@ runInEachFileSystem(() => {
return {dependencies, missing, deepImports}; return {dependencies, missing, deepImports};
}; };
} }
function getEntryPointsWithDeps(
resolver: DependencyResolver, entryPoints: EntryPoint[]): EntryPointWithDependencies[] {
return entryPoints.map(entryPoint => resolver.getEntryPointWithDependencies(entryPoint));
}
}); });
}); });
}); });

View File

@ -1107,7 +1107,12 @@ runInEachFileSystem(() => {
// had removed earlier. // had removed earlier.
manifest = JSON.parse(fs.readFile(_('/node_modules/__ngcc_entry_points__.json'))); manifest = JSON.parse(fs.readFile(_('/node_modules/__ngcc_entry_points__.json')));
expect(manifest.entryPointPaths).toContain([ expect(manifest.entryPointPaths).toContain([
_('/node_modules/@angular/common'), _('/node_modules/@angular/common/testing') _('/node_modules/@angular/common'), _('/node_modules/@angular/common/testing'),
[
_('/node_modules/@angular/core'), _('/node_modules/@angular/common'),
_('/node_modules/rxjs')
],
[], []
]); ]);
}); });
}); });

View File

@ -7,12 +7,12 @@
*/ */
import {createHash} from 'crypto'; import {createHash} from 'crypto';
import {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system'; import {absoluteFrom, FileSystem, getFileSystem, relativeFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers'; import {loadTestFiles} from '../../../test/helpers';
import {EntryPointWithDependencies} from '../../src/dependencies/dependency_host';
import {NGCC_VERSION} from '../../src/packages/build_marker'; import {NGCC_VERSION} from '../../src/packages/build_marker';
import {NgccConfiguration} from '../../src/packages/configuration'; import {NgccConfiguration} from '../../src/packages/configuration';
import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointManifest, EntryPointManifestFile} from '../../src/packages/entry_point_manifest'; import {EntryPointManifest, EntryPointManifestFile} from '../../src/packages/entry_point_manifest';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
@ -129,22 +129,48 @@ runInEachFileSystem(() => {
]); ]);
manifestFile.entryPointPaths.push([ manifestFile.entryPointPaths.push([
_Abs('/project/node_modules/some_package'), _Abs('/project/node_modules/some_package'),
_Abs('/project/node_modules/some_package/valid_entry_point') _Abs('/project/node_modules/some_package/valid_entry_point'),
[
_Abs('/project/node_modules/other_package_1'),
_Abs('/project/node_modules/other_package_2'),
],
[
_Abs('/project/node_modules/missing_1'),
relativeFrom('missing_2'),
],
[
_Abs('/project/node_modules/deep/import/path'),
],
]); ]);
fs.writeFile( fs.writeFile(
_Abs('/project/node_modules/__ngcc_entry_points__.json'), JSON.stringify(manifestFile)); _Abs('/project/node_modules/__ngcc_entry_points__.json'), JSON.stringify(manifestFile));
const entryPoints = manifest.readEntryPointsUsingManifest(_Abs('/project/node_modules')); const entryPoints = manifest.readEntryPointsUsingManifest(_Abs('/project/node_modules'));
expect(entryPoints).toEqual([{ expect(entryPoints).toEqual([{
name: 'some_package/valid_entry_point', entryPoint: {
packageJson: jasmine.any(Object), name: 'some_package/valid_entry_point',
package: _Abs('/project/node_modules/some_package'), packageJson: jasmine.any(Object),
path: _Abs('/project/node_modules/some_package/valid_entry_point'), package: _Abs('/project/node_modules/some_package'),
typings: path: _Abs('/project/node_modules/some_package/valid_entry_point'),
_Abs('/project/node_modules/some_package/valid_entry_point/valid_entry_point.d.ts'), typings:
compiledByAngular: true, _Abs('/project/node_modules/some_package/valid_entry_point/valid_entry_point.d.ts'),
ignoreMissingDependencies: false, compiledByAngular: true,
generateDeepReexports: false, ignoreMissingDependencies: false,
} as any]); generateDeepReexports: false,
} as any,
depInfo: {
dependencies: new Set([
_Abs('/project/node_modules/other_package_1'),
_Abs('/project/node_modules/other_package_2'),
]),
missing: new Set([
_Abs('/project/node_modules/missing_1'),
relativeFrom('missing_2'),
]),
deepImports: new Set([
_Abs('/project/node_modules/deep/import/path'),
])
}
}]);
}); });
it('should return null if any of the entry-points are not valid', () => { it('should return null if any of the entry-points are not valid', () => {
@ -152,7 +178,7 @@ runInEachFileSystem(() => {
fs.writeFile(_Abs('/project/yarn.lock'), 'LOCK FILE CONTENTS'); fs.writeFile(_Abs('/project/yarn.lock'), 'LOCK FILE CONTENTS');
manifestFile.entryPointPaths.push([ manifestFile.entryPointPaths.push([
_Abs('/project/node_modules/some_package'), _Abs('/project/node_modules/some_package'),
_Abs('/project/node_modules/some_package/valid_entry_point') _Abs('/project/node_modules/some_package/valid_entry_point'), [], [], []
]); ]);
fs.writeFile( fs.writeFile(
_Abs('/project/node_modules/__ngcc_entry_points__.json'), JSON.stringify(manifestFile)); _Abs('/project/node_modules/__ngcc_entry_points__.json'), JSON.stringify(manifestFile));
@ -223,14 +249,36 @@ runInEachFileSystem(() => {
it('should write the package path and entry-point path of each entry-point provided', () => { it('should write the package path and entry-point path of each entry-point provided', () => {
fs.writeFile(_Abs('/project/package-lock.json'), 'LOCK FILE CONTENTS'); fs.writeFile(_Abs('/project/package-lock.json'), 'LOCK FILE CONTENTS');
const entryPoint1 = { const entryPoint1: EntryPointWithDependencies = {
package: _Abs('/project/node_modules/package-1/'), entryPoint: {
path: _Abs('/project/node_modules/package-1/'), package: _Abs('/project/node_modules/package-1/'),
} as unknown as EntryPoint; path: _Abs('/project/node_modules/package-1/'),
const entryPoint2 = { } as any,
package: _Abs('/project/node_modules/package-2/'), depInfo: {
path: _Abs('/project/node_modules/package-2/entry-point'), dependencies: new Set([
} as unknown as EntryPoint; _Abs('/project/node_modules/other_package_1'),
_Abs('/project/node_modules/other_package_2'),
]),
missing: new Set(),
deepImports: new Set()
}
};
const entryPoint2: EntryPointWithDependencies = {
entryPoint: {
package: _Abs('/project/node_modules/package-2/'),
path: _Abs('/project/node_modules/package-2/entry-point'),
} as any,
depInfo: {
dependencies: new Set(),
missing: new Set([
_Abs('/project/node_modules/missing_1'),
relativeFrom('missing_2'),
]),
deepImports: new Set([
_Abs('/project/node_modules/deep/import/path'),
])
}
};
manifest.writeEntryPointManifest(_Abs('/project/node_modules'), [entryPoint1, entryPoint2]); manifest.writeEntryPointManifest(_Abs('/project/node_modules'), [entryPoint1, entryPoint2]);
const file: EntryPointManifestFile = const file: EntryPointManifestFile =
JSON.parse(fs.readFile(_Abs('/project/node_modules/__ngcc_entry_points__.json'))); JSON.parse(fs.readFile(_Abs('/project/node_modules/__ngcc_entry_points__.json')));
@ -238,10 +286,24 @@ runInEachFileSystem(() => {
[ [
_Abs('/project/node_modules/package-1/'), _Abs('/project/node_modules/package-1/'),
_Abs('/project/node_modules/package-1/'), _Abs('/project/node_modules/package-1/'),
[
_Abs('/project/node_modules/other_package_1'),
_Abs('/project/node_modules/other_package_2'),
],
[],
[],
], ],
[ [
_Abs('/project/node_modules/package-2/'), _Abs('/project/node_modules/package-2/'),
_Abs('/project/node_modules/package-2/entry-point'), _Abs('/project/node_modules/package-2/entry-point'),
[],
[
_Abs('/project/node_modules/missing_1'),
relativeFrom('missing_2'),
],
[
_Abs('/project/node_modules/deep/import/path'),
],
] ]
]); ]);
}); });