/** * @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): TaskDependencies { const dependencies = new TaskDependencies(); const candidateDependencies = new Map(); 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 { 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> { const blockedTasks = new Map>(); 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(); 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); }); }