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