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