1import { sync as findUpSync } from 'find-up';
2import findYarnOrNpmWorkspaceRootUnsafe from 'find-yarn-workspace-root';
3import fs from 'fs';
4import yaml from 'js-yaml';
5import micromatch from 'micromatch';
6import path from 'path';
7
8export const NPM_LOCK_FILE = 'package-lock.json';
9export const YARN_LOCK_FILE = 'yarn.lock';
10export const PNPM_LOCK_FILE = 'pnpm-lock.yaml';
11export const PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml';
12export const BUN_LOCK_FILE = 'bun.lockb';
13
14/** Wraps `find-yarn-workspace-root` and guards against having an empty `package.json` file in an upper directory. */
15export function findYarnOrNpmWorkspaceRoot(projectRoot: string): string | null {
16  try {
17    return findYarnOrNpmWorkspaceRootUnsafe(projectRoot);
18  } catch (error: any) {
19    if (error.message.includes('Unexpected end of JSON input')) {
20      return null;
21    }
22    throw error;
23  }
24}
25
26/**
27 * Find the `pnpm-workspace.yaml` file that represents the root of the monorepo.
28 * This is a synchronous function based on the original async library.
29 * @see https://github.com/pnpm/pnpm/blob/main/packages/find-workspace-dir/src/index.ts
30 */
31export function findPnpmWorkspaceRoot(projectRoot: string): string | null {
32  const workspaceEnvName = 'NPM_CONFIG_WORKSPACE_DIR';
33  const workspaceEnvValue =
34    process.env[workspaceEnvName] ?? process.env[workspaceEnvName.toLowerCase()];
35
36  const workspaceFile = workspaceEnvValue
37    ? path.join(workspaceEnvValue, PNPM_WORKSPACE_FILE)
38    : findUpSync(PNPM_WORKSPACE_FILE, { cwd: projectRoot });
39
40  if (!workspaceFile || !fs.existsSync(workspaceFile)) {
41    return null;
42  }
43
44  try {
45    // See: https://pnpm.io/pnpm-workspace_yaml
46    const { packages: workspaces } = yaml.load(fs.readFileSync(workspaceFile, 'utf8'));
47    // See: https://github.com/square/find-yarn-workspace-root/blob/11f6e31d3fa15a5bb7b7419f0091390e4c16204c/index.js#L26-L33
48    const workspaceRoot = path.dirname(workspaceFile);
49    const relativePath = path.relative(workspaceRoot, projectRoot);
50
51    if (relativePath === '' || micromatch([relativePath], workspaces).length > 0) {
52      return workspaceRoot;
53    }
54  } catch {
55    // TODO: implement debug logger?
56    return null;
57  }
58
59  return null;
60}
61