esbuild.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import * as fs from "fs"
  2. import * as path from "path"
  3. import { execSync } from "child_process"
  4. import { ViewsContainer, Views, Menus, Configuration, Keybindings, contributesSchema } from "./types.js"
  5. function copyDir(srcDir: string, dstDir: string, count: number): number {
  6. const entries = fs.readdirSync(srcDir, { withFileTypes: true })
  7. for (const entry of entries) {
  8. const srcPath = path.join(srcDir, entry.name)
  9. const dstPath = path.join(dstDir, entry.name)
  10. if (entry.isDirectory()) {
  11. fs.mkdirSync(dstPath, { recursive: true })
  12. count = copyDir(srcPath, dstPath, count)
  13. } else {
  14. count = count + 1
  15. fs.copyFileSync(srcPath, dstPath)
  16. }
  17. }
  18. return count
  19. }
  20. function rmDir(dirPath: string, maxRetries: number = 5): void {
  21. for (let attempt = 1; attempt <= maxRetries; attempt++) {
  22. try {
  23. fs.rmSync(dirPath, { recursive: true, force: true })
  24. return
  25. } catch (error) {
  26. const isLastAttempt = attempt === maxRetries
  27. const isRetryableError =
  28. error instanceof Error &&
  29. "code" in error &&
  30. (error.code === "ENOTEMPTY" ||
  31. error.code === "EBUSY" ||
  32. error.code === "EPERM" ||
  33. error.code === "EACCES")
  34. if (isLastAttempt) {
  35. // On the last attempt, try alternative cleanup methods.
  36. try {
  37. console.warn(`[rmDir] Final attempt using alternative cleanup for ${dirPath}`)
  38. // Try to clear readonly flags on Windows.
  39. if (process.platform === "win32") {
  40. try {
  41. execSync(`attrib -R "${dirPath}\\*.*" /S /D`, { stdio: "ignore" })
  42. } catch {
  43. // Ignore attrib errors.
  44. }
  45. }
  46. fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
  47. return
  48. } catch (finalError) {
  49. console.error(`[rmDir] Failed to remove ${dirPath} after ${maxRetries} attempts:`, finalError)
  50. throw finalError
  51. }
  52. }
  53. if (!isRetryableError) {
  54. throw error // Re-throw if it's not a retryable error.
  55. }
  56. // Wait with exponential backoff before retrying, with longer delays for Windows.
  57. const baseDelay = process.platform === "win32" ? 200 : 100
  58. const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), 2000) // Cap at 2s
  59. console.warn(`[rmDir] Attempt ${attempt} failed for ${dirPath}, retrying in ${delay}ms...`)
  60. // Synchronous sleep for simplicity in build scripts.
  61. const start = Date.now()
  62. while (Date.now() - start < delay) {
  63. /* Busy wait */
  64. }
  65. }
  66. }
  67. }
  68. type CopyPathOptions = {
  69. optional?: boolean
  70. }
  71. export function copyPaths(copyPaths: [string, string, CopyPathOptions?][], srcDir: string, dstDir: string) {
  72. copyPaths.forEach(([srcRelPath, dstRelPath, options = {}]) => {
  73. try {
  74. const stats = fs.lstatSync(path.join(srcDir, srcRelPath))
  75. if (stats.isDirectory()) {
  76. if (fs.existsSync(path.join(dstDir, dstRelPath))) {
  77. rmDir(path.join(dstDir, dstRelPath))
  78. }
  79. fs.mkdirSync(path.join(dstDir, dstRelPath), { recursive: true })
  80. const count = copyDir(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath), 0)
  81. console.log(`[copyPaths] Copied ${count} files from ${srcRelPath} to ${dstRelPath}`)
  82. } else {
  83. fs.copyFileSync(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath))
  84. console.log(`[copyPaths] Copied ${srcRelPath} to ${dstRelPath}`)
  85. }
  86. } catch (error) {
  87. if (options.optional) {
  88. console.warn(`[copyPaths] Optional file not found: ${srcRelPath}`)
  89. } else {
  90. throw error
  91. }
  92. }
  93. })
  94. }
  95. export function copyWasms(srcDir: string, distDir: string): void {
  96. const nodeModulesDir = path.join(srcDir, "node_modules")
  97. fs.mkdirSync(distDir, { recursive: true })
  98. // Tiktoken WASM file.
  99. fs.copyFileSync(
  100. path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
  101. path.join(distDir, "tiktoken_bg.wasm"),
  102. )
  103. console.log(`[copyWasms] Copied tiktoken WASMs to ${distDir}`)
  104. // Also copy Tiktoken WASMs to the workers directory.
  105. const workersDir = path.join(distDir, "workers")
  106. fs.mkdirSync(workersDir, { recursive: true })
  107. fs.copyFileSync(
  108. path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
  109. path.join(workersDir, "tiktoken_bg.wasm"),
  110. )
  111. console.log(`[copyWasms] Copied tiktoken WASMs to ${workersDir}`)
  112. // Main tree-sitter WASM file.
  113. fs.copyFileSync(
  114. path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"),
  115. path.join(distDir, "tree-sitter.wasm"),
  116. )
  117. console.log(`[copyWasms] Copied tree-sitter.wasm to ${distDir}`)
  118. // Copy language-specific WASM files.
  119. const languageWasmDir = path.join(nodeModulesDir, "tree-sitter-wasms", "out")
  120. if (!fs.existsSync(languageWasmDir)) {
  121. throw new Error(`Directory does not exist: ${languageWasmDir}`)
  122. }
  123. // Dynamically read all WASM files from the directory instead of using a hardcoded list.
  124. const wasmFiles = fs.readdirSync(languageWasmDir).filter((file) => file.endsWith(".wasm"))
  125. wasmFiles.forEach((filename) => {
  126. fs.copyFileSync(path.join(languageWasmDir, filename), path.join(distDir, filename))
  127. })
  128. console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`)
  129. // Copy esbuild-wasm files for custom tool transpilation (cross-platform).
  130. copyEsbuildWasmFiles(nodeModulesDir, distDir)
  131. }
  132. /**
  133. * Copy esbuild-wasm files to the dist/bin directory.
  134. *
  135. * This function copies the esbuild-wasm CLI and WASM binary, which provides
  136. * a cross-platform esbuild implementation that works on all platforms.
  137. *
  138. * Files copied:
  139. * - bin/esbuild (Node.js CLI script)
  140. * - esbuild.wasm (WASM binary)
  141. * - wasm_exec_node.js (Go WASM runtime for Node.js)
  142. * - wasm_exec.js (Go WASM runtime dependency)
  143. */
  144. function copyEsbuildWasmFiles(nodeModulesDir: string, distDir: string): void {
  145. const esbuildWasmDir = path.join(nodeModulesDir, "esbuild-wasm")
  146. if (!fs.existsSync(esbuildWasmDir)) {
  147. throw new Error(`Directory does not exist: ${esbuildWasmDir}`)
  148. }
  149. // Create bin directory in dist.
  150. const binDir = path.join(distDir, "bin")
  151. fs.mkdirSync(binDir, { recursive: true })
  152. // Files to copy - the esbuild CLI script expects wasm_exec_node.js and esbuild.wasm
  153. // to be one directory level up from the bin directory (i.e., in distDir directly).
  154. // wasm_exec_node.js requires wasm_exec.js, so we need to copy that too.
  155. const filesToCopy = [
  156. { src: path.join(esbuildWasmDir, "bin", "esbuild"), dest: path.join(binDir, "esbuild") },
  157. { src: path.join(esbuildWasmDir, "esbuild.wasm"), dest: path.join(distDir, "esbuild.wasm") },
  158. { src: path.join(esbuildWasmDir, "wasm_exec_node.js"), dest: path.join(distDir, "wasm_exec_node.js") },
  159. { src: path.join(esbuildWasmDir, "wasm_exec.js"), dest: path.join(distDir, "wasm_exec.js") },
  160. ]
  161. for (const { src, dest } of filesToCopy) {
  162. fs.copyFileSync(src, dest)
  163. // Make CLI executable.
  164. if (src.endsWith("esbuild")) {
  165. try {
  166. fs.chmodSync(dest, 0o755)
  167. } catch {
  168. // Ignore chmod errors on Windows.
  169. }
  170. }
  171. }
  172. console.log(`[copyWasms] Copied ${filesToCopy.length} esbuild-wasm files to ${distDir}`)
  173. }
  174. export function copyLocales(srcDir: string, distDir: string): void {
  175. const destDir = path.join(distDir, "i18n", "locales")
  176. fs.mkdirSync(destDir, { recursive: true })
  177. const count = copyDir(path.join(srcDir, "i18n", "locales"), destDir, 0)
  178. console.log(`[copyLocales] Copied ${count} locale files to ${destDir}`)
  179. }
  180. export function setupLocaleWatcher(srcDir: string, distDir: string) {
  181. const localesDir = path.join(srcDir, "i18n", "locales")
  182. if (!fs.existsSync(localesDir)) {
  183. console.warn(`Cannot set up watcher: Source locales directory does not exist: ${localesDir}`)
  184. return
  185. }
  186. console.log(`Setting up watcher for locale files in ${localesDir}`)
  187. let debounceTimer: NodeJS.Timeout | null = null
  188. const debouncedCopy = () => {
  189. if (debounceTimer) {
  190. clearTimeout(debounceTimer)
  191. }
  192. // Wait 300ms after last change before copying.
  193. debounceTimer = setTimeout(() => {
  194. console.log("Locale files changed, copying...")
  195. copyLocales(srcDir, distDir)
  196. }, 300)
  197. }
  198. try {
  199. fs.watch(localesDir, { recursive: true }, (_eventType, filename) => {
  200. if (filename && filename.endsWith(".json")) {
  201. console.log(`Locale file ${filename} changed, triggering copy...`)
  202. debouncedCopy()
  203. }
  204. })
  205. console.log("Watcher for locale files is set up")
  206. } catch (error) {
  207. console.error(
  208. `Error setting up watcher for ${localesDir}:`,
  209. error instanceof Error ? error.message : "Unknown error",
  210. )
  211. }
  212. }
  213. export function generatePackageJson({
  214. packageJson: { contributes, ...packageJson },
  215. overrideJson,
  216. substitution,
  217. }: {
  218. packageJson: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
  219. overrideJson: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
  220. substitution: [string, string]
  221. }) {
  222. const { viewsContainers, views, commands, menus, submenus, keybindings, configuration } =
  223. contributesSchema.parse(contributes)
  224. const [from, to] = substitution
  225. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  226. const result: Record<string, any> = {
  227. ...packageJson,
  228. ...overrideJson,
  229. contributes: {
  230. viewsContainers: transformArrayRecord<ViewsContainer>(viewsContainers, from, to, ["id"]),
  231. views: transformArrayRecord<Views>(views, from, to, ["id"]),
  232. commands: transformArray(commands, from, to, "command"),
  233. menus: transformArrayRecord<Menus>(menus, from, to, ["command", "submenu", "when"]),
  234. submenus: transformArray(submenus, from, to, "id"),
  235. configuration: {
  236. title: configuration.title,
  237. properties: transformRecord<Configuration["properties"]>(configuration.properties, from, to),
  238. },
  239. },
  240. }
  241. // Only add keybindings if they exist
  242. if (keybindings) {
  243. result.contributes.keybindings = transformArray<Keybindings>(keybindings, from, to, "command")
  244. }
  245. return result
  246. }
  247. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  248. function transformArrayRecord<T>(obj: Record<string, any[]>, from: string, to: string, props: string[]): T {
  249. return Object.entries(obj).reduce(
  250. (acc, [key, ary]) => ({
  251. ...acc,
  252. [key.replaceAll(from, to)]: ary.map((item) => {
  253. const transformedItem = { ...item }
  254. for (const prop of props) {
  255. if (prop in item && typeof item[prop] === "string") {
  256. transformedItem[prop] = item[prop].replaceAll(from, to)
  257. }
  258. }
  259. return transformedItem
  260. }),
  261. }),
  262. {} as T,
  263. )
  264. }
  265. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  266. function transformArray<T>(arr: any[], from: string, to: string, idProp: string): T[] {
  267. return arr.map(({ [idProp]: id, ...rest }) => ({
  268. [idProp]: id.replaceAll(from, to),
  269. ...rest,
  270. }))
  271. }
  272. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  273. function transformRecord<T>(obj: Record<string, any>, from: string, to: string): T {
  274. return Object.entries(obj).reduce(
  275. (acc, [key, value]) => ({
  276. ...acc,
  277. [key.replaceAll(from, to)]: value,
  278. }),
  279. {} as T,
  280. )
  281. }