feat(ivy): ngcc - support targeting a start entry-point (#29092)

You can now, programmatically, specify an entry-point where
the ngcc compilation will occur.
Only this entry-point and its dependencies will be compiled.

FW-1119

PR Close #29092
This commit is contained in:
Pete Bacon Darwin 2019-03-20 13:47:58 +00:00 committed by Matias Niemelä
parent 66239b9d09
commit 4bb0259bc0
5 changed files with 94 additions and 23 deletions

View File

@ -24,6 +24,11 @@ export interface NgccOptions {
formats: EntryPointFormat[]; formats: EntryPointFormat[];
/** The path to the node_modules folder where modified files should be written. */ /** The path to the node_modules folder where modified files should be written. */
baseTargetPath?: string; baseTargetPath?: string;
/**
* The path, relative to `baseSourcePath` of the primary package to be compiled.
* All its dependencies will need to be compiled too.
*/
targetEntryPointPath?: string;
} }
/** /**
@ -34,14 +39,14 @@ export interface NgccOptions {
* *
* @param options The options telling ngcc what to compile and how. * @param options The options telling ngcc what to compile and how.
*/ */
export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePath}: NgccOptions): export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePath,
void { targetEntryPointPath}: NgccOptions): void {
const transformer = new Transformer(baseSourcePath, baseTargetPath); const transformer = new Transformer(baseSourcePath, baseTargetPath);
const host = new DependencyHost(); const host = new DependencyHost();
const resolver = new DependencyResolver(host); const resolver = new DependencyResolver(host);
const finder = new EntryPointFinder(resolver); const finder = new EntryPointFinder(resolver);
const {entryPoints} = finder.findEntryPoints(baseSourcePath); const {entryPoints} = finder.findEntryPoints(baseSourcePath, targetEntryPointPath);
entryPoints.forEach(entryPoint => { entryPoints.forEach(entryPoint => {
// Are we compiling the Angular core? // Are we compiling the Angular core?

View File

@ -44,7 +44,7 @@ export interface IgnoredDependency {
} }
/** /**
* The result of sorting the entry-points by their dependencies. * A list of entry-points, sorted by their dependencies.
* *
* The `entryPoints` array will be ordered so that no entry point depends upon an entry point that * The `entryPoints` array will be ordered so that no entry point depends upon an entry point that
* appears later in the array. * appears later in the array.
@ -67,9 +67,35 @@ export class DependencyResolver {
* Sort the array of entry points so that the dependant entry points always come later than * Sort the array of entry points so that the dependant entry points always come later than
* their dependencies in the array. * their dependencies in the array.
* @param entryPoints An array entry points to sort. * @param entryPoints An array entry points to sort.
* @returns the result of sorting the entry points. * @param target If provided, only return entry-points depended on by this entry-point.
* @returns the result of sorting the entry points by dependency.
*/ */
sortEntryPointsByDependency(entryPoints: EntryPoint[]): SortedEntryPointsInfo { sortEntryPointsByDependency(entryPoints: EntryPoint[], target?: EntryPoint):
SortedEntryPointsInfo {
const {invalidEntryPoints, ignoredDependencies, graph} = this.createDependencyInfo(entryPoints);
let sortedEntryPointNodes: string[];
if (target) {
sortedEntryPointNodes = graph.dependenciesOf(target.path);
sortedEntryPointNodes.push(target.path);
} else {
sortedEntryPointNodes = graph.overallOrder();
}
return {
entryPoints: sortedEntryPointNodes.map(path => graph.getNodeData(path)),
invalidEntryPoints,
ignoredDependencies,
};
}
/**
* Computes a dependency graph of the given entry-points.
*
* The graph only holds entry-points that ngcc cares about and whose dependencies
* (direct and transitive) all exist.
*/
private createDependencyInfo(entryPoints: EntryPoint[]) {
const invalidEntryPoints: InvalidEntryPoint[] = []; const invalidEntryPoints: InvalidEntryPoint[] = [];
const ignoredDependencies: IgnoredDependency[] = []; const ignoredDependencies: IgnoredDependency[] = [];
const graph = new DepGraph<EntryPoint>(); const graph = new DepGraph<EntryPoint>();
@ -79,15 +105,10 @@ export class DependencyResolver {
// Now add the dependencies between them // Now add the dependencies between them
entryPoints.forEach(entryPoint => { entryPoints.forEach(entryPoint => {
const entryPointPath = entryPoint.fesm2015 || entryPoint.esm2015;
if (!entryPointPath) {
throw new Error(
`ESM2015 format (flat and non-flat) missing in '${entryPoint.path}' entry-point.`);
}
const dependencies = new Set<string>(); const dependencies = new Set<string>();
const missing = new Set<string>(); const missing = new Set<string>();
const deepImports = new Set<string>(); const deepImports = new Set<string>();
const entryPointPath = getEntryPointPath(entryPoint);
this.host.computeDependencies(entryPointPath, dependencies, missing, deepImports); this.host.computeDependencies(entryPointPath, dependencies, missing, deepImports);
if (missing.size > 0) { if (missing.size > 0) {
@ -119,13 +140,7 @@ export class DependencyResolver {
} }
}); });
// The map now only holds entry-points that ngcc cares about and whose dependencies return {invalidEntryPoints, ignoredDependencies, graph};
// (direct and transitive) all exist.
return {
entryPoints: graph.overallOrder().map(path => graph.getNodeData(path)),
invalidEntryPoints,
ignoredDependencies
};
function removeNodes(entryPoint: EntryPoint, missingDependencies: string[]) { function removeNodes(entryPoint: EntryPoint, missingDependencies: string[]) {
const nodesToRemove = [entryPoint.path, ...graph.dependantsOf(entryPoint.path)]; const nodesToRemove = [entryPoint.path, ...graph.dependantsOf(entryPoint.path)];
@ -136,3 +151,13 @@ export class DependencyResolver {
} }
} }
} }
function getEntryPointPath(entryPoint: EntryPoint): string {
const entryPointPath =
entryPoint.fesm2015 || entryPoint.fesm5 || entryPoint.esm2015 || entryPoint.esm5;
if (!entryPointPath) {
throw new Error(
`There is no format with import statements in '${entryPoint.path}' entry-point.`);
}
return entryPointPath;
}

View File

@ -18,9 +18,14 @@ export class EntryPointFinder {
* Search the given directory, and sub-directories, for Angular package entry points. * Search the given directory, and sub-directories, for Angular package entry points.
* @param sourceDirectory An absolute path to the directory to search for entry points. * @param sourceDirectory An absolute path to the directory to search for entry points.
*/ */
findEntryPoints(sourceDirectory: string): SortedEntryPointsInfo { findEntryPoints(sourceDirectory: string, targetEntryPointPath?: string): SortedEntryPointsInfo {
const unsortedEntryPoints = walkDirectoryForEntryPoints(sourceDirectory); const unsortedEntryPoints = walkDirectoryForEntryPoints(sourceDirectory);
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints); targetEntryPointPath =
targetEntryPointPath && path.resolve(sourceDirectory, targetEntryPointPath);
const targetEntryPoint = targetEntryPointPath ?
unsortedEntryPoints.find(entryPoint => entryPoint.path === targetEntryPointPath) :
undefined;
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints, targetEntryPoint);
} }
} }

View File

@ -33,6 +33,25 @@ describe('ngcc main()', () => {
it('should run ngcc without errors for esm5', () => { it('should run ngcc without errors for esm5', () => {
expect(() => mainNgcc({baseSourcePath: '/node_modules', formats: ['esm5']})).not.toThrow(); expect(() => mainNgcc({baseSourcePath: '/node_modules', formats: ['esm5']})).not.toThrow();
}); });
it('should only compile the given package entry-point (and its dependencies)', () => {
mainNgcc({
baseSourcePath: '/node_modules',
formats: ['esm2015'],
targetEntryPointPath: '@angular/common'
});
expect(loadPackage('@angular/common').__modified_by_ngcc__).toEqual({
esm2015: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/core').__modified_by_ngcc__).toEqual({
esm2015: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__modified_by_ngcc__).toBeUndefined();
expect(loadPackage('@angular/common/http').__modified_by_ngcc__).toBeUndefined();
});
}); });

View File

@ -7,7 +7,7 @@
*/ */
import {DependencyHost} from '../../src/packages/dependency_host'; import {DependencyHost} from '../../src/packages/dependency_host';
import {DependencyResolver} from '../../src/packages/dependency_resolver'; import {DependencyResolver, SortedEntryPointsInfo} from '../../src/packages/dependency_resolver';
import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPoint} from '../../src/packages/entry_point';
describe('DependencyResolver', () => { describe('DependencyResolver', () => {
@ -83,7 +83,7 @@ describe('DependencyResolver', () => {
it('should error if the entry point does not have either the fesm2015 nor esm2015 formats', it('should error if the entry point does not have either the fesm2015 nor esm2015 formats',
() => { () => {
expect(() => resolver.sortEntryPointsByDependency([{ path: 'first' } as EntryPoint])) expect(() => resolver.sortEntryPointsByDependency([{ path: 'first' } as EntryPoint]))
.toThrowError(`ESM2015 format (flat and non-flat) missing in 'first' entry-point.`); .toThrowError(`There is no format with import statements in 'first' entry-point.`);
}); });
it('should capture any dependencies that were ignored', () => { it('should capture any dependencies that were ignored', () => {
@ -95,6 +95,23 @@ describe('DependencyResolver', () => {
]); ]);
}); });
it('should only return dependencies of the target, if provided', () => {
spyOn(host, 'computeDependencies').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]);
});
interface DepMap { interface DepMap {
[path: string]: {resolved: string[], missing: string[]}; [path: string]: {resolved: string[], missing: string[]};
} }