5.1 KiB
Prompt 03: Create esbuild-Based Build System
Context
You are working in /workspaces/claude-code. This is the Claude Code CLI — a TypeScript/TSX terminal app using React + Ink. It was originally built using Bun's bundler with feature flags, but that build config wasn't included in the leak.
We need to create a build system that:
- Bundles the entire
src/tree into a runnable output - Aliases
bun:bundle→ our shim atsrc/shims/bun-bundle.ts - Injects the
MACROglobal (viasrc/shims/macro.tspreload) - Handles TSX/JSX (React)
- Handles ESM
.jsextension imports (the code usesimport from './foo.js'which maps to./foo.ts) - Produces output that can run under Bun (primary) or Node.js 20+ (secondary)
Existing Files
src/shims/bun-bundle.ts— runtimefeature()function (created in Prompt 02)src/shims/macro.ts— globalMACROobject (created in Prompt 02)src/shims/preload.ts— preload bootstrap (created in Prompt 02)src/entrypoints/cli.tsx— main entrypointtsconfig.json— has"jsx": "react-jsx","module": "ESNext","moduleResolution": "bundler"
Task
Part A: Install esbuild
bun add -d esbuild
Part B: Create build script
Create scripts/build-bundle.ts (a Bun-runnable build script):
// scripts/build-bundle.ts
// Usage: bun scripts/build-bundle.ts [--watch] [--minify]
import * as esbuild from 'esbuild'
import { resolve } from 'path'
const ROOT = resolve(import.meta.dir, '..')
const watch = process.argv.includes('--watch')
const minify = process.argv.includes('--minify')
const buildOptions: esbuild.BuildOptions = {
entryPoints: [resolve(ROOT, 'src/entrypoints/cli.tsx')],
bundle: true,
platform: 'node',
target: 'node20',
format: 'esm',
outdir: resolve(ROOT, 'dist'),
outExtension: { '.js': '.mjs' },
// Inject the MACRO global before all other code
inject: [resolve(ROOT, 'src/shims/macro.ts')],
// Alias bun:bundle to our runtime shim
alias: {
'bun:bundle': resolve(ROOT, 'src/shims/bun-bundle.ts'),
},
// Don't bundle node built-ins or native packages
external: [
// Node built-ins
'fs', 'path', 'os', 'crypto', 'child_process', 'http', 'https',
'net', 'tls', 'url', 'util', 'stream', 'events', 'buffer',
'querystring', 'readline', 'zlib', 'assert', 'tty', 'worker_threads',
'perf_hooks', 'async_hooks', 'dns', 'dgram', 'cluster',
'node:*',
// Native addons that can't be bundled
'fsevents',
],
jsx: 'automatic',
// Source maps for debugging
sourcemap: true,
minify,
// Banner: shebang for CLI + preload the MACRO global
banner: {
js: '#!/usr/bin/env node\n',
},
// Handle the .js → .ts resolution that the codebase uses
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
logLevel: 'info',
}
async function main() {
if (watch) {
const ctx = await esbuild.context(buildOptions)
await ctx.watch()
console.log('Watching for changes...')
} else {
const result = await esbuild.build(buildOptions)
if (result.errors.length > 0) {
console.error('Build failed')
process.exit(1)
}
console.log('Build complete → dist/')
}
}
main().catch(err => {
console.error(err)
process.exit(1)
})
Important: This is a starting point. You will likely need to iterate on the externals list and alias configuration. The codebase has ~1,900 files — some imports may need special handling. When you run the build:
- Run it:
bun scripts/build-bundle.ts - Look at the errors
- Fix them (add externals, fix aliases, etc.)
- Repeat until it bundles successfully
Common issues you'll hit:
- npm packages that use native modules → add to
external - Dynamic
require()calls behindprocess.env.USER_TYPE === 'ant'→ these are Anthropic-internal, wrap them or stub them - Circular dependencies → esbuild handles these but may warn
- Re-exports from barrel files → should work but watch for issues
Part C: Add npm scripts
Add these to package.json "scripts":
{
"build": "bun scripts/build-bundle.ts",
"build:watch": "bun scripts/build-bundle.ts --watch",
"build:prod": "bun scripts/build-bundle.ts --minify"
}
Part D: Create dist output directory
Add dist/ to .gitignore (create one if it doesn't exist).
Part E: Iterate on build errors
Run the build and fix whatever comes up. The goal is a clean bun scripts/build-bundle.ts that produces dist/cli.mjs.
Strategy for unresolvable modules: If modules reference Anthropic-internal packages or Bun-specific APIs (like Bun.hash, Bun.file), create minimal stubs in src/shims/ that provide compatible fallbacks.
Part F: Test the output
After a successful build:
node dist/cli.mjs --version
# or
bun dist/cli.mjs --version
This should print the version. It will likely crash after that because no API key is configured — that's fine for now.
Verification
bun scripts/build-bundle.tscompletes without errorsdist/cli.mjsexistsbun dist/cli.mjs --versionornode dist/cli.mjs --versionprints a version stringpackage.jsonhasbuild,build:watch,build:prodscripts