Pete Bacon Darwin 8d13f631d9 refactor(ngcc): support processing only the typings files of packages (#40976)
Some tools (such as Language Server and ng-packagr) only care about
the processed typings generated by ngcc. Forcing these tools to process
the JavaScript files as well has two disadvantages:

First, unnecessary work is being done, which is time consuming.

But more importantly, it is not always possible to know how the final bundling
tools will want the processed JavaScript to be configured. For example,
the CLI would prefer the `--create-ivy-entry-points` option but this would
break non-CLI build tooling.

This commit adds a new option (`--typings-only` on the command line, and
`typingsOnly` via programmatic API) that instructs ngcc to only render changes
to the typings files for the entry-points that it finds, and not to write any
JavaScript files.

In order to process the typings, a JavaScript format will need to be analysed, but
it will not be rendered to disk. When using this option, it is best to offer ngcc a
wide range of possible JavaScript formats to choose from, and it will use the
first format that it finds. Ideally you would configure it to try the `ES2015` FESM
format first, since this will be the most performant.

Fixes #40969

PR Close #40976
2021-02-24 14:23:14 -08:00

176 lines
7.8 KiB
TypeScript

/**
* @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 {DepGraph} from 'dependency-graph';
import {FileSystem} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
import {InvalidEntryPoint} from '../dependencies/dependency_resolver';
import {EntryPointFinder} from '../entry_point_finder/interface';
import {ParallelTaskQueue} from '../execution/tasks/queues/parallel_task_queue';
import {SerialTaskQueue} from '../execution/tasks/queues/serial_task_queue';
import {computeTaskDependencies} from '../execution/tasks/utils';
import {hasBeenProcessed} from '../packages/build_marker';
import {EntryPoint, EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../packages/entry_point';
import {cleanOutdatedPackages} from '../writing/cleaning/package_cleaner';
import {AnalyzeEntryPointsFn} from './api';
import {DtsProcessing, PartiallyOrderedTasks, TaskQueue} from './tasks/api';
/**
* Create the function for performing the analysis of the entry-points.
*/
export function getAnalyzeEntryPointsFn(
logger: Logger, finder: EntryPointFinder, fileSystem: FileSystem,
supportedPropertiesToConsider: EntryPointJsonProperty[], typingsOnly: boolean,
compileAllFormats: boolean, propertiesToConsider: string[],
inParallel: boolean): AnalyzeEntryPointsFn {
return () => {
logger.debug('Analyzing entry-points...');
const startTime = Date.now();
let entryPointInfo = finder.findEntryPoints();
const cleaned = cleanOutdatedPackages(fileSystem, entryPointInfo.entryPoints);
if (cleaned) {
// If we had to clean up one or more packages then we must read in the entry-points again.
entryPointInfo = finder.findEntryPoints();
}
const {entryPoints, invalidEntryPoints, graph} = entryPointInfo;
logInvalidEntryPoints(logger, invalidEntryPoints);
const unprocessableEntryPointPaths: string[] = [];
// The tasks are partially ordered by virtue of the entry-points being partially ordered too.
const tasks: PartiallyOrderedTasks = [] as any;
for (const entryPoint of entryPoints) {
const packageJson = entryPoint.packageJson;
const hasProcessedTypings = hasBeenProcessed(packageJson, 'typings');
const {propertiesToProcess, equivalentPropertiesMap} = getPropertiesToProcess(
packageJson, supportedPropertiesToConsider, compileAllFormats, typingsOnly);
let processDts = hasProcessedTypings ? DtsProcessing.No :
typingsOnly ? DtsProcessing.Only : DtsProcessing.Yes;
if (propertiesToProcess.length === 0) {
// This entry-point is unprocessable (i.e. there is no format property that is of interest
// and can be processed). This will result in an error, but continue looping over
// entry-points in order to collect all unprocessable ones and display a more informative
// error.
unprocessableEntryPointPaths.push(entryPoint.path);
continue;
}
for (const formatProperty of propertiesToProcess) {
if (hasBeenProcessed(entryPoint.packageJson, formatProperty)) {
// The format-path which the property maps to is already processed - nothing to do.
logger.debug(`Skipping ${entryPoint.name} : ${formatProperty} (already compiled).`);
continue;
}
const formatPropertiesToMarkAsProcessed = equivalentPropertiesMap.get(formatProperty)!;
tasks.push({entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts});
// Only process typings for the first property (if not already processed).
processDts = DtsProcessing.No;
}
}
// Check for entry-points for which we could not process any format at all.
if (unprocessableEntryPointPaths.length > 0) {
throw new Error(
'Unable to process any formats for the following entry-points (tried ' +
`${propertiesToConsider.join(', ')}): ` +
unprocessableEntryPointPaths.map(path => `\n - ${path}`).join(''));
}
const duration = Math.round((Date.now() - startTime) / 100) / 10;
logger.debug(
`Analyzed ${entryPoints.length} entry-points in ${duration}s. ` +
`(Total tasks: ${tasks.length})`);
return getTaskQueue(logger, inParallel, tasks, graph);
};
}
function logInvalidEntryPoints(logger: Logger, invalidEntryPoints: InvalidEntryPoint[]): void {
invalidEntryPoints.forEach(invalidEntryPoint => {
logger.debug(
`Invalid entry-point ${invalidEntryPoint.entryPoint.path}.`,
`It is missing required dependencies:\n` +
invalidEntryPoint.missingDependencies.map(dep => ` - ${dep}`).join('\n'));
});
}
/**
* This function computes and returns the following:
* - `propertiesToProcess`: An (ordered) list of properties that exist and need to be processed,
* based on the provided `propertiesToConsider`, the properties in `package.json` and their
* corresponding format-paths. NOTE: Only one property per format-path needs to be processed.
* - `equivalentPropertiesMap`: A mapping from each property in `propertiesToProcess` to the list of
* other format properties in `package.json` that need to be marked as processed as soon as the
* former has been processed.
*/
function getPropertiesToProcess(
packageJson: EntryPointPackageJson, propertiesToConsider: EntryPointJsonProperty[],
compileAllFormats: boolean, typingsOnly: boolean): {
propertiesToProcess: EntryPointJsonProperty[];
equivalentPropertiesMap: Map<EntryPointJsonProperty, EntryPointJsonProperty[]>;
} {
const formatPathsToConsider = new Set<string>();
const propertiesToProcess: EntryPointJsonProperty[] = [];
for (const prop of propertiesToConsider) {
const formatPath = packageJson[prop];
// Ignore properties that are not defined in `package.json`.
if (typeof formatPath !== 'string') continue;
// Ignore properties that map to the same format-path as a preceding property.
if (formatPathsToConsider.has(formatPath)) continue;
// Process this property, because it is the first one to map to this format-path.
formatPathsToConsider.add(formatPath);
propertiesToProcess.push(prop);
// If we only need one format processed, there is no need to process any more properties.
if (!compileAllFormats) break;
}
const formatPathToProperties: {[formatPath: string]: EntryPointJsonProperty[]} = {};
for (const prop of SUPPORTED_FORMAT_PROPERTIES) {
const formatPath = packageJson[prop];
// Ignore properties that are not defined in `package.json`.
if (typeof formatPath !== 'string') continue;
// Ignore properties that do not map to a format-path that will be considered.
if (!formatPathsToConsider.has(formatPath)) continue;
// Add this property to the map.
const list = formatPathToProperties[formatPath] || (formatPathToProperties[formatPath] = []);
list.push(prop);
}
const equivalentPropertiesMap = new Map<EntryPointJsonProperty, EntryPointJsonProperty[]>();
for (const prop of propertiesToConsider) {
const formatPath = packageJson[prop]!;
// If we are only processing typings then there should be no format properties to mark
const equivalentProperties = typingsOnly ? [] : formatPathToProperties[formatPath];
equivalentPropertiesMap.set(prop, equivalentProperties);
}
return {propertiesToProcess, equivalentPropertiesMap};
}
function getTaskQueue(
logger: Logger, inParallel: boolean, tasks: PartiallyOrderedTasks,
graph: DepGraph<EntryPoint>): TaskQueue {
const dependencies = computeTaskDependencies(tasks, graph);
return inParallel ? new ParallelTaskQueue(logger, tasks, dependencies) :
new SerialTaskQueue(logger, tasks, dependencies);
}