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