diff --git a/README.md b/README.md index 9e53bbe3..dde6d008 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,25 @@ This repository contains packages that are shared dependencies of Compass, the MongoDB extension for VSCode and MongoSH. +## Package Manager Configuration + +By default, this repository uses `npm` for package management. You can optionally use `pnpm` instead by setting the `MONOREPO_TOOLS_USE_PNPM` environment variable: + +```bash +export MONOREPO_TOOLS_USE_PNPM=true +``` + +When `MONOREPO_TOOLS_USE_PNPM=true`, all monorepo tools scripts will use `pnpm` instead of `npm` and `pnpm dlx` instead of `npx`. + +## Getting Started + To start working: ``` npm run bootstrap ``` + Lint code and dependencies ``` diff --git a/packages/monorepo-tools/src/bump-packages.ts b/packages/monorepo-tools/src/bump-packages.ts index 94915ae6..a7dc7994 100644 --- a/packages/monorepo-tools/src/bump-packages.ts +++ b/packages/monorepo-tools/src/bump-packages.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import childProcess from 'child_process'; import { promises as fs } from 'fs'; // @ts-expect-error No definitions available import gitLogParser from 'git-log-parser'; @@ -7,14 +6,13 @@ import path from 'path'; import type { ReleaseType } from 'semver'; import semver from 'semver'; import { PassThrough } from 'stream'; -import { isDeepStrictEqual, promisify } from 'util'; +import { isDeepStrictEqual } from 'util'; import type { GitCommit } from './utils/get-conventional-bump'; import { getConventionalBump } from './utils/get-conventional-bump'; import { getPackagesInTopologicalOrder } from './utils/get-packages-in-topological-order'; import { maxIncrement } from './utils/max-increment'; - -const execFile = promisify(childProcess.execFile); +import { installDependencies } from './utils/package-manager'; const LAST_BUMP_COMMIT_MESSAGE = process.env.LAST_BUMP_COMMIT_MESSAGE || 'chore(ci): bump packages'; @@ -58,7 +56,7 @@ async function main() { ); } - await execFile('npm', ['install', '--package-lock-only']); + await installDependencies({ packageLockOnly: true }); } main().catch((err) => diff --git a/packages/monorepo-tools/src/depalign.ts b/packages/monorepo-tools/src/depalign.ts index 6b23e00f..17cce7c3 100644 --- a/packages/monorepo-tools/src/depalign.ts +++ b/packages/monorepo-tools/src/depalign.ts @@ -5,7 +5,6 @@ import path from 'path'; import { promises as fs } from 'fs'; import chalk from 'chalk'; import pacote from 'pacote'; -import { runInDir } from './utils/run-in-dir'; import { listAllPackages } from './utils/list-all-packages'; import { updatePackageJson } from './utils/update-package-json'; import { withProgress } from './utils/with-progress'; @@ -18,6 +17,7 @@ import { calculateReplacements, intersects } from './utils/semver-helpers'; import type { ParsedArgs } from 'minimist'; import minimist from 'minimist'; import type ora from 'ora'; +import { installDependencies } from './utils/package-manager'; const DEPENDENCY_GROUPS = [ 'peerDependencies', @@ -228,7 +228,7 @@ async function alignPackageToRange(packageName: string, range: string) { }); } - await runInDir('npm install'); + await installDependencies(); } function hasDep( @@ -424,7 +424,7 @@ async function applyFixes( spinner.text = `${spinnerText} for ${totalToUpdate} dependencies (updating package-lock.json)`; - await runInDir('npm install --package-lock-only'); + await installDependencies({ packageLockOnly: true }); spinner.text = `${spinnerText} for ${totalToUpdate} dependencies`; diff --git a/packages/monorepo-tools/src/precommit.ts b/packages/monorepo-tools/src/precommit.ts index 142c9a67..96d6c6d3 100644 --- a/packages/monorepo-tools/src/precommit.ts +++ b/packages/monorepo-tools/src/precommit.ts @@ -3,10 +3,11 @@ import path from 'path'; import pkgUp from 'pkg-up'; -import { promisify } from 'util'; -import { execFile } from 'child_process'; import * as fs from 'fs/promises'; import findUp from 'find-up'; +import { executePackage } from './utils/package-manager'; +import { promisify } from 'util'; +import { execFile } from 'child_process'; const execFileAsync = promisify(execFile); async function main(fileList: string[]) { @@ -78,15 +79,17 @@ async function main(fileList: string[]) { console.log(` - ${path.relative(process.cwd(), filePath)}`); }); - await execFileAsync('npx', [ - 'prettier', - '--config', - require.resolve('@mongodb-js/prettier-config-devtools/.prettierrc.json'), - // Silently ignore files that are of format that is not supported by prettier - '--ignore-unknown', - '--write', - ...filesToPrettify, - ]); + await executePackage({ + packageName: 'prettier', + args: [ + '--config', + require.resolve('@mongodb-js/prettier-config-devtools/.prettierrc.json'), + // Silently ignore files that are of format that is not supported by prettier + '--ignore-unknown', + '--write', + ...filesToPrettify, + ], + }); // Re-add potentially reformatted files await execFileAsync('git', ['add', ...filesToPrettify]); diff --git a/packages/monorepo-tools/src/utils/package-manager.spec.ts b/packages/monorepo-tools/src/utils/package-manager.spec.ts new file mode 100644 index 00000000..08cd6e09 --- /dev/null +++ b/packages/monorepo-tools/src/utils/package-manager.spec.ts @@ -0,0 +1,40 @@ +import assert from 'assert'; +import * as packageManager from './package-manager'; + +describe('package-manager', function () { + let originalEnv: string | undefined; + + beforeEach(function () { + originalEnv = process.env.MONOREPO_TOOLS_USE_PNPM; + }); + + afterEach(function () { + if (originalEnv === undefined) { + delete process.env.MONOREPO_TOOLS_USE_PNPM; + } else { + process.env.MONOREPO_TOOLS_USE_PNPM = originalEnv; + } + }); + + describe('getPackageManager', function () { + it('returns npm by default', function () { + delete process.env.MONOREPO_TOOLS_USE_PNPM; + assert.strictEqual(packageManager.getPackageManager(), 'npm'); + }); + + it('returns npm when MONOREPO_TOOLS_USE_PNPM is false', function () { + process.env.MONOREPO_TOOLS_USE_PNPM = 'false'; + assert.strictEqual(packageManager.getPackageManager(), 'npm'); + }); + + it('returns npm when MONOREPO_TOOLS_USE_PNPM is empty string', function () { + process.env.MONOREPO_TOOLS_USE_PNPM = ''; + assert.strictEqual(packageManager.getPackageManager(), 'npm'); + }); + + it('returns pnpm when MONOREPO_TOOLS_USE_PNPM is true', function () { + process.env.MONOREPO_TOOLS_USE_PNPM = 'true'; + assert.strictEqual(packageManager.getPackageManager(), 'pnpm'); + }); + }); +}); diff --git a/packages/monorepo-tools/src/utils/package-manager.ts b/packages/monorepo-tools/src/utils/package-manager.ts new file mode 100644 index 00000000..9d1da60d --- /dev/null +++ b/packages/monorepo-tools/src/utils/package-manager.ts @@ -0,0 +1,94 @@ +import { promisify } from 'util'; +import { execFile as execFileCallback, execFileSync } from 'child_process'; + +const execFile = promisify(execFileCallback); + +/** + * Get the package manager to use based on environment variable. + * Defaults to 'npm' if MONOREPO_TOOLS_USE_PNPM is not set or set to anything other than 'true'. + * + * Set MONOREPO_TOOLS_USE_PNPM=true to use pnpm instead of npm. + */ +export function getPackageManager(): 'npm' | 'pnpm' { + return process.env.MONOREPO_TOOLS_USE_PNPM === 'true' ? 'pnpm' : 'npm'; +} + +/** + * Install dependencies using the configured package manager. + * Equivalent to: npm install / pnpm install + * + * @param options.cwd Working directory (defaults to process.cwd()) + * @param options.packageLockOnly If true, only updates lock file without installing (defaults to false) + */ +export async function installDependencies( + options: { cwd?: string; packageLockOnly?: boolean } = {}, +): Promise<{ stdout: string; stderr: string }> { + const { cwd, packageLockOnly = false } = options; + const packageManager = getPackageManager(); + const args = ['install']; + + if (packageLockOnly) { + // npm uses --package-lock-only, pnpm uses --lockfile-only + args.push( + packageManager === 'pnpm' ? '--lockfile-only' : '--package-lock-only', + ); + } + + return await execFile(packageManager, args, { cwd }); +} + +/** + * Get the version of the configured package manager. + * Returns the version string (e.g., "9.0.0" for npm or "8.15.0" for pnpm). + */ +export async function getPackageManagerVersion(): Promise { + const packageManager = getPackageManager(); + const { stdout } = await execFile(packageManager, ['-v']); + return stdout.trim(); +} + +/** + * Run a package manager command for specific workspaces. + * For npm: uses --workspace flags + * For pnpm: uses --filter flags + * + * @param options.workspaces Array of workspace names to target + * @param options.args Package manager command arguments (e.g., ['run', 'test']) + * @param options.stdio stdio configuration for child process (defaults to 'inherit') + */ +export function runForWorkspaces(options: { + workspaces: string[]; + args: string[]; + stdio?: 'inherit' | 'pipe' | 'ignore'; +}): void { + const { workspaces, args, stdio = 'inherit' } = options; + const packageManager = getPackageManager(); + + const workspaceArgs = + packageManager === 'pnpm' + ? workspaces.map((name) => `--filter=${name}`) + : workspaces.map((name) => `--workspace=${name}`); + + execFileSync(packageManager, [...workspaceArgs, ...args], { stdio }); +} + +/** + * Execute a package with the configured package executor (npx or pnpm dlx). + * + * @param options.packageName The package/command to execute + * @param options.args Arguments to pass to the package + * @param options.cwd Working directory (defaults to process.cwd()) + */ +export async function executePackage(options: { + packageName: string; + args: string[]; + cwd?: string; +}): Promise<{ stdout: string; stderr: string }> { + const { packageName, args, cwd } = options; + const packageManager = getPackageManager(); + + if (packageManager === 'pnpm') { + return await execFile('pnpm', ['dlx', packageName, ...args], { cwd }); + } + return await execFile('npx', [packageName, ...args], { cwd }); +} diff --git a/packages/monorepo-tools/src/where.ts b/packages/monorepo-tools/src/where.ts index 67602be5..664f7e71 100644 --- a/packages/monorepo-tools/src/where.ts +++ b/packages/monorepo-tools/src/where.ts @@ -23,6 +23,11 @@ import { runInContext, createContext } from 'vm'; import { execFileSync } from 'child_process'; import { listAllPackages } from './utils/list-all-packages'; import { findMonorepoRoot } from './utils/find-monorepo-root'; +import { + getPackageManager, + getPackageManagerVersion, + runForWorkspaces, +} from './utils/package-manager'; const [expr, ...execCommandArgs] = process.argv.slice(2); let useLernaExec = false; @@ -67,34 +72,40 @@ async function lernaExec(packages: string[]) { }); } -// eslint-disable-next-line @typescript-eslint/require-await async function npmWorkspaces(packages: string[]) { - const npmVersion = execFileSync('npm', ['-v']).toString(); + const packageManager = getPackageManager(); + const packageManagerVersion = await getPackageManagerVersion(); - if (Number(npmVersion.substr(0, 2)) < 7) { + if (packageManager !== 'npm') { throw Error( - `"npm run where" relies on npm@7 features, using npm@${npmVersion}. Update npm to 7 or use the command with --lerna-exec instead`, + `"npm run where" only supports npm, using ${packageManager}. Use the command with pnpm --filter or --lerna-exec instead`, ); } - const workspaces = packages.map((name) => `--workspace=${name}`); + if (Number(packageManagerVersion.substr(0, 2)) < 7) { + throw Error( + `"npm run where" relies on npm@7 features, using npm@${packageManagerVersion}. Update npm to 7 or use the command with --lerna-exec instead`, + ); + } - if (workspaces.length === 0) { + if (packages.length === 0) { console.info(`No packages matched filter "${expr}"`); return; } console.log(); console.log( - 'Running "npm %s" for the following packages:', + 'Running "%s %s" for the following packages:', + packageManager, execCommandArgs.join(' '), ); console.log(); console.log(util.inspect(packages)); console.log(); - execFileSync('npm', [...workspaces, ...execCommandArgs], { - stdio: 'inherit', + runForWorkspaces({ + workspaces: packages, + args: execCommandArgs, }); }