/ build.mjs
build.mjs
1 /** 2 * Build script for Claude Code (external/non-Bun build). 3 * 4 * Uses esbuild to transpile all TS/TSX → JS while preserving directory 5 * structure. Handles: 6 * 1. bun:bundle → shim where feature() always returns false 7 * 2. bun:ffi → shim with no-op exports 8 * 3. MACRO.VERSION / MACRO.PACKAGE_URL etc. → real values 9 * 4. .js extension imports (TypeScript ESM convention) → resolved correctly 10 * 11 * Output: dist/ (mirrors src/ structure as runnable ESM JavaScript) 12 * cli.js (root entry point that loads dist/entrypoints/cli.js) 13 * 14 * Usage: node build.mjs 15 */ 16 17 import * as esbuild from 'esbuild'; 18 import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync, rmSync, readdirSync } from 'fs'; 19 import { join, dirname, relative } from 'path'; 20 import { fileURLToPath } from 'url'; 21 22 const __dirname = dirname(fileURLToPath(import.meta.url)); 23 const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8')); 24 const DIST = join(__dirname, 'dist'); 25 26 console.log(`\n Building Claude Code v${pkg.version}\n`); 27 28 // ── Step 0: Clean previous build ──────────────────────────────────────── 29 if (existsSync(DIST)) { 30 rmSync(DIST, { recursive: true }); 31 console.log(' ✓ Cleaned previous dist/'); 32 } 33 34 // ── Step 1: Discover all TS/TSX source files ──────────────────────────── 35 function walkDir(dir, exts) { 36 const results = []; 37 function walk(d) { 38 for (const entry of readdirSync(d, { withFileTypes: true })) { 39 const full = join(d, entry.name); 40 if (entry.isDirectory()) { 41 if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git' || entry.name === 'typings') continue; 42 walk(full); 43 } else if (exts.some(ext => entry.name.endsWith(ext))) { 44 results.push(full); 45 } 46 } 47 } 48 walk(dir); 49 return results; 50 } 51 52 const srcDir = join(__dirname, 'src'); 53 const vendorDir = join(__dirname, 'vendor'); 54 const srcFiles = walkDir(srcDir, ['.ts', '.tsx']); 55 const vendorFiles = existsSync(vendorDir) ? walkDir(vendorDir, ['.ts', '.tsx']) : []; 56 const allFiles = [...srcFiles, ...vendorFiles]; 57 58 console.log(` ✓ Found ${srcFiles.length} source files + ${vendorFiles.length} vendor files`); 59 60 // ── Step 2: Build with esbuild (transpile-only, no bundling) ──────────── 61 // Note: esbuild plugins only work in bundle mode, so we post-process 62 // the output to replace bun:bundle / bun:ffi imports. 63 console.log(' ⏳ Transpiling TypeScript → JavaScript...'); 64 65 try { 66 await esbuild.build({ 67 entryPoints: allFiles, 68 outdir: DIST, 69 outbase: __dirname, 70 format: 'esm', 71 platform: 'node', 72 target: 'node18', 73 jsx: 'automatic', 74 bundle: false, 75 define: { 76 'MACRO.VERSION': JSON.stringify(pkg.version), 77 'MACRO.PACKAGE_URL': JSON.stringify(pkg.homepage || 'https://github.com/anthropics/claude-code'), 78 'MACRO.NATIVE_PACKAGE_URL': JSON.stringify('https://github.com/anthropics/claude-code'), 79 'MACRO.FEEDBACK_CHANNEL': JSON.stringify('https://github.com/anthropics/claude-code/issues'), 80 }, 81 logLevel: 'warning', 82 }); 83 84 const outFiles = walkDir(DIST, ['.js', '.mjs']); 85 console.log(` ✓ Transpiled ${outFiles.length} files → dist/`); 86 } catch (err) { 87 console.error(' ✗ esbuild transpilation failed:', err.message); 88 process.exit(1); 89 } 90 91 // ── Step 3: Post-process — replace bun:bundle and bun:ffi imports ─────── 92 console.log(' ⏳ Patching bun:bundle / bun:ffi imports...'); 93 94 const BUN_BUNDLE_SHIM = `const feature = (name) => false;`; 95 const BUN_FFI_SHIM = `const dlopen = () => { throw new Error("bun:ffi not available"); }; 96 const ptr = () => 0; 97 const toBuffer = () => Buffer.alloc(0); 98 const toArrayBuffer = () => new ArrayBuffer(0); 99 const CString = () => ""; 100 const suffix = process.platform === "darwin" ? "dylib" : process.platform === "win32" ? "dll" : "so";`; 101 102 const distJsFiles = walkDir(DIST, ['.js']); 103 let patchedCount = 0; 104 for (const f of distJsFiles) { 105 let code = readFileSync(f, 'utf-8'); 106 let changed = false; 107 108 // Replace: import { feature } from "bun:bundle"; 109 // Also handles: import { feature } from 'bun:bundle'; 110 if (code.includes('bun:bundle')) { 111 code = code.replace( 112 /import\s*\{[^}]*\}\s*from\s*["']bun:bundle["']\s*;?/g, 113 BUN_BUNDLE_SHIM 114 ); 115 changed = true; 116 } 117 118 // Replace: import { ... } from "bun:ffi"; 119 if (code.includes('bun:ffi')) { 120 code = code.replace( 121 /import\s*\{[^}]*\}\s*from\s*["']bun:ffi["']\s*;?/g, 122 BUN_FFI_SHIM 123 ); 124 changed = true; 125 } 126 127 if (changed) { 128 writeFileSync(f, code); 129 patchedCount++; 130 } 131 } 132 console.log(` ✓ Patched ${patchedCount} files with bun: shims`); 133 134 // ── Step 3b: Rewrite bare "src/" imports to relative paths ───────────── 135 // The source uses tsconfig paths like `import ... from 'src/utils/foo.js'`. 136 // TypeScript resolves these via baseUrl, but Node.js can't. We convert them 137 // to relative paths based on each file's position within dist/src/. 138 console.log(' ⏳ Rewriting bare src/ imports to relative paths...'); 139 140 const distSrcDir = join(DIST, 'src'); 141 let srcImportCount = 0; 142 for (const f of distJsFiles) { 143 let code = readFileSync(f, 'utf-8'); 144 if (!code.includes('"src/') && !code.includes("'src/")) continue; 145 146 const fileDir = dirname(f); 147 // Only rewrite files inside dist/src/ 148 if (!f.startsWith(distSrcDir)) continue; 149 150 const fileDirRelToSrc = relative(distSrcDir, fileDir); // e.g. "utils/model" 151 152 code = code.replace( 153 /(from\s+["'])src\/([^"']+)(["'])/g, 154 (match, prefix, importPath, suffix) => { 155 // importPath is e.g. "utils/debug.js" or "components/Markdown.js" 156 // We need the relative path from the current file's dir to dist/src/<importPath> 157 const targetFromSrc = importPath; // already relative to src/ 158 let rel = relative(fileDirRelToSrc, targetFromSrc); 159 // Ensure it starts with ./ or ../ 160 if (!rel.startsWith('.')) rel = './' + rel; 161 return prefix + rel + suffix; 162 } 163 ); 164 165 // Also handle dynamic import("src/...") 166 code = code.replace( 167 /(import\s*\(\s*["'])src\/([^"']+)(["']\s*\))/g, 168 (match, prefix, importPath, suffix) => { 169 const targetFromSrc = importPath; 170 let rel = relative(fileDirRelToSrc, targetFromSrc); 171 if (!rel.startsWith('.')) rel = './' + rel; 172 return prefix + rel + suffix; 173 } 174 ); 175 176 writeFileSync(f, code); 177 srcImportCount++; 178 } 179 console.log(` ✓ Rewrote bare src/ imports in ${srcImportCount} files`); 180 181 // ── Step 3c: Rewrite .jsx → .js in imports ────────────────────────────── 182 // esbuild outputs .tsx → .js but some imports reference .jsx explicitly 183 let jsxFixCount = 0; 184 for (const f of distJsFiles) { 185 let code = readFileSync(f, 'utf-8'); 186 if (!code.includes('.jsx')) continue; 187 const updated = code.replace(/(from\s+["'][^"']*?)\.jsx(["'])/g, '$1.js$2') 188 .replace(/(import\s*\(\s*["'][^"']*?)\.jsx(["']\s*\))/g, '$1.js$2'); 189 if (updated !== code) { 190 writeFileSync(f, updated); 191 jsxFixCount++; 192 } 193 } 194 if (jsxFixCount > 0) console.log(` ✓ Fixed .jsx → .js in ${jsxFixCount} files`); 195 196 // ── Step 3c2: Strip .d.ts imports (type-only, not valid at runtime) ───── 197 let dtsStripCount = 0; 198 for (const f of distJsFiles) { 199 let code = readFileSync(f, 'utf-8'); 200 if (!code.includes('.d.ts')) continue; 201 const updated = code.replace(/^import\s+.*["'][^"']*\.d\.ts["']\s*;?\s*$/gm, '// [stripped .d.ts import]'); 202 if (updated !== code) { 203 writeFileSync(f, updated); 204 dtsStripCount++; 205 } 206 } 207 if (dtsStripCount > 0) console.log(` ✓ Stripped .d.ts imports in ${dtsStripCount} files`); 208 209 // ── Step 3d: Generate empty stubs for missing internal modules ────────── 210 // Many imports reference Anthropic-internal modules (commands, types, tools) 211 // that were stripped from the public source. We create empty ES module stubs 212 // so Node.js can resolve them at runtime (they export nothing). 213 console.log(' ⏳ Generating stubs for missing internal modules...'); 214 215 let stubCount = 0; 216 for (const f of distJsFiles) { 217 const code = readFileSync(f, 'utf-8'); 218 const re = /from\s+["'](\.[^"']+)["']/g; 219 let m; 220 while ((m = re.exec(code)) !== null) { 221 const importPath = m[1]; 222 const resolved = join(dirname(f), importPath); 223 if (!existsSync(resolved)) { 224 mkdirSync(dirname(resolved), { recursive: true }); 225 if (resolved.endsWith('.js')) { 226 writeFileSync(resolved, '// Auto-generated empty stub for missing internal module\nexport default null;\n'); 227 stubCount++; 228 } else if (resolved.endsWith('.md')) { 229 writeFileSync(resolved, ''); 230 stubCount++; 231 } 232 } 233 } 234 } 235 console.log(` ✓ Generated ${stubCount} empty module stubs`); 236 237 // ── Step 3e: Create runtime shims for @ant/* and internal packages ────── 238 // tsconfig paths only work for TypeScript. At runtime, Node.js needs 239 // actual packages in node_modules for bare specifier resolution. 240 console.log(' ⏳ Creating runtime shims for internal packages...'); 241 242 const internalPackages = [ 243 '@ant/claude-for-chrome-mcp', 244 '@ant/computer-use-input', 245 '@ant/computer-use-mcp', 246 '@ant/computer-use-mcp/types', 247 '@ant/computer-use-mcp/sentinelApps', 248 '@ant/computer-use-swift', 249 '@anthropic-ai/claude-agent-sdk', 250 '@anthropic-ai/sandbox-runtime', 251 'image-processor-napi', 252 'audio-capture-napi', 253 'url-handler-napi', 254 'color-diff-napi', 255 ]; 256 257 let shimCount = 0; 258 for (const pkg of internalPackages) { 259 // Handle subpath exports like @ant/computer-use-mcp/types 260 const parts = pkg.startsWith('@') ? pkg.split('/') : [pkg]; 261 let pkgName, subpath; 262 if (parts[0].startsWith('@')) { 263 pkgName = parts[0] + '/' + parts[1]; 264 subpath = parts.slice(2).join('/'); 265 } else { 266 pkgName = parts[0]; 267 subpath = parts.slice(1).join('/'); 268 } 269 270 const pkgDir = join(__dirname, 'node_modules', pkgName); 271 if (!existsSync(pkgDir)) { 272 mkdirSync(pkgDir, { recursive: true }); 273 writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ 274 name: pkgName, 275 version: '0.0.0-stub', 276 type: 'module', 277 main: 'index.js', 278 exports: { '.': './index.js', './*': './*.js' }, 279 }, null, 2)); 280 writeFileSync(join(pkgDir, 'index.js'), 281 '// Runtime stub — internal package not available in external builds\nexport default null;\n'); 282 shimCount++; 283 } 284 285 // Create subpath file if needed 286 if (subpath) { 287 const subFile = join(pkgDir, subpath + '.js'); 288 if (!existsSync(subFile)) { 289 mkdirSync(dirname(subFile), { recursive: true }); 290 writeFileSync(subFile, '// Runtime stub — subpath export\nexport default null;\n'); 291 } 292 } 293 } 294 console.log(` ✓ Created ${shimCount} runtime package shims`); 295 296 // ── Step 4: Copy non-TS assets (JSON, etc.) ───────────────────────────── 297 const assetExts = ['.json', '.md', '.txt', '.yaml', '.yml', '.html', '.css', '.svg', '.png']; 298 const assetFiles = walkDir(srcDir, assetExts); 299 for (const f of assetFiles) { 300 const rel = relative(__dirname, f); 301 const dest = join(DIST, rel); 302 mkdirSync(dirname(dest), { recursive: true }); 303 cpSync(f, dest); 304 } 305 if (assetFiles.length > 0) { 306 console.log(` ✓ Copied ${assetFiles.length} asset files`); 307 } 308 309 // ── Step 5: Create root cli.js entry point ────────────────────────────── 310 writeFileSync(join(__dirname, 'cli.js'), `#!/usr/bin/env node 311 // Auto-generated by build.mjs — Claude Code v${pkg.version} 312 // Entry point that loads the transpiled CLI. 313 import './dist/src/entrypoints/cli.js'; 314 `); 315 console.log(' ✓ Created cli.js entry point'); 316 317 console.log(`\n Build complete! 🎉\n`); 318 console.log(` Run with: node cli.js\n`);