Environment
Environment
| Item |
Version |
| Node.js (builder) |
24.4 |
| Bun (runtime) |
latest (alpine) |
| Nuxt |
4.x |
| @nuxt/content |
3.15.1 (3.14.0 also affected) |
| Nitro preset |
bun |
| Build command |
nuxt build --preset bun (executed in Node.js) |
| Deployment |
oven/bun:alpine container |
Version
3.15.1
Reproduction
` ERROR Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'bun:'
at throwIfUnsupportedURLScheme (node:internal/modules/esm/load:209:11)
at defaultLoad (node:internal/modules/esm/load:107:3)
at ModuleLoader.load (node:internal/modules/esm/loader:800:12)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:580:43)
at #createModuleJob (node:internal/modules/esm/loader:604:36)
at #getJobFromResolveResult (node:internal/modules/esm/loader:338:34)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:306:41) `
Description
Description
Scenario
A Dockerfile uses node:24.4-alpine as the builder image (Node.js environment), runs nuxt build --preset bun, then deploys the output to an oven/bun:alpine runtime container.
Original error (@nuxt/content 3.14.0, no extra config)
At runtime, Bun cannot load the better-sqlite3 native binding:
'better-sqlite3' is not yet supported in Bun.
Track the status in https://github.com/oven-sh/bun/issues/4290
In the meantime, you could try bun:sqlite which has a similar API.
at dlopen (unknown)
at bindings (/app/.output/server/node_modules/bindings/bindings.js:112:48)
at new Database (/app/.output/server/node_modules/better-sqlite3/lib/database.js:48:29)
at getDB (/app/.output/server/index.mjs:59792:13)
Cause: process.versions.bun is absent during the Node.js build, so findBestSqliteAdapter() falls back to better-sqlite3 and bundles it into the server output. Bun cannot use better-sqlite3 at runtime.
Attempted fix 1: Upgrade to 3.15.1 + sqliteConnector: 'bun'
Following nuxt/content#3741 (merged in v3.15.0):
// nuxt.config.ts
export default defineNuxtConfig({
content: {
experimental: {
sqliteConnector: 'bun',
},
},
})
Result: Client and Server builds succeed, but the prerender stage fails:
✔ Client built in 183765ms
✔ Server built in 32539ms
ℹ Initializing prerenderer
ERROR Only URLs with a scheme in: file, data, and node are supported by the default ESM loader.
Received protocol 'bun:'
at throwIfUnsupportedURLScheme (node:internal/modules/esm/load:209:11)
at defaultLoad (node:internal/modules/esm/load:107:3)
Attempted fix 2: database.type = 'bunsqlite' + Nitro externals
Set content.database.type = 'bunsqlite' directly and add bun:sqlite to Nitro externals via nitro:config hook:
content: {
database: {
type: 'bunsqlite' as any,
filename: './contents.sqlite',
},
},
hooks: {
'nitro:config'(nitroConfig) {
nitroConfig.externals = nitroConfig.externals || {} as any
nitroConfig.externals.external = nitroConfig.externals.external || []
nitroConfig.externals.external.push('bun:sqlite')
},
}
Result: Same prerender-stage error.
Root cause analysis
The issue lies in the interaction between Nitro's prerender flow and @nuxt/content's database.server.js.
1. Static top-level import in database.server.js
// @nuxt/content/dist/runtime/internal/database.server.js
import adapter from "#content/adapter"; // ← static top-level import
import localAdapter from "#content/local-adapter";
export default function loadDatabaseAdapter(config) {
if (import.meta.dev || ["nitro-prerender", "nitro-dev"].includes(import.meta.preset)) {
db = localAdapter(refineDatabaseConfig(localDatabase)); // prerender uses localAdapter
} else {
db = adapter(refineDatabaseConfig(database)); // production uses adapter
}
}
Although the prerender path only uses localAdapter at runtime (line 13), adapter (i.e. #content/adapter, pointing to the bun-sqlite connector) is statically imported at the top level (line 6). Node.js's ESM loader attempts to resolve bun:sqlite during module loading, regardless of whether adapter is actually called.
2. findBestSqliteAdapter does not differentiate prerender
// @nuxt/content 3.15.x
async function findBestSqliteAdapter(opts) {
if (opts.sqliteConnector === "bun") {
if (!process.versions.bun) {
console.warn("..."); // only warns, does NOT fall back
}
return opts.resolver.resolve("./runtime/internal/connectors/bun-sqlite");
// ← always returns bun-sqlite regardless of build/runtime environment
}
// ...
}
3. @nuxt/content sets prerender: true on sql_dump routes
// @nuxt/content module.mjs
manifest.collections.forEach((collection) => {
if (!collection.private) {
const key = `/__nuxt_content/${collection.name}/sql_dump.txt`;
nuxt.options.routeRules[key] = { ...nuxt.options.routeRules[key], prerender: true };
}
});
Even if the user does not configure any prerender routes, @nuxt/content automatically adds routes that require prerendering, which triggers prerenderer initialization.
4. Nitro prerender execution chain
Nitro bundles server bundle → bun:sqlite marked as external ✓
Nitro bundles prerender bundle → bun:sqlite marked as external ✓
Nitro executes prerender bundle in Node.js → import "bun:sqlite" → Node.js error ✗
Rollup-level external only solves the bundling phase resolve. It cannot solve the bun: protocol issue when Node.js actually executes the output during prerender.
Affected scope
All projects that meet the following conditions:
- Build in Node.js (
nuxt build --preset bun, or CI/CD using Node.js)
- Deploy to Bun runtime
- Use
@nuxt/content module
- Have any prerender routes (content module's own
sql_dump.txt routes will trigger this)
Possible solutions
Approach 1: Use dynamic import for #content/adapter (recommended)
Change the static top-level import of #content/adapter in database.server.js to a lazy dynamic import, only loading it when actually needed in production (non-prerender):
// Before
import adapter from "#content/adapter";
// After
let _adapter;
async function getAdapter() {
if (!_adapter) {
_adapter = (await import("#content/adapter")).default;
}
return _adapter;
}
This way, the prerender stage will not trigger the bun:sqlite import, since prerender only uses localAdapter.
Pros: Minimal change, does not affect any other scenario.
Cons: Requires making the adapter usage path in loadDatabaseAdapter async.
Approach 2: Override #content/adapter alias for prerender builds
In @nuxt/content's nitro:config hook, detect cross-compilation and set #content/adapter to the same value as #content/local-adapter (i.e. a Node.js-compatible connector):
nuxt.hook('nitro:config', async (config) => {
if (sqliteConnector === 'bun' && !process.versions.bun) {
config.alias["#content/adapter"] = config.alias["#content/local-adapter"];
}
});
Pros: No changes to database.server.js needed.
Cons: Requires careful hook execution ordering (must run after the alias is initially set). Also, the production #content/adapter alias in the main server bundle would also be overridden, so this approach would need to target only the prerender build somehow.
Approach 3: Skip content prerender routes during cross-compilation
In @nuxt/content's module setup, when sqliteConnector: 'bun' is set and the build environment is Node.js, do not set prerender: true on sql_dump.txt routes:
manifest.collections.forEach((collection) => {
if (!collection.private) {
const key = `/__nuxt_content/${collection.name}/sql_dump.txt`;
const skipPrerender = sqliteConnector === 'bun' && !process.versions.bun;
nuxt.options.routeRules[key] = {
...nuxt.options.routeRules[key],
prerender: !skipPrerender,
};
}
});
Pros: Precisely scoped.
Cons: Only addresses content's own prerender triggers. User-defined prerender routes can still trigger the issue because the static import in database.server.js remains.
Approach 4 (user-side workaround): Disable all prerender in bun builds
In nuxt.config.ts:
const isBunPreset = (process.env.NITRO_PRESET || '').toLowerCase() === 'bun'
|| process.argv.some((a, i) =>
(a === '--preset' && process.argv[i + 1] === 'bun') || a === '--preset=bun'
)
export default defineNuxtConfig({
content: {
experimental: { sqliteConnector: 'bun' },
},
nitro: {
prerender: {
routes: isBunPreset ? [] : ['/spa-loading'],
},
},
hooks: {
...(isBunPreset ? {
'nitro:config'(nitroConfig) {
for (const key of Object.keys(nitroConfig.routeRules || {})) {
if (key.startsWith('/__nuxt_content/') && key.endsWith('/sql_dump.txt')) {
nitroConfig.routeRules![key].prerender = false
}
}
},
} : {}),
},
})
Pros: No changes to @nuxt/content source code; can be applied immediately.
Cons: Loses prerender capability; all pages must be generated at runtime. This is a workaround, not a real fix.
Related issues
- oven-sh/bun#4290 —
better-sqlite3 is not yet supported in Bun
- nuxt/content#3741 — feat: add explicit
bun sqlite connector for Bun runtime deployments (merged, but does not cover the prerender scenario)
- nuxt/content#3758 — Support force
node:sqlite instead of bun:sqlite (related but different scenario)
Reproduction steps
- Create a Nuxt 4 project with
@nuxt/content >= 3.15.0
- Set
content.experimental.sqliteConnector: 'bun' in nuxt.config.ts
- Add at least one content file (triggers collection generation →
sql_dump.txt prerender route)
- Run
nuxt build --preset bun in a Node.js environment
- Build fails at the
Initializing prerenderer stage with: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'bun:'
Additional context
No response
Logs
Environment
Environment
bunnuxt build --preset bun(executed in Node.js)oven/bun:alpinecontainerVersion
3.15.1
Reproduction
` ERROR Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'bun:'
Description
Description
Scenario
A Dockerfile uses
node:24.4-alpineas the builder image (Node.js environment), runsnuxt build --preset bun, then deploys the output to anoven/bun:alpineruntime container.Original error (@nuxt/content 3.14.0, no extra config)
At runtime, Bun cannot load the
better-sqlite3native binding:Cause:
process.versions.bunis absent during the Node.js build, sofindBestSqliteAdapter()falls back tobetter-sqlite3and bundles it into the server output. Bun cannot usebetter-sqlite3at runtime.Attempted fix 1: Upgrade to 3.15.1 +
sqliteConnector: 'bun'Following nuxt/content#3741 (merged in v3.15.0):
Result: Client and Server builds succeed, but the prerender stage fails:
Attempted fix 2:
database.type = 'bunsqlite'+ Nitro externalsSet
content.database.type = 'bunsqlite'directly and addbun:sqliteto Nitro externals vianitro:confighook:Result: Same prerender-stage error.
Root cause analysis
The issue lies in the interaction between Nitro's prerender flow and
@nuxt/content'sdatabase.server.js.1. Static top-level import in
database.server.jsAlthough the prerender path only uses
localAdapterat runtime (line 13),adapter(i.e.#content/adapter, pointing to thebun-sqliteconnector) is statically imported at the top level (line 6). Node.js's ESM loader attempts to resolvebun:sqliteduring module loading, regardless of whetheradapteris actually called.2.
findBestSqliteAdapterdoes not differentiate prerender3.
@nuxt/contentsetsprerender: trueon sql_dump routesEven if the user does not configure any prerender routes,
@nuxt/contentautomatically adds routes that require prerendering, which triggers prerenderer initialization.4. Nitro prerender execution chain
Rollup-level
externalonly solves the bundling phase resolve. It cannot solve thebun:protocol issue when Node.js actually executes the output during prerender.Affected scope
All projects that meet the following conditions:
nuxt build --preset bun, or CI/CD using Node.js)@nuxt/contentmodulesql_dump.txtroutes will trigger this)Possible solutions
Approach 1: Use dynamic import for
#content/adapter(recommended)Change the static top-level import of
#content/adapterindatabase.server.jsto a lazy dynamic import, only loading it when actually needed in production (non-prerender):This way, the prerender stage will not trigger the
bun:sqliteimport, since prerender only useslocalAdapter.Pros: Minimal change, does not affect any other scenario.
Cons: Requires making the
adapterusage path inloadDatabaseAdapterasync.Approach 2: Override
#content/adapteralias for prerender buildsIn
@nuxt/content'snitro:confighook, detect cross-compilation and set#content/adapterto the same value as#content/local-adapter(i.e. a Node.js-compatible connector):Pros: No changes to
database.server.jsneeded.Cons: Requires careful hook execution ordering (must run after the alias is initially set). Also, the production
#content/adapteralias in the main server bundle would also be overridden, so this approach would need to target only the prerender build somehow.Approach 3: Skip content prerender routes during cross-compilation
In
@nuxt/content's module setup, whensqliteConnector: 'bun'is set and the build environment is Node.js, do not setprerender: trueonsql_dump.txtroutes:Pros: Precisely scoped.
Cons: Only addresses content's own prerender triggers. User-defined prerender routes can still trigger the issue because the static import in
database.server.jsremains.Approach 4 (user-side workaround): Disable all prerender in bun builds
In
nuxt.config.ts:Pros: No changes to
@nuxt/contentsource code; can be applied immediately.Cons: Loses prerender capability; all pages must be generated at runtime. This is a workaround, not a real fix.
Related issues
better-sqlite3is not yet supported in Bunbunsqlite connector for Bun runtime deployments (merged, but does not cover the prerender scenario)node:sqliteinstead ofbun:sqlite(related but different scenario)Reproduction steps
@nuxt/content>= 3.15.0content.experimental.sqliteConnector: 'bun'innuxt.config.tssql_dump.txtprerender route)nuxt build --preset bunin a Node.js environmentInitializing prerendererstage with:Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'bun:'Additional context
No response
Logs