From 3ee0315f9b2a4bf04f76563010dca6160d7765f9 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Thu, 26 Feb 2026 15:28:00 +0100 Subject: [PATCH 1/4] feat(monorepo-tools): add pnpm mode support We have hardcoded npm function calls here, this adjusts it to support PNPM mode with MONOREPO_TOOLS_USE_PNPM --- README.md | 13 +++ packages/monorepo-tools/src/bump-packages.ts | 3 +- packages/monorepo-tools/src/depalign.ts | 6 +- packages/monorepo-tools/src/precommit.ts | 25 ++--- .../src/utils/package-manager.spec.ts | 40 ++++++++ .../src/utils/package-manager.ts | 94 +++++++++++++++++++ packages/monorepo-tools/src/where.ts | 29 ++++-- 7 files changed, 186 insertions(+), 24 deletions(-) create mode 100644 packages/monorepo-tools/src/utils/package-manager.spec.ts create mode 100644 packages/monorepo-tools/src/utils/package-manager.ts diff --git a/README.md b/README.md index 9e53bbe3..404b8409 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 `USE_PNPM` environment variable: + +```bash +export USE_PNPM=true +``` + +When `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..8e8afc59 100644 --- a/packages/monorepo-tools/src/bump-packages.ts +++ b/packages/monorepo-tools/src/bump-packages.ts @@ -13,6 +13,7 @@ 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'; +import { installDependencies } from './utils/package-manager'; const execFile = promisify(childProcess.execFile); @@ -58,7 +59,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..09b2659d --- /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.USE_PNPM; + }); + + afterEach(function () { + if (originalEnv === undefined) { + delete process.env.USE_PNPM; + } else { + process.env.USE_PNPM = originalEnv; + } + }); + + describe('getPackageManager', function () { + it('returns npm by default', function () { + delete process.env.USE_PNPM; + assert.strictEqual(packageManager.getPackageManager(), 'npm'); + }); + + it('returns npm when USE_PNPM is false', function () { + process.env.USE_PNPM = 'false'; + assert.strictEqual(packageManager.getPackageManager(), 'npm'); + }); + + it('returns npm when USE_PNPM is empty string', function () { + process.env.USE_PNPM = ''; + assert.strictEqual(packageManager.getPackageManager(), 'npm'); + }); + + it('returns pnpm when USE_PNPM is true', function () { + process.env.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..63bcc2ff --- /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 USE_PNPM is not set or set to anything other than 'true'. + * + * Set 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..db737e73 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, }); } From 50aedf7f6928a347b9cecd7569c117458708e28c Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Thu, 26 Feb 2026 15:40:24 +0100 Subject: [PATCH 2/4] chore: fix check --- packages/monorepo-tools/src/bump-packages.ts | 3 --- packages/monorepo-tools/src/where.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/monorepo-tools/src/bump-packages.ts b/packages/monorepo-tools/src/bump-packages.ts index 8e8afc59..13667b5a 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'; @@ -15,8 +14,6 @@ import { getPackagesInTopologicalOrder } from './utils/get-packages-in-topologic import { maxIncrement } from './utils/max-increment'; import { installDependencies } from './utils/package-manager'; -const execFile = promisify(childProcess.execFile); - const LAST_BUMP_COMMIT_MESSAGE = process.env.LAST_BUMP_COMMIT_MESSAGE || 'chore(ci): bump packages'; diff --git a/packages/monorepo-tools/src/where.ts b/packages/monorepo-tools/src/where.ts index db737e73..664f7e71 100644 --- a/packages/monorepo-tools/src/where.ts +++ b/packages/monorepo-tools/src/where.ts @@ -76,7 +76,7 @@ async function npmWorkspaces(packages: string[]) { const packageManager = getPackageManager(); const packageManagerVersion = await getPackageManagerVersion(); - if (packageManager != 'npm') { + if (packageManager !== 'npm') { throw Error( `"npm run where" only supports npm, using ${packageManager}. Use the command with pnpm --filter or --lerna-exec instead`, ); From 8e1fe0fcb032505a40cd42c2681ffd3a12b797de Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Thu, 26 Feb 2026 15:47:35 +0100 Subject: [PATCH 3/4] chore: format --- packages/monorepo-tools/src/bump-packages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/monorepo-tools/src/bump-packages.ts b/packages/monorepo-tools/src/bump-packages.ts index 13667b5a..a7dc7994 100644 --- a/packages/monorepo-tools/src/bump-packages.ts +++ b/packages/monorepo-tools/src/bump-packages.ts @@ -6,7 +6,7 @@ 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'; From 92a2964d6a8b77565774b57fa1052761b0475ba9 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Thu, 26 Feb 2026 16:03:08 +0100 Subject: [PATCH 4/4] chore: use correct env --- README.md | 6 +++--- .../src/utils/package-manager.spec.ts | 20 +++++++++---------- .../src/utils/package-manager.ts | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 404b8409..dde6d008 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ This repository contains packages that are shared dependencies of Compass, the M ## Package Manager Configuration -By default, this repository uses `npm` for package management. You can optionally use `pnpm` instead by setting the `USE_PNPM` environment variable: +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 USE_PNPM=true +export MONOREPO_TOOLS_USE_PNPM=true ``` -When `USE_PNPM=true`, all monorepo tools scripts will use `pnpm` instead of `npm` and `pnpm dlx` instead of `npx`. +When `MONOREPO_TOOLS_USE_PNPM=true`, all monorepo tools scripts will use `pnpm` instead of `npm` and `pnpm dlx` instead of `npx`. ## Getting Started diff --git a/packages/monorepo-tools/src/utils/package-manager.spec.ts b/packages/monorepo-tools/src/utils/package-manager.spec.ts index 09b2659d..08cd6e09 100644 --- a/packages/monorepo-tools/src/utils/package-manager.spec.ts +++ b/packages/monorepo-tools/src/utils/package-manager.spec.ts @@ -5,35 +5,35 @@ describe('package-manager', function () { let originalEnv: string | undefined; beforeEach(function () { - originalEnv = process.env.USE_PNPM; + originalEnv = process.env.MONOREPO_TOOLS_USE_PNPM; }); afterEach(function () { if (originalEnv === undefined) { - delete process.env.USE_PNPM; + delete process.env.MONOREPO_TOOLS_USE_PNPM; } else { - process.env.USE_PNPM = originalEnv; + process.env.MONOREPO_TOOLS_USE_PNPM = originalEnv; } }); describe('getPackageManager', function () { it('returns npm by default', function () { - delete process.env.USE_PNPM; + delete process.env.MONOREPO_TOOLS_USE_PNPM; assert.strictEqual(packageManager.getPackageManager(), 'npm'); }); - it('returns npm when USE_PNPM is false', function () { - process.env.USE_PNPM = 'false'; + 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 USE_PNPM is empty string', function () { - process.env.USE_PNPM = ''; + 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 USE_PNPM is true', function () { - process.env.USE_PNPM = 'true'; + 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 index 63bcc2ff..9d1da60d 100644 --- a/packages/monorepo-tools/src/utils/package-manager.ts +++ b/packages/monorepo-tools/src/utils/package-manager.ts @@ -5,9 +5,9 @@ const execFile = promisify(execFileCallback); /** * Get the package manager to use based on environment variable. - * Defaults to 'npm' if USE_PNPM is not set or set to anything other than 'true'. + * Defaults to 'npm' if MONOREPO_TOOLS_USE_PNPM is not set or set to anything other than 'true'. * - * Set USE_PNPM=true to use pnpm instead of npm. + * 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';