1import { sync as findUpSync } from 'find-up';
2import findYarnOrNpmWorkspaceRoot from 'find-yarn-workspace-root';
3import { existsSync } from 'fs';
4import path from 'path';
5
6import type { NodePackageManager } from '../NodePackageManagers';
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 managerResolutionOrder: NodePackageManager[] = ['yarn', 'npm', 'pnpm'];
13
14/**
15 * Find the `pnpm-workspace.yaml` file that represents the root of the monorepo.
16 * This is a synchronous function based on the original async library.
17 * @see https://github.com/pnpm/pnpm/blob/main/packages/find-workspace-dir/src/index.ts
18 */
19function findPnpmWorkspaceRoot(projectRoot: string) {
20  const workspaceEnvName = 'NPM_CONFIG_WORKSPACE_DIR';
21
22  const workspaceEnvValue =
23    process.env[workspaceEnvName] ?? process.env[workspaceEnvName.toLowerCase()];
24  const manifestLocation = workspaceEnvValue
25    ? path.join(workspaceEnvValue, PNPM_WORKSPACE_FILE)
26    : findUpSync(PNPM_WORKSPACE_FILE, { cwd: projectRoot });
27
28  return manifestLocation ? path.dirname(manifestLocation) : null;
29}
30
31/** Wraps `findYarnOrNpmWorkspaceRoot` and guards against having an empty `package.json` file in an upper directory. */
32export function findYarnOrNpmWorkspaceRootSafe(projectRoot: string): string | null {
33  try {
34    return findYarnOrNpmWorkspaceRoot(projectRoot);
35  } catch (error: any) {
36    if (error.message.includes('Unexpected end of JSON input')) {
37      return null;
38    }
39    throw error;
40  }
41}
42
43/**
44 * Resolve the workspace root for a project, if its part of a monorepo.
45 * Optionally, provide a specific packager to only resolve that one specifically.
46 *
47 * By default, this tries to resolve the workspaces in order of:
48 *  - npm
49 *  - yarn
50 *  - pnpm
51 */
52export function findWorkspaceRoot(
53  projectRoot: string,
54  packageManager?: NodePackageManager
55): string | null {
56  const strategies: Record<NodePackageManager, (projectRoot: string) => string | null> = {
57    npm: findYarnOrNpmWorkspaceRootSafe,
58    yarn: findYarnOrNpmWorkspaceRootSafe,
59    pnpm: findPnpmWorkspaceRoot,
60  };
61
62  if (packageManager) {
63    return strategies[packageManager](projectRoot);
64  }
65
66  for (const strategy of managerResolutionOrder.map((name) => strategies[name])) {
67    const root = strategy(projectRoot);
68    if (root) {
69      return root;
70    }
71  }
72
73  return null;
74}
75
76/**
77 * Resolve the used node package manager for a project by checking the lockfile.
78 * This also tries to resolve the workspace root, if its part of a monorepo.
79 * Optionally, provide a specific packager to only resolve that one specifically.
80 *
81 * By default, this tries to resolve the workspaces in order of:
82 *  - npm
83 *  - yarn
84 *  - pnpm
85 */
86export function resolvePackageManager(
87  projectRoot: string,
88  packageManager?: NodePackageManager
89): NodePackageManager | null {
90  const workspaceRoot = findWorkspaceRoot(projectRoot, packageManager) || projectRoot;
91  const lockfileNames: Record<NodePackageManager, string> = {
92    npm: NPM_LOCK_FILE,
93    yarn: YARN_LOCK_FILE,
94    pnpm: PNPM_LOCK_FILE,
95  };
96
97  if (packageManager) {
98    const lockfilePath = path.join(workspaceRoot, lockfileNames[packageManager]);
99    if (existsSync(lockfilePath)) {
100      return packageManager;
101    }
102    return null;
103  }
104
105  for (const manager of managerResolutionOrder) {
106    const lockfilePath = path.join(workspaceRoot, lockfileNames[manager]);
107    if (existsSync(lockfilePath)) {
108      return manager;
109    }
110  }
111
112  return null;
113}
114
115/**
116 * Returns true if the project is using yarn, false if the project is using another package manager.
117 */
118export function isUsingYarn(projectRoot: string): boolean {
119  return !!resolvePackageManager(projectRoot, 'yarn');
120}
121
122/**
123 * Returns true if the project is using npm, false if the project is using another package manager.
124 */
125export function isUsingNpm(projectRoot: string): boolean {
126  return !!resolvePackageManager(projectRoot, 'npm');
127}
128