monorepo-utils.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import fs from 'fs-extra';
  2. import path from 'node:path';
  3. /**
  4. * Common monorepo directory names (e.g., Nx, Turborepo, Lerna conventions)
  5. * - packages: Most common, used by most tools for shared libraries
  6. * - apps: Turborepo/Nx convention for applications
  7. * - libs: Nx convention for libraries
  8. * - services: Common for backend services/microservices
  9. * - modules: Alternative to packages (some projects prefer this naming)
  10. */
  11. export const MONOREPO_PACKAGE_DIRS = ['packages', 'apps', 'libs', 'services', 'modules'] as const;
  12. export interface MonorepoInfo {
  13. isMonorepo: boolean;
  14. /**
  15. * The root directory of the monorepo (if in a monorepo)
  16. */
  17. root?: string;
  18. /**
  19. * The package directory name that contains this path (e.g., 'packages', 'apps', 'libs')
  20. */
  21. packageDir?: (typeof MONOREPO_PACKAGE_DIRS)[number];
  22. }
  23. /**
  24. * Detects if a given path is inside a monorepo structure and extracts the monorepo root.
  25. * Handles cases where multiple monorepo directory types exist (e.g., both 'apps' and 'libs').
  26. *
  27. * @example
  28. * detectMonorepoStructure('/monorepo/packages/backend')
  29. * // => { isMonorepo: true, root: '/monorepo', packageDir: 'packages' }
  30. *
  31. * detectMonorepoStructure('/monorepo/apps/frontend')
  32. * // => { isMonorepo: true, root: '/monorepo', packageDir: 'apps' }
  33. *
  34. * detectMonorepoStructure('/regular-project')
  35. * // => { isMonorepo: false }
  36. */
  37. export function detectMonorepoStructure(dirPath: string): MonorepoInfo {
  38. const normalizedPath = path.normalize(dirPath);
  39. for (const dir of MONOREPO_PACKAGE_DIRS) {
  40. const pattern = path.sep + dir + path.sep;
  41. if (normalizedPath.includes(pattern)) {
  42. // Extract the monorepo root (the part before /packages/, /apps/, or /libs/)
  43. const parts = normalizedPath.split(pattern);
  44. return {
  45. isMonorepo: true,
  46. root: parts[0],
  47. packageDir: dir,
  48. };
  49. }
  50. }
  51. return { isMonorepo: false };
  52. }
  53. /**
  54. * Searches for a package.json file with a specific dependency within monorepo structures.
  55. * Searches common monorepo directories (packages, apps, libs) for subdirectories containing
  56. * a package.json with the specified dependency.
  57. *
  58. * @param rootDir - The root directory to search from
  59. * @param dependencyName - The dependency name to look for (e.g., '@vendure/core')
  60. * @returns The path to the package.json file, or null if not found
  61. */
  62. export function findPackageJsonWithDependency(rootDir: string, dependencyName: string): string | null {
  63. // First check if the root package.json has the dependency
  64. const rootPackageJsonPath = path.join(rootDir, 'package.json');
  65. if (hasNamedDependency(rootPackageJsonPath, dependencyName)) {
  66. return rootPackageJsonPath;
  67. }
  68. // Search in monorepo package directories
  69. for (const dir of MONOREPO_PACKAGE_DIRS) {
  70. const monorepoDir = path.join(rootDir, dir);
  71. if (fs.existsSync(monorepoDir)) {
  72. for (const subDir of fs.readdirSync(monorepoDir)) {
  73. const packageJsonPath = path.join(monorepoDir, subDir, 'package.json');
  74. if (hasNamedDependency(packageJsonPath, dependencyName)) {
  75. return packageJsonPath;
  76. }
  77. }
  78. }
  79. }
  80. return null;
  81. }
  82. /**
  83. * Checks if a package.json file exists and has the specified dependency.
  84. */
  85. function hasNamedDependency(packageJsonPath: string, dependencyName: string): boolean {
  86. if (!fs.existsSync(packageJsonPath)) {
  87. return false;
  88. }
  89. try {
  90. const packageJson = fs.readJsonSync(packageJsonPath);
  91. return !!packageJson.dependencies?.[dependencyName];
  92. } catch {
  93. return false;
  94. }
  95. }
  96. /**
  97. * Finds tsconfig files in a directory, preferring 'tsconfig.json' if it exists.
  98. */
  99. export function findTsConfigInDir(dir: string): string | null {
  100. if (!fs.existsSync(dir)) {
  101. return null;
  102. }
  103. const tsConfigCandidates = fs.readdirSync(dir).filter(f => /^tsconfig.*\.json$/.test(f));
  104. if (tsConfigCandidates.includes('tsconfig.json')) {
  105. return path.join(dir, 'tsconfig.json');
  106. }
  107. if (tsConfigCandidates.length > 0) {
  108. return path.join(dir, tsConfigCandidates[0]);
  109. }
  110. return null;
  111. }