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[];
/** The path to the node_modules folder where modified files should be written. */
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.
*/
export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePath}: NgccOptions):
void {
export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePath,
targetEntryPointPath}: NgccOptions): void {
const transformer = new Transformer(baseSourcePath, baseTargetPath);
const host = new DependencyHost();
const resolver = new DependencyResolver(host);
const finder = new EntryPointFinder(resolver);
const {entryPoints} = finder.findEntryPoints(baseSourcePath);
const {entryPoints} = finder.findEntryPoints(baseSourcePath, targetEntryPointPath);
entryPoints.forEach(entryPoint => {
// 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
* 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
* their dependencies in the array.
* @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 ignoredDependencies: IgnoredDependency[] = [];
const graph = new DepGraph<EntryPoint>();
@ -79,15 +105,10 @@ export class DependencyResolver {
// Now add the dependencies between them
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 missing = new Set<string>();
const deepImports = new Set<string>();
const entryPointPath = getEntryPointPath(entryPoint);
this.host.computeDependencies(entryPointPath, dependencies, missing, deepImports);
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
// (direct and transitive) all exist.
return {
entryPoints: graph.overallOrder().map(path => graph.getNodeData(path)),
invalidEntryPoints,
ignoredDependencies
};
return {invalidEntryPoints, ignoredDependencies, graph};
function removeNodes(entryPoint: EntryPoint, missingDependencies: string[]) {
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.
* @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);
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', () => {
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 {DependencyResolver} from '../../src/packages/dependency_resolver';
import {DependencyResolver, SortedEntryPointsInfo} from '../../src/packages/dependency_resolver';
import {EntryPoint} from '../../src/packages/entry_point';
describe('DependencyResolver', () => {
@ -83,7 +83,7 @@ describe('DependencyResolver', () => {
it('should error if the entry point does not have either the fesm2015 nor esm2015 formats',
() => {
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', () => {
@ -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 {
[path: string]: {resolved: string[], missing: string[]};
}