fix(ngcc): do not crash on entry-point that fails to compile (#36083)
Previously, when an entry-point contained code that caused its compilation to fail, ngcc would exit in the middle of processing, possibly leaving other entry-points in a corrupt state. This change adds a new `errorOnFailedEntryPoint` option to `mainNgcc` that specifies whether ngcc should exit immediately or log an error and continue processing other entry-points. The default is `false` so that ngcc will not error but continue processing as much as possible. This is useful in post-install hooks, and async CLI integration, where we do not have as much control over which entry-points should be processed. The option is forced to true if the `targetEntryPointPath` is provided, such as the sync integration with the CLI, since in that case it is targeting an entry-point that will actually be used in the current project so we do want ngcc to exit with an error at that point. PR Close #36083
This commit is contained in:

committed by
Andrew Kushnir

parent
1790b63a5d
commit
ff665b9e6a
@ -110,6 +110,13 @@ export interface TaskQueue {
|
||||
*/
|
||||
markTaskCompleted(task: Task): void;
|
||||
|
||||
/**
|
||||
* Mark a task as failed.
|
||||
*
|
||||
* Do not process the tasks that depend upon the given task.
|
||||
*/
|
||||
markAsFailed(task: Task): void;
|
||||
|
||||
/**
|
||||
* Return a string representation of the task queue (for debugging purposes).
|
||||
*
|
||||
|
@ -6,10 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {FileSystem, resolve} from '../../../../src/ngtsc/file_system';
|
||||
import {Logger} from '../../logging/logger';
|
||||
import {markAsProcessed} from '../../packages/build_marker';
|
||||
import {PackageJsonFormatProperties, getEntryPointFormat} from '../../packages/entry_point';
|
||||
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
||||
import {Task, TaskCompletedCallback, TaskProcessingOutcome} from './api';
|
||||
import {Task, TaskCompletedCallback, TaskProcessingOutcome, TaskQueue} from './api';
|
||||
|
||||
/**
|
||||
* A function that can handle a specific outcome of a task completion.
|
||||
@ -70,3 +71,17 @@ export function createThrowErrorHandler(fs: FileSystem): TaskCompletedHandler {
|
||||
(message !== null ? ` due to ${message}` : ''));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a handler that logs an error and marks the task as failed.
|
||||
*/
|
||||
export function createLogErrorHandler(
|
||||
logger: Logger, fs: FileSystem, taskQueue: TaskQueue): TaskCompletedHandler {
|
||||
return (task: Task, message: string | null): void => {
|
||||
taskQueue.markAsFailed(task);
|
||||
const format = getEntryPointFormat(fs, task.entryPoint, task.formatProperty);
|
||||
logger.error(
|
||||
`Failed to compile entry-point ${task.entryPoint.name} (${task.formatProperty} as ${format})` +
|
||||
(message !== null ? ` due to ${message}` : ''));
|
||||
};
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
* 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 {Logger} from '../../../logging/logger';
|
||||
import {PartiallyOrderedTasks, Task, TaskDependencies, TaskQueue} from '../api';
|
||||
import {stringifyTask} from '../utils';
|
||||
|
||||
@ -19,9 +19,40 @@ export abstract class BaseTaskQueue implements TaskQueue {
|
||||
}
|
||||
protected inProgressTasks = new Set<Task>();
|
||||
|
||||
constructor(protected tasks: PartiallyOrderedTasks, protected dependencies: TaskDependencies) {}
|
||||
/**
|
||||
* A map of tasks that should be skipped, mapped to the task that caused them to be skipped.
|
||||
*/
|
||||
private tasksToSkip = new Map<Task, Task>();
|
||||
|
||||
abstract getNextTask(): Task|null;
|
||||
constructor(
|
||||
protected logger: Logger, protected tasks: PartiallyOrderedTasks,
|
||||
protected dependencies: TaskDependencies) {}
|
||||
|
||||
protected abstract computeNextTask(): Task|null;
|
||||
|
||||
getNextTask(): Task|null {
|
||||
let nextTask = this.computeNextTask();
|
||||
while (nextTask !== null) {
|
||||
if (!this.tasksToSkip.has(nextTask)) {
|
||||
break;
|
||||
}
|
||||
// We are skipping this task so mark it as complete
|
||||
this.markTaskCompleted(nextTask);
|
||||
const failedTask = this.tasksToSkip.get(nextTask) !;
|
||||
this.logger.warn(
|
||||
`Skipping processing of ${nextTask.entryPoint.name} because its dependency ${failedTask.entryPoint.name} failed to compile.`);
|
||||
nextTask = this.computeNextTask();
|
||||
}
|
||||
return nextTask;
|
||||
}
|
||||
|
||||
markAsFailed(task: Task) {
|
||||
if (this.dependencies.has(task)) {
|
||||
for (const dependentTask of this.dependencies.get(task) !) {
|
||||
this.skipDependentTasks(dependentTask, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markTaskCompleted(task: Task): void {
|
||||
if (!this.inProgressTasks.has(task)) {
|
||||
@ -41,6 +72,21 @@ export abstract class BaseTaskQueue implements TaskQueue {
|
||||
` In-progress tasks (${inProgTasks.length}): ${this.stringifyTasks(inProgTasks, ' ')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given `task` as to be skipped, then recursive skip all its dependents.
|
||||
*
|
||||
* @param task The task to skip
|
||||
* @param failedTask The task that failed, causing this task to be skipped
|
||||
*/
|
||||
protected skipDependentTasks(task: Task, failedTask: Task) {
|
||||
this.tasksToSkip.set(task, failedTask);
|
||||
if (this.dependencies.has(task)) {
|
||||
for (const dependentTask of this.dependencies.get(task) !) {
|
||||
this.skipDependentTasks(dependentTask, failedTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected stringifyTasks(tasks: Task[], indentation: string): string {
|
||||
return tasks.map(task => `\n${indentation}- ${stringifyTask(task)}`).join('');
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
* 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 {Logger} from '../../../logging/logger';
|
||||
import {PartiallyOrderedTasks, Task, TaskDependencies} from '../api';
|
||||
import {getBlockedTasks, sortTasksByPriority, stringifyTask} from '../utils';
|
||||
import {BaseTaskQueue} from './base_task_queue';
|
||||
@ -21,12 +22,12 @@ export class ParallelTaskQueue extends BaseTaskQueue {
|
||||
*/
|
||||
private blockedTasks: Map<Task, Set<Task>>;
|
||||
|
||||
constructor(tasks: PartiallyOrderedTasks, dependents: TaskDependencies) {
|
||||
super(sortTasksByPriority(tasks, dependents), dependents);
|
||||
this.blockedTasks = getBlockedTasks(dependents);
|
||||
constructor(logger: Logger, tasks: PartiallyOrderedTasks, dependencies: TaskDependencies) {
|
||||
super(logger, sortTasksByPriority(tasks, dependencies), dependencies);
|
||||
this.blockedTasks = getBlockedTasks(dependencies);
|
||||
}
|
||||
|
||||
getNextTask(): Task|null {
|
||||
computeNextTask(): Task|null {
|
||||
// Look for the first available (i.e. not blocked) task.
|
||||
// (NOTE: Since tasks are sorted by priority, the first available one is the best choice.)
|
||||
const nextTaskIdx = this.tasks.findIndex(task => !this.blockedTasks.has(task));
|
||||
|
@ -17,7 +17,7 @@ import {BaseTaskQueue} from './base_task_queue';
|
||||
* before requesting the next one.
|
||||
*/
|
||||
export class SerialTaskQueue extends BaseTaskQueue {
|
||||
getNextTask(): Task|null {
|
||||
computeNextTask(): Task|null {
|
||||
const nextTask = this.tasks.shift() || null;
|
||||
|
||||
if (nextTask) {
|
||||
|
Reference in New Issue
Block a user