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