fix: replace broken bun:bundle shim with source pre-processing (#657)
* 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
This commit is contained in:
committed by
GitHub
parent
03e0b06e07
commit
adbe391e63
126
scripts/build.ts
126
scripts/build.ts
@@ -8,7 +8,8 @@
|
|||||||
* - src/ path aliases
|
* - src/ path aliases
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync, readdirSync, writeFileSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
import { noTelemetryPlugin } from './no-telemetry-plugin'
|
import { noTelemetryPlugin } from './no-telemetry-plugin'
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
|
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
|
||||||
@@ -43,6 +44,64 @@ const featureFlags: Record<string, boolean> = {
|
|||||||
COWORKER_TYPE_TELEMETRY: false,
|
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<string, string>() // 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<string, boolean>)[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({
|
const result = await Bun.build({
|
||||||
entrypoints: ['./src/entrypoints/cli.tsx'],
|
entrypoints: ['./src/entrypoints/cli.tsx'],
|
||||||
outdir: './dist',
|
outdir: './dist',
|
||||||
@@ -103,18 +162,11 @@ export async function handleBgFlag() { throw new Error("Background sessions are
|
|||||||
],
|
],
|
||||||
] as const)
|
] as const)
|
||||||
|
|
||||||
// Resolve `import { feature } from 'bun:bundle'` to a shim
|
// bun:bundle feature() replacement is handled by the source
|
||||||
build.onResolve({ filter: /^bun:bundle$/ }, () => ({
|
// pre-processing step above (see preProcessFeatureFlags).
|
||||||
path: 'bun:bundle',
|
// The previous onResolve/onLoad shim was ineffective in Bun
|
||||||
namespace: 'bun-bundle-shim',
|
// v1.3.9+ because the bun: namespace is resolved natively
|
||||||
}))
|
// before the JS plugin phase runs.
|
||||||
build.onLoad(
|
|
||||||
{ filter: /.*/, namespace: 'bun-bundle-shim' },
|
|
||||||
() => ({
|
|
||||||
contents: `const featureFlags = ${JSON.stringify(featureFlags)};\nexport function feature(name) { return featureFlags[name] ?? false; }`,
|
|
||||||
loader: 'js',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
build.onResolve(
|
build.onResolve(
|
||||||
{ filter: /^\.\.\/(daemon\/workerRegistry|daemon\/main|cli\/bg|cli\/handlers\/templateJobs|environment-runner\/main|self-hosted-runner\/main)\.js$/ },
|
{ 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
|
// Scan source to find imports that can't resolve
|
||||||
function scanForMissingImports() {
|
function scanForMissingImports() {
|
||||||
function walk(dir: string) {
|
function checkAndRegister(specifier: string, fileDir: string, namedPart: 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] || ''
|
|
||||||
const names = namedPart.split(',')
|
const names = namedPart.split(',')
|
||||||
.map((s: string) => s.trim().replace(/^type\s+/, ''))
|
.map((s: string) => s.trim().replace(/^type\s+/, ''))
|
||||||
.filter((s: string) => s && !s.startsWith('type '))
|
.filter((s: string) => s && !s.startsWith('type '))
|
||||||
@@ -303,8 +346,7 @@ export const SeverityNumber = {};
|
|||||||
}
|
}
|
||||||
// Check relative .js imports
|
// Check relative .js imports
|
||||||
else if (specifier.endsWith('.js') && (specifier.startsWith('./') || specifier.startsWith('../'))) {
|
else if (specifier.endsWith('.js') && (specifier.startsWith('./') || specifier.startsWith('../'))) {
|
||||||
const dir2 = pathMod.dirname(full)
|
const resolved = pathMod.resolve(fileDir, specifier)
|
||||||
const resolved = pathMod.resolve(dir2, specifier)
|
|
||||||
const tsVariant = resolved.replace(/\.js$/, '.ts')
|
const tsVariant = resolved.replace(/\.js$/, '.ts')
|
||||||
const tsxVariant = resolved.replace(/\.js$/, '.tsx')
|
const tsxVariant = resolved.replace(/\.js$/, '.tsx')
|
||||||
if (!fs.existsSync(resolved) && !fs.existsSync(tsVariant) && !fs.existsSync(tsxVariant)) {
|
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())
|
if (!missingModuleExports.has(specifier)) missingModuleExports.set(specifier, new Set())
|
||||||
for (const n of names) missingModuleExports.get(specifier)!.add(n)
|
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) {
|
for (const log of result.logs) {
|
||||||
console.error(log)
|
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)`)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user