From adbe391e63721918b5d147f4f845111c1a3143db Mon Sep 17 00:00:00 2001 From: Nourrisse Florian <3023852+Flo5k5@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:07:08 +0200 Subject: [PATCH] fix: replace broken bun:bundle shim with source pre-processing (#657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: replace broken bun:bundle shim with source pre-processing The `onResolve`/`onLoad` plugin shim for `bun:bundle` was silently ineffective in Bun v1.3.9+ — the `bun:` namespace is resolved by Bun's native C++ resolver before the JS plugin phase runs. This meant ALL `feature()` flags evaluated to `false` regardless of the `featureFlags` map in build.ts (including `MONITOR_TOOL: true`). Replace the shim with a source pre-processing step that: 1. Strips `import { feature } from 'bun:bundle'` from .ts/.tsx files 2. Replaces `feature('FLAG')` calls with boolean literals 3. Restores original files in a `finally` block after Bun.build() Also extend the missing-module scanner to detect `require()` and dynamic `import()` calls — not just static `import ... from` — since modules behind feature() gates become resolvable when flags are enabled. * fix: ensure source files are always restored after build - Add SIGINT/SIGTERM handlers to restore pre-processed source files on abrupt termination (Ctrl+C, kill) - Replace process.exit(1) with process.exitCode = 1 so the finally block runs on build failure --- scripts/build.ts | 126 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 27 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index b79c8f77..2982f1f8 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -8,7 +8,8 @@ * - src/ path aliases */ -import { readFileSync } from 'fs' +import { readFileSync, readdirSync, writeFileSync } from 'fs' +import { join } from 'path' import { noTelemetryPlugin } from './no-telemetry-plugin' const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) @@ -43,6 +44,64 @@ const featureFlags: Record = { COWORKER_TYPE_TELEMETRY: false, } +// ── Pre-process: replace feature() calls with boolean literals ────── +// Bun v1.3.9+ resolves `import { feature } from 'bun:bundle'` natively +// before plugins can intercept it via onResolve. The bun: namespace is +// handled by Bun's C++ resolver which runs before the JS plugin phase, +// so the previous onResolve/onLoad shim was silently ineffective — ALL +// feature() calls evaluated to false regardless of the featureFlags map. +// +// Fix: pre-process source files to strip the bun:bundle import and +// replace feature('FLAG') calls with their boolean literal. Files are +// modified in-place before Bun.build() and restored in a finally block. + +// Match feature('FLAG') calls, including multi-line: feature(\n 'FLAG',\n) +const featureCallRe = /\bfeature\(\s*['"](\w+)['"][,\s]*\)/gs +const featureImportRe = /import\s*\{[^}]*\bfeature\b[^}]*\}\s*from\s*['"]bun:bundle['"];?\s*\n?/g +const modifiedFiles = new Map() // path → original content + +function preProcessFeatureFlags(dir: string) { + for (const ent of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, ent.name) + if (ent.isDirectory()) { preProcessFeatureFlags(full); continue } + if (!/\.(ts|tsx)$/.test(ent.name)) continue + + const raw = readFileSync(full, 'utf-8') + if (!raw.includes('feature(')) continue + + let contents = raw + contents = contents.replace(featureImportRe, '') + contents = contents.replace(featureCallRe, (_match, name) => + String((featureFlags as Record)[name] ?? false), + ) + + if (contents !== raw) { + modifiedFiles.set(full, raw) + writeFileSync(full, contents) + } + } +} + +function restoreModifiedFiles() { + for (const [path, original] of modifiedFiles) { + writeFileSync(path, original) + } + modifiedFiles.clear() +} + +preProcessFeatureFlags(join(import.meta.dir, '..', 'src')) +const numModified = modifiedFiles.size + +// Restore source files on abrupt termination (Ctrl+C, kill, etc.) +for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.on(signal, () => { + restoreModifiedFiles() + process.exit(signal === 'SIGINT' ? 130 : 143) + }) +} + +try { + const result = await Bun.build({ entrypoints: ['./src/entrypoints/cli.tsx'], outdir: './dist', @@ -103,18 +162,11 @@ export async function handleBgFlag() { throw new Error("Background sessions are ], ] as const) - // Resolve `import { feature } from 'bun:bundle'` to a shim - build.onResolve({ filter: /^bun:bundle$/ }, () => ({ - path: 'bun:bundle', - namespace: 'bun-bundle-shim', - })) - build.onLoad( - { filter: /.*/, namespace: 'bun-bundle-shim' }, - () => ({ - contents: `const featureFlags = ${JSON.stringify(featureFlags)};\nexport function feature(name) { return featureFlags[name] ?? false; }`, - loader: 'js', - }), - ) + // bun:bundle feature() replacement is handled by the source + // pre-processing step above (see preProcessFeatureFlags). + // The previous onResolve/onLoad shim was ineffective in Bun + // v1.3.9+ because the bun: namespace is resolved natively + // before the JS plugin phase runs. build.onResolve( { filter: /^\.\.\/(daemon\/workerRegistry|daemon\/main|cli\/bg|cli\/handlers\/templateJobs|environment-runner\/main|self-hosted-runner\/main)\.js$/ }, @@ -274,16 +326,7 @@ export const SeverityNumber = {}; // Scan source to find imports that can't resolve function scanForMissingImports() { - function walk(dir: string) { - for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { - const full = pathMod.join(dir, ent.name) - if (ent.isDirectory()) { walk(full); continue } - if (!/\.(ts|tsx)$/.test(ent.name)) continue - const code: string = fs.readFileSync(full, 'utf-8') - // Collect all imports - for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) { - const specifier = m[4] - const namedPart = m[1] || m[3] || '' + function checkAndRegister(specifier: string, fileDir: string, namedPart: string) { const names = namedPart.split(',') .map((s: string) => s.trim().replace(/^type\s+/, '')) .filter((s: string) => s && !s.startsWith('type ')) @@ -303,8 +346,7 @@ export const SeverityNumber = {}; } // Check relative .js imports else if (specifier.endsWith('.js') && (specifier.startsWith('./') || specifier.startsWith('../'))) { - const dir2 = pathMod.dirname(full) - const resolved = pathMod.resolve(dir2, specifier) + const resolved = pathMod.resolve(fileDir, specifier) const tsVariant = resolved.replace(/\.js$/, '.ts') const tsxVariant = resolved.replace(/\.js$/, '.tsx') if (!fs.existsSync(resolved) && !fs.existsSync(tsVariant) && !fs.existsSync(tsxVariant)) { @@ -317,6 +359,30 @@ export const SeverityNumber = {}; if (!missingModuleExports.has(specifier)) missingModuleExports.set(specifier, new Set()) for (const n of names) missingModuleExports.get(specifier)!.add(n) } + } + + function walk(dir: string) { + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const full = pathMod.join(dir, ent.name) + if (ent.isDirectory()) { walk(full); continue } + if (!/\.(ts|tsx)$/.test(ent.name)) continue + const code: string = fs.readFileSync(full, 'utf-8') + const fileDir = pathMod.dirname(full) + + // Collect static imports: import { X } from '...' + for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) { + checkAndRegister(m[4], fileDir, m[1] || m[3] || '') + } + + // Collect dynamic requires: require('...') — these are used + // behind feature() gates and become live when flags are enabled. + for (const m of code.matchAll(/require\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) { + checkAndRegister(m[1], fileDir, '') + } + + // Collect dynamic imports: import('...') + for (const m of code.matchAll(/import\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) { + checkAndRegister(m[1], fileDir, '') } } } @@ -389,7 +455,13 @@ if (!result.success) { for (const log of result.logs) { console.error(log) } - process.exit(1) + process.exitCode = 1 +} else { + console.log(`✓ Built openclaude v${version} → dist/cli.mjs`) } -console.log(`✓ Built openclaude v${version} → dist/cli.mjs`) +} finally { + // Always restore source files, even if Bun.build() throws + restoreModifiedFiles() + console.log(` 🔄 feature-flags: pre-processed ${numModified} files (restored)`) +}