Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
8 changes: 3 additions & 5 deletions packages/monorepo-tools/src/bump-packages.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
/* 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';
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';
Expand Down Expand Up @@ -58,7 +56,7 @@ async function main() {
);
}

await execFile('npm', ['install', '--package-lock-only']);
await installDependencies({ packageLockOnly: true });
}

main().catch((err) =>
Expand Down
6 changes: 3 additions & 3 deletions packages/monorepo-tools/src/depalign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -228,7 +228,7 @@ async function alignPackageToRange(packageName: string, range: string) {
});
}

await runInDir('npm install');
await installDependencies();
}

function hasDep(
Expand Down Expand Up @@ -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`;

Expand Down
25 changes: 14 additions & 11 deletions packages/monorepo-tools/src/precommit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down Expand Up @@ -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]);
Expand Down
40 changes: 40 additions & 0 deletions packages/monorepo-tools/src/utils/package-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
94 changes: 94 additions & 0 deletions packages/monorepo-tools/src/utils/package-manager.ts
Original file line number Diff line number Diff line change
@@ -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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a pnpm-lock.yaml to align behavior locally and on CI? And if we do add pnpm-lock do we have a way of keeping it in sync with package-lock.json?

Copy link
Collaborator Author

@gagik gagik Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a repo uses pnpm, then we will only have pnpm-lock, we should not have package-lock.
One can also set pnpm as default package manager in package.json so then even if someone runs npm i it'll redirect to pnpm

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tools could try to look for pnpm-lock to determine what mode to run in maybe but pnpm lock might be at a higher level folder etc so env just seems more flexible

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood but this PR is for adding optional use of pnpm, so there is no pnpm-lock file, as a result is anyone who uses pnpm going to suffer from a broken experience at least locally when they go to install and they get a very different set of transient dependencies than are used on CI? (as a comparable example, I often accidentally work in compass on the wrong npm version and even that deviation causes a change in the result of npm i and tests fail)

Main concern here, sure we exec pnpm, but is it useful? (will it be useful? because it may work today but when it breaks what do we do about it to fix it, what will be the urgency to fix it, etc.)

);
}

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<string> {
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 });
}
29 changes: 20 additions & 9 deletions packages/monorepo-tools/src/where.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
});
}

Expand Down
Loading