I noticed a warning in my build output, and Claude traced it to the Nuxt Image dependency. Cleaning on the warning would be nice, but this shouldn't have a runtime impact.
Claude investigation below.
Environment
@nuxt/image 2.0.0
- Reproduced with Nuxt 4, default config, no user-provided
image options.
- Generated bundle contains the duplicate regardless of bundler; the warning is surfaced when a downstream pass (e.g.
wrangler deploy's esbuild step) re-parses the bundled output. nuxt build itself does not print the warning — verified with a minimal repro.
Symptom
During wrangler deploy (or any downstream esbuild parse of the Nitro output):
▲ [WARNING] Duplicate key "provider" in object literal [duplicate-object-key]
.output/server/chunks/build/server.mjs:<line>:<col>:
35 │ ...ities:[1,2],format:["webp"],provider:"none",providers:{none:{...
╵ ~~~~~~~~
The original key "provider" is here:
.output/server/chunks/build/server.mjs:<line>:<col>:
35 │ ...1280,"2xl":1536},presets:{},provider:"none",domains:[],alias:{}...
╵ ~~~~~~~~
Direct inspection of the Nitro bundle confirms two provider: keys in the same imageOptions object literal:
$ grep -c 'provider:"none"' .output/server/chunks/build/server.mjs
# 2 occurrences inside the same literal (single line); see context below
$ python3 -c "import re,sys; s=open('.output/server/chunks/build/server.mjs').read(); ps=[m.start() for m in re.finditer(r'provider:\"none\"', s)]; print(len(ps)); [print(s[max(0,p-80):p+80]) for p in ps]"
2
...d 0},r)}const pe={screens:{sm:640,...,presets:{},provider:"none",domains:[],alias:{},densities:[1,2],format:["webp"],provider:"none",providers:{none:{...
,presets:{},provider:"none",domains:[],alias:{},densities:[1,2],format:["webp"],provider:"none",providers:{none:{setup:function(e)...
Root cause
src/module.ts on main, two locations:
L117–124 — imageOptions is built via pick(options, [...]) which already includes 'provider':
const imageOptions: Omit<CreateImageOptions, 'providers' | 'nuxt' | 'runtimeConfig'> = pick(options, [
'screens',
'presets',
'provider', // ← included here
'domains',
'alias',
'densities',
'format',
'quality',
])
L139–151 — generateImageOptions spreads imageOptions (which carries provider) and writes provider explicitly on the next line:
export const imageOptions = {
...${JSON.stringify(imageOptions, null, 2)}, // ← spreads provider
/** @type {${JSON.stringify(imageOptions.provider)}} */
provider: ${JSON.stringify(imageOptions.provider)}, // ← second write of provider
providers: { ... }
}
When bundled, the spread is flattened by esbuild because its RHS is a literal object known at compile time. The result: two provider: keys side-by-side in the same object literal. Runtime is unaffected (last-key-wins, same value), but esbuild's duplicate-object-key rule fires when it parses the output.
Reproduction
mkdir nuxt-image-repro && cd nuxt-image-repro
cat > package.json <<'EOF'
{
"name": "nuxt-image-repro",
"private": true,
"type": "module",
"scripts": { "build": "nuxt build" },
"devDependencies": {
"nuxt": "^4.0.0",
"@nuxt/image": "2.0.0",
"wrangler": "*"
}
}
EOF
cat > nuxt.config.ts <<'EOF'
export default defineNuxtConfig({ modules: ['@nuxt/image'] })
EOF
mkdir -p app && cat > app/app.vue <<'EOF'
<template><NuxtImg src="/test.png" width="100" height="100" /></template>
EOF
cat > wrangler.jsonc <<'EOF'
{
"name": "nuxt-image-repro",
"compatibility_date": "2026-02-24",
"compatibility_flags": ["nodejs_compat"],
"main": ".output/server/index.mjs",
"assets": { "binding": "ASSETS", "directory": ".output/public" }
}
EOF
pnpm install
NITRO_PRESET=cloudflare-module pnpm exec nuxt build
pnpm exec wrangler deploy --dry-run --outdir wrangler-out
# → warning is printed during the wrangler step
Or, without wrangler, observe the duplicate directly in the build output:
NITRO_PRESET=cloudflare-module pnpm exec nuxt build
grep -c 'provider:"none"' .output/server/chunks/build/server.mjs # → 2
A <NuxtImg> (or any consumer) is required — without it, tree-shaking drops imageOptions from the bundle and the duplicate isn't present in .output.
Proposed fix
Remove 'provider' from the pick() array. The explicit emission keeps the @type JSDoc that lets editors infer the literal-string type of provider in the generated bundle; the spread doesn't carry that annotation, so the explicit assignment should stay.
--- a/src/module.ts
+++ b/src/module.ts
@@ -117,7 +117,6 @@
const imageOptions: Omit<CreateImageOptions, 'providers' | 'nuxt' | 'runtimeConfig'> = pick(options, [
'screens',
'presets',
- 'provider',
'domains',
'alias',
'densities',
Alternative: drop the explicit provider: line and rely on the spread, but that loses the @type annotation. The pick-side change is preferable.
I noticed a warning in my build output, and Claude traced it to the Nuxt Image dependency. Cleaning on the warning would be nice, but this shouldn't have a runtime impact.
Claude investigation below.
Environment
@nuxt/image2.0.0imageoptions.wrangler deploy's esbuild step) re-parses the bundled output.nuxt builditself does not print the warning — verified with a minimal repro.Symptom
During
wrangler deploy(or any downstream esbuild parse of the Nitro output):Direct inspection of the Nitro bundle confirms two
provider:keys in the sameimageOptionsobject literal:Root cause
src/module.tsonmain, two locations:L117–124 —
imageOptionsis built viapick(options, [...])which already includes'provider':L139–151 —
generateImageOptionsspreadsimageOptions(which carriesprovider) and writesproviderexplicitly on the next line:When bundled, the spread is flattened by esbuild because its RHS is a literal object known at compile time. The result: two
provider:keys side-by-side in the same object literal. Runtime is unaffected (last-key-wins, same value), but esbuild'sduplicate-object-keyrule fires when it parses the output.Reproduction
Or, without wrangler, observe the duplicate directly in the build output:
A
<NuxtImg>(or any consumer) is required — without it, tree-shaking dropsimageOptionsfrom the bundle and the duplicate isn't present in.output.Proposed fix
Remove
'provider'from thepick()array. The explicit emission keeps the@typeJSDoc that lets editors infer the literal-string type ofproviderin the generated bundle; the spread doesn't carry that annotation, so the explicit assignment should stay.Alternative: drop the explicit
provider:line and rely on the spread, but that loses the@typeannotation. The pick-side change is preferable.