2020-04-06 09:26:58 -07:00

139 lines
5.7 KiB
TypeScript

/**
* @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 {DepGraph} from 'dependency-graph';
import {EntryPoint} from '../../packages/entry_point';
import {PartiallyOrderedTasks, Task, TaskDependencies} from './api';
/** Stringify a task for debugging purposes. */
export const stringifyTask = (task: Task): string => `{entryPoint: ${
task.entryPoint.name}, formatProperty: ${task.formatProperty}, processDts: ${task.processDts}}`;
/**
* Compute a mapping of tasks to the tasks that are dependent on them (if any).
*
* Task A can depend upon task B, if either:
*
* * A and B have the same entry-point _and_ B is generating the typings for that entry-point
* (i.e. has `processDts: true`).
* * A's entry-point depends on B's entry-point _and_ B is also generating typings.
*
* NOTE: If a task is not generating typings, then it cannot affect anything which depends on its
* entry-point, regardless of the dependency graph. To put this another way, only the task
* which produces the typings for a dependency needs to have been completed.
*
* As a performance optimization, we take into account the fact that `tasks` are sorted in such a
* way that a task can only depend on earlier tasks (i.e. dependencies always come before
* dependents in the list of tasks).
*
* @param tasks A (partially ordered) list of tasks.
* @param graph The dependency graph between entry-points.
* @return A map from each task to those tasks directly dependent upon it.
*/
export function computeTaskDependencies(
tasks: PartiallyOrderedTasks, graph: DepGraph<EntryPoint>): TaskDependencies {
const dependencies = new TaskDependencies();
const candidateDependencies = new Map<string, Task>();
tasks.forEach(task => {
const entryPointPath = task.entryPoint.path;
// Find the earlier tasks (`candidateDependencies`) that this task depends upon.
const deps = graph.dependenciesOf(entryPointPath);
const taskDependencies = deps.filter(dep => candidateDependencies.has(dep))
.map(dep => candidateDependencies.get(dep)!);
// If this task has dependencies, add it to the dependencies and dependents maps.
if (taskDependencies.length > 0) {
for (const dependency of taskDependencies) {
const taskDependents = getDependentsSet(dependencies, dependency);
taskDependents.add(task);
}
}
if (task.processDts) {
// SANITY CHECK:
// There should only be one task per entry-point that generates typings (and thus can be a
// dependency of other tasks), so the following should theoretically never happen, but check
// just in case.
if (candidateDependencies.has(entryPointPath)) {
const otherTask = candidateDependencies.get(entryPointPath)!;
throw new Error(
'Invariant violated: Multiple tasks are assigned generating typings for ' +
`'${entryPointPath}':\n - ${stringifyTask(otherTask)}\n - ${stringifyTask(task)}`);
}
// This task can potentially be a dependency (i.e. it generates typings), so add it to the
// list of candidate dependencies for subsequent tasks.
candidateDependencies.set(entryPointPath, task);
} else {
// This task is not generating typings so we need to add it to the dependents of the task that
// does generate typings, if that exists
if (candidateDependencies.has(entryPointPath)) {
const typingsTask = candidateDependencies.get(entryPointPath)!;
const typingsTaskDependents = getDependentsSet(dependencies, typingsTask);
typingsTaskDependents.add(task);
}
}
});
return dependencies;
}
export function getDependentsSet(map: TaskDependencies, task: Task): Set<Task> {
if (!map.has(task)) {
map.set(task, new Set());
}
return map.get(task)!;
}
/**
* Invert the given mapping of Task dependencies.
*
* @param dependencies The mapping of tasks to the tasks that depend upon them.
* @returns A mapping of tasks to the tasks that they depend upon.
*/
export function getBlockedTasks(dependencies: TaskDependencies): Map<Task, Set<Task>> {
const blockedTasks = new Map<Task, Set<Task>>();
for (const [dependency, dependents] of dependencies) {
for (const dependent of dependents) {
const dependentSet = getDependentsSet(blockedTasks, dependent);
dependentSet.add(dependency);
}
}
return blockedTasks;
}
/**
* Sort a list of tasks by priority.
*
* Priority is determined by the number of other tasks that a task is (transitively) blocking:
* The more tasks a task is blocking the higher its priority is, because processing it will
* potentially unblock more tasks.
*
* To keep the behavior predictable, if two tasks block the same number of other tasks, their
* relative order in the original `tasks` lists is preserved.
*
* @param tasks A (partially ordered) list of tasks.
* @param dependencies The mapping of tasks to the tasks that depend upon them.
* @return The list of tasks sorted by priority.
*/
export function sortTasksByPriority(
tasks: PartiallyOrderedTasks, dependencies: TaskDependencies): PartiallyOrderedTasks {
const priorityPerTask = new Map<Task, [number, number]>();
const computePriority = (task: Task, idx: number):
[number, number] => [dependencies.has(task) ? dependencies.get(task)!.size : 0, idx];
tasks.forEach((task, i) => priorityPerTask.set(task, computePriority(task, i)));
return tasks.slice().sort((task1, task2) => {
const [p1, idx1] = priorityPerTask.get(task1)!;
const [p2, idx2] = priorityPerTask.get(task2)!;
return (p2 - p1) || (idx1 - idx2);
});
}