Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions packages/compiler-cli/ngcc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,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 {CachedFileSystem, NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system';

import {NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system';
import {mainNgcc} from './src/main';
import {hasBeenProcessed as _hasBeenProcessed} from './src/packages/build_marker';
import {EntryPointJsonProperty, EntryPointPackageJson} from './src/packages/entry_point';

export {ConsoleLogger, LogLevel} from './src/logging/console_logger';
export {Logger} from './src/logging/logger';
export {NgccOptions, mainNgcc as process} from './src/main';
export {NgccOptions} from './src/main';
export {PathMappings} from './src/utils';

export function hasBeenProcessed(packageJson: object, format: string) {
// We are wrapping this function to hide the internal types.
// Recreate the file system on each call to reset the cache
setFileSystem(new CachedFileSystem(new NodeJSFileSystem()));
return _hasBeenProcessed(packageJson as EntryPointPackageJson, format as EntryPointJsonProperty);
}

// Configure the file-system for external users.
setFileSystem(new NodeJSFileSystem());
export function process(...args: Parameters<typeof mainNgcc>) {
// Recreate the file system on each call to reset the cache
setFileSystem(new CachedFileSystem(new NodeJSFileSystem()));
return mainNgcc(...args);
}
4 changes: 2 additions & 2 deletions packages/compiler-cli/ngcc/main-ngcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/
import * as yargs from 'yargs';

import {resolve, setFileSystem, NodeJSFileSystem} from '../src/ngtsc/file_system';
import {resolve, setFileSystem, CachedFileSystem, NodeJSFileSystem} from '../src/ngtsc/file_system';
import {mainNgcc} from './src/main';
import {ConsoleLogger, LogLevel} from './src/logging/console_logger';

Expand Down Expand Up @@ -57,7 +57,7 @@ if (require.main === module) {
process.exit(1);
}

setFileSystem(new NodeJSFileSystem());
setFileSystem(new CachedFileSystem(new NodeJSFileSystem()));

const baseSourcePath = resolve(options['s'] || './node_modules');
const propertiesToConsider: string[] = options['p'];
Expand Down
20 changes: 12 additions & 8 deletions packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {DepGraph} from 'dependency-graph';
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';
import {DependencyHost, DependencyInfo} from './dependency_host';

/**
* Holds information about entry points that are removed because
Expand Down Expand Up @@ -98,6 +98,16 @@ export class DependencyResolver {
};
}

getEntryPointDependencies(entryPoint: EntryPoint): DependencyInfo {
const formatInfo = this.getEntryPointFormatInfo(entryPoint);
const host = this.hosts[formatInfo.format];
if (!host) {
throw new Error(
`Could not find a suitable format for computing dependencies of entry-point: '${entryPoint.path}'.`);
}
return host.findDependencies(formatInfo.path);
}

/**
* Computes a dependency graph of the given entry-points.
*
Expand All @@ -116,13 +126,7 @@ export class DependencyResolver {

// Now add the dependencies between them
angularEntryPoints.forEach(entryPoint => {
const formatInfo = this.getEntryPointFormatInfo(entryPoint);
const host = this.hosts[formatInfo.format];
if (!host) {
throw new Error(
`Could not find a suitable format for computing dependencies of entry-point: '${entryPoint.path}'.`);
}
const {dependencies, missing, deepImports} = host.findDependencies(formatInfo.path);
const {dependencies, missing, deepImports} = this.getEntryPointDependencies(entryPoint);

if (missing.size > 0) {
// This entry point has dependencies that are missing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,61 +8,31 @@
import {AbsoluteFsPath, FileSystem, join, resolve} from '../../../src/ngtsc/file_system';
import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
import {Logger} from '../logging/logger';
import {NgccConfiguration} from '../packages/configuration';
import {EntryPoint, getEntryPointInfo} from '../packages/entry_point';
import {PathMappings} from '../utils';
import {NgccConfiguration} from './configuration';
import {EntryPoint, getEntryPointInfo} from './entry_point';
import {EntryPointFinder} from './interface';
import {getBasePaths} from './utils';

export class EntryPointFinder {
/**
* An EntryPointFinder that searches for all entry-points that can be found given a `basePath` and
* `pathMappings`.
*/
export class DirectoryWalkerEntryPointFinder implements EntryPointFinder {
private basePaths = getBasePaths(this.sourceDirectory, this.pathMappings);
constructor(
private fs: FileSystem, private config: NgccConfiguration, private logger: Logger,
private resolver: DependencyResolver) {}
private resolver: DependencyResolver, private sourceDirectory: AbsoluteFsPath,
private pathMappings: PathMappings|undefined) {}
/**
* 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.
* Search the `sourceDirectory`, and sub-directories, using `pathMappings` as necessary, to find
* all package entry-points.
*/
findEntryPoints(
sourceDirectory: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath,
pathMappings?: PathMappings): SortedEntryPointsInfo {
const basePaths = this.getBasePaths(sourceDirectory, pathMappings);
const unsortedEntryPoints = basePaths.reduce<EntryPoint[]>(
findEntryPoints(): SortedEntryPointsInfo {
const unsortedEntryPoints = this.basePaths.reduce<EntryPoint[]>(
(entryPoints, basePath) => entryPoints.concat(this.walkDirectoryForEntryPoints(basePath)),
[]);
const targetEntryPoint = targetEntryPointPath ?
unsortedEntryPoints.find(entryPoint => entryPoint.path === targetEntryPointPath) :
undefined;
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints, targetEntryPoint);
}

/**
* Extract all the base-paths that we need to search for entry-points.
*
* This always contains the standard base-path (`sourceDirectory`).
* But it also parses the `paths` mappings object to guess additional base-paths.
*
* For example:
*
* ```
* getBasePaths('/node_modules', {baseUrl: '/dist', paths: {'*': ['lib/*', 'lib/generated/*']}})
* > ['/node_modules', '/dist/lib']
* ```
*
* Notice that `'/dist'` is not included as there is no `'*'` path,
* and `'/dist/lib/generated'` is not included as it is covered by `'/dist/lib'`.
*
* @param sourceDirectory The standard base-path (e.g. node_modules).
* @param pathMappings Path mapping configuration, from which to extract additional base-paths.
*/
private getBasePaths(sourceDirectory: AbsoluteFsPath, pathMappings?: PathMappings):
AbsoluteFsPath[] {
const basePaths = [sourceDirectory];
if (pathMappings) {
const baseUrl = resolve(pathMappings.baseUrl);
values(pathMappings.paths).forEach(paths => paths.forEach(path => {
basePaths.push(join(baseUrl, extractPathPrefix(path)));
}));
}
basePaths.sort(); // Get the paths in order with the shorter ones first.
return basePaths.filter(removeDeeperPaths);
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints);
}

/**
Expand Down Expand Up @@ -168,38 +138,6 @@ export class EntryPointFinder {
}
}

/**
* Extract everything in the `path` up to the first `*`.
* @param path The path to parse.
* @returns The extracted prefix.
*/
function extractPathPrefix(path: string) {
return path.split('*', 1)[0];
}

/**
* A filter function that removes paths that are already covered by higher paths.
*
* @param value The current path.
* @param index The index of the current path.
* @param array The array of paths (sorted alphabetically).
* @returns true if this path is not already covered by a previous path.
*/
function removeDeeperPaths(value: AbsoluteFsPath, index: number, array: AbsoluteFsPath[]) {
for (let i = 0; i < index; i++) {
if (value.startsWith(array[i])) return false;
}
return true;
}

/**
* Extract all the values (not keys) from an object.
* @param obj The object to process.
*/
function values<T>(obj: {[key: string]: T}): T[] {
return Object.keys(obj).map(key => obj[key]);
}

function stripJsExtension<T extends string>(filePath: T): T {
return filePath.replace(/\.js$/, '') as T;
}
15 changes: 15 additions & 0 deletions packages/compiler-cli/ngcc/src/entry_point_finder/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @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 {SortedEntryPointsInfo} from '../dependencies/dependency_resolver';

export interface EntryPointFinder {
/**
* Search for Angular package entry-points.
*/
findEntryPoints(): SortedEntryPointsInfo;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* @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, FileSystem, PathSegment, join, relative, relativeFrom} from '../../../src/ngtsc/file_system';
import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
import {Logger} from '../logging/logger';
import {NgccConfiguration} from '../packages/configuration';
import {EntryPoint, getEntryPointInfo} from '../packages/entry_point';
import {PathMappings} from '../utils';
import {EntryPointFinder} from './interface';
import {getBasePaths} from './utils';

/**
* An EntryPointFinder that starts from a target entry-point and only finds
* entry-points that are dependencies of the target.
*
* This is faster than searching the entire file-system for all the entry-points,
* and is used primarily by the CLI integration.
*/
export class TargetedEntryPointFinder implements EntryPointFinder {
private unprocessedPaths: AbsoluteFsPath[] = [];
private unsortedEntryPoints = new Map<AbsoluteFsPath, EntryPoint>();
private basePaths = getBasePaths(this.basePath, this.pathMappings);

constructor(
private fs: FileSystem, private config: NgccConfiguration, private logger: Logger,
private resolver: DependencyResolver, private basePath: AbsoluteFsPath,
private targetPath: AbsoluteFsPath, private pathMappings: PathMappings|undefined) {}

findEntryPoints(): SortedEntryPointsInfo {
this.unprocessedPaths = [this.targetPath];
while (this.unprocessedPaths.length > 0) {
this.processNextPath();
}
const targetEntryPoint = this.unsortedEntryPoints.get(this.targetPath);
return this.resolver.sortEntryPointsByDependency(
Array.from(this.unsortedEntryPoints.values()), targetEntryPoint);
}

private processNextPath(): void {
const path = this.unprocessedPaths.shift() !;
const entryPoint = this.getEntryPoint(path);
if (entryPoint !== null) {
this.unsortedEntryPoints.set(entryPoint.path, entryPoint);
const deps = this.resolver.getEntryPointDependencies(entryPoint);
deps.dependencies.forEach(dep => {
if (!this.unsortedEntryPoints.has(dep)) {
this.unprocessedPaths.push(dep);
}
});
}
}

private getEntryPoint(entryPointPath: AbsoluteFsPath): EntryPoint|null {
const packagePath = this.computePackagePath(entryPointPath);
return getEntryPointInfo(this.fs, this.config, this.logger, packagePath, entryPointPath);
}

/**
* Search down to the `entryPointPath` from each `basePath` for the first `package.json` that we
* come to. This is the path to the entry-point's containing package. For example if `basePath` is
* `/a/b/c` and `entryPointPath` is `/a/b/c/d/e` and there exists `/a/b/c/d/package.json` and
* `/a/b/c/d/e/package.json`, then we will return `/a/b/c/d`.
*
* To account for nested `node_modules` we actually start the search at the last `node_modules` in
* the `entryPointPath` that is below the `basePath`. E.g. if `basePath` is `/a/b/c` and
* `entryPointPath` is `/a/b/c/d/node_modules/x/y/z`, we start the search at
* `/a/b/c/d/node_modules`.
*/
private computePackagePath(entryPointPath: AbsoluteFsPath): AbsoluteFsPath {
for (const basePath of this.basePaths) {
if (entryPointPath.startsWith(basePath)) {
let packagePath = basePath;
const segments = this.splitPath(relative(basePath, entryPointPath));

// Start the search at the last nested `node_modules` folder if the relative
// `entryPointPath` contains one or more.
let nodeModulesIndex = segments.lastIndexOf(relativeFrom('node_modules'));
while (nodeModulesIndex >= 0) {
packagePath = join(packagePath, segments.shift() !);
nodeModulesIndex--;
}
Comment thread
petebacondarwin marked this conversation as resolved.
Resolved

// Note that we skip the first `packagePath` and start looking from the first folder below
// it because that will be the `node_modules` folder.
for (const segment of segments) {
packagePath = join(packagePath, segment);
if (this.fs.exists(join(packagePath, 'package.json'))) {
return packagePath;
}
}

// If we got here then we couldn't find a `packagePath` for the current `basePath` but since
// `basePath`s are guaranteed not to be a sub-directory each other then no other `basePath`
// will match either.
break;
}
Comment thread
petebacondarwin marked this conversation as resolved.
Resolved
}
// If we get here then none of the `basePaths` matched the `entryPointPath`, which
// is somewhat unexpected and means that this entry-point lives completely outside
// any of the `basePaths`.
// All we can do is assume that his entry-point is a primary entry-point to a package.
Comment thread
petebacondarwin marked this conversation as resolved.
Resolved
return entryPointPath;
}


/**
* Split the given `path` into path segments using an FS independent algorithm.
* @param path The path to split.
*/
private splitPath(path: PathSegment) {
const segments = [];
while (path !== '.') {
segments.unshift(this.fs.basename(path));
path = this.fs.dirname(path);
}
return segments;
}
}
Loading