link-packages.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import { spawn, execSync, type ChildProcess } from "child_process"
  2. import * as path from "path"
  3. import * as fs from "fs"
  4. import { fileURLToPath } from "url"
  5. import { glob } from "glob"
  6. // @ts-expect-error - TS1470: We only run this script with tsx so it will never
  7. // compile to CJS and it's safe to ignore this tsc error.
  8. const __filename = fileURLToPath(import.meta.url)
  9. const __dirname = path.dirname(__filename)
  10. interface PackageConfig {
  11. readonly name: string
  12. readonly sourcePath: string
  13. readonly targetPaths: readonly string[]
  14. readonly replacePath?: string
  15. readonly npmPath: string
  16. readonly watchCommand?: string
  17. readonly watchOutput?: {
  18. readonly start: string[]
  19. readonly stop: string[]
  20. }
  21. }
  22. interface Config {
  23. readonly packages: readonly PackageConfig[]
  24. }
  25. interface WatcherResult {
  26. child: ChildProcess
  27. }
  28. interface NpmPackage {
  29. name?: string
  30. version?: string
  31. type: "module"
  32. dependencies: Record<string, string>
  33. main: string
  34. module: string
  35. types: string
  36. exports: {
  37. ".": {
  38. types: string
  39. import: string
  40. require: {
  41. types: string
  42. default: string
  43. }
  44. }
  45. }
  46. files: string[]
  47. }
  48. const config: Config = {
  49. packages: [
  50. {
  51. name: "@roo-code/cloud",
  52. sourcePath: "../Roo-Code-Cloud/packages/sdk",
  53. targetPaths: ["src/node_modules/@roo-code/cloud"] as const,
  54. replacePath: "node_modules/.pnpm/@roo-code+cloud*",
  55. npmPath: "npm",
  56. watchCommand: "pnpm build:development:watch",
  57. watchOutput: {
  58. start: ["CLI Building", "CLI Change detected"],
  59. stop: ["DTS ⚡️ Build success"],
  60. },
  61. },
  62. ],
  63. } as const
  64. const args = process.argv.slice(2)
  65. const packageName = args.find((arg) => !arg.startsWith("--"))
  66. const watchMode = !args.includes("--no-watch")
  67. const unlink = args.includes("--unlink")
  68. const packages: readonly PackageConfig[] = packageName
  69. ? config.packages.filter((p) => p.name === packageName)
  70. : config.packages
  71. if (!packages.length) {
  72. console.error(`Package '${packageName}' not found`)
  73. process.exit(1)
  74. }
  75. function pathExists(filePath: string): boolean {
  76. try {
  77. fs.accessSync(filePath)
  78. return true
  79. } catch {
  80. return false
  81. }
  82. }
  83. function copyRecursiveSync(src: string, dest: string): void {
  84. const exists = pathExists(src)
  85. if (!exists) {
  86. return
  87. }
  88. const stats = fs.statSync(src)
  89. const isDirectory = stats.isDirectory()
  90. if (isDirectory) {
  91. if (!pathExists(dest)) {
  92. fs.mkdirSync(dest, { recursive: true })
  93. }
  94. const children = fs.readdirSync(src)
  95. children.forEach((childItemName) => {
  96. copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
  97. })
  98. } else {
  99. fs.copyFileSync(src, dest)
  100. }
  101. }
  102. function generateNpmPackageJson(sourcePath: string, npmPath: string): string {
  103. const npmDir = path.join(sourcePath, npmPath)
  104. const npmPackagePath = path.join(npmDir, "package.json")
  105. const npmMetadataPath = path.join(npmDir, "package.metadata.json")
  106. const monorepoPackagePath = path.join(sourcePath, "package.json")
  107. if (pathExists(npmPackagePath)) {
  108. return npmPackagePath
  109. }
  110. if (!pathExists(npmMetadataPath)) {
  111. throw new Error(`No package.metadata.json found in ${npmDir}`)
  112. }
  113. const monorepoPackageContent = fs.readFileSync(monorepoPackagePath, "utf8")
  114. const monorepoPackage = JSON.parse(monorepoPackageContent) as {
  115. dependencies?: Record<string, string>
  116. }
  117. const npmMetadataContent = fs.readFileSync(npmMetadataPath, "utf8")
  118. const npmMetadata = JSON.parse(npmMetadataContent) as Partial<NpmPackage>
  119. const npmPackage: NpmPackage = {
  120. ...npmMetadata,
  121. type: "module",
  122. dependencies: monorepoPackage.dependencies || {},
  123. main: "./dist/index.cjs",
  124. module: "./dist/index.js",
  125. types: "./dist/index.d.ts",
  126. exports: {
  127. ".": {
  128. types: "./dist/index.d.ts",
  129. import: "./dist/index.js",
  130. require: {
  131. types: "./dist/index.d.cts",
  132. default: "./dist/index.cjs",
  133. },
  134. },
  135. },
  136. files: ["dist"],
  137. }
  138. fs.writeFileSync(npmPackagePath, JSON.stringify(npmPackage, null, 2) + "\n")
  139. return npmPackagePath
  140. }
  141. function linkPackage(pkg: PackageConfig): void {
  142. const sourcePath = path.resolve(__dirname, "..", pkg.sourcePath)
  143. if (!pathExists(sourcePath)) {
  144. console.error(`❌ Source not found: ${sourcePath}`)
  145. process.exit(1)
  146. }
  147. generateNpmPackageJson(sourcePath, pkg.npmPath)
  148. for (const currentTargetPath of pkg.targetPaths) {
  149. const targetPath = path.resolve(__dirname, "..", currentTargetPath)
  150. if (pathExists(targetPath)) {
  151. fs.rmSync(targetPath, { recursive: true, force: true })
  152. }
  153. const parentDir = path.dirname(targetPath)
  154. fs.mkdirSync(parentDir, { recursive: true })
  155. const linkSource = pkg.npmPath ? path.join(sourcePath, pkg.npmPath) : sourcePath
  156. copyRecursiveSync(linkSource, targetPath)
  157. }
  158. }
  159. function unlinkPackage(pkg: PackageConfig): void {
  160. for (const currentTargetPath of pkg.targetPaths) {
  161. const targetPath = path.resolve(__dirname, "..", currentTargetPath)
  162. if (pathExists(targetPath)) {
  163. fs.rmSync(targetPath, { recursive: true, force: true })
  164. console.log(`🗑️ Removed ${pkg.name} from ${currentTargetPath}`)
  165. }
  166. }
  167. }
  168. function startWatch(pkg: PackageConfig): WatcherResult {
  169. if (!pkg.watchCommand) {
  170. throw new Error(`Package ${pkg.name} has no watch command configured`)
  171. }
  172. const commandParts = pkg.watchCommand.split(" ")
  173. const [cmd, ...args] = commandParts
  174. if (!cmd) {
  175. throw new Error(`Invalid watch command for ${pkg.name}`)
  176. }
  177. console.log(`👀 Watching for changes to ${pkg.sourcePath} with ${cmd} ${args.join(" ")}`)
  178. const child = spawn(cmd, args, {
  179. cwd: path.resolve(__dirname, "..", pkg.sourcePath),
  180. stdio: "pipe",
  181. shell: true,
  182. })
  183. let debounceTimer: NodeJS.Timeout | null = null
  184. const DEBOUNCE_DELAY = 500
  185. if (child.stdout) {
  186. child.stdout.on("data", (data: Buffer) => {
  187. const output = data.toString()
  188. const isStarting = pkg.watchOutput?.start.some((start) => output.includes(start))
  189. const isDone = pkg.watchOutput?.stop.some((stop) => output.includes(stop))
  190. if (isStarting) {
  191. console.log(`🔨 Building ${pkg.name}...`)
  192. if (debounceTimer) {
  193. clearTimeout(debounceTimer)
  194. debounceTimer = null
  195. }
  196. }
  197. if (isDone) {
  198. console.log(`✅ Built ${pkg.name}`)
  199. if (debounceTimer) {
  200. clearTimeout(debounceTimer)
  201. }
  202. debounceTimer = setTimeout(() => {
  203. linkPackage(pkg)
  204. console.log(`♻️ Copied ${pkg.name} to ${pkg.targetPaths.length} paths\n`)
  205. debounceTimer = null
  206. }, DEBOUNCE_DELAY)
  207. }
  208. })
  209. }
  210. if (child.stderr) {
  211. child.stderr.on("data", (data: Buffer) => {
  212. console.log(`❌ "${data.toString()}"`)
  213. })
  214. }
  215. return { child }
  216. }
  217. function main(): void {
  218. if (unlink) {
  219. packages.forEach(unlinkPackage)
  220. console.log("\n📦 Restoring npm packages...")
  221. try {
  222. execSync("pnpm install", { cwd: __dirname, stdio: "ignore" })
  223. console.log("✅ npm packages restored")
  224. } catch (error) {
  225. console.error(`❌ Failed to restore packages: ${error instanceof Error ? error.message : String(error)}`)
  226. console.log(" Run 'pnpm install' manually if needed")
  227. }
  228. } else {
  229. packages.forEach((pkg) => {
  230. linkPackage(pkg)
  231. if (pkg.replacePath) {
  232. const replacePattern = path.resolve(__dirname, "..", pkg.replacePath)
  233. try {
  234. const matchedPaths = glob.sync(replacePattern)
  235. if (matchedPaths.length > 0) {
  236. matchedPaths.forEach((matchedPath: string) => {
  237. if (pathExists(matchedPath)) {
  238. fs.rmSync(matchedPath, { recursive: true, force: true })
  239. console.log(`🗑️ Removed ${pkg.name} from ${matchedPath}`)
  240. }
  241. })
  242. } else {
  243. if (pathExists(replacePattern)) {
  244. fs.rmSync(replacePattern, { recursive: true, force: true })
  245. console.log(`🗑️ Removed ${pkg.name} from ${replacePattern}`)
  246. }
  247. }
  248. } catch (error) {
  249. console.error(
  250. `❌ Error processing replace path: ${error instanceof Error ? error.message : String(error)}`,
  251. )
  252. }
  253. }
  254. })
  255. if (watchMode) {
  256. const packagesWithWatch = packages.filter(
  257. (pkg): pkg is PackageConfig & { watchCommand: string } => pkg.watchCommand !== undefined,
  258. )
  259. const watchers = packagesWithWatch.map(startWatch)
  260. if (watchers.length > 0) {
  261. process.on("SIGINT", () => {
  262. console.log("\n👋 Stopping watchers...")
  263. watchers.forEach((w) => {
  264. if (w.child) {
  265. w.child.kill()
  266. }
  267. })
  268. process.exit(0)
  269. })
  270. }
  271. }
  272. }
  273. }
  274. main()