refactor(ivy): ngcc - split work into distinct analyze/compile/execute phases (#32052)
This refactoring more clearly separates the different phases of the work performed by `ngcc`, setting the ground for being able to run each phase independently in the future and improve performance via parallelization. Inspired by/Based on @alxhub's prototype: alxhub/angular@cb631bdb1 PR Close #32052
This commit is contained in:
parent
2954d1b5ca
commit
ef12e10e59
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* @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 {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
|
||||||
|
|
||||||
|
/** The type of the function that analyzes entry-points and creates the list of tasks. */
|
||||||
|
export type AnalyzeFn = () => {
|
||||||
|
processingMetadataPerEntryPoint: Map<string, EntryPointProcessingMetadata>;
|
||||||
|
tasks: Task[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the function that creates the `compile()` function, which in turn can be used to
|
||||||
|
* process tasks.
|
||||||
|
*/
|
||||||
|
export type CreateCompileFn =
|
||||||
|
(onTaskCompleted: (task: Task, outcome: TaskProcessingOutcome) => void) => (task: Task) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the function that orchestrates and executes the required work (i.e. analyzes the
|
||||||
|
* entry-points, processes the resulting tasks, does book-keeping and validates the final outcome).
|
||||||
|
*/
|
||||||
|
export type ExecuteFn = (analyzeFn: AnalyzeFn, createCompileFn: CreateCompileFn) => void;
|
||||||
|
|
||||||
|
/** Represents metadata related to the processing of an entry-point. */
|
||||||
|
export interface EntryPointProcessingMetadata {
|
||||||
|
/**
|
||||||
|
* A mapping from a format property (i.e. an `EntryPointJsonProperty`) to the list of format
|
||||||
|
* properties that point to the same format-path and as a result need to be marked as processed,
|
||||||
|
* once the former is processed.
|
||||||
|
*/
|
||||||
|
propertyToPropertiesToMarkAsProcessed: Map<EntryPointJsonProperty, EntryPointJsonProperty[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the typings for the entry-point have been successfully processed (or were already
|
||||||
|
* processed).
|
||||||
|
*/
|
||||||
|
hasProcessedTypings: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether at least one format has been successfully processed (or was already processed) for the
|
||||||
|
* entry-point.
|
||||||
|
*/
|
||||||
|
hasAnyProcessedFormat: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents a unit of work: processing a specific format property of an entry-point. */
|
||||||
|
export interface Task {
|
||||||
|
/** The `EntryPoint` which needs to be processed as part of the task. */
|
||||||
|
entryPoint: EntryPoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `package.json` format property to process (i.e. the property which points to the file that
|
||||||
|
* is the program entry-point).
|
||||||
|
*/
|
||||||
|
formatProperty: EntryPointJsonProperty;
|
||||||
|
|
||||||
|
/** Whether to also process typings for this entry-point as part of the task. */
|
||||||
|
processDts: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents the outcome of processing a `Task`. */
|
||||||
|
export const enum TaskProcessingOutcome {
|
||||||
|
/** The target format property was already processed - didn't have to do anything. */
|
||||||
|
AlreadyProcessed,
|
||||||
|
|
||||||
|
/** Successfully processed the target format property. */
|
||||||
|
Processed,
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import {ModuleResolver} from './dependencies/module_resolver';
|
||||||
import {UmdDependencyHost} from './dependencies/umd_dependency_host';
|
import {UmdDependencyHost} from './dependencies/umd_dependency_host';
|
||||||
import {DirectoryWalkerEntryPointFinder} from './entry_point_finder/directory_walker_entry_point_finder';
|
import {DirectoryWalkerEntryPointFinder} from './entry_point_finder/directory_walker_entry_point_finder';
|
||||||
import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder';
|
import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder';
|
||||||
|
import {AnalyzeFn, CreateCompileFn, EntryPointProcessingMetadata, ExecuteFn, Task, TaskProcessingOutcome} from './execution/api';
|
||||||
import {ConsoleLogger, LogLevel} from './logging/console_logger';
|
import {ConsoleLogger, LogLevel} from './logging/console_logger';
|
||||||
import {Logger} from './logging/logger';
|
import {Logger} from './logging/logger';
|
||||||
import {hasBeenProcessed, markAsProcessed} from './packages/build_marker';
|
import {hasBeenProcessed, markAsProcessed} from './packages/build_marker';
|
||||||
|
@ -82,46 +83,68 @@ export function mainNgcc(
|
||||||
{basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
{basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
||||||
compileAllFormats = true, createNewEntryPointFormats = false,
|
compileAllFormats = true, createNewEntryPointFormats = false,
|
||||||
logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void {
|
logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void {
|
||||||
const supportedPropertiesToConsider = ensureSupportedProperties(propertiesToConsider);
|
|
||||||
|
|
||||||
const fileSystem = getFileSystem();
|
const fileSystem = getFileSystem();
|
||||||
const transformer = new Transformer(fileSystem, logger);
|
|
||||||
const moduleResolver = new ModuleResolver(fileSystem, pathMappings);
|
|
||||||
const esmDependencyHost = new EsmDependencyHost(fileSystem, moduleResolver);
|
|
||||||
const umdDependencyHost = new UmdDependencyHost(fileSystem, moduleResolver);
|
|
||||||
const commonJsDependencyHost = new CommonJsDependencyHost(fileSystem, moduleResolver);
|
|
||||||
const resolver = new DependencyResolver(fileSystem, logger, {
|
|
||||||
esm5: esmDependencyHost,
|
|
||||||
esm2015: esmDependencyHost,
|
|
||||||
umd: umdDependencyHost,
|
|
||||||
commonjs: commonJsDependencyHost
|
|
||||||
});
|
|
||||||
const absBasePath = absoluteFrom(basePath);
|
|
||||||
const config = new NgccConfiguration(fileSystem, dirname(absBasePath));
|
|
||||||
const fileWriter = getFileWriter(fileSystem, createNewEntryPointFormats);
|
|
||||||
const entryPoints = getEntryPoints(
|
|
||||||
fileSystem, config, logger, resolver, absBasePath, targetEntryPointPath, pathMappings,
|
|
||||||
supportedPropertiesToConsider, compileAllFormats);
|
|
||||||
|
|
||||||
for (const entryPoint of entryPoints) {
|
// The function for performing the analysis.
|
||||||
// Are we compiling the Angular core?
|
const analyzeFn: AnalyzeFn = () => {
|
||||||
const isCore = entryPoint.name === '@angular/core';
|
const supportedPropertiesToConsider = ensureSupportedProperties(propertiesToConsider);
|
||||||
|
|
||||||
const entryPointPackageJson = entryPoint.packageJson;
|
const moduleResolver = new ModuleResolver(fileSystem, pathMappings);
|
||||||
const entryPointPackageJsonPath = fileSystem.resolve(entryPoint.path, 'package.json');
|
const esmDependencyHost = new EsmDependencyHost(fileSystem, moduleResolver);
|
||||||
const {propertiesToProcess, propertyToPropertiesToMarkAsProcessed} =
|
const umdDependencyHost = new UmdDependencyHost(fileSystem, moduleResolver);
|
||||||
getPropertiesToProcessAndMarkAsProcessed(
|
const commonJsDependencyHost = new CommonJsDependencyHost(fileSystem, moduleResolver);
|
||||||
entryPointPackageJson, supportedPropertiesToConsider);
|
const dependencyResolver = new DependencyResolver(fileSystem, logger, {
|
||||||
|
esm5: esmDependencyHost,
|
||||||
|
esm2015: esmDependencyHost,
|
||||||
|
umd: umdDependencyHost,
|
||||||
|
commonjs: commonJsDependencyHost
|
||||||
|
});
|
||||||
|
|
||||||
let hasAnyProcessedFormat = false;
|
const absBasePath = absoluteFrom(basePath);
|
||||||
let processDts = !hasBeenProcessed(entryPointPackageJson, 'typings');
|
const config = new NgccConfiguration(fileSystem, dirname(absBasePath));
|
||||||
|
const entryPoints = getEntryPoints(
|
||||||
|
fileSystem, config, logger, dependencyResolver, absBasePath, targetEntryPointPath,
|
||||||
|
pathMappings, supportedPropertiesToConsider, compileAllFormats);
|
||||||
|
|
||||||
for (const property of propertiesToProcess) {
|
const processingMetadataPerEntryPoint = new Map<string, EntryPointProcessingMetadata>();
|
||||||
// If we only need one format processed and we already have one, exit the loop.
|
const tasks: Task[] = [];
|
||||||
if (!compileAllFormats && hasAnyProcessedFormat) break;
|
|
||||||
|
|
||||||
const formatPath = entryPointPackageJson[property];
|
for (const entryPoint of entryPoints) {
|
||||||
const format = getEntryPointFormat(fileSystem, entryPoint, property);
|
const packageJson = entryPoint.packageJson;
|
||||||
|
const hasProcessedTypings = hasBeenProcessed(packageJson, 'typings');
|
||||||
|
const {propertiesToProcess, propertyToPropertiesToMarkAsProcessed} =
|
||||||
|
getPropertiesToProcessAndMarkAsProcessed(packageJson, supportedPropertiesToConsider);
|
||||||
|
let processDts = !hasProcessedTypings;
|
||||||
|
|
||||||
|
for (const formatProperty of propertiesToProcess) {
|
||||||
|
tasks.push({entryPoint, formatProperty, processDts});
|
||||||
|
|
||||||
|
// Only process typings for the first property (if not already processed).
|
||||||
|
processDts = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
processingMetadataPerEntryPoint.set(entryPoint.path, {
|
||||||
|
propertyToPropertiesToMarkAsProcessed,
|
||||||
|
hasProcessedTypings,
|
||||||
|
hasAnyProcessedFormat: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {processingMetadataPerEntryPoint, tasks};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The function for creating the `compile()` function.
|
||||||
|
const createCompileFn: CreateCompileFn = onTaskCompleted => {
|
||||||
|
const fileWriter = getFileWriter(fileSystem, createNewEntryPointFormats);
|
||||||
|
const transformer = new Transformer(fileSystem, logger);
|
||||||
|
|
||||||
|
return (task: Task) => {
|
||||||
|
const {entryPoint, formatProperty, processDts} = task;
|
||||||
|
|
||||||
|
const isCore = entryPoint.name === '@angular/core'; // Are we compiling the Angular core?
|
||||||
|
const packageJson = entryPoint.packageJson;
|
||||||
|
const formatPath = packageJson[formatProperty];
|
||||||
|
const format = getEntryPointFormat(fileSystem, entryPoint, formatProperty);
|
||||||
|
|
||||||
// All properties listed in `propertiesToProcess` are guaranteed to point to a format-path
|
// All properties listed in `propertiesToProcess` are guaranteed to point to a format-path
|
||||||
// (i.e. they exist in `entryPointPackageJson`). Furthermore, they are also guaranteed to be
|
// (i.e. they exist in `entryPointPackageJson`). Furthermore, they are also guaranteed to be
|
||||||
|
@ -131,42 +154,78 @@ export function mainNgcc(
|
||||||
if (!formatPath || !format) {
|
if (!formatPath || !format) {
|
||||||
// This should never happen.
|
// This should never happen.
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invariant violated: No format-path or format for ${entryPoint.path} : ${property} ` +
|
`Invariant violated: No format-path or format for ${entryPoint.path} : ` +
|
||||||
`(formatPath: ${formatPath} | format: ${format})`);
|
`${formatProperty} (formatPath: ${formatPath} | format: ${format})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The `formatPath` which the property maps to is already processed - nothing to do.
|
// The format-path which the property maps to is already processed - nothing to do.
|
||||||
if (hasBeenProcessed(entryPointPackageJson, property)) {
|
if (hasBeenProcessed(packageJson, formatProperty)) {
|
||||||
hasAnyProcessedFormat = true;
|
logger.debug(`Skipping ${entryPoint.name} : ${formatProperty} (already compiled).`);
|
||||||
logger.debug(`Skipping ${entryPoint.name} : ${property} (already compiled).`);
|
onTaskCompleted(task, TaskProcessingOutcome.AlreadyProcessed);
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bundle = makeEntryPointBundle(
|
const bundle = makeEntryPointBundle(
|
||||||
fileSystem, entryPoint, formatPath, isCore, property, format, processDts, pathMappings,
|
fileSystem, entryPoint, formatPath, isCore, formatProperty, format, processDts,
|
||||||
true);
|
pathMappings, true);
|
||||||
|
|
||||||
|
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
|
||||||
|
|
||||||
logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`);
|
|
||||||
const transformedFiles = transformer.transform(bundle);
|
const transformedFiles = transformer.transform(bundle);
|
||||||
fileWriter.writeBundle(entryPoint, bundle, transformedFiles);
|
fileWriter.writeBundle(entryPoint, bundle, transformedFiles);
|
||||||
hasAnyProcessedFormat = true;
|
|
||||||
|
|
||||||
const propsToMarkAsProcessed: (EntryPointJsonProperty | 'typings')[] =
|
onTaskCompleted(task, TaskProcessingOutcome.Processed);
|
||||||
propertyToPropertiesToMarkAsProcessed.get(property) !;
|
};
|
||||||
if (processDts) {
|
};
|
||||||
propsToMarkAsProcessed.push('typings');
|
|
||||||
processDts = false;
|
// The function for actually planning and getting the work done.
|
||||||
|
const executeFn: ExecuteFn = (analyzeFn: AnalyzeFn, createCompileFn: CreateCompileFn) => {
|
||||||
|
const {processingMetadataPerEntryPoint, tasks} = analyzeFn();
|
||||||
|
const compile = createCompileFn(({entryPoint, formatProperty, processDts}, outcome) => {
|
||||||
|
const processingMeta = processingMetadataPerEntryPoint.get(entryPoint.path) !;
|
||||||
|
processingMeta.hasAnyProcessedFormat = true;
|
||||||
|
|
||||||
|
if (outcome === TaskProcessingOutcome.Processed) {
|
||||||
|
const packageJsonPath = fileSystem.resolve(entryPoint.path, 'package.json');
|
||||||
|
const propsToMarkAsProcessed: (EntryPointJsonProperty | 'typings')[] =
|
||||||
|
processingMeta.propertyToPropertiesToMarkAsProcessed.get(formatProperty) !;
|
||||||
|
|
||||||
|
if (processDts) {
|
||||||
|
processingMeta.hasProcessedTypings = true;
|
||||||
|
propsToMarkAsProcessed.push('typings');
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsProcessed(
|
||||||
|
fileSystem, entryPoint.packageJson, packageJsonPath, propsToMarkAsProcessed);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
markAsProcessed(
|
// Process all tasks.
|
||||||
fileSystem, entryPointPackageJson, entryPointPackageJsonPath, propsToMarkAsProcessed);
|
for (const task of tasks) {
|
||||||
|
const processingMeta = processingMetadataPerEntryPoint.get(task.entryPoint.path) !;
|
||||||
|
|
||||||
|
// If we only need one format processed and we already have one for the corresponding
|
||||||
|
// entry-point, skip the task.
|
||||||
|
if (!compileAllFormats && processingMeta.hasAnyProcessedFormat) continue;
|
||||||
|
|
||||||
|
compile(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasAnyProcessedFormat) {
|
// Check for entry-points for which we could not process any format at all.
|
||||||
|
const unprocessedEntryPointPaths =
|
||||||
|
Array.from(processingMetadataPerEntryPoint.entries())
|
||||||
|
.filter(([, processingMeta]) => !processingMeta.hasAnyProcessedFormat)
|
||||||
|
.map(([entryPointPath]) => `\n - ${entryPointPath}`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
if (unprocessedEntryPointPaths) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to compile any formats for entry-point at (${entryPoint.path}). Tried ${supportedPropertiesToConsider}.`);
|
'Failed to compile any formats for the following entry-points (tried ' +
|
||||||
|
`${propertiesToConsider.join(', ')}): ${unprocessedEntryPointPaths}`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
return executeFn(analyzeFn, createCompileFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureSupportedProperties(properties: string[]): EntryPointJsonProperty[] {
|
function ensureSupportedProperties(properties: string[]): EntryPointJsonProperty[] {
|
||||||
|
|
Loading…
Reference in New Issue