Skip to content

ngcc: implement a new program-based entry-point finder #37075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
13 changes: 11 additions & 2 deletions packages/compiler-cli/ngcc/src/command_line_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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`.
Expand All @@ -134,6 +142,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
async: options['async'],
invalidateEntryPointManifest,
errorOnFailedEntryPoint,
tsConfigPath
tsConfigPath,
findEntryPointsFromTsConfigProgram,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,22 @@ import * as ts from 'typescript';

import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {isRequireCall, isWildcardReexportStatement, RequireCall} from '../host/commonjs_umd_utils';
import {isAssignment, isAssignmentStatement} from '../host/esm2015_host';

import {DependencyHostBase} from './dependency_host';
import {ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';

/**
* Helper functions for computing dependencies.
*/
export class CommonJsDependencyHost extends DependencyHostBase {
/**
* Compute the dependencies of the given file.
*
* @param file An absolute path to the file whose dependencies we want to get.
* @param dependencies A set that will have the absolute paths of resolved entry points added to
* it.
* @param missing A set that will have the dependencies that could not be found added to it.
* @param deepImports A set that will have the import paths that exist but cannot be mapped to
* entry-points, i.e. deep-imports.
* @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck
* in a circular dependency loop.
*/
protected recursivelyCollectDependencies(
file: AbsoluteFsPath, dependencies: Set<AbsoluteFsPath>, missing: Set<string>,
deepImports: Set<AbsoluteFsPath>, alreadySeen: Set<AbsoluteFsPath>): void {
const fromContents = this.fs.readFile(file);

if (!this.hasRequireCalls(fromContents)) {
// Avoid parsing the source file as there are no imports.
return;
}
protected canSkipFile(fileContents: string): boolean {
return !hasRequireCalls(fileContents);
}

protected extractImports(file: AbsoluteFsPath, fileContents: string): Set<string> {
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
const sf =
ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
ts.createSourceFile(file, fileContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
const requireCalls: RequireCall[] = [];

for (const stmt of sf.statements) {
Expand Down Expand Up @@ -92,37 +75,20 @@ export class CommonJsDependencyHost extends DependencyHostBase {
}
}

const importPaths = new Set(requireCalls.map(call => call.arguments[0].text));
for (const importPath of importPaths) {
const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file);
if (resolvedModule === null) {
missing.add(importPath);
} else if (resolvedModule instanceof ResolvedRelativeModule) {
const internalDependency = resolvedModule.modulePath;
if (!alreadySeen.has(internalDependency)) {
alreadySeen.add(internalDependency);
this.recursivelyCollectDependencies(
internalDependency, dependencies, missing, deepImports, alreadySeen);
}
} else if (resolvedModule instanceof ResolvedDeepImport) {
deepImports.add(resolvedModule.importPath);
} else {
dependencies.add(resolvedModule.entryPointPath);
}
}
return new Set(requireCalls.map(call => call.arguments[0].text));
}
}

/**
* Check whether a source file needs to be parsed for imports.
* This is a performance short-circuit, which saves us from creating
* a TypeScript AST unnecessarily.
*
* @param source The content of the source file to check.
*
* @returns false if there are definitely no require calls
* in this file, true otherwise.
*/
private hasRequireCalls(source: string): boolean {
return /require\(['"]/.test(source);
}
/**
* Check whether a source file needs to be parsed for imports.
* This is a performance short-circuit, which saves us from creating
* a TypeScript AST unnecessarily.
*
* @param source The content of the source file to check.
*
* @returns false if there are definitely no require calls
* in this file, true otherwise.
*/
export function hasRequireCalls(source: string): boolean {
return /require\(['"]/.test(source);
}
53 changes: 50 additions & 3 deletions packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_s
import {EntryPoint} from '../packages/entry_point';
import {resolveFileWithPostfixes} from '../utils';

import {ModuleResolver} from './module_resolver';
import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';

export interface DependencyHost {
collectDependencies(
Expand Down Expand Up @@ -65,7 +65,54 @@ export abstract class DependencyHostBase implements DependencyHost {
* @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck
* in a circular dependency loop.
*/
protected abstract recursivelyCollectDependencies(
protected recursivelyCollectDependencies(
file: AbsoluteFsPath, dependencies: Set<AbsoluteFsPath>, missing: Set<string>,
deepImports: Set<AbsoluteFsPath>, alreadySeen: Set<AbsoluteFsPath>): void;
deepImports: Set<string>, alreadySeen: Set<AbsoluteFsPath>): void {
const fromContents = this.fs.readFile(file);
if (this.canSkipFile(fromContents)) {
return;
}
const imports = this.extractImports(file, fromContents);
for (const importPath of imports) {
const resolved =
this.processImport(importPath, file, dependencies, missing, deepImports, alreadySeen);
if (!resolved) {
missing.add(importPath);
}
}
}

protected abstract canSkipFile(fileContents: string): boolean;
protected abstract extractImports(file: AbsoluteFsPath, fileContents: string): Set<string>;

/**
* Resolve the given `importPath` from `file` and add it to the appropriate set.
*
* If the import is local to this package then follow it by calling
* `recursivelyCollectDependencies()`.
*
* @returns `true` if the import was resolved (to an entry-point, a local import, or a
* deep-import), `false` otherwise.
*/
protected processImport(
importPath: string, file: AbsoluteFsPath, dependencies: Set<AbsoluteFsPath>,
missing: Set<string>, deepImports: Set<string>, alreadySeen: Set<AbsoluteFsPath>): boolean {
const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file);
if (resolvedModule === null) {
return false;
}
if (resolvedModule instanceof ResolvedRelativeModule) {
const internalDependency = resolvedModule.modulePath;
if (!alreadySeen.has(internalDependency)) {
alreadySeen.add(internalDependency);
this.recursivelyCollectDependencies(
internalDependency, dependencies, missing, deepImports, alreadySeen);
}
} else if (resolvedModule instanceof ResolvedDeepImport) {
deepImports.add(resolvedModule.importPath);
} else {
dependencies.add(resolvedModule.entryPointPath);
}
return true;
}
}
Loading