diff --git a/packages/color-diff-napi/src/index.ts b/packages/color-diff-napi/src/index.ts
index 9b662b6d1..692728e2a 100644
--- a/packages/color-diff-napi/src/index.ts
+++ b/packages/color-diff-napi/src/index.ts
@@ -17,32 +17,21 @@
* getSyntaxTheme always returns the default for the given Claude theme.
*/
-import { createRequire } from 'node:module'
import { diffArrays } from 'diff'
-import type * as hljsNamespace from 'highlight.js'
+import hljs from 'highlight.js'
import { basename, extname } from 'path'
-// createRequire works in both Bun and Node.js ESM contexts.
-// Needed because this package is "type": "module" but uses require() for
-// lazy loading — bare require is not available in Node.js ESM.
-const nodeRequire = createRequire(import.meta.url)
-
-// Lazy: defers loading highlight.js until first render. The full bundle
-// registers 190+ language grammars at require time (~50MB, 100-200ms on
-// macOS, several× that on Windows). With a top-level import, any caller
-// chunk that reaches this module — including test/preload.ts via
-// StructuredDiff.tsx → colorDiff.ts — pays that cost at module-eval time
-// and carries the heap for the rest of the process. On Windows CI this
-// pushed later tests in the same shard into GC-pause territory and a
-// beforeEach/afterEach hook timeout (officialRegistry.test.ts, PR #24150).
-// Same lazy pattern the NAPI wrapper used for dlopen.
-type HLJSApi = typeof hljsNamespace.default
+// Static import — createRequire(import.meta.url) fails in Bun --compile mode
+// because the resolved path points to the internal bunfs binary path where
+// node_modules cannot be found. A top-level import ensures the module is
+// bundled and accessible at runtime.
+type HLJSApi = typeof hljs
let cachedHljs: HLJSApi | null = null
-function hljs(): HLJSApi {
+function hljsApi(): HLJSApi {
if (cachedHljs) return cachedHljs
- const mod = nodeRequire('highlight.js')
// highlight.js uses `export =` (CJS). Under bun/ESM the interop wraps it
// in .default; under node CJS the module IS the API. Check at runtime.
+ const mod = hljs as HLJSApi & { default?: HLJSApi }
cachedHljs = 'default' in mod && mod.default ? mod.default : mod
return cachedHljs!
}
@@ -441,9 +430,9 @@ function detectLanguage(
// Filename-based lookup (handles Dockerfile, Makefile, CMakeLists.txt, etc.)
const stem = base.split('.')[0] ?? ''
const byName = FILENAME_LANGS[base] ?? FILENAME_LANGS[stem]
- if (byName && hljs().getLanguage(byName)) return byName
+ if (byName && hljsApi().getLanguage(byName)) return byName
if (ext) {
- const lang = hljs().getLanguage(ext)
+ const lang = hljsApi().getLanguage(ext)
if (lang) return ext
}
// Shebang / first-line detection (strip UTF-8 BOM)
@@ -525,7 +514,7 @@ function highlightLine(
}
let result
try {
- result = hljs().highlight(code, {
+ result = hljsApi().highlight(code, {
language: state.lang,
ignoreIllegals: true,
})
diff --git a/src/utils/cliHighlight.ts b/src/utils/cliHighlight.ts
index e87663c1f..3b248a045 100644
--- a/src/utils/cliHighlight.ts
+++ b/src/utils/cliHighlight.ts
@@ -1,11 +1,13 @@
// highlight.js's type defs carry `/// `. SSETransport,
// mcp/client, ssh, dumpPrompts use DOM types (TextDecodeOptions, RequestInfo)
-// that only typecheck because this file's `typeof import('highlight.js')` pulls
-// lib.dom in. tsconfig has lib: ["ESNext"] only — fixing the actual DOM-type
-// deps is a separate sweep; this ref preserves the status quo.
+// that only typecheck because the hljs import below pulls lib.dom in.
+// tsconfig has lib: ["ESNext"] only — this ref preserves the status quo.
///
import { extname } from 'path'
+// Static import — dynamic import('highlight.js') fails in Bun --compile mode
+// because module resolution points to the internal bunfs binary path.
+import hljs from 'highlight.js'
export type CliHighlight = {
highlight: typeof import('cli-highlight').highlight
@@ -13,9 +15,6 @@ export type CliHighlight = {
}
// One promise shared by Fallback.tsx, markdown.ts, events.ts, getLanguageName.
-// The highlight.js import piggybacks: cli-highlight has already pulled it into
-// the module cache, so the second import() is a cache hit — no extra bytes
-// faulted in.
let cliHighlightPromise: Promise | undefined
let loadedGetLanguage: ((name: string) => { name: string } | undefined) | undefined
@@ -23,9 +22,9 @@ let loadedGetLanguage: ((name: string) => { name: string } | undefined) | undefi
async function loadCliHighlight(): Promise {
try {
const cliHighlight = await import('cli-highlight')
- // cache hit — cli-highlight already loaded highlight.js
- const highlightJs = await import('highlight.js')
- loadedGetLanguage = (highlightJs as { getLanguage?: typeof loadedGetLanguage }).getLanguage
+ // highlight.js CJS interop: `export =` wraps in .default under ESM
+ const hljsMod = hljs as { getLanguage?: typeof loadedGetLanguage; default?: typeof hljs }
+ loadedGetLanguage = hljsMod.getLanguage ?? hljsMod.default?.getLanguage
return {
highlight: cliHighlight.highlight,
supportsLanguage: cliHighlight.supportsLanguage,