esbuild.mts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import fs from "node:fs"
  2. import path from "node:path"
  3. import { fileURLToPath } from "node:url"
  4. import dotenv from "dotenv"
  5. import * as esbuild from "esbuild"
  6. const __filename = fileURLToPath(import.meta.url)
  7. const __dirname = path.dirname(__filename)
  8. const rootDir = path.resolve(__dirname, "..")
  9. // Load .env from repo root
  10. dotenv.config({ path: path.join(rootDir, ".env") })
  11. const production = process.argv.includes("--production")
  12. const watch = process.argv.includes("--watch")
  13. /**
  14. * Plugin to resolve path aliases from the parent project
  15. */
  16. const aliasResolverPlugin: esbuild.Plugin = {
  17. name: "alias-resolver",
  18. setup(build) {
  19. const aliases = {
  20. "@": path.resolve(rootDir, "src"),
  21. "@core": path.resolve(rootDir, "src/core"),
  22. "@integrations": path.resolve(rootDir, "src/integrations"),
  23. "@services": path.resolve(rootDir, "src/services"),
  24. "@shared": path.resolve(rootDir, "src/shared"),
  25. "@utils": path.resolve(rootDir, "src/utils"),
  26. "@packages": path.resolve(rootDir, "src/packages"),
  27. "@hosts": path.resolve(rootDir, "src/hosts"),
  28. "@generated": path.resolve(rootDir, "src/generated"),
  29. "@api": path.resolve(rootDir, "src/core/api"),
  30. }
  31. // For each alias entry, create a resolver
  32. Object.entries(aliases).forEach(([alias, aliasPath]) => {
  33. const aliasRegex = new RegExp(`^${alias}($|/.*)`)
  34. build.onResolve({ filter: aliasRegex }, (args) => {
  35. const importPath = args.path.replace(alias, aliasPath)
  36. // First, check if the path exists as is
  37. if (fs.existsSync(importPath)) {
  38. const stats = fs.statSync(importPath)
  39. if (stats.isDirectory()) {
  40. // If it's a directory, try to find index files
  41. const extensions = [".ts", ".tsx", ".js", ".jsx"]
  42. for (const ext of extensions) {
  43. const indexFile = path.join(importPath, `index${ext}`)
  44. if (fs.existsSync(indexFile)) {
  45. return { path: indexFile }
  46. }
  47. }
  48. } else {
  49. // It's a file that exists, so return it
  50. return { path: importPath }
  51. }
  52. }
  53. // If the path doesn't exist, try appending extensions
  54. const extensions = [".ts", ".tsx", ".js", ".jsx"]
  55. for (const ext of extensions) {
  56. const pathWithExtension = `${importPath}${ext}`
  57. if (fs.existsSync(pathWithExtension)) {
  58. return { path: pathWithExtension }
  59. }
  60. }
  61. // Handle .js -> .ts extension mapping (common in ESM TypeScript projects)
  62. if (importPath.endsWith(".js")) {
  63. const tsPath = importPath.replace(/\.js$/, ".ts")
  64. if (fs.existsSync(tsPath)) {
  65. return { path: tsPath }
  66. }
  67. const tsxPath = importPath.replace(/\.js$/, ".tsx")
  68. if (fs.existsSync(tsxPath)) {
  69. return { path: tsxPath }
  70. }
  71. }
  72. // If nothing worked, return the original path and let esbuild handle the error
  73. return { path: importPath }
  74. })
  75. })
  76. },
  77. }
  78. /**
  79. * Plugin to redirect vscode imports to our shim
  80. */
  81. const vscodeStubPlugin: esbuild.Plugin = {
  82. name: "vscode-stub",
  83. setup(build) {
  84. // Redirect 'vscode' imports to our shim
  85. build.onResolve({ filter: /^vscode$/ }, () => {
  86. return { path: path.join(__dirname, "src", "vscode-shim.ts") }
  87. })
  88. },
  89. }
  90. const esbuildProblemMatcherPlugin: esbuild.Plugin = {
  91. name: "esbuild-problem-matcher",
  92. setup(build) {
  93. build.onStart(() => {
  94. console.log("[cli esbuild] Build started...")
  95. })
  96. build.onEnd((result) => {
  97. result.errors.forEach(({ text, location }) => {
  98. console.error(`✘ [ERROR] ${text}`)
  99. if (location) {
  100. console.error(` ${location.file}:${location.line}:${location.column}:`)
  101. }
  102. })
  103. console.log("[cli esbuild] Build finished")
  104. })
  105. },
  106. }
  107. // Plugin to stub out optional devtools module
  108. const stubOptionalModulesPlugin: esbuild.Plugin = {
  109. name: "stub-optional-modules",
  110. setup(build) {
  111. build.onResolve({ filter: /^react-devtools-core$/ }, () => {
  112. return { path: path.join(__dirname, "src", "stub-devtools.js"), external: false }
  113. })
  114. },
  115. }
  116. const copyWasmFiles: esbuild.Plugin = {
  117. name: "copy-wasm-files",
  118. setup(build) {
  119. build.onEnd(() => {
  120. const destDir = path.join(__dirname, "dist")
  121. // Ensure dist directory exists
  122. if (!fs.existsSync(destDir)) {
  123. fs.mkdirSync(destDir, { recursive: true })
  124. }
  125. // tree sitter
  126. const sourceDir = path.join(rootDir, "node_modules", "web-tree-sitter")
  127. // Copy tree-sitter.wasm
  128. const treeSitterWasm = path.join(sourceDir, "tree-sitter.wasm")
  129. if (fs.existsSync(treeSitterWasm)) {
  130. fs.copyFileSync(treeSitterWasm, path.join(destDir, "tree-sitter.wasm"))
  131. }
  132. // Copy language-specific WASM files
  133. const languageWasmDir = path.join(rootDir, "node_modules", "tree-sitter-wasms", "out")
  134. const languages = [
  135. "typescript",
  136. "tsx",
  137. "python",
  138. "rust",
  139. "javascript",
  140. "go",
  141. "cpp",
  142. "c",
  143. "c_sharp",
  144. "ruby",
  145. "java",
  146. "php",
  147. "swift",
  148. "kotlin",
  149. ]
  150. if (fs.existsSync(languageWasmDir)) {
  151. languages.forEach((lang) => {
  152. const filename = `tree-sitter-${lang}.wasm`
  153. const sourcePath = path.join(languageWasmDir, filename)
  154. if (fs.existsSync(sourcePath)) {
  155. fs.copyFileSync(sourcePath, path.join(destDir, filename))
  156. }
  157. })
  158. }
  159. })
  160. },
  161. }
  162. const buildEnvVars: Record<string, string> = {
  163. "process.env.IS_STANDALONE": JSON.stringify("true"),
  164. "process.env.IS_CLI": JSON.stringify("true"),
  165. }
  166. const buildTimeEnvs = [
  167. "TELEMETRY_SERVICE_API_KEY",
  168. "ERROR_SERVICE_API_KEY",
  169. "ENABLE_ERROR_AUTOCAPTURE",
  170. "POSTHOG_TELEMETRY_ENABLED",
  171. "OTEL_TELEMETRY_ENABLED",
  172. "OTEL_LOGS_EXPORTER",
  173. "OTEL_METRICS_EXPORTER",
  174. "OTEL_EXPORTER_OTLP_PROTOCOL",
  175. "OTEL_EXPORTER_OTLP_ENDPOINT",
  176. "OTEL_EXPORTER_OTLP_HEADERS",
  177. "OTEL_METRIC_EXPORT_INTERVAL",
  178. "CLINE_ENVIRONMENT",
  179. ]
  180. buildTimeEnvs.forEach((envVar) => {
  181. if (process.env[envVar]) {
  182. console.log(`[cli esbuild] ${envVar} env var is set`)
  183. buildEnvVars[`process.env.${envVar}`] = JSON.stringify(process.env[envVar])
  184. }
  185. })
  186. if (production) {
  187. buildEnvVars["process.env.IS_DEV"] = "false"
  188. }
  189. // Shared build options
  190. const sharedOptions: Partial<esbuild.BuildOptions> = {
  191. bundle: true,
  192. minify: production,
  193. sourcemap: !production,
  194. logLevel: "silent",
  195. define: buildEnvVars,
  196. tsconfig: path.join(__dirname, "tsconfig.json"),
  197. plugins: [copyWasmFiles, aliasResolverPlugin, vscodeStubPlugin, stubOptionalModulesPlugin, esbuildProblemMatcherPlugin],
  198. format: "esm",
  199. sourcesContent: false,
  200. platform: "node",
  201. target: "node20",
  202. // These modules need to load files from the module directory at runtime
  203. external: [
  204. "@grpc/reflection",
  205. "grpc-health-check",
  206. "better-sqlite3",
  207. "ink",
  208. "ink-spinner",
  209. "ink-picture",
  210. "react",
  211. "aws4fetch",
  212. "pino",
  213. "pino-roll",
  214. "@vscode/ripgrep", // Uses __dirname to locate the binary
  215. ],
  216. supported: { "top-level-await": true },
  217. }
  218. // CLI executable configuration
  219. const cliConfig: esbuild.BuildOptions = {
  220. ...sharedOptions,
  221. entryPoints: [path.join(__dirname, "src", "index.ts")],
  222. outfile: path.join(__dirname, "dist", "cli.mjs"),
  223. banner: {
  224. js: `#!/usr/bin/env node
  225. // Suppress all Node.js warnings (deprecation, experimental, etc.)
  226. process.emitWarning = () => {};
  227. import { createRequire as _createRequire } from 'module';
  228. import { fileURLToPath as _fileURLToPath } from 'url';
  229. import { dirname as _dirname } from 'path';
  230. const require = _createRequire(import.meta.url);
  231. const __filename = _fileURLToPath(import.meta.url);
  232. const __dirname = _dirname(__filename);`,
  233. },
  234. }
  235. // Library configuration for programmatic use
  236. const libConfig: esbuild.BuildOptions = {
  237. ...sharedOptions,
  238. entryPoints: [path.join(__dirname, "src", "exports.ts")],
  239. outfile: path.join(__dirname, "dist", "lib.mjs"),
  240. banner: {
  241. js: `// Cline Library - Programmatic API
  242. import { createRequire as _createRequire } from 'module';
  243. import { fileURLToPath as _fileURLToPath } from 'url';
  244. import { dirname as _dirname } from 'path';
  245. const require = _createRequire(import.meta.url);
  246. const __filename = _fileURLToPath(import.meta.url);
  247. const __dirname = _dirname(__filename);`,
  248. },
  249. }
  250. async function main() {
  251. if (watch) {
  252. // In watch mode, only watch the CLI (primary use case for development)
  253. const ctx = await esbuild.context(cliConfig)
  254. await ctx.watch()
  255. console.log("[cli] Watching for changes...")
  256. } else {
  257. // Build both CLI and library
  258. console.log("[cli esbuild] Building CLI executable...")
  259. const cliCtx = await esbuild.context(cliConfig)
  260. await cliCtx.rebuild()
  261. await cliCtx.dispose()
  262. console.log("[cli esbuild] Building library bundle...")
  263. const libCtx = await esbuild.context(libConfig)
  264. await libCtx.rebuild()
  265. await libCtx.dispose()
  266. // Make the CLI output executable
  267. const cliOutfile = path.join(__dirname, "dist", "cli.mjs")
  268. if (fs.existsSync(cliOutfile)) {
  269. fs.chmodSync(cliOutfile, "755")
  270. }
  271. }
  272. }
  273. main().catch((e) => {
  274. console.error(e)
  275. process.exit(1)
  276. })