1import fs from 'fs';
2import path from 'path';
3
4import {
5  findPnpmWorkspaceRoot,
6  findYarnOrNpmWorkspaceRoot,
7  NPM_LOCK_FILE,
8  PNPM_LOCK_FILE,
9  YARN_LOCK_FILE,
10} from './nodeWorkspaces';
11import { PackageManagerOptions } from '../PackageManager';
12import { NpmPackageManager } from '../node/NpmPackageManager';
13import { PnpmPackageManager } from '../node/PnpmPackageManager';
14import { YarnPackageManager } from '../node/YarnPackageManager';
15
16export type NodePackageManager = NpmPackageManager | PnpmPackageManager | YarnPackageManager;
17
18export type NodePackageManagerForProject = PackageManagerOptions &
19  Partial<Record<NodePackageManager['name'], boolean>>;
20
21/** The order of the package managers to use when resolving automatically */
22export const RESOLUTION_ORDER: NodePackageManager['name'][] = ['yarn', 'npm', 'pnpm'];
23
24/**
25 * Resolve the workspace root for a project, if its part of a monorepo.
26 * Optionally, provide a specific packager to only resolve that one specifically.
27 */
28export function findWorkspaceRoot(
29  projectRoot: string,
30  preferredManager?: NodePackageManager['name']
31): string | null {
32  const strategies: Record<NodePackageManager['name'], (projectRoot: string) => string | null> = {
33    npm: findYarnOrNpmWorkspaceRoot,
34    yarn: findYarnOrNpmWorkspaceRoot,
35    pnpm: findPnpmWorkspaceRoot,
36  };
37
38  if (preferredManager) {
39    return strategies[preferredManager](projectRoot);
40  }
41
42  for (const strategy of RESOLUTION_ORDER) {
43    const root = strategies[strategy](projectRoot);
44    if (root) {
45      return root;
46    }
47  }
48
49  return null;
50}
51
52/**
53 * Resolve the used node package manager for a project by checking the lockfile.
54 * This also tries to resolve the workspace root, if its part of a monorepo.
55 * Optionally, provide a preferred packager to only resolve that one specifically.
56 */
57export function resolvePackageManager(
58  projectRoot: string,
59  preferredManager?: NodePackageManager['name']
60): NodePackageManager['name'] | null {
61  const root = findWorkspaceRoot(projectRoot, preferredManager) ?? projectRoot;
62  const lockFiles: Record<NodePackageManager['name'], string> = {
63    npm: NPM_LOCK_FILE,
64    pnpm: PNPM_LOCK_FILE,
65    yarn: YARN_LOCK_FILE,
66  };
67
68  if (preferredManager) {
69    if (fs.existsSync(path.join(root, lockFiles[preferredManager]))) {
70      return preferredManager;
71    }
72
73    return null;
74  }
75
76  for (const managerName of RESOLUTION_ORDER) {
77    if (fs.existsSync(path.join(root, lockFiles[managerName]))) {
78      return managerName;
79    }
80  }
81
82  return null;
83}
84
85/**
86 * This creates a Node package manager from the provided options.
87 * If these options are not provided, it will infer the package manager from lockfiles.
88 * When no package manager is found, it falls back to npm.
89 */
90export function createForProject(
91  projectRoot: string,
92  options: NodePackageManagerForProject = {}
93): NodePackageManager {
94  if (options.npm) {
95    return new NpmPackageManager({ cwd: projectRoot, ...options });
96  } else if (options.yarn) {
97    return new YarnPackageManager({ cwd: projectRoot, ...options });
98  } else if (options.pnpm) {
99    return new PnpmPackageManager({ cwd: projectRoot, ...options });
100  }
101
102  switch (resolvePackageManager(projectRoot)) {
103    case 'npm':
104      return new NpmPackageManager({ cwd: projectRoot, ...options });
105    case 'pnpm':
106      return new PnpmPackageManager({ cwd: projectRoot, ...options });
107    case 'yarn':
108      return new YarnPackageManager({ cwd: projectRoot, ...options });
109    default:
110      return new NpmPackageManager({ cwd: projectRoot, ...options });
111  }
112}
113