From 92e2193ccedc5bce96f186239d190cd77c93a24d Mon Sep 17 00:00:00 2001 From: Sebastian Valle Date: Sun, 1 Oct 2023 12:21:03 -0500 Subject: [PATCH] Support monorepo projects that use npm workspaces When run in a monorepo, the `npm ls` command lists only the local packages as top-level dependencies, and does not automatically hoist the sub-dependents. This change makes it so that if a local package is detected (the resolved location starts with `file:`) then the dependents of that package are recursively added to the top level dependencies. --- src/dependencies.ts | 63 +++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/dependencies.ts b/src/dependencies.ts index abaceaa..de4d15a 100644 --- a/src/dependencies.ts +++ b/src/dependencies.ts @@ -6,9 +6,17 @@ import type { PackageManager } from "./types.js"; const { valid } = semver; +type DependencyData = { + required?: { version?: string } | string; + version?: string; + resolved?: string; + dependencies?: ParsedDependency; + devDependencies?: ParsedDependency; +}; + type ParsedDependency = Record< string, - { required?: { version?: string } | string; version?: string } + DependencyData >; type ParsedDependencies = Record< "dependencies" | "devDependencies", @@ -58,6 +66,19 @@ const getParsedDependencies = async ( } }); +const transformDependency = ([dependency, data]: [string, DependencyData]): [string, string] => { + return [ + dependency, + data.version ?? + ( + (data.required as { version?: string })?.version || + (data.required as string) + ).replace(/[<=>^~]+/u, ""), + ]; +} + +const isLocalDependency = (data: DependencyData) => data.resolved?.startsWith('file:'); + export const getDependencies = async ( packageManager: PackageManager, flags?: { all?: boolean }, @@ -70,22 +91,26 @@ export const getDependencies = async ( } as Record )[packageManager] ?? "npm ls --depth=0 --json --silent"; - return getParsedDependencies(packageManager, cmd).then( - (json) => - new Map( - Object.entries({ - ...json.dependencies, - ...json.devDependencies, - }) - .map(([dependency, data]) => [ - dependency, - data.version ?? - ( - (data.required as { version?: string })?.version || - (data.required as string) - ).replace(/[<=>^~]+/u, ""), - ]) - .filter(([_, version]) => valid(version)) as [[string, string]], - ), - ); + const json = await getParsedDependencies(packageManager, cmd); + + const dependencies = Object.entries({ + ...json.dependencies, + ...json.devDependencies, + }); + + // Keep track of which dependencies are linked locally, ie: monorepos + const localDeps = new Map(dependencies.filter(([_, data]) => isLocalDependency(data))); + + return new Map(dependencies + .flatMap(([dependency, data]) => { + if (isLocalDependency(data)) { + // This is a local package, recursively add its dependencies instead, but only up to 1 level deep + return Object.entries({ ...data.dependencies, ...data.devDependencies }) + .map(transformDependency) + .filter(([depName]) => !localDeps.has(depName)); + } + return [transformDependency([dependency, data])]; + }) + .filter(([_, version]) => valid(version))); }; +