88 * - src/ path aliases
99 */
1010
11- import { readFileSync } from 'fs'
11+ import { readFileSync , readdirSync , writeFileSync } from 'fs'
12+ import { join } from 'path'
1213import { noTelemetryPlugin } from './no-telemetry-plugin'
1314
1415const pkg = JSON . parse ( readFileSync ( './package.json' , 'utf-8' ) )
@@ -43,6 +44,56 @@ const featureFlags: Record<string, boolean> = {
4344 COWORKER_TYPE_TELEMETRY : false ,
4445}
4546
47+ // ── Pre-process: replace feature() calls with boolean literals ──────
48+ // Bun v1.3.9+ resolves `import { feature } from 'bun:bundle'` natively
49+ // before plugins can intercept it via onResolve. The bun: namespace is
50+ // handled by Bun's C++ resolver which runs before the JS plugin phase,
51+ // so the previous onResolve/onLoad shim was silently ineffective — ALL
52+ // feature() calls evaluated to false regardless of the featureFlags map.
53+ //
54+ // Fix: pre-process source files to strip the bun:bundle import and
55+ // replace feature('FLAG') calls with their boolean literal. Files are
56+ // modified in-place before Bun.build() and restored in a finally block.
57+
58+ // Match feature('FLAG') calls, including multi-line: feature(\n 'FLAG',\n)
59+ const featureCallRe = / \b f e a t u r e \( \s * [ ' " ] ( \w + ) [ ' " ] [ , \s ] * \) / gs
60+ const featureImportRe = / i m p o r t \s * \{ [ ^ } ] * \b f e a t u r e \b [ ^ } ] * \} \s * f r o m \s * [ ' " ] b u n : b u n d l e [ ' " ] ; ? \s * \n ? / g
61+ const modifiedFiles = new Map < string , string > ( ) // path → original content
62+
63+ function preProcessFeatureFlags ( dir : string ) {
64+ for ( const ent of readdirSync ( dir , { withFileTypes : true } ) ) {
65+ const full = join ( dir , ent . name )
66+ if ( ent . isDirectory ( ) ) { preProcessFeatureFlags ( full ) ; continue }
67+ if ( ! / \. ( t s | t s x ) $ / . test ( ent . name ) ) continue
68+
69+ const raw = readFileSync ( full , 'utf-8' )
70+ if ( ! raw . includes ( 'feature(' ) ) continue
71+
72+ let contents = raw
73+ contents = contents . replace ( featureImportRe , '' )
74+ contents = contents . replace ( featureCallRe , ( _match , name ) =>
75+ String ( ( featureFlags as Record < string , boolean > ) [ name ] ?? false ) ,
76+ )
77+
78+ if ( contents !== raw ) {
79+ modifiedFiles . set ( full , raw )
80+ writeFileSync ( full , contents )
81+ }
82+ }
83+ }
84+
85+ function restoreModifiedFiles ( ) {
86+ for ( const [ path , original ] of modifiedFiles ) {
87+ writeFileSync ( path , original )
88+ }
89+ modifiedFiles . clear ( )
90+ }
91+
92+ preProcessFeatureFlags ( join ( import . meta. dir , '..' , 'src' ) )
93+ const numModified = modifiedFiles . size
94+
95+ try {
96+
4697const result = await Bun . build ( {
4798 entrypoints : [ './src/entrypoints/cli.tsx' ] ,
4899 outdir : './dist' ,
@@ -103,18 +154,11 @@ export async function handleBgFlag() { throw new Error("Background sessions are
103154 ] ,
104155 ] as const )
105156
106- // Resolve `import { feature } from 'bun:bundle'` to a shim
107- build . onResolve ( { filter : / ^ b u n : b u n d l e $ / } , ( ) => ( {
108- path : 'bun:bundle' ,
109- namespace : 'bun-bundle-shim' ,
110- } ) )
111- build . onLoad (
112- { filter : / .* / , namespace : 'bun-bundle-shim' } ,
113- ( ) => ( {
114- contents : `const featureFlags = ${ JSON . stringify ( featureFlags ) } ;\nexport function feature(name) { return featureFlags[name] ?? false; }` ,
115- loader : 'js' ,
116- } ) ,
117- )
157+ // bun:bundle feature() replacement is handled by the source
158+ // pre-processing step above (see preProcessFeatureFlags).
159+ // The previous onResolve/onLoad shim was ineffective in Bun
160+ // v1.3.9+ because the bun: namespace is resolved natively
161+ // before the JS plugin phase runs.
118162
119163 build . onResolve (
120164 { filter : / ^ \. \. \/ ( d a e m o n \/ w o r k e r R e g i s t r y | d a e m o n \/ m a i n | c l i \/ b g | c l i \/ h a n d l e r s \/ t e m p l a t e J o b s | e n v i r o n m e n t - r u n n e r \/ m a i n | s e l f - h o s t e d - r u n n e r \/ m a i n ) \. j s $ / } ,
@@ -274,16 +318,7 @@ export const SeverityNumber = {};
274318
275319 // Scan source to find imports that can't resolve
276320 function scanForMissingImports ( ) {
277- function walk ( dir : string ) {
278- for ( const ent of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
279- const full = pathMod . join ( dir , ent . name )
280- if ( ent . isDirectory ( ) ) { walk ( full ) ; continue }
281- if ( ! / \. ( t s | t s x ) $ / . test ( ent . name ) ) continue
282- const code : string = fs . readFileSync ( full , 'utf-8' )
283- // Collect all imports
284- for ( const m of code . matchAll ( / i m p o r t \s + (?: \{ ( [ ^ } ] * ) \} | ( \w + ) ) ? \s * (?: , \s * \{ ( [ ^ } ] * ) \} ) ? \s * f r o m \s + [ ' " ] ( .* ?) [ ' " ] / g) ) {
285- const specifier = m [ 4 ]
286- const namedPart = m [ 1 ] || m [ 3 ] || ''
321+ function checkAndRegister ( specifier : string , fileDir : string , namedPart : string ) {
287322 const names = namedPart . split ( ',' )
288323 . map ( ( s : string ) => s . trim ( ) . replace ( / ^ t y p e \s + / , '' ) )
289324 . filter ( ( s : string ) => s && ! s . startsWith ( 'type ' ) )
@@ -303,8 +338,7 @@ export const SeverityNumber = {};
303338 }
304339 // Check relative .js imports
305340 else if ( specifier . endsWith ( '.js' ) && ( specifier . startsWith ( './' ) || specifier . startsWith ( '../' ) ) ) {
306- const dir2 = pathMod . dirname ( full )
307- const resolved = pathMod . resolve ( dir2 , specifier )
341+ const resolved = pathMod . resolve ( fileDir , specifier )
308342 const tsVariant = resolved . replace ( / \. j s $ / , '.ts' )
309343 const tsxVariant = resolved . replace ( / \. j s $ / , '.tsx' )
310344 if ( ! fs . existsSync ( resolved ) && ! fs . existsSync ( tsVariant ) && ! fs . existsSync ( tsxVariant ) ) {
@@ -317,6 +351,30 @@ export const SeverityNumber = {};
317351 if ( ! missingModuleExports . has ( specifier ) ) missingModuleExports . set ( specifier , new Set ( ) )
318352 for ( const n of names ) missingModuleExports . get ( specifier ) ! . add ( n )
319353 }
354+ }
355+
356+ function walk ( dir : string ) {
357+ for ( const ent of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
358+ const full = pathMod . join ( dir , ent . name )
359+ if ( ent . isDirectory ( ) ) { walk ( full ) ; continue }
360+ if ( ! / \. ( t s | t s x ) $ / . test ( ent . name ) ) continue
361+ const code : string = fs . readFileSync ( full , 'utf-8' )
362+ const fileDir = pathMod . dirname ( full )
363+
364+ // Collect static imports: import { X } from '...'
365+ for ( const m of code . matchAll ( / i m p o r t \s + (?: \{ ( [ ^ } ] * ) \} | ( \w + ) ) ? \s * (?: , \s * \{ ( [ ^ } ] * ) \} ) ? \s * f r o m \s + [ ' " ] ( .* ?) [ ' " ] / g) ) {
366+ checkAndRegister ( m [ 4 ] , fileDir , m [ 1 ] || m [ 3 ] || '' )
367+ }
368+
369+ // Collect dynamic requires: require('...') — these are used
370+ // behind feature() gates and become live when flags are enabled.
371+ for ( const m of code . matchAll ( / r e q u i r e \( \s * [ ' " ] ( \. \. ? \/ [ ^ ' " ] + ) [ ' " ] \s * \) / g) ) {
372+ checkAndRegister ( m [ 1 ] , fileDir , '' )
373+ }
374+
375+ // Collect dynamic imports: import('...')
376+ for ( const m of code . matchAll ( / i m p o r t \( \s * [ ' " ] ( \. \. ? \/ [ ^ ' " ] + ) [ ' " ] \s * \) / g) ) {
377+ checkAndRegister ( m [ 1 ] , fileDir , '' )
320378 }
321379 }
322380 }
@@ -393,3 +451,9 @@ if (!result.success) {
393451}
394452
395453console . log ( `✓ Built openclaude v${ version } → dist/cli.mjs` )
454+
455+ } finally {
456+ // Always restore source files, even if Bun.build() throws
457+ restoreModifiedFiles ( )
458+ console . log ( ` 🔄 feature-flags: pre-processed ${ numModified } files (restored)` )
459+ }
0 commit comments