From 57411c85b9119c9061795eea363f509151c91203 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 4 Jun 2020 08:43:05 +0100 Subject: [PATCH] feat(ngcc): implement a program-based entry-point finder (#37075) This finder is designed to only process entry-points that are reachable by the program defined by a tsconfig.json file. It is triggered by calling `mainNgcc()` with the `findEntryPointsFromTsConfigProgram` option set to true. It is ignored if a `targetEntryPointPath` has been provided as well. It is triggered from the command line by adding the `--use-program-dependencies` option, which is also ignored if the `--target` option has been provided. Using this option can speed up processing in cases where there is a large number of dependencies installed but only a small proportion of the entry-points are actually imported into the application. PR Close #37075 --- .../ngcc/src/command_line_options.ts | 13 +- .../program_based_entry_point_finder.ts | 51 +++++ .../targeted_entry_point_finder.ts | 175 +-------------- .../tracing_entry_point_finder.ts | 206 ++++++++++++++++++ packages/compiler-cli/ngcc/src/main.ts | 19 +- .../compiler-cli/ngcc/src/ngcc_options.ts | 11 +- packages/compiler-cli/ngcc/test/BUILD.bazel | 1 + .../test/dependencies/module_resolver_spec.ts | 10 + .../program_based_entry_point_finder_spec.ts | 166 ++++++++++++++ .../targeted_entry_point_finder_spec.ts | 64 +++--- .../ngcc/test/integration/ngcc_spec.ts | 49 ++++- 11 files changed, 549 insertions(+), 216 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/entry_point_finder/program_based_entry_point_finder.ts create mode 100644 packages/compiler-cli/ngcc/src/entry_point_finder/tracing_entry_point_finder.ts create mode 100644 packages/compiler-cli/ngcc/test/entry_point_finder/program_based_entry_point_finder_spec.ts diff --git a/packages/compiler-cli/ngcc/src/command_line_options.ts b/packages/compiler-cli/ngcc/src/command_line_options.ts index 9e7e60358b..3d70b30969 100644 --- a/packages/compiler-cli/ngcc/src/command_line_options.ts +++ b/packages/compiler-cli/ngcc/src/command_line_options.ts @@ -36,7 +36,14 @@ export function parseCommandLineOptions(args: string[]): NgccOptions { alias: 'target', describe: 'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' + - 'If this property is provided then `error-on-failed-entry-point` is forced to true', + 'If this property is provided then `error-on-failed-entry-point` is forced to true.\n' + + 'This option overrides the `--use-program-dependencies` option.', + }) + .option('use-program-dependencies', { + type: 'boolean', + describe: + 'If this property is provided then the entry-points to process are parsed from the program defined by the loaded tsconfig.json. See `--tsconfig`.\n' + + 'This option is overridden by the `--target` option.', }) .option('first-only', { describe: @@ -116,6 +123,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions { const enableI18nLegacyMessageIdFormat = options['legacy-message-ids']; const invalidateEntryPointManifest = options['invalidate-entry-point-manifest']; const errorOnFailedEntryPoint = options['error-on-failed-entry-point']; + const findEntryPointsFromTsConfigProgram = options['use-program-dependencies']; // yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a // string "false" to capture the `tsconfig=false` option. // And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`. @@ -134,6 +142,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions { async: options['async'], invalidateEntryPointManifest, errorOnFailedEntryPoint, - tsConfigPath + tsConfigPath, + findEntryPointsFromTsConfigProgram, }; } diff --git a/packages/compiler-cli/ngcc/src/entry_point_finder/program_based_entry_point_finder.ts b/packages/compiler-cli/ngcc/src/entry_point_finder/program_based_entry_point_finder.ts new file mode 100644 index 0000000000..16989cd242 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/entry_point_finder/program_based_entry_point_finder.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright Google LLC 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} from '../../../src/ngtsc/file_system'; +import {ParsedConfiguration} from '../../../src/perform_compile'; + +import {createDependencyInfo} from '../dependencies/dependency_host'; +import {DependencyResolver} from '../dependencies/dependency_resolver'; +import {EsmDependencyHost} from '../dependencies/esm_dependency_host'; +import {ModuleResolver} from '../dependencies/module_resolver'; +import {Logger} from '../logging/logger'; +import {NgccConfiguration} from '../packages/configuration'; +import {getPathMappingsFromTsConfig} from '../path_mappings'; + +import {TracingEntryPointFinder} from './tracing_entry_point_finder'; + +/** + * An EntryPointFinder that starts from the files in the program defined by the given tsconfig.json + * and only returns entry-points that are dependencies of these files. + * + * This is faster than searching the entire file-system for all the entry-points, + * and is used primarily by the CLI integration. + */ +export class ProgramBasedEntryPointFinder extends TracingEntryPointFinder { + constructor( + fs: FileSystem, config: NgccConfiguration, logger: Logger, resolver: DependencyResolver, + basePath: AbsoluteFsPath, private tsConfig: ParsedConfiguration, + projectPath: AbsoluteFsPath) { + super( + fs, config, logger, resolver, basePath, getPathMappingsFromTsConfig(tsConfig, projectPath)); + } + + protected getInitialEntryPointPaths(): AbsoluteFsPath[] { + const moduleResolver = new ModuleResolver(this.fs, this.pathMappings, ['', '.ts', '/index.ts']); + const host = new EsmDependencyHost(this.fs, moduleResolver); + const dependencies = createDependencyInfo(); + this.logger.debug( + `Using the program from ${this.tsConfig.project} to seed the entry-point finding.`); + this.logger.debug( + `Collecting dependencies from the following files:` + + this.tsConfig.rootNames.map(file => `\n- ${file}`)); + this.tsConfig.rootNames.forEach(rootName => { + host.collectDependencies(this.fs.resolve(rootName), dependencies); + }); + return Array.from(dependencies.dependencies); + } +} \ No newline at end of file diff --git a/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts b/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts index 740842b757..4bf2f113d3 100644 --- a/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts @@ -15,6 +15,7 @@ import {EntryPoint, EntryPointJsonProperty, getEntryPointInfo, INCOMPATIBLE_ENTR import {PathMappings} from '../path_mappings'; import {EntryPointFinder} from './interface'; +import {TracingEntryPointFinder} from './tracing_entry_point_finder'; import {getBasePaths} from './utils'; /** @@ -24,30 +25,16 @@ import {getBasePaths} from './utils'; * 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(); - private basePaths: AbsoluteFsPath[]|null = null; - private getBasePaths() { - if (this.basePaths === null) { - this.basePaths = getBasePaths(this.logger, this.basePath, this.pathMappings); - } - return this.basePaths; +export class TargetedEntryPointFinder extends TracingEntryPointFinder { + constructor( + fs: FileSystem, config: NgccConfiguration, logger: Logger, resolver: DependencyResolver, + basePath: AbsoluteFsPath, pathMappings: PathMappings|undefined, + private targetPath: AbsoluteFsPath) { + super(fs, config, logger, resolver, basePath, 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); - const entryPoints = this.resolver.sortEntryPointsByDependency( - Array.from(this.unsortedEntryPoints.values()), targetEntryPoint?.entryPoint); + const entryPoints = super.findEntryPoints(); const invalidTarget = entryPoints.invalidEntryPoints.find(i => i.entryPoint.path === this.targetPath); @@ -83,149 +70,7 @@ export class TargetedEntryPointFinder implements EntryPointFinder { return false; } - private processNextPath(): void { - const path = this.unprocessedPaths.shift()!; - const entryPoint = this.getEntryPoint(path); - if (entryPoint === null || !entryPoint.compiledByAngular) { - return; - } - const entryPointWithDeps = this.resolver.getEntryPointWithDependencies(entryPoint); - this.unsortedEntryPoints.set(entryPoint.path, entryPointWithDeps); - entryPointWithDeps.depInfo.dependencies.forEach(dep => { - if (!this.unsortedEntryPoints.has(dep)) { - this.unprocessedPaths.push(dep); - } - }); - } - - private getEntryPoint(entryPointPath: AbsoluteFsPath): EntryPoint|null { - const packagePath = this.computePackagePath(entryPointPath); - const entryPoint = - getEntryPointInfo(this.fs, this.config, this.logger, packagePath, entryPointPath); - if (entryPoint === NO_ENTRY_POINT || entryPoint === INCOMPATIBLE_ENTRY_POINT) { - return null; - } - return entryPoint; - } - - private computePackagePath(entryPointPath: AbsoluteFsPath): AbsoluteFsPath { - // First try the main basePath, to avoid having to compute the other basePaths from the paths - // mappings, which can be computationally intensive. - if (entryPointPath.startsWith(this.basePath)) { - const packagePath = this.computePackagePathFromContainingPath(entryPointPath, this.basePath); - if (packagePath !== null) { - return packagePath; - } - } - - // The main `basePath` didn't work out so now we try the `basePaths` computed from the paths - // mappings in `tsconfig.json`. - for (const basePath of this.getBasePaths()) { - if (entryPointPath.startsWith(basePath)) { - const packagePath = this.computePackagePathFromContainingPath(entryPointPath, basePath); - if (packagePath !== null) { - return packagePath; - } - // If we got here then we couldn't find a `packagePath` for the current `basePath`. - // Since `basePath`s are guaranteed not to be a sub-directory of each other then no other - // `basePath` will match either. - break; - } - } - - // Finally, if we couldn't find a `packagePath` using `basePaths` then try to find the nearest - // `node_modules` that contains the `entryPointPath`, if there is one, and use it as a - // `basePath`. - return this.computePackagePathFromNearestNodeModules(entryPointPath); - } - - /** - * Search down to the `entryPointPath` from the `containingPath` for the first `package.json` that - * we come to. This is the path to the entry-point's containing package. For example if - * `containingPath` 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 `containingPath`. E.g. if `containingPath` 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 computePackagePathFromContainingPath( - entryPointPath: AbsoluteFsPath, containingPath: AbsoluteFsPath): AbsoluteFsPath|null { - let packagePath = containingPath; - const segments = this.splitPath(relative(containingPath, entryPointPath)); - let nodeModulesIndex = segments.lastIndexOf(relativeFrom('node_modules')); - - // If there are no `node_modules` in the relative path between the `basePath` and the - // `entryPointPath` then just try the `basePath` as the `packagePath`. - // (This can be the case with path-mapped entry-points.) - if (nodeModulesIndex === -1) { - if (this.fs.exists(join(packagePath, 'package.json'))) { - return packagePath; - } - } - - // Start the search at the deepest nested `node_modules` folder that is below the `basePath` - // but above the `entryPointPath`, if there are any. - while (nodeModulesIndex >= 0) { - packagePath = join(packagePath, segments.shift()!); - nodeModulesIndex--; - } - - // Note that we start at the folder below the current candidate `packagePath` because the - // initial candidate `packagePath` is either a `node_modules` folder or the `basePath` with - // no `package.json`. - for (const segment of segments) { - packagePath = join(packagePath, segment); - if (this.fs.exists(join(packagePath, 'package.json'))) { - return packagePath; - } - } - return null; - } - - /** - * Search up the directory tree from the `entryPointPath` looking for a `node_modules` directory - * that we can use as a potential starting point for computing the package path. - */ - private computePackagePathFromNearestNodeModules(entryPointPath: AbsoluteFsPath): AbsoluteFsPath { - let packagePath = entryPointPath; - let scopedPackagePath = packagePath; - let containerPath = this.fs.dirname(packagePath); - while (!this.fs.isRoot(containerPath) && !containerPath.endsWith('node_modules')) { - scopedPackagePath = packagePath; - packagePath = containerPath; - containerPath = this.fs.dirname(containerPath); - } - - if (this.fs.exists(join(packagePath, 'package.json'))) { - // The directory directly below `node_modules` is a package - use it - return packagePath; - } else if ( - this.fs.basename(packagePath).startsWith('@') && - this.fs.exists(join(scopedPackagePath, 'package.json'))) { - // The directory directly below the `node_modules` is a scope and the directory directly - // below that is a scoped package - use it - return scopedPackagePath; - } else { - // If we get here then none of the `basePaths` contained the `entryPointPath` and the - // `entryPointPath` contains no `node_modules` that contains a package or a scoped - // package. All we can do is assume that this entry-point is a primary entry-point to a - // package. - 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; + protected getInitialEntryPointPaths(): AbsoluteFsPath[] { + return [this.targetPath]; } } diff --git a/packages/compiler-cli/ngcc/src/entry_point_finder/tracing_entry_point_finder.ts b/packages/compiler-cli/ngcc/src/entry_point_finder/tracing_entry_point_finder.ts new file mode 100644 index 0000000000..6f4f0c0088 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/entry_point_finder/tracing_entry_point_finder.ts @@ -0,0 +1,206 @@ +/** + * @license + * Copyright Google LLC 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, join, PathSegment, relative, relativeFrom} from '../../../src/ngtsc/file_system'; + +import {EntryPointWithDependencies} from '../dependencies/dependency_host'; +import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver'; +import {Logger} from '../logging/logger'; +import {NgccConfiguration} from '../packages/configuration'; +import {EntryPoint, getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT} from '../packages/entry_point'; +import {PathMappings} from '../path_mappings'; + +import {EntryPointFinder} from './interface'; +import {getBasePaths} from './utils'; + +/** + * An EntryPointFinder that starts from a set of initial files and only returns entry-points that + * are dependencies of these files. + * + * This is faster than searching the entire file-system for all the entry-points, + * and is used primarily by the CLI integration. + * + * There are two concrete implementation of this class. + * + * * `TargetEntryPointFinder` - is given a single entry-point as the initial entry-point + * * `ProgramBasedEntryPointFinder` - computes the initial entry-points from program files given by + * a `tsconfig.json` file. + */ +export abstract class TracingEntryPointFinder implements EntryPointFinder { + protected unprocessedPaths: AbsoluteFsPath[] = []; + protected unsortedEntryPoints = new Map(); + private basePaths: AbsoluteFsPath[]|null = null; + + constructor( + protected fs: FileSystem, protected config: NgccConfiguration, protected logger: Logger, + protected resolver: DependencyResolver, protected basePath: AbsoluteFsPath, + protected pathMappings: PathMappings|undefined) {} + + protected getBasePaths() { + if (this.basePaths === null) { + this.basePaths = getBasePaths(this.logger, this.basePath, this.pathMappings); + } + return this.basePaths; + } + + findEntryPoints(): SortedEntryPointsInfo { + this.unprocessedPaths = this.getInitialEntryPointPaths(); + while (this.unprocessedPaths.length > 0) { + this.processNextPath(); + } + return this.resolver.sortEntryPointsByDependency(Array.from(this.unsortedEntryPoints.values())); + } + + protected abstract getInitialEntryPointPaths(): AbsoluteFsPath[]; + + protected getEntryPoint(entryPointPath: AbsoluteFsPath): EntryPoint|null { + const packagePath = this.computePackagePath(entryPointPath); + const entryPoint = + getEntryPointInfo(this.fs, this.config, this.logger, packagePath, entryPointPath); + if (entryPoint === NO_ENTRY_POINT || entryPoint === INCOMPATIBLE_ENTRY_POINT) { + return null; + } + return entryPoint; + } + + private processNextPath(): void { + const path = this.unprocessedPaths.shift()!; + const entryPoint = this.getEntryPoint(path); + if (entryPoint === null || !entryPoint.compiledByAngular) { + return; + } + const entryPointWithDeps = this.resolver.getEntryPointWithDependencies(entryPoint); + this.unsortedEntryPoints.set(entryPoint.path, entryPointWithDeps); + entryPointWithDeps.depInfo.dependencies.forEach(dep => { + if (!this.unsortedEntryPoints.has(dep)) { + this.unprocessedPaths.push(dep); + } + }); + } + + private computePackagePath(entryPointPath: AbsoluteFsPath): AbsoluteFsPath { + // First try the main basePath, to avoid having to compute the other basePaths from the paths + // mappings, which can be computationally intensive. + if (entryPointPath.startsWith(this.basePath)) { + const packagePath = this.computePackagePathFromContainingPath(entryPointPath, this.basePath); + if (packagePath !== null) { + return packagePath; + } + } + + // The main `basePath` didn't work out so now we try the `basePaths` computed from the paths + // mappings in `tsconfig.json`. + for (const basePath of this.getBasePaths()) { + if (entryPointPath.startsWith(basePath)) { + const packagePath = this.computePackagePathFromContainingPath(entryPointPath, basePath); + if (packagePath !== null) { + return packagePath; + } + // If we got here then we couldn't find a `packagePath` for the current `basePath`. + // Since `basePath`s are guaranteed not to be a sub-directory of each other then no other + // `basePath` will match either. + break; + } + } + + // Finally, if we couldn't find a `packagePath` using `basePaths` then try to find the nearest + // `node_modules` that contains the `entryPointPath`, if there is one, and use it as a + // `basePath`. + return this.computePackagePathFromNearestNodeModules(entryPointPath); + } + + + /** + * Search down to the `entryPointPath` from the `containingPath` for the first `package.json` that + * we come to. This is the path to the entry-point's containing package. For example if + * `containingPath` 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 `containingPath`. E.g. if `containingPath` 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 computePackagePathFromContainingPath( + entryPointPath: AbsoluteFsPath, containingPath: AbsoluteFsPath): AbsoluteFsPath|null { + let packagePath = containingPath; + const segments = this.splitPath(relative(containingPath, entryPointPath)); + let nodeModulesIndex = segments.lastIndexOf(relativeFrom('node_modules')); + + // If there are no `node_modules` in the relative path between the `basePath` and the + // `entryPointPath` then just try the `basePath` as the `packagePath`. + // (This can be the case with path-mapped entry-points.) + if (nodeModulesIndex === -1) { + if (this.fs.exists(join(packagePath, 'package.json'))) { + return packagePath; + } + } + + // Start the search at the deepest nested `node_modules` folder that is below the `basePath` + // but above the `entryPointPath`, if there are any. + while (nodeModulesIndex >= 0) { + packagePath = join(packagePath, segments.shift()!); + nodeModulesIndex--; + } + + // Note that we start at the folder below the current candidate `packagePath` because the + // initial candidate `packagePath` is either a `node_modules` folder or the `basePath` with + // no `package.json`. + for (const segment of segments) { + packagePath = join(packagePath, segment); + if (this.fs.exists(join(packagePath, 'package.json'))) { + return packagePath; + } + } + return null; + } + + /** + * Search up the directory tree from the `entryPointPath` looking for a `node_modules` directory + * that we can use as a potential starting point for computing the package path. + */ + private computePackagePathFromNearestNodeModules(entryPointPath: AbsoluteFsPath): AbsoluteFsPath { + let packagePath = entryPointPath; + let scopedPackagePath = packagePath; + let containerPath = this.fs.dirname(packagePath); + while (!this.fs.isRoot(containerPath) && !containerPath.endsWith('node_modules')) { + scopedPackagePath = packagePath; + packagePath = containerPath; + containerPath = this.fs.dirname(containerPath); + } + + if (this.fs.exists(join(packagePath, 'package.json'))) { + // The directory directly below `node_modules` is a package - use it + return packagePath; + } else if ( + this.fs.basename(packagePath).startsWith('@') && + this.fs.exists(join(scopedPackagePath, 'package.json'))) { + // The directory directly below the `node_modules` is a scope and the directory directly + // below that is a scoped package - use it + return scopedPackagePath; + } else { + // If we get here then none of the `basePaths` contained the `entryPointPath` and the + // `entryPointPath` contains no `node_modules` that contains a package or a scoped + // package. All we can do is assume that this entry-point is a primary entry-point to a + // package. + 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; + } +} diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 780c4ff493..c2f3e77adf 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -11,6 +11,7 @@ import * as os from 'os'; import {AbsoluteFsPath, FileSystem, resolve} from '../../src/ngtsc/file_system'; +import {ParsedConfiguration} from '../../src/perform_compile'; import {CommonJsDependencyHost} from './dependencies/commonjs_dependency_host'; import {DependencyResolver} from './dependencies/dependency_resolver'; @@ -20,6 +21,7 @@ import {ModuleResolver} from './dependencies/module_resolver'; import {UmdDependencyHost} from './dependencies/umd_dependency_host'; import {DirectoryWalkerEntryPointFinder} from './entry_point_finder/directory_walker_entry_point_finder'; import {EntryPointFinder} from './entry_point_finder/interface'; +import {ProgramBasedEntryPointFinder} from './entry_point_finder/program_based_entry_point_finder'; import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder'; import {getAnalyzeEntryPointsFn} from './execution/analyze_entry_points'; import {Executor} from './execution/api'; @@ -81,7 +83,8 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis targetEntryPointPath !== undefined ? resolve(basePath, targetEntryPointPath) : null; const finder = getEntryPointFinder( fileSystem, logger, dependencyResolver, config, entryPointManifest, absBasePath, - absoluteTargetEntryPointPath, pathMappings); + absoluteTargetEntryPointPath, pathMappings, + options.findEntryPointsFromTsConfigProgram ? tsConfig : null, projectPath); if (finder instanceof TargetedEntryPointFinder && !finder.targetNeedsProcessingOrCleaning(supportedPropertiesToConsider, compileAllFormats)) { logger.debug('The target entry-point has already been processed'); @@ -195,13 +198,15 @@ function getDependencyResolver( function getEntryPointFinder( fs: FileSystem, logger: Logger, resolver: DependencyResolver, config: NgccConfiguration, entryPointManifest: EntryPointManifest, basePath: AbsoluteFsPath, - absoluteTargetEntryPointPath: AbsoluteFsPath|null, - pathMappings: PathMappings|undefined): EntryPointFinder { + absoluteTargetEntryPointPath: AbsoluteFsPath|null, pathMappings: PathMappings|undefined, + tsConfig: ParsedConfiguration|null, projectPath: AbsoluteFsPath): EntryPointFinder { if (absoluteTargetEntryPointPath !== null) { return new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, absoluteTargetEntryPointPath, pathMappings); - } else { - return new DirectoryWalkerEntryPointFinder( - fs, config, logger, resolver, entryPointManifest, basePath, pathMappings); + fs, config, logger, resolver, basePath, pathMappings, absoluteTargetEntryPointPath); + } else if (tsConfig !== null) { + return new ProgramBasedEntryPointFinder( + fs, config, logger, resolver, basePath, tsConfig, projectPath); } + return new DirectoryWalkerEntryPointFinder( + fs, config, logger, resolver, entryPointManifest, basePath, pathMappings); } diff --git a/packages/compiler-cli/ngcc/src/ngcc_options.ts b/packages/compiler-cli/ngcc/src/ngcc_options.ts index 4dc1642df3..5cbd50974f 100644 --- a/packages/compiler-cli/ngcc/src/ngcc_options.ts +++ b/packages/compiler-cli/ngcc/src/ngcc_options.ts @@ -124,6 +124,14 @@ export interface SyncNgccOptions { * If `null`, ngcc will not attempt to load any TS config file at all. */ tsConfigPath?: string|null; + + /** + * Use the program defined in the loaded tsconfig.json (if available - see + * `tsConfigPath` option) to identify the entry-points that should be processed. + * If this is set to `true` then only the entry-points reachable from the given + * program (and their dependencies) will be processed. + */ + findEntryPointsFromTsConfigProgram?: boolean; } /** @@ -136,7 +144,8 @@ export type AsyncNgccOptions = Omit&{async: true}; */ export type NgccOptions = AsyncNgccOptions|SyncNgccOptions; -export type OptionalNgccOptionKeys = 'targetEntryPointPath'|'tsConfigPath'|'pathMappings'; +export type OptionalNgccOptionKeys = + 'targetEntryPointPath'|'tsConfigPath'|'pathMappings'|'findEntryPointsFromTsConfigProgram'; export type RequiredNgccOptions = Required>; export type OptionalNgccOptions = Pick; export type SharedSetup = { diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 9e603dcfcf..80ff42aa20 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( ), deps = [ "//packages/compiler", + "//packages/compiler-cli", "//packages/compiler-cli/ngcc", "//packages/compiler-cli/ngcc/test/helpers", "//packages/compiler-cli/src/ngtsc/diagnostics", diff --git a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts index 265956e5d7..a41b16efb4 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts @@ -257,6 +257,16 @@ runInEachFileSystem(() => { .toEqual(new ResolvedExternalModule(_('/dist/package-4/secondary-entry-point'))); }); }); + + describe('with mapped path relative paths', () => { + it('should resolve to a relative file if found via a paths mapping', () => { + const resolver = new ModuleResolver( + getFileSystem(), {baseUrl: '/', paths: {'mapped/*': ['libs/local-package/*']}}); + + expect(resolver.resolveModuleImport('mapped/x', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); + }); + }); }); }); }); diff --git a/packages/compiler-cli/ngcc/test/entry_point_finder/program_based_entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/entry_point_finder/program_based_entry_point_finder_spec.ts new file mode 100644 index 0000000000..cb2deb2319 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/entry_point_finder/program_based_entry_point_finder_spec.ts @@ -0,0 +1,166 @@ +/** + * @license + * Copyright Google LLC 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 {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, relative} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing'; +import {readConfiguration} from '../../../src/perform_compile'; +import {loadTestFiles} from '../../../test/helpers'; +import {DependencyResolver} from '../../src/dependencies/dependency_resolver'; +import {DtsDependencyHost} from '../../src/dependencies/dts_dependency_host'; +import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; +import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {ProgramBasedEntryPointFinder} from '../../src/entry_point_finder/program_based_entry_point_finder'; +import {NgccConfiguration} from '../../src/packages/configuration'; +import {EntryPoint} from '../../src/packages/entry_point'; +import {EntryPointManifest} from '../../src/packages/entry_point_manifest'; +import {MockLogger} from '../helpers/mock_logger'; + +runInEachFileSystem(() => { + describe('ProgramBasedEntryPointFinder', () => { + let fs: FileSystem; + let _Abs: typeof absoluteFrom; + let projectPath: AbsoluteFsPath; + let basePath: AbsoluteFsPath; + let angularNamespacePath: AbsoluteFsPath; + + beforeEach(() => { + fs = getFileSystem(); + _Abs = absoluteFrom; + projectPath = _Abs('/sub_entry_points'); + basePath = _Abs('/sub_entry_points/node_modules'); + angularNamespacePath = fs.resolve(basePath, '@angular'); + }); + + describe('findEntryPoints()', () => { + it('should find entry-points imported into the program', () => { + loadTestFiles([ + ...createProgram(projectPath), + ...createPackage(angularNamespacePath, 'core'), + ...createPackage(angularNamespacePath, 'common'), + ...createPackage(fs.resolve(angularNamespacePath, 'common'), 'http', ['@angular/common']), + ...createPackage( + fs.resolve(angularNamespacePath, 'common/http'), 'testing', + ['common/http', 'common/testing']), + ...createPackage( + fs.resolve(angularNamespacePath, 'common'), 'testing', ['@angular/common']), + ]); + const finder = createFinder(); + const {entryPoints} = finder.findEntryPoints(); + expect(dumpEntryPointPaths(basePath, entryPoints)).toEqual([ + ['@angular/core', '@angular/core'], + ['@angular/common', '@angular/common'], + ['@angular/common', '@angular/common/http'], + ]); + }); + + function createFinder(): ProgramBasedEntryPointFinder { + const tsConfig = readConfiguration(`${projectPath}/tsconfig.json`); + const baseUrl = fs.resolve(projectPath, tsConfig.options.basePath!); + const paths = tsConfig.options.paths!; + + const logger = new MockLogger(); + const srcHost = new EsmDependencyHost(fs, new ModuleResolver(fs, {baseUrl, paths})); + const dtsHost = new DtsDependencyHost(fs); + const config = new NgccConfiguration(fs, projectPath); + const resolver = new DependencyResolver(fs, logger, config, {esm2015: srcHost}, dtsHost); + const manifest = new EntryPointManifest(fs, config, logger); + return new ProgramBasedEntryPointFinder( + fs, config, logger, resolver, basePath, tsConfig, projectPath); + } + + function createProgram(projectPath: AbsoluteFsPath): TestFile[] { + return [ + { + name: _Abs(`${projectPath}/package.json`), + contents: '', + }, + { + name: _Abs(`${projectPath}/tsconfig.json`), + contents: `{ + "files": [ + "src/main.ts" + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "lib/*": ["lib/*"] + } + } + }`, + }, + { + name: _Abs(`${projectPath}/src/main.ts`), + contents: ` + import {AppComponent} from './app.component'; + import * from './app.module'; + import * from 'lib/service'; + `, + }, + { + name: _Abs(`${projectPath}/src/app.component.ts`), + contents: ` + import * as core from '@angular/core'; + export class AppComponent {} + `, + }, + { + name: _Abs(`${projectPath}/src/app.module.ts`), + contents: ` + import {NgModule} from '@angular/core'; + import * as common from '@angular/common'; + import {AppComponent} from './app.component'; + export class AppModule {} + `, + }, + { + name: _Abs(`${projectPath}/lib/service/index.ts`), + contents: ` + import * as http from '@angular/common/http'; + export class Service {} + `, + }, + ]; + } + + function createPackage( + basePath: AbsoluteFsPath, packageName: string, deps: string[] = [], + isCompiledByAngular = true): TestFile[] { + const files: TestFile[] = [ + { + name: _Abs(`${basePath}/${packageName}/package.json`), + contents: JSON.stringify({ + typings: `./${packageName}.d.ts`, + fesm2015: `./fesm2015/${packageName}.js`, + }) + }, + { + name: _Abs(`${basePath}/${packageName}/${packageName}.d.ts`), + contents: deps.map((dep, i) => `import * as i${i} from '${dep}';`).join('\n'), + }, + { + name: _Abs(`${basePath}/${packageName}/fesm2015/${packageName}.js`), + contents: deps.map((dep, i) => `import * as i${i} from '${dep}';`).join('\n'), + }, + ]; + + if (isCompiledByAngular) { + files.push({ + name: _Abs(`${basePath}/${packageName}/${packageName}.metadata.json`), + contents: 'metadata info' + }); + } + + return files; + } + + function dumpEntryPointPaths( + basePath: AbsoluteFsPath, entryPoints: EntryPoint[]): [string, string][] { + return entryPoints.map(x => [relative(basePath, x.package), relative(basePath, x.path)]); + } + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts index c319d7917f..bccff4338b 100644 --- a/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts @@ -50,8 +50,8 @@ runInEachFileSystem(() => { ...createPackage(fs.resolve(basePath, 'common'), 'testing', ['@angular/common']), ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/sub_entry_points/node_modules'), targetPath, - undefined); + fs, config, logger, resolver, _Abs('/sub_entry_points/node_modules'), undefined, + targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(basePath, entryPoints)).toEqual([ ['common', 'common'], @@ -69,8 +69,8 @@ runInEachFileSystem(() => { ...createPackage(fs.resolve(basePath, 'common'), 'testing', ['common']), ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/sub_entry_points/node_modules'), targetPath, - undefined); + fs, config, logger, resolver, _Abs('/sub_entry_points/node_modules'), undefined, + targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(basePath, entryPoints)).toEqual([ ['common', 'common'], @@ -92,7 +92,7 @@ runInEachFileSystem(() => { ...createPackage(fs.resolve(basePath, '@angular/common'), 'testing', ['@angular/common']), ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/namespaced/node_modules'), targetPath, undefined); + fs, config, logger, resolver, _Abs('/namespaced/node_modules'), undefined, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(basePath, entryPoints)).toEqual([ ['@angular/common', '@angular/common'], @@ -104,7 +104,7 @@ runInEachFileSystem(() => { const targetPath = _Abs('/no_packages/node_modules/should_not_be_found'); fs.ensureDir(_Abs('/no_packages/node_modules/should_not_be_found')); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/no_packages/node_modules'), targetPath, undefined); + fs, config, logger, resolver, _Abs('/no_packages/node_modules'), undefined, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(entryPoints).toEqual([]); }); @@ -118,8 +118,8 @@ runInEachFileSystem(() => { }, ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), targetPath, - undefined); + fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), undefined, + targetPath); const {entryPoints} = finder.findEntryPoints(); expect(entryPoints).toEqual([]); }); @@ -143,8 +143,8 @@ runInEachFileSystem(() => { }, ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), - targetPath, undefined); + fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), undefined, + targetPath); const {entryPoints} = finder.findEntryPoints(); expect(entryPoints).toEqual([]); }); @@ -156,8 +156,8 @@ runInEachFileSystem(() => { ...createPackage(_Abs('/nested_node_modules/node_modules/outer/node_modules'), 'inner'), ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/nested_node_modules/node_modules'), targetPath, - undefined); + fs, config, logger, resolver, _Abs('/nested_node_modules/node_modules'), undefined, + targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(_Abs('/nested_node_modules/node_modules'), entryPoints)) .toEqual([ @@ -175,7 +175,7 @@ runInEachFileSystem(() => { ...createPackage(_Abs('/nested_node_modules/node_modules/package'), 'entry-point'), ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(_Abs('/nested_node_modules'), entryPoints)).toEqual([ ['node_modules/package', 'node_modules/package/entry-point'], @@ -200,7 +200,7 @@ runInEachFileSystem(() => { // entry-point info for the `lib3/entry-point` dependency. const targetPath = _Abs('/project/node_modules/lib1/node_modules/lib2'); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(_Abs('/project/node_modules'), entryPoints)).toEqual([ ['lib3', 'lib3/entry-point'], @@ -232,7 +232,7 @@ runInEachFileSystem(() => { // entry-point info for the `lib3/entry-point` dependency. const targetPath = _Abs('/project/node_modules/lib1/node_modules/lib2'); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(_Abs('/project/node_modules'), entryPoints)).toEqual([ ['@scope/lib3/node_modules/lib4', '@scope/lib3/node_modules/lib4/entry-point'], @@ -266,7 +266,7 @@ runInEachFileSystem(() => { const dtsHost = new DtsDependencyHost(fs, pathMappings); resolver = new DependencyResolver(fs, logger, config, {esm2015: srcHost}, dtsHost); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, pathMappings); + fs, config, logger, resolver, basePath, pathMappings, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(basePath, entryPoints)).toEqual([ ['pkg1', 'pkg1'], @@ -294,7 +294,7 @@ runInEachFileSystem(() => { const dtsHost = new DtsDependencyHost(fs, pathMappings); resolver = new DependencyResolver(fs, logger, config, {esm2015: srcHost}, dtsHost); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, pathMappings); + fs, config, logger, resolver, basePath, pathMappings, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(entryPoints.length).toEqual(1); const entryPoint = entryPoints[0]; @@ -327,7 +327,7 @@ runInEachFileSystem(() => { const dtsHost = new DtsDependencyHost(fs, pathMappings); resolver = new DependencyResolver(fs, logger, config, {esm2015: srcHost}, dtsHost); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, pathMappings); + fs, config, logger, resolver, basePath, pathMappings, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(basePath, entryPoints)).toEqual([ ['../dist/my-lib/a', '../dist/my-lib/a'], @@ -354,7 +354,7 @@ runInEachFileSystem(() => { const dtsHost = new DtsDependencyHost(fs, pathMappings); resolver = new DependencyResolver(fs, logger, config, {esm2015: srcHost}, dtsHost); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, pathMappings); + fs, config, logger, resolver, basePath, pathMappings, targetPath); const {entryPoints} = finder.findEntryPoints(); expect(dumpEntryPointPaths(basePath, entryPoints)).toEqual([ ['test', 'test'], @@ -372,7 +372,7 @@ runInEachFileSystem(() => { const targetPath = _Abs('/no_packages/node_modules/should_not_be_found'); fs.ensureDir(targetPath); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/no_packages/node_modules'), targetPath, undefined); + fs, config, logger, resolver, _Abs('/no_packages/node_modules'), undefined, targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015'], true)).toBe(false); }); @@ -385,8 +385,8 @@ runInEachFileSystem(() => { }, ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), targetPath, - undefined); + fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), undefined, + targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015'], true)).toBe(false); }); @@ -408,8 +408,8 @@ runInEachFileSystem(() => { }, ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), targetPath, - undefined); + fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), undefined, + targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015'], true)).toBe(false); }); @@ -431,8 +431,8 @@ runInEachFileSystem(() => { }, ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), - targetPath, undefined); + fs, config, logger, resolver, _Abs('/no_valid_entry_points/node_modules'), undefined, + targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015'], true)).toBe(false); }); @@ -448,7 +448,7 @@ runInEachFileSystem(() => { ...createPackage(fs.resolve(basePath, 'common'), 'testing', ['common']), ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015', 'esm5'], true)).toBe(true); }); @@ -474,7 +474,7 @@ runInEachFileSystem(() => { fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015', 'esm5'], true)).toBe(true); }); @@ -500,7 +500,7 @@ runInEachFileSystem(() => { fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015', 'esm5'], true)).toBe(false); }); }); @@ -518,7 +518,7 @@ runInEachFileSystem(() => { ]); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015', 'esm5'], false)).toBe(true); }); @@ -544,7 +544,7 @@ runInEachFileSystem(() => { fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015', 'esm5'], false)).toBe(true); }); @@ -570,7 +570,7 @@ runInEachFileSystem(() => { fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); const finder = new TargetedEntryPointFinder( - fs, config, logger, resolver, basePath, targetPath, undefined); + fs, config, logger, resolver, basePath, undefined, targetPath); expect(finder.targetNeedsProcessingOrCleaning(['fesm2015', 'esm5'], false)) .toBe(false); }); diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 7278491b3b..dbf6dd65c0 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -32,6 +32,14 @@ runInEachFileSystem(() => { let _: typeof absoluteFrom; let fs: FileSystem; let pkgJsonUpdater: PackageJsonUpdater; + const STANDARD_MARKERS = { + main: '0.0.0-PLACEHOLDER', + module: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', + esm2015: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }; beforeEach(() => { _ = absoluteFrom; @@ -644,15 +652,6 @@ runInEachFileSystem(() => { describe('with targetEntryPointPath', () => { it('should only compile the given package entry-point (and its dependencies).', () => { - const STANDARD_MARKERS = { - main: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', - es2015: '0.0.0-PLACEHOLDER', - esm2015: '0.0.0-PLACEHOLDER', - fesm2015: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }; - mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing'}); expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__) .toEqual(STANDARD_MARKERS); @@ -818,6 +817,27 @@ runInEachFileSystem(() => { pkgJsonUpdater, targetPackage, targetPackageJsonPath, ['typings', ...properties]); } + describe('with findEntryPointsFromTsConfigProgram', () => { + it('should only compile the package entry-points (and their dependencies) reachable from the program in tsconfig.json.', + () => { + mainNgcc({basePath: '/node_modules', findEntryPointsFromTsConfigProgram: true}); + // * `common/testing` is a dependency of `./y`, so is compiled. + expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__) + .toEqual(STANDARD_MARKERS); + // * `common/http` is a dependency of `./x`, so is compiled. + expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__) + .toEqual(STANDARD_MARKERS); + // * `core` is a dependency of `common/http`, so is compiled. + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS); + // * `common` is a private (only in .js not .d.ts) dependency so is compiled. + expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__) + .toEqual(STANDARD_MARKERS); + // * `common/http/testing` is not a dependency of the program so is not compiled. + expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__) + .toBeUndefined(); + }); + }); + it('should clean up outdated artifacts', () => { compileIntoFlatEs2015Package('test-package', { 'index.ts': ` @@ -2113,6 +2133,17 @@ runInEachFileSystem(() => { }, {name: _('/node_modules/invalid-package/index.metadata.json'), contents: 'DUMMY DATA'}, ]); + + // A sample application that imports entry-points + loadTestFiles([ + {name: _('/tsconfig.json'), contents: '{"files": ["src/index.ts"]}'}, + {name: _('/src/index.ts'), contents: `import {X} from './x';\nimport {Y} from './y';`}, + {name: _('/src/x.ts'), contents: `import '@angular/common/http';\nexport class X {}`}, + { + name: _('/src/y.ts'), + contents: `import * as t from '@angular/common/testing';\n export class Y {}` + }, + ]); } }); });