Skip to content

Commit f3ccd29

Browse files
petebacondarwinatscott
authored andcommitted
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
1 parent 5c0bdae commit f3ccd29

File tree

11 files changed

+549
-216
lines changed

11 files changed

+549
-216
lines changed

packages/compiler-cli/ngcc/src/command_line_options.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
3636
alias: 'target',
3737
describe:
3838
'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' +
39-
'If this property is provided then `error-on-failed-entry-point` is forced to true',
39+
'If this property is provided then `error-on-failed-entry-point` is forced to true.\n' +
40+
'This option overrides the `--use-program-dependencies` option.',
41+
})
42+
.option('use-program-dependencies', {
43+
type: 'boolean',
44+
describe:
45+
'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' +
46+
'This option is overridden by the `--target` option.',
4047
})
4148
.option('first-only', {
4249
describe:
@@ -116,6 +123,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
116123
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
117124
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
118125
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
126+
const findEntryPointsFromTsConfigProgram = options['use-program-dependencies'];
119127
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
120128
// string "false" to capture the `tsconfig=false` option.
121129
// 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 {
134142
async: options['async'],
135143
invalidateEntryPointManifest,
136144
errorOnFailedEntryPoint,
137-
tsConfigPath
145+
tsConfigPath,
146+
findEntryPointsFromTsConfigProgram,
138147
};
139148
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
9+
import {ParsedConfiguration} from '../../../src/perform_compile';
10+
11+
import {createDependencyInfo} from '../dependencies/dependency_host';
12+
import {DependencyResolver} from '../dependencies/dependency_resolver';
13+
import {EsmDependencyHost} from '../dependencies/esm_dependency_host';
14+
import {ModuleResolver} from '../dependencies/module_resolver';
15+
import {Logger} from '../logging/logger';
16+
import {NgccConfiguration} from '../packages/configuration';
17+
import {getPathMappingsFromTsConfig} from '../path_mappings';
18+
19+
import {TracingEntryPointFinder} from './tracing_entry_point_finder';
20+
21+
/**
22+
* An EntryPointFinder that starts from the files in the program defined by the given tsconfig.json
23+
* and only returns entry-points that are dependencies of these files.
24+
*
25+
* This is faster than searching the entire file-system for all the entry-points,
26+
* and is used primarily by the CLI integration.
27+
*/
28+
export class ProgramBasedEntryPointFinder extends TracingEntryPointFinder {
29+
constructor(
30+
fs: FileSystem, config: NgccConfiguration, logger: Logger, resolver: DependencyResolver,
31+
basePath: AbsoluteFsPath, private tsConfig: ParsedConfiguration,
32+
projectPath: AbsoluteFsPath) {
33+
super(
34+
fs, config, logger, resolver, basePath, getPathMappingsFromTsConfig(tsConfig, projectPath));
35+
}
36+
37+
protected getInitialEntryPointPaths(): AbsoluteFsPath[] {
38+
const moduleResolver = new ModuleResolver(this.fs, this.pathMappings, ['', '.ts', '/index.ts']);
39+
const host = new EsmDependencyHost(this.fs, moduleResolver);
40+
const dependencies = createDependencyInfo();
41+
this.logger.debug(
42+
`Using the program from ${this.tsConfig.project} to seed the entry-point finding.`);
43+
this.logger.debug(
44+
`Collecting dependencies from the following files:` +
45+
this.tsConfig.rootNames.map(file => `\n- ${file}`));
46+
this.tsConfig.rootNames.forEach(rootName => {
47+
host.collectDependencies(this.fs.resolve(rootName), dependencies);
48+
});
49+
return Array.from(dependencies.dependencies);
50+
}
51+
}

packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts

Lines changed: 10 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {EntryPoint, EntryPointJsonProperty, getEntryPointInfo, INCOMPATIBLE_ENTR
1515
import {PathMappings} from '../path_mappings';
1616

1717
import {EntryPointFinder} from './interface';
18+
import {TracingEntryPointFinder} from './tracing_entry_point_finder';
1819
import {getBasePaths} from './utils';
1920

2021
/**
@@ -24,30 +25,16 @@ import {getBasePaths} from './utils';
2425
* This is faster than searching the entire file-system for all the entry-points,
2526
* and is used primarily by the CLI integration.
2627
*/
27-
export class TargetedEntryPointFinder implements EntryPointFinder {
28-
private unprocessedPaths: AbsoluteFsPath[] = [];
29-
private unsortedEntryPoints = new Map<AbsoluteFsPath, EntryPointWithDependencies>();
30-
private basePaths: AbsoluteFsPath[]|null = null;
31-
private getBasePaths() {
32-
if (this.basePaths === null) {
33-
this.basePaths = getBasePaths(this.logger, this.basePath, this.pathMappings);
34-
}
35-
return this.basePaths;
36-
}
37-
28+
export class TargetedEntryPointFinder extends TracingEntryPointFinder {
3829
constructor(
39-
private fs: FileSystem, private config: NgccConfiguration, private logger: Logger,
40-
private resolver: DependencyResolver, private basePath: AbsoluteFsPath,
41-
private targetPath: AbsoluteFsPath, private pathMappings: PathMappings|undefined) {}
30+
fs: FileSystem, config: NgccConfiguration, logger: Logger, resolver: DependencyResolver,
31+
basePath: AbsoluteFsPath, pathMappings: PathMappings|undefined,
32+
private targetPath: AbsoluteFsPath) {
33+
super(fs, config, logger, resolver, basePath, pathMappings);
34+
}
4235

4336
findEntryPoints(): SortedEntryPointsInfo {
44-
this.unprocessedPaths = [this.targetPath];
45-
while (this.unprocessedPaths.length > 0) {
46-
this.processNextPath();
47-
}
48-
const targetEntryPoint = this.unsortedEntryPoints.get(this.targetPath);
49-
const entryPoints = this.resolver.sortEntryPointsByDependency(
50-
Array.from(this.unsortedEntryPoints.values()), targetEntryPoint?.entryPoint);
37+
const entryPoints = super.findEntryPoints();
5138

5239
const invalidTarget =
5340
entryPoints.invalidEntryPoints.find(i => i.entryPoint.path === this.targetPath);
@@ -83,149 +70,7 @@ export class TargetedEntryPointFinder implements EntryPointFinder {
8370
return false;
8471
}
8572

86-
private processNextPath(): void {
87-
const path = this.unprocessedPaths.shift()!;
88-
const entryPoint = this.getEntryPoint(path);
89-
if (entryPoint === null || !entryPoint.compiledByAngular) {
90-
return;
91-
}
92-
const entryPointWithDeps = this.resolver.getEntryPointWithDependencies(entryPoint);
93-
this.unsortedEntryPoints.set(entryPoint.path, entryPointWithDeps);
94-
entryPointWithDeps.depInfo.dependencies.forEach(dep => {
95-
if (!this.unsortedEntryPoints.has(dep)) {
96-
this.unprocessedPaths.push(dep);
97-
}
98-
});
99-
}
100-
101-
private getEntryPoint(entryPointPath: AbsoluteFsPath): EntryPoint|null {
102-
const packagePath = this.computePackagePath(entryPointPath);
103-
const entryPoint =
104-
getEntryPointInfo(this.fs, this.config, this.logger, packagePath, entryPointPath);
105-
if (entryPoint === NO_ENTRY_POINT || entryPoint === INCOMPATIBLE_ENTRY_POINT) {
106-
return null;
107-
}
108-
return entryPoint;
109-
}
110-
111-
private computePackagePath(entryPointPath: AbsoluteFsPath): AbsoluteFsPath {
112-
// First try the main basePath, to avoid having to compute the other basePaths from the paths
113-
// mappings, which can be computationally intensive.
114-
if (entryPointPath.startsWith(this.basePath)) {
115-
const packagePath = this.computePackagePathFromContainingPath(entryPointPath, this.basePath);
116-
if (packagePath !== null) {
117-
return packagePath;
118-
}
119-
}
120-
121-
// The main `basePath` didn't work out so now we try the `basePaths` computed from the paths
122-
// mappings in `tsconfig.json`.
123-
for (const basePath of this.getBasePaths()) {
124-
if (entryPointPath.startsWith(basePath)) {
125-
const packagePath = this.computePackagePathFromContainingPath(entryPointPath, basePath);
126-
if (packagePath !== null) {
127-
return packagePath;
128-
}
129-
// If we got here then we couldn't find a `packagePath` for the current `basePath`.
130-
// Since `basePath`s are guaranteed not to be a sub-directory of each other then no other
131-
// `basePath` will match either.
132-
break;
133-
}
134-
}
135-
136-
// Finally, if we couldn't find a `packagePath` using `basePaths` then try to find the nearest
137-
// `node_modules` that contains the `entryPointPath`, if there is one, and use it as a
138-
// `basePath`.
139-
return this.computePackagePathFromNearestNodeModules(entryPointPath);
140-
}
141-
142-
/**
143-
* Search down to the `entryPointPath` from the `containingPath` for the first `package.json` that
144-
* we come to. This is the path to the entry-point's containing package. For example if
145-
* `containingPath` is `/a/b/c` and `entryPointPath` is `/a/b/c/d/e` and there exists
146-
* `/a/b/c/d/package.json` and `/a/b/c/d/e/package.json`, then we will return `/a/b/c/d`.
147-
*
148-
* To account for nested `node_modules` we actually start the search at the last `node_modules` in
149-
* the `entryPointPath` that is below the `containingPath`. E.g. if `containingPath` is `/a/b/c`
150-
* and `entryPointPath` is `/a/b/c/d/node_modules/x/y/z`, we start the search at
151-
* `/a/b/c/d/node_modules`.
152-
*/
153-
private computePackagePathFromContainingPath(
154-
entryPointPath: AbsoluteFsPath, containingPath: AbsoluteFsPath): AbsoluteFsPath|null {
155-
let packagePath = containingPath;
156-
const segments = this.splitPath(relative(containingPath, entryPointPath));
157-
let nodeModulesIndex = segments.lastIndexOf(relativeFrom('node_modules'));
158-
159-
// If there are no `node_modules` in the relative path between the `basePath` and the
160-
// `entryPointPath` then just try the `basePath` as the `packagePath`.
161-
// (This can be the case with path-mapped entry-points.)
162-
if (nodeModulesIndex === -1) {
163-
if (this.fs.exists(join(packagePath, 'package.json'))) {
164-
return packagePath;
165-
}
166-
}
167-
168-
// Start the search at the deepest nested `node_modules` folder that is below the `basePath`
169-
// but above the `entryPointPath`, if there are any.
170-
while (nodeModulesIndex >= 0) {
171-
packagePath = join(packagePath, segments.shift()!);
172-
nodeModulesIndex--;
173-
}
174-
175-
// Note that we start at the folder below the current candidate `packagePath` because the
176-
// initial candidate `packagePath` is either a `node_modules` folder or the `basePath` with
177-
// no `package.json`.
178-
for (const segment of segments) {
179-
packagePath = join(packagePath, segment);
180-
if (this.fs.exists(join(packagePath, 'package.json'))) {
181-
return packagePath;
182-
}
183-
}
184-
return null;
185-
}
186-
187-
/**
188-
* Search up the directory tree from the `entryPointPath` looking for a `node_modules` directory
189-
* that we can use as a potential starting point for computing the package path.
190-
*/
191-
private computePackagePathFromNearestNodeModules(entryPointPath: AbsoluteFsPath): AbsoluteFsPath {
192-
let packagePath = entryPointPath;
193-
let scopedPackagePath = packagePath;
194-
let containerPath = this.fs.dirname(packagePath);
195-
while (!this.fs.isRoot(containerPath) && !containerPath.endsWith('node_modules')) {
196-
scopedPackagePath = packagePath;
197-
packagePath = containerPath;
198-
containerPath = this.fs.dirname(containerPath);
199-
}
200-
201-
if (this.fs.exists(join(packagePath, 'package.json'))) {
202-
// The directory directly below `node_modules` is a package - use it
203-
return packagePath;
204-
} else if (
205-
this.fs.basename(packagePath).startsWith('@') &&
206-
this.fs.exists(join(scopedPackagePath, 'package.json'))) {
207-
// The directory directly below the `node_modules` is a scope and the directory directly
208-
// below that is a scoped package - use it
209-
return scopedPackagePath;
210-
} else {
211-
// If we get here then none of the `basePaths` contained the `entryPointPath` and the
212-
// `entryPointPath` contains no `node_modules` that contains a package or a scoped
213-
// package. All we can do is assume that this entry-point is a primary entry-point to a
214-
// package.
215-
return entryPointPath;
216-
}
217-
}
218-
219-
/**
220-
* Split the given `path` into path segments using an FS independent algorithm.
221-
* @param path The path to split.
222-
*/
223-
private splitPath(path: PathSegment) {
224-
const segments = [];
225-
while (path !== '.') {
226-
segments.unshift(this.fs.basename(path));
227-
path = this.fs.dirname(path);
228-
}
229-
return segments;
73+
protected getInitialEntryPointPaths(): AbsoluteFsPath[] {
74+
return [this.targetPath];
23075
}
23176
}

0 commit comments

Comments
 (0)