Skip to content

Commit b8eba4f

Browse files
Jarred-Sumneralii
andauthored
Mark bun builtin modules as external (fixes #75220) (#77616)
Fixes #75220 This marks Bun's builtin modules as external, following the same pattern as for Node. - `"bun:ffi"` - `"bun:jsc"` - `"bun:sqlite"` - `"bun:test"` - `"bun:wrap"` - `"bun"` I have not manually tested this change yet, nor attempted to write tests for this. I did check for other places in the code that should be updated and _maybe_ `externals/crates/next-custom-transforms/src/transforms/warn_for_edge_runtime.rs` should as well, but I'll leave that up to you whether or not that makes sense. Opening as a draft until this can be at least manually tested. By the way, node's `sqlite` module is missing from `NODE_EXTERNALS`. That seemed out of scope but worth mentioning. Current error message: ![image](https://github.com/user-attachments/assets/0538fd35-a1ce-44fe-af7f-1db696cd3e7b) --------- Co-authored-by: Alistair Smith <hi@alistair.sh>
1 parent b07bf2a commit b8eba4f

File tree

19 files changed

+538
-7
lines changed

19 files changed

+538
-7
lines changed

packages/next/src/build/handle-externals.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ export function makeExternalHandler({
188188
return `commonjs ${request}`
189189
}
190190

191+
// Handle Bun builtins as external modules
192+
if (request === 'bun' || request.startsWith('bun:')) {
193+
return `commonjs ${request}`
194+
}
195+
191196
const notExternalModules =
192197
/^(?:private-next-pages\/|next\/(?:dist\/pages\/|(?:app|cache|document|link|form|head|image|legacy\/image|constants|dynamic|script|navigation|headers|router|compat\/router|server)$)|string-hash|private-next-rsc-action-validate|private-next-rsc-action-client-wrapper|private-next-rsc-server-reference|private-next-rsc-cache-wrapper|private-next-rsc-track-dynamic-import$)/
193198
if (notExternalModules.test(request)) {

packages/next/src/build/webpack-config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,15 @@ export default async function getBaseWebpackConfig(
913913
const builtinModules = (require('module') as typeof import('module'))
914914
.builtinModules
915915

916+
const bunExternals = [
917+
'bun:ffi',
918+
'bun:jsc',
919+
'bun:sqlite',
920+
'bun:test',
921+
'bun:wrap',
922+
'bun',
923+
]
924+
916925
const shouldEnableSlowModuleDetection =
917926
!!config.experimental.slowModuleDetection && dev
918927

@@ -986,6 +995,7 @@ export default async function getBaseWebpackConfig(
986995
]
987996
: [
988997
...builtinModules,
998+
...bunExternals,
989999
({
9901000
context,
9911001
request,

packages/next/src/build/webpack/plugins/middleware-plugin.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ function isNodeJsModule(moduleName: string) {
288288
)
289289
}
290290

291+
function isBunModule(moduleName: string) {
292+
return moduleName === 'bun' || moduleName.startsWith('bun:')
293+
}
294+
291295
function isDynamicCodeEvaluationAllowed(
292296
fileName: string,
293297
middlewareConfig?: MiddlewareConfig,
@@ -521,12 +525,13 @@ function getCodeAnalyzer(params: {
521525

522526
if (
523527
!dev &&
524-
isNodeJsModule(importedModule) &&
528+
(isNodeJsModule(importedModule) || isBunModule(importedModule)) &&
525529
!SUPPORTED_NATIVE_MODULES.includes(importedModule)
526530
) {
531+
const isBun = isBunModule(importedModule)
527532
compilation.warnings.push(
528533
buildWebpackError({
529-
message: `A Node.js module is loaded ('${importedModule}' at line ${node.loc.start.line}) which is not supported in the Edge Runtime.
534+
message: `A ${isBun ? 'Bun' : 'Node.js'} module is loaded ('${importedModule}' at line ${node.loc.start.line}) which is not supported in the Edge Runtime.
530535
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`,
531536
compilation,
532537
parser,
@@ -921,7 +926,7 @@ export async function handleWebpackExternalForEdgeRuntime({
921926
if (
922927
(contextInfo.issuerLayer === WEBPACK_LAYERS.middleware ||
923928
contextInfo.issuerLayer === WEBPACK_LAYERS.apiEdge) &&
924-
isNodeJsModule(request) &&
929+
(isNodeJsModule(request) || isBunModule(request)) &&
925930
!supportedEdgePolyfills.has(request)
926931
) {
927932
// allows user to provide and use their polyfills, as we do with buffer.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export async function GET() {
4+
const results: Record<string, string> = {}
5+
6+
const modules = [
7+
{ name: 'bunFfi', module: 'bun:ffi' },
8+
{ name: 'bunJsc', module: 'bun:jsc' },
9+
{ name: 'bunSqlite', module: 'bun:sqlite' },
10+
{ name: 'bunTest', module: 'bun:test' },
11+
{ name: 'bunWrap', module: 'bun:wrap' },
12+
{ name: 'bun', module: 'bun' },
13+
]
14+
15+
for (const { name, module } of modules) {
16+
try {
17+
require(module)
18+
results[name] = 'loaded'
19+
} catch (e: any) {
20+
// Expected: Cannot find module error when not in Bun runtime
21+
// This confirms the module was externalized (not bundled)
22+
results[name] = e.message.includes('Cannot find module')
23+
? 'external'
24+
: 'error'
25+
}
26+
}
27+
28+
return NextResponse.json(results)
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export const runtime = 'edge'
4+
5+
// Edge runtime should not allow Bun imports
6+
// This test verifies that Bun modules are handled correctly in edge runtime
7+
export async function GET() {
8+
const modules = [
9+
{ name: 'bunFfi', module: 'bun:ffi' },
10+
{ name: 'bunJsc', module: 'bun:jsc' },
11+
{ name: 'bunSqlite', module: 'bun:sqlite' },
12+
{ name: 'bunTest', module: 'bun:test' },
13+
{ name: 'bunWrap', module: 'bun:wrap' },
14+
{ name: 'bun', module: 'bun' },
15+
]
16+
17+
try {
18+
for (const { module } of modules) {
19+
await import(module)
20+
}
21+
22+
return NextResponse.json('Did not throw')
23+
} catch (e) {
24+
return NextResponse.json(String(e))
25+
}
26+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default function RootLayout({
2+
children,
3+
}: {
4+
children: React.ReactNode
5+
}) {
6+
return (
7+
<html lang="en">
8+
<body>{children}</body>
9+
</html>
10+
)
11+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
export default function Page() {
2+
// These should be handled as external modules
3+
// When not running in Bun, require() will throw "Cannot find module"
4+
let bunFfiStatus = 'not loaded'
5+
let bunJscStatus = 'not loaded'
6+
let bunSqliteStatus = 'not loaded'
7+
let bunTestStatus = 'not loaded'
8+
let bunWrapStatus = 'not loaded'
9+
let bunStatus = 'not loaded'
10+
11+
try {
12+
require('bun:ffi')
13+
bunFfiStatus = 'loaded successfully'
14+
} catch (e: any) {
15+
// Expected error when not running in Bun
16+
bunFfiStatus = e.message.includes('Cannot find module')
17+
? 'external (not found)'
18+
: 'error: ' + e.message
19+
}
20+
21+
try {
22+
require('bun:jsc')
23+
bunJscStatus = 'loaded successfully'
24+
} catch (e: any) {
25+
bunJscStatus = e.message.includes('Cannot find module')
26+
? 'external (not found)'
27+
: 'error: ' + e.message
28+
}
29+
30+
try {
31+
require('bun:sqlite')
32+
bunSqliteStatus = 'loaded successfully'
33+
} catch (e: any) {
34+
bunSqliteStatus = e.message.includes('Cannot find module')
35+
? 'external (not found)'
36+
: 'error: ' + e.message
37+
}
38+
39+
try {
40+
require('bun:test')
41+
bunTestStatus = 'loaded successfully'
42+
} catch (e: any) {
43+
bunTestStatus = e.message.includes('Cannot find module')
44+
? 'external (not found)'
45+
: 'error: ' + e.message
46+
}
47+
48+
try {
49+
require('bun:wrap')
50+
bunWrapStatus = 'loaded successfully'
51+
} catch (e: any) {
52+
bunWrapStatus = e.message.includes('Cannot find module')
53+
? 'external (not found)'
54+
: 'error: ' + e.message
55+
}
56+
57+
try {
58+
require('bun')
59+
bunStatus = 'loaded successfully'
60+
} catch (e: any) {
61+
bunStatus = e.message.includes('Cannot find module')
62+
? 'external (not found)'
63+
: 'error: ' + e.message
64+
}
65+
66+
return (
67+
<div>
68+
<h1>Bun Externals Test</h1>
69+
<div id="bun-ffi">{bunFfiStatus}</div>
70+
<div id="bun-jsc">{bunJscStatus}</div>
71+
<div id="bun-sqlite">{bunSqliteStatus}</div>
72+
<div id="bun-test">{bunTestStatus}</div>
73+
<div id="bun-wrap">{bunWrapStatus}</div>
74+
<div id="bun">{bunStatus}</div>
75+
</div>
76+
)
77+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use server'
2+
3+
export async function testBunExternals() {
4+
const modules = [
5+
'bun:ffi',
6+
'bun:jsc',
7+
'bun:sqlite',
8+
'bun:test',
9+
'bun:wrap',
10+
'bun',
11+
]
12+
const results: Record<string, string> = {}
13+
14+
for (const mod of modules) {
15+
try {
16+
require(mod)
17+
results[mod] = 'loaded'
18+
} catch (e: any) {
19+
// Expected: Cannot find module error when not in Bun runtime
20+
results[mod] = e.message.includes('Cannot find module')
21+
? 'external (not found)'
22+
: 'error'
23+
}
24+
}
25+
26+
// All modules should be external (not found when not in Bun)
27+
const allExternal = Object.values(results).every(
28+
(r) => r === 'external (not found)' || r === 'loaded'
29+
)
30+
return allExternal
31+
? 'All Bun modules are external'
32+
: 'Some modules were not properly externalized'
33+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { testBunExternals } from './actions'
5+
6+
export default function ServerActionPage() {
7+
const [result, setResult] = useState('')
8+
9+
async function handleClick() {
10+
const res = await testBunExternals()
11+
setResult(res)
12+
}
13+
14+
return (
15+
<div>
16+
<h1>Server Action Test</h1>
17+
<button id="test-action" onClick={handleClick}>
18+
Test Bun Externals in Server Action
19+
</button>
20+
{result && <div id="action-result">{result}</div>}
21+
</div>
22+
)
23+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
3+
describe('app-dir - bun externals', () => {
4+
const { next, isNextDev, isTurbopack, skipped } = nextTestSetup({
5+
files: __dirname,
6+
skipDeployment: true,
7+
})
8+
9+
if (skipped) {
10+
return
11+
}
12+
13+
it('should handle bun builtins as external modules', async () => {
14+
const $ = await next.render$('/')
15+
16+
// When not running in Bun, these should throw "Cannot find module" errors
17+
expect($('#bun-ffi').text()).toBe('external (not found)')
18+
expect($('#bun-jsc').text()).toBe('external (not found)')
19+
expect($('#bun-sqlite').text()).toBe('external (not found)')
20+
expect($('#bun-test').text()).toBe('external (not found)')
21+
expect($('#bun-wrap').text()).toBe('external (not found)')
22+
expect($('#bun').text()).toBe('external (not found)')
23+
})
24+
25+
it('should handle bun builtins in server actions', async () => {
26+
const browser = await next.browser('/server-action')
27+
28+
await browser.elementByCss('#test-action').click()
29+
30+
await browser.waitForElementByCss('#action-result')
31+
const result = await browser.elementByCss('#action-result').text()
32+
33+
expect(result).toContain('All Bun modules are external')
34+
})
35+
36+
it('should handle bun builtins in route handlers', async () => {
37+
const response = await next.fetch('/api/bun-externals')
38+
const data = await response.json()
39+
40+
expect(data.bunFfi).toBe('external')
41+
expect(data.bunJsc).toBe('external')
42+
expect(data.bunSqlite).toBe('external')
43+
expect(data.bunTest).toBe('external')
44+
expect(data.bunWrap).toBe('external')
45+
expect(data.bun).toBe('external')
46+
})
47+
48+
it('should handle bun builtins in edge runtime', async () => {
49+
const response = await next.fetch('/api/edge-bun-externals')
50+
expect(await response.json()).toMatch(
51+
/(Error: Cannot find module 'bun.*'|Error: Failed to load external module bun.*)/
52+
)
53+
})
54+
55+
if (!isTurbopack && !isNextDev) {
56+
it('should not bundle bun builtins in server bundles', async () => {
57+
await next.fetch('/')
58+
const rscBundle = await next.readFile('.next/server/app/page.js')
59+
60+
expect(rscBundle).not.toContain('bun:ffi implementation')
61+
expect(rscBundle).not.toContain('bun:jsc implementation')
62+
expect(rscBundle).not.toContain('bun:sqlite implementation')
63+
expect(rscBundle).not.toContain('bun:test implementation')
64+
expect(rscBundle).not.toContain('bun:wrap implementation')
65+
66+
expect(rscBundle).toMatch(/require\(["']bun:ffi["']\)/)
67+
expect(rscBundle).toMatch(/require\(["']bun:jsc["']\)/)
68+
expect(rscBundle).toMatch(/require\(["']bun:sqlite["']\)/)
69+
expect(rscBundle).toMatch(/require\(["']bun:test["']\)/)
70+
expect(rscBundle).toMatch(/require\(["']bun:wrap["']\)/)
71+
expect(rscBundle).toMatch(/require\(["']bun["']\)/)
72+
})
73+
}
74+
})

0 commit comments

Comments
 (0)