1import chalk from 'chalk';
2
3import * as Changelogs from '../../Changelogs';
4import Git from '../../Git';
5import { getListOfPackagesAsync, Package } from '../../Packages';
6import { Task } from '../../TasksRunner';
7import { runWithSpinner } from '../../Utils';
8import { PackagesGraph, PackagesGraphNode } from '../../packages-graph';
9import { getMinReleaseTypeAsync, getPackageGitLogsAsync } from '../helpers';
10import { CommandOptions, Parcel, TaskArgs } from '../types';
11
12const { green } = chalk;
13const parcelsCache = new Map<PackagesGraphNode, Parcel>();
14
15/**
16 * Gets a list of public packages in the monorepo and wraps them into parcels.
17 * If only a subset of packages were requested, it also includes all their dependencies.
18 */
19export const loadRequestedParcels = new Task<TaskArgs>(
20  {
21    name: 'loadRequestedParcels',
22  },
23  async (parcels: Parcel[], options: CommandOptions) => {
24    const { packageNames } = options;
25
26    const allPackages = await runWithSpinner(
27      'Loading requested workspace packages',
28      () => getListOfPackagesAsync(),
29      'Loaded requested workspace packages'
30    );
31
32    const graph = new PackagesGraph(allPackages);
33    const allPackagesObj = allPackages.reduce((acc, pkg) => {
34      acc[pkg.packageName] = pkg;
35      return acc;
36    }, {});
37
38    // Verify that provided package names are valid.
39    for (const packageName of packageNames) {
40      if (!allPackagesObj[packageName]) {
41        throw new Error(`Package with provided name ${green(packageName)} does not exist.`);
42      }
43    }
44
45    const filteredPackages = allPackages.filter((pkg) => {
46      const isPrivate = pkg.packageJson.private;
47      const isIncluded = packageNames.length === 0 || packageNames.includes(pkg.packageName);
48      return !isPrivate && isIncluded;
49    });
50
51    // Create parcels only for requested packages (or all if none was provided).
52    const { requestedParcels, dependenciesParcels } = await runWithSpinner(
53      'Collecting changes in the packages and their dependencies',
54      async () => {
55        const requestedParcels = await createParcelsForPackages(filteredPackages, graph);
56
57        requestedParcels.forEach((parcel) => {
58          parcel.state.isRequested = true;
59        });
60
61        // Include dependencies if only some specific packages were requested.
62        const dependenciesParcels =
63          options.deps && packageNames.length > 0
64            ? await createParcelsForDependenciesOf(requestedParcels)
65            : new Set<Parcel>();
66
67        return { requestedParcels, dependenciesParcels };
68      },
69      'Collected changes in the packages and their dependencies'
70    );
71
72    // A set of all parcels to select to publish.
73    // The dependencies must precede the requested ones to select them first.
74    const parcelsToSelect = new Set<Parcel>([...dependenciesParcels, ...requestedParcels]);
75
76    return [[...parcelsToSelect], options];
77  }
78);
79
80/**
81 * Gets the parcel of the provided node from cache, or creates a new one when not found.
82 */
83export async function getCachedParcel(node: PackagesGraphNode): Promise<Parcel> {
84  const cachedPromise = parcelsCache.get(node);
85
86  if (cachedPromise) {
87    return cachedPromise;
88  }
89  const newParcel = await createParcelAsync(node);
90  parcelsCache.set(node, newParcel);
91  return newParcel;
92}
93
94export async function createParcelsForPackages(
95  packages: Package[],
96  graph: PackagesGraph
97): Promise<Set<Parcel>> {
98  const nodes = packages
99    .map((pkg) => graph.getNode(pkg.packageName))
100    .filter(Boolean) as PackagesGraphNode[];
101
102  return await createParcelsForGraphNodes(nodes);
103}
104
105export async function createParcelsForGraphNodes(nodes: PackagesGraphNode[]): Promise<Set<Parcel>> {
106  const parcels = await Promise.all(nodes.map((node) => getCachedParcel(node)));
107  return new Set<Parcel>(parcels);
108}
109
110export async function createParcelsForDependenciesOf(parcels: Set<Parcel>): Promise<Set<Parcel>> {
111  // A set of parcels that will precede the requested ones, which consist of their dependencies.
112  const allDependencies = new Set<Parcel>();
113
114  for (const parcel of parcels) {
115    // Add all dependencies of the current package.
116    const dependencies = await createParcelsForGraphNodes(parcel.graphNode.getAllDependencies());
117
118    for (const dependencyParcel of dependencies) {
119      parcel.dependencies.add(dependencyParcel);
120      dependencyParcel.dependents.add(parcel);
121
122      allDependencies.add(dependencyParcel);
123    }
124  }
125  return new Set<Parcel>([...allDependencies, ...parcels]);
126}
127
128/**
129 * Wraps `Package` object into a parcels - convenient wrapper providing more package-related helpers.
130 * As part of creating the parcel, it loads the latest manifest from npm, loads the changelog and git logs.
131 */
132export async function createParcelAsync(packageNode: PackagesGraphNode): Promise<Parcel> {
133  const { pkg } = packageNode;
134  const pkgView = await pkg.getPackageViewAsync();
135  const changelog = Changelogs.loadFrom(pkg.changelogPath);
136  const gitDir = new Git.Directory(pkg.path);
137  const changelogChanges = await changelog.getChangesAsync();
138  const logs = await getPackageGitLogsAsync(gitDir, pkgView?.gitHead);
139
140  return {
141    pkg,
142    pkgView,
143    changelog,
144    gitDir,
145    graphNode: packageNode,
146    dependents: new Set<Parcel>(),
147    dependencies: new Set<Parcel>(),
148    logs,
149    changelogChanges,
150    minReleaseType: await getMinReleaseTypeAsync(pkg, logs, changelogChanges),
151    state: {},
152  };
153}
154