Environment
- Nitro
2.13.4 (v2, rollup) and 3.0.260610-beta (main, builder: "rollup")
- Node.js v24.19.0 — the failure applies to every Node.js ≥ 22
- Windows 11, npm
Reproduction
https://github.com/agantelin/nitro-import-attributes-repro — four self-contained cases, each with
its own npm install and the exact commands in the README.
Describe the bug
Two independent defects. Both are quiet: the build is green, the server starts, and only the route
that touches the module answers 500.
1. output.importAttributesKey is never set
Rollup 4 re-prints the import attributes of external modules with its default key, assert:
// source
() => import("@iconify-json/mdi/icons.json", { with: { type: "json" } })
// .output/server/chunks/routes/index.mjs
() => import('@iconify-json/mdi/icons.json', { assert: { type: 'json' } })
assert was removed from Node.js in 22.0.0 (nodejs/node#52104, "esm: drop support for import
assertions"). In the dynamic form the emitted code isn't a syntax error, it just no longer carries an
attributes bag, so Node refuses the module:
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "…/data.json" needs an import attribute of "type: json"
→ HTTP 500
Anything whose attribute is load-bearing (type: "json", wasm) is unloadable this way; attributes a
runtime ignores are of course harmless.
git grep importAttributesKey is empty on both branches. On main this affects the rollup builder
and the rollup branch of src/build/vite/bundler.ts — the default builder is rolldown, which emits
with correctly, so on v3 you have to opt into rollup to hit it. On v2 rollup is the only builder.
Rollup's default flips in v5 (rollup/rollup#6248, merged to the rollup-5 branch on 2026-01-30;
the user-facing report is rollup/rollup#5685, open since 2024-09), and output.importAttributesKey
is the documented workaround until then. No 5.x is published.
On the risk of setting it: the only runtimes that take assert but reject with are Node.js
16.14–18.19 and 20.0–20.9 (with landed in 21.0.0 / 20.10.0 and was backported to 18.20.0) — all
below engines.node (^20.19.0 || >=22.12.0), so the only person who could notice is someone
pinned below the supported floor. Deno 2 turned assert into a hard error; Bun accepts both.
Nitro doesn't create such an import on its own: externals.external doesn't apply to a .json on
either branch (on v2 the externals plugin gates on isValidNodeImport, which returns false for
it), so the import always comes from a module or a user plugin. @nuxt/icon's
serverBundle.externalizeIconsJson is the live case I hit: it generates a correct
with { type: "json" } import and has no way to know it will be rewritten. Note this also means
#4431's docs (import logo from "./logo.png" with { type: "bytes" }) describe the key users write,
not the key that ships for anything left external.
2. esbuild strips the attributes in .ts / .js sources (v2 only)
getRollupConfig runs the esbuild plugin with target: "es2019", and esbuild drops import
attributes, silently, for every esXXXX target — including es2025:
es2019 dropped · es2022 dropped · es2024 dropped · es2025 dropped
esnext kept · node18 kept · node20 kept · chrome120 kept
(esbuild 0.28.1, the version pinned here. The gate is whether the named target shipped attributes,
not how new the year is.)
The plugin's loader map covers .ts, .js, .tsx, .jsx, so on v2 the attributes never reach
Rollup at all when the import is written in TypeScript — which is how server routes are normally
written. .mjs isn't matched, which is the only reason case 2 in the repro (the attribute-key bug)
is observable in isolation.
main transpiles with oxc at target: "esnext" and is unaffected by this second defect.
Suggested fix
output.importAttributesKey: "with" — src/rollup/config.ts on v2, src/build/rollup/config.ts
plus src/build/vite/bundler.ts on main. Both sit under the user's rollupConfig in the merge,
so explicit config still wins.
supported: { "import-attributes": true, ...user.supported } on the esbuild plugin options —
v2 only.
I've pushed both as PRs since they're one line each and easier to judge as a diff — happy to close
them if you'd rather shape the fix differently.
Environment
2.13.4(v2, rollup) and3.0.260610-beta(main,builder: "rollup")Reproduction
https://github.com/agantelin/nitro-import-attributes-repro — four self-contained cases, each with
its own
npm installand the exact commands in the README.Describe the bug
Two independent defects. Both are quiet: the build is green, the server starts, and only the route
that touches the module answers 500.
1.
output.importAttributesKeyis never setRollup 4 re-prints the import attributes of external modules with its default key,
assert:assertwas removed from Node.js in 22.0.0 (nodejs/node#52104, "esm: drop support for importassertions"). In the dynamic form the emitted code isn't a syntax error, it just no longer carries an
attributes bag, so Node refuses the module:
Anything whose attribute is load-bearing (
type: "json", wasm) is unloadable this way; attributes aruntime ignores are of course harmless.
git grep importAttributesKeyis empty on both branches. Onmainthis affects the rollup builderand the rollup branch of
src/build/vite/bundler.ts— the default builder is rolldown, which emitswithcorrectly, so on v3 you have to opt into rollup to hit it. On v2 rollup is the only builder.Rollup's default flips in v5 (rollup/rollup#6248, merged to the
rollup-5branch on 2026-01-30;the user-facing report is rollup/rollup#5685, open since 2024-09), and
output.importAttributesKeyis the documented workaround until then. No 5.x is published.
On the risk of setting it: the only runtimes that take
assertbut rejectwithare Node.js16.14–18.19 and 20.0–20.9 (
withlanded in 21.0.0 / 20.10.0 and was backported to 18.20.0) — allbelow
engines.node(^20.19.0 || >=22.12.0), so the only person who could notice is someonepinned below the supported floor. Deno 2 turned
assertinto a hard error; Bun accepts both.Nitro doesn't create such an import on its own:
externals.externaldoesn't apply to a.jsononeither branch (on v2 the externals plugin gates on
isValidNodeImport, which returnsfalseforit), so the import always comes from a module or a user plugin.
@nuxt/icon'sserverBundle.externalizeIconsJsonis the live case I hit: it generates a correctwith { type: "json" }import and has no way to know it will be rewritten. Note this also means#4431's docs (
import logo from "./logo.png" with { type: "bytes" }) describe the key users write,not the key that ships for anything left external.
2. esbuild strips the attributes in
.ts/.jssources (v2 only)getRollupConfigruns the esbuild plugin withtarget: "es2019", and esbuild drops importattributes, silently, for every
esXXXXtarget — includinges2025:(esbuild 0.28.1, the version pinned here. The gate is whether the named target shipped attributes,
not how new the year is.)
The plugin's loader map covers
.ts,.js,.tsx,.jsx, so on v2 the attributes never reachRollup at all when the import is written in TypeScript — which is how server routes are normally
written.
.mjsisn't matched, which is the only reason case 2 in the repro (the attribute-key bug)is observable in isolation.
maintranspiles with oxc attarget: "esnext"and is unaffected by this second defect.Suggested fix
output.importAttributesKey: "with"—src/rollup/config.tsonv2,src/build/rollup/config.tsplus
src/build/vite/bundler.tsonmain. Both sit under the user'srollupConfigin the merge,so explicit config still wins.
supported: { "import-attributes": true, ...user.supported }on the esbuild plugin options —v2only.I've pushed both as PRs since they're one line each and easier to judge as a diff — happy to close
them if you'd rather shape the fix differently.