|
| 1 | +import type { BunFile } from 'bun'; |
| 2 | +import path from 'path'; |
| 3 | +import type { CommentObject } from 'comment-json'; |
| 4 | +import { findWorkspaceDir } from '@pnpm/find-workspace-dir'; |
| 5 | +import { findWorkspacePackages, type Project } from '@pnpm/find-workspace-packages'; |
| 6 | + |
| 7 | +export async function getMonorepoRoot() { |
| 8 | + const workspaceDir = await findWorkspaceDir(process.cwd()); |
| 9 | + |
| 10 | + if (workspaceDir) { |
| 11 | + return workspaceDir; |
| 12 | + } |
| 13 | + |
| 14 | + const MAX_DEPTH = 10; |
| 15 | + // are we in the root? |
| 16 | + let currentDir = process.cwd(); |
| 17 | + let depth = 0; |
| 18 | + while (depth < MAX_DEPTH) { |
| 19 | + const lockfileFile = path.join(currentDir, 'pnpm-lock.yaml'); |
| 20 | + if (await Bun.file(lockfileFile).exists()) { |
| 21 | + return currentDir; |
| 22 | + } |
| 23 | + currentDir = path.join(currentDir, '../'); |
| 24 | + depth++; |
| 25 | + } |
| 26 | + |
| 27 | + throw new Error(`Could not find monorepo root from cwd ${process.cwd()}`); |
| 28 | +} |
| 29 | + |
| 30 | +export async function getPackageJson({ packageDir, packagesDir }: { packageDir: string; packagesDir: string }) { |
| 31 | + const packageJsonPath = path.join(packagesDir, packageDir, 'package.json'); |
| 32 | + const packageJsonFile = Bun.file(packageJsonPath); |
| 33 | + const pkg = await packageJsonFile.json(); |
| 34 | + return { file: packageJsonFile, pkg, path: packageJsonPath, nicePath: path.join(packageDir, 'package.json') }; |
| 35 | +} |
| 36 | + |
| 37 | +type PkgJsonFile = { |
| 38 | + name: string; |
| 39 | + version: string; |
| 40 | + files?: string[]; |
| 41 | + license?: string; |
| 42 | + private?: boolean; |
| 43 | + dependencies?: Record<string, string>; |
| 44 | + devDependencies?: Record<string, string>; |
| 45 | + peerDependencies?: Record<string, string>; |
| 46 | + scripts?: Record<string, string>; |
| 47 | + main?: string; |
| 48 | + peerDependenciesMeta?: Record<string, { optional: boolean }>; |
| 49 | +}; |
| 50 | + |
| 51 | +export type TsConfigFile = { |
| 52 | + include?: string[]; |
| 53 | + compilerOptions?: { |
| 54 | + lib?: string[]; |
| 55 | + module?: string; |
| 56 | + target?: string; |
| 57 | + moduleResolution?: string; |
| 58 | + moduleDetection?: string; |
| 59 | + erasableSyntaxOnly?: boolean; |
| 60 | + allowImportingTsExtensions?: boolean; |
| 61 | + verbatimModuleSyntax?: boolean; |
| 62 | + isolatedModules?: boolean; |
| 63 | + isolatedDeclarations?: boolean; |
| 64 | + pretty?: boolean; |
| 65 | + strict?: boolean; |
| 66 | + experimentalDecorators?: boolean; |
| 67 | + allowJs?: boolean; |
| 68 | + checkJs?: boolean; |
| 69 | + rootDir?: string; |
| 70 | + baseUrl?: string; |
| 71 | + declarationMap?: boolean; |
| 72 | + inlineSourceMap?: boolean; |
| 73 | + inlineSources?: boolean; |
| 74 | + skipLibCheck?: boolean; |
| 75 | + declaration?: boolean; |
| 76 | + declarationDir?: string; |
| 77 | + incremental?: boolean; |
| 78 | + composite?: boolean; |
| 79 | + emitDeclarationOnly?: boolean; |
| 80 | + noEmit?: boolean; |
| 81 | + paths?: Record<string, string[]>; |
| 82 | + types?: string[]; |
| 83 | + }; |
| 84 | + references?: { path: string }[]; |
| 85 | +}; |
| 86 | + |
| 87 | +interface BaseProjectPackage { |
| 88 | + project: Project; |
| 89 | + packages: Map<string, Project>; |
| 90 | + pkgFile: BunFile; |
| 91 | + tsconfigFile: BunFile; |
| 92 | + pkgPath: string; |
| 93 | + tsconfigPath: string; |
| 94 | + pkg: PkgJsonFile; |
| 95 | + save: (editStatus: { pkgEdited: boolean; configEdited: Boolean }) => Promise<void>; |
| 96 | +} |
| 97 | + |
| 98 | +export interface ProjectPackageWithTsConfig extends BaseProjectPackage { |
| 99 | + tsconfig: CommentObject & TsConfigFile; |
| 100 | + hasTsConfig: true; |
| 101 | +} |
| 102 | + |
| 103 | +interface ProjectPackageWithoutTsConfig extends BaseProjectPackage { |
| 104 | + tsconfig: null; |
| 105 | + hasTsConfig: false; |
| 106 | +} |
| 107 | + |
| 108 | +export type ProjectPackage = ProjectPackageWithTsConfig | ProjectPackageWithoutTsConfig; |
| 109 | + |
| 110 | +async function collectAllPackages(dir: string) { |
| 111 | + const packages = await findWorkspacePackages(dir); |
| 112 | + const pkgMap = new Map<string, Project>(); |
| 113 | + for (const pkg of packages) { |
| 114 | + if (!pkg.manifest.name) { |
| 115 | + throw new Error(`Package at ${pkg.dir} does not have a name`); |
| 116 | + } |
| 117 | + pkgMap.set(pkg.manifest.name, pkg); |
| 118 | + } |
| 119 | + |
| 120 | + return pkgMap; |
| 121 | +} |
| 122 | + |
| 123 | +export async function walkPackages( |
| 124 | + cb: (pkg: ProjectPackage, projects: Map<string, ProjectPackage>) => void | Promise<void>, |
| 125 | + options: { |
| 126 | + excludeTests?: boolean; |
| 127 | + excludePrivate?: boolean; |
| 128 | + excludeRoot?: boolean; |
| 129 | + excludeTooling?: boolean; |
| 130 | + excludeConfig?: boolean; |
| 131 | + } = {} |
| 132 | +) { |
| 133 | + const config = Object.assign( |
| 134 | + { excludeTests: false, excludePrivate: false, excludeRoot: true, excludeTooling: true, excludeConfig: true }, |
| 135 | + options |
| 136 | + ); |
| 137 | + const JSONC = await import('comment-json'); |
| 138 | + const dir = await getMonorepoRoot(); |
| 139 | + const packages = await collectAllPackages(dir); |
| 140 | + const projects = new Map<string, ProjectPackageWithTsConfig>(); |
| 141 | + |
| 142 | + for (const [name, project] of packages) { |
| 143 | + if (config.excludeRoot && name === 'root') continue; |
| 144 | + if (config.excludePrivate && project.manifest.private) continue; |
| 145 | + if (config.excludeTooling && name === '@warp-drive/internal-tooling') continue; |
| 146 | + if (config.excludeConfig && name === '@warp-drive/config') continue; |
| 147 | + if (config.excludeTests && project.dir === 'tests') continue; |
| 148 | + |
| 149 | + const pkgPath = path.join(project.dir, 'package.json'); |
| 150 | + const tsconfigPath = path.join(project.dir, 'tsconfig.json'); |
| 151 | + const pkgFile = Bun.file(pkgPath); |
| 152 | + const tsconfigFile = Bun.file(tsconfigPath); |
| 153 | + const pkg = (await pkgFile.json()) as PkgJsonFile; |
| 154 | + const hasTsConfig = await tsconfigFile.exists(); |
| 155 | + const tsconfig = hasTsConfig ? (JSONC.parse(await tsconfigFile.text()) as CommentObject & TsConfigFile) : null; |
| 156 | + |
| 157 | + const pkgObj = { |
| 158 | + project, |
| 159 | + packages, |
| 160 | + pkgFile, |
| 161 | + tsconfigFile, |
| 162 | + pkgPath, |
| 163 | + hasTsConfig, |
| 164 | + tsconfigPath, |
| 165 | + pkg, |
| 166 | + tsconfig, |
| 167 | + save: async ({ pkgEdited, configEdited }: { pkgEdited: boolean; configEdited: Boolean }) => { |
| 168 | + if (pkgEdited) await pkgFile.write(JSON.stringify(pkg, null, 2)); |
| 169 | + if (configEdited) await tsconfigFile.write(JSONC.stringify(tsconfig, null, 2)); |
| 170 | + }, |
| 171 | + } as ProjectPackageWithTsConfig; |
| 172 | + |
| 173 | + projects.set(name, pkgObj); |
| 174 | + } |
| 175 | + |
| 176 | + for (const project of projects.values()) { |
| 177 | + await cb(project, projects); |
| 178 | + } |
| 179 | +} |
0 commit comments