/ 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`);