| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- import * as fs from "fs"
- import * as path from "path"
- import { execSync } from "child_process"
- import { ViewsContainer, Views, Menus, Configuration, Keybindings, contributesSchema } from "./types.js"
- function copyDir(srcDir: string, dstDir: string, count: number): number {
- const entries = fs.readdirSync(srcDir, { withFileTypes: true })
- for (const entry of entries) {
- const srcPath = path.join(srcDir, entry.name)
- const dstPath = path.join(dstDir, entry.name)
- if (entry.isDirectory()) {
- fs.mkdirSync(dstPath, { recursive: true })
- count = copyDir(srcPath, dstPath, count)
- } else {
- count = count + 1
- fs.copyFileSync(srcPath, dstPath)
- }
- }
- return count
- }
- function rmDir(dirPath: string, maxRetries: number = 5): void {
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
- try {
- fs.rmSync(dirPath, { recursive: true, force: true })
- return
- } catch (error) {
- const isLastAttempt = attempt === maxRetries
- const isRetryableError =
- error instanceof Error &&
- "code" in error &&
- (error.code === "ENOTEMPTY" ||
- error.code === "EBUSY" ||
- error.code === "EPERM" ||
- error.code === "EACCES")
- if (isLastAttempt) {
- // On the last attempt, try alternative cleanup methods.
- try {
- console.warn(`[rmDir] Final attempt using alternative cleanup for ${dirPath}`)
- // Try to clear readonly flags on Windows.
- if (process.platform === "win32") {
- try {
- execSync(`attrib -R "${dirPath}\\*.*" /S /D`, { stdio: "ignore" })
- } catch {
- // Ignore attrib errors.
- }
- }
- fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
- return
- } catch (finalError) {
- console.error(`[rmDir] Failed to remove ${dirPath} after ${maxRetries} attempts:`, finalError)
- throw finalError
- }
- }
- if (!isRetryableError) {
- throw error // Re-throw if it's not a retryable error.
- }
- // Wait with exponential backoff before retrying, with longer delays for Windows.
- const baseDelay = process.platform === "win32" ? 200 : 100
- const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), 2000) // Cap at 2s
- console.warn(`[rmDir] Attempt ${attempt} failed for ${dirPath}, retrying in ${delay}ms...`)
- // Synchronous sleep for simplicity in build scripts.
- const start = Date.now()
- while (Date.now() - start < delay) {
- /* Busy wait */
- }
- }
- }
- }
- type CopyPathOptions = {
- optional?: boolean
- }
- export function copyPaths(copyPaths: [string, string, CopyPathOptions?][], srcDir: string, dstDir: string) {
- copyPaths.forEach(([srcRelPath, dstRelPath, options = {}]) => {
- try {
- const stats = fs.lstatSync(path.join(srcDir, srcRelPath))
- if (stats.isDirectory()) {
- if (fs.existsSync(path.join(dstDir, dstRelPath))) {
- rmDir(path.join(dstDir, dstRelPath))
- }
- fs.mkdirSync(path.join(dstDir, dstRelPath), { recursive: true })
- const count = copyDir(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath), 0)
- console.log(`[copyPaths] Copied ${count} files from ${srcRelPath} to ${dstRelPath}`)
- } else {
- fs.copyFileSync(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath))
- console.log(`[copyPaths] Copied ${srcRelPath} to ${dstRelPath}`)
- }
- } catch (error) {
- if (options.optional) {
- console.warn(`[copyPaths] Optional file not found: ${srcRelPath}`)
- } else {
- throw error
- }
- }
- })
- }
- export function copyWasms(srcDir: string, distDir: string): void {
- const nodeModulesDir = path.join(srcDir, "node_modules")
- fs.mkdirSync(distDir, { recursive: true })
- // Tiktoken WASM file.
- fs.copyFileSync(
- path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
- path.join(distDir, "tiktoken_bg.wasm"),
- )
- console.log(`[copyWasms] Copied tiktoken WASMs to ${distDir}`)
- // Also copy Tiktoken WASMs to the workers directory.
- const workersDir = path.join(distDir, "workers")
- fs.mkdirSync(workersDir, { recursive: true })
- fs.copyFileSync(
- path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
- path.join(workersDir, "tiktoken_bg.wasm"),
- )
- console.log(`[copyWasms] Copied tiktoken WASMs to ${workersDir}`)
- // Main tree-sitter WASM file.
- fs.copyFileSync(
- path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"),
- path.join(distDir, "tree-sitter.wasm"),
- )
- console.log(`[copyWasms] Copied tree-sitter.wasm to ${distDir}`)
- // Copy language-specific WASM files.
- const languageWasmDir = path.join(nodeModulesDir, "tree-sitter-wasms", "out")
- if (!fs.existsSync(languageWasmDir)) {
- throw new Error(`Directory does not exist: ${languageWasmDir}`)
- }
- // Dynamically read all WASM files from the directory instead of using a hardcoded list.
- const wasmFiles = fs.readdirSync(languageWasmDir).filter((file) => file.endsWith(".wasm"))
- wasmFiles.forEach((filename) => {
- fs.copyFileSync(path.join(languageWasmDir, filename), path.join(distDir, filename))
- })
- console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`)
- // Copy esbuild-wasm files for custom tool transpilation (cross-platform).
- copyEsbuildWasmFiles(nodeModulesDir, distDir)
- }
- /**
- * Copy esbuild-wasm files to the dist/bin directory.
- *
- * This function copies the esbuild-wasm CLI and WASM binary, which provides
- * a cross-platform esbuild implementation that works on all platforms.
- *
- * Files copied:
- * - bin/esbuild (Node.js CLI script)
- * - esbuild.wasm (WASM binary)
- * - wasm_exec_node.js (Go WASM runtime for Node.js)
- * - wasm_exec.js (Go WASM runtime dependency)
- */
- function copyEsbuildWasmFiles(nodeModulesDir: string, distDir: string): void {
- const esbuildWasmDir = path.join(nodeModulesDir, "esbuild-wasm")
- if (!fs.existsSync(esbuildWasmDir)) {
- throw new Error(`Directory does not exist: ${esbuildWasmDir}`)
- }
- // Create bin directory in dist.
- const binDir = path.join(distDir, "bin")
- fs.mkdirSync(binDir, { recursive: true })
- // Files to copy - the esbuild CLI script expects wasm_exec_node.js and esbuild.wasm
- // to be one directory level up from the bin directory (i.e., in distDir directly).
- // wasm_exec_node.js requires wasm_exec.js, so we need to copy that too.
- const filesToCopy = [
- { src: path.join(esbuildWasmDir, "bin", "esbuild"), dest: path.join(binDir, "esbuild") },
- { src: path.join(esbuildWasmDir, "esbuild.wasm"), dest: path.join(distDir, "esbuild.wasm") },
- { src: path.join(esbuildWasmDir, "wasm_exec_node.js"), dest: path.join(distDir, "wasm_exec_node.js") },
- { src: path.join(esbuildWasmDir, "wasm_exec.js"), dest: path.join(distDir, "wasm_exec.js") },
- ]
- for (const { src, dest } of filesToCopy) {
- fs.copyFileSync(src, dest)
- // Make CLI executable.
- if (src.endsWith("esbuild")) {
- try {
- fs.chmodSync(dest, 0o755)
- } catch {
- // Ignore chmod errors on Windows.
- }
- }
- }
- console.log(`[copyWasms] Copied ${filesToCopy.length} esbuild-wasm files to ${distDir}`)
- }
- export function copyLocales(srcDir: string, distDir: string): void {
- const destDir = path.join(distDir, "i18n", "locales")
- fs.mkdirSync(destDir, { recursive: true })
- const count = copyDir(path.join(srcDir, "i18n", "locales"), destDir, 0)
- console.log(`[copyLocales] Copied ${count} locale files to ${destDir}`)
- }
- export function setupLocaleWatcher(srcDir: string, distDir: string) {
- const localesDir = path.join(srcDir, "i18n", "locales")
- if (!fs.existsSync(localesDir)) {
- console.warn(`Cannot set up watcher: Source locales directory does not exist: ${localesDir}`)
- return
- }
- console.log(`Setting up watcher for locale files in ${localesDir}`)
- let debounceTimer: NodeJS.Timeout | null = null
- const debouncedCopy = () => {
- if (debounceTimer) {
- clearTimeout(debounceTimer)
- }
- // Wait 300ms after last change before copying.
- debounceTimer = setTimeout(() => {
- console.log("Locale files changed, copying...")
- copyLocales(srcDir, distDir)
- }, 300)
- }
- try {
- fs.watch(localesDir, { recursive: true }, (_eventType, filename) => {
- if (filename && filename.endsWith(".json")) {
- console.log(`Locale file ${filename} changed, triggering copy...`)
- debouncedCopy()
- }
- })
- console.log("Watcher for locale files is set up")
- } catch (error) {
- console.error(
- `Error setting up watcher for ${localesDir}:`,
- error instanceof Error ? error.message : "Unknown error",
- )
- }
- }
- export function generatePackageJson({
- packageJson: { contributes, ...packageJson },
- overrideJson,
- substitution,
- }: {
- packageJson: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
- overrideJson: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
- substitution: [string, string]
- }) {
- const { viewsContainers, views, commands, menus, submenus, keybindings, configuration } =
- contributesSchema.parse(contributes)
- const [from, to] = substitution
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const result: Record<string, any> = {
- ...packageJson,
- ...overrideJson,
- contributes: {
- viewsContainers: transformArrayRecord<ViewsContainer>(viewsContainers, from, to, ["id"]),
- views: transformArrayRecord<Views>(views, from, to, ["id"]),
- commands: transformArray(commands, from, to, "command"),
- menus: transformArrayRecord<Menus>(menus, from, to, ["command", "submenu", "when"]),
- submenus: transformArray(submenus, from, to, "id"),
- configuration: {
- title: configuration.title,
- properties: transformRecord<Configuration["properties"]>(configuration.properties, from, to),
- },
- },
- }
- // Only add keybindings if they exist
- if (keybindings) {
- result.contributes.keybindings = transformArray<Keybindings>(keybindings, from, to, "command")
- }
- return result
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function transformArrayRecord<T>(obj: Record<string, any[]>, from: string, to: string, props: string[]): T {
- return Object.entries(obj).reduce(
- (acc, [key, ary]) => ({
- ...acc,
- [key.replaceAll(from, to)]: ary.map((item) => {
- const transformedItem = { ...item }
- for (const prop of props) {
- if (prop in item && typeof item[prop] === "string") {
- transformedItem[prop] = item[prop].replaceAll(from, to)
- }
- }
- return transformedItem
- }),
- }),
- {} as T,
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function transformArray<T>(arr: any[], from: string, to: string, idProp: string): T[] {
- return arr.map(({ [idProp]: id, ...rest }) => ({
- [idProp]: id.replaceAll(from, to),
- ...rest,
- }))
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function transformRecord<T>(obj: Record<string, any>, from: string, to: string): T {
- return Object.entries(obj).reduce(
- (acc, [key, value]) => ({
- ...acc,
- [key.replaceAll(from, to)]: value,
- }),
- {} as T,
- )
- }
|