139 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
}
|