Reproductions for two independent defects that make import attributes unusable in a Nitro server build. Both are silent at build time: the build is green, the server starts, and only the route that touches the module returns a 500.
| # | Directory | What it shows |
|---|---|---|
| 1 | 1-rollup/ |
Rollup alone rewrites with { type: "json" } to assert { type: "json" } on external modules (output.importAttributesKey defaults to "assert") |
| 2 | 2-nitro-v2/ |
The same, end to end, in a nitropack@2.13.4 build → HTTP 500 ERR_IMPORT_ATTRIBUTE_MISSING |
| 3 | 3-nitro-v2-ts/ |
Nitro 2 additionally drops import attributes entirely from .ts/.js sources (esbuild target: "es2019") |
| 4 | 4-nitro-v3/ |
Case 2 still reproduces on nitro@3.0.260610-beta with builder: "rollup". Case 3 does not (oxc), and the default rolldown builder is fine |
Verified on Node.js v24.19.0, Windows 11, npm.
Nitro declares "engines": { "node": "^20.19.0 || >=22.12.0" } (both v2 and v3).
withis supported since Node 21.0 / 20.10, backported to 18.20 — i.e. by every version in that range.assertwas removed in Node 22.0.0 (nodejs/node#52104, "esm: drop support for import assertions").
So with is understood by every runtime in that range and assert by none of the ones from 22
upwards. The only versions that would prefer assert — Node 16.14–18.19 and 20.0–20.9 — are below
Nitro's own floor. Outside Node: Deno 2 turned assert into a hard error, and Bun accepts both.
The static form fails as SyntaxError: Unexpected identifier 'assert'; the dynamic form that
Rollup emits for import() is syntactically valid but loses the attribute's meaning, so Node
rejects the module with TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: … needs an import attribute of "type: json".
Rollup's own default is known and changes only in Rollup 5
(rollup/rollup#6248, merged to the rollup-5 branch
on 2026‑01‑30; the user-facing report is
#5685, open since 2024‑09). No 5.x is published, and
output.importAttributesKey is the documented workaround until then.
This repository is about the two things downstream of that: Nitro never sets the option, and
Nitro 2 removes the attributes before Rollup ever sees them.
cd 1-rollup && npm install && npm startinput export const load = () => import("json-dep/data.json", { with: { type: "json" } })
default const load = () => import('json-dep/data.json', { assert: { type: 'json' } });
with the option const load = () => import('json-dep/data.json', { with: { type: 'json' } });
nitro.config.ts marks a JSON module as external with a five-line Rollup plugin. This mirrors
@nuxt/icon's serverBundle.externalizeIconsJson,
which is where this was found in the wild: the module generates a correct with { type: "json" }
import and has no way to know that it will be re-printed as assert.
A plugin is needed because Nitro's own externals.external option cannot express this: the externals
plugin gates on isValidNodeImport, which returns false for a .json file, so Nitro always inlines
it. Every real occurrence of this bug therefore comes from a module or a user plugin.
cd 2-nitro-v2 && npm install && npm run build
grep -o "import('json-dep/data.json'[^)]*)" .output/server/chunks/routes/index.mjs
npm start # then: curl localhost:3000Emitted, and the response:
import('json-dep/data.json', { assert: { type: 'json' } })
HTTP 500
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "…/packages/json-dep/data.json"
needs an import attribute of "type: json"
Uncommenting output: { importAttributesKey: 'with' } in nitro.config.ts emits with { type: 'json' } and the route returns {"hello":"world"}.
Same project, with the route written in TypeScript instead of .mjs — the form every real Nitro
and Nuxt app uses.
cd 3-nitro-v2-ts && npm install && npm run build
grep -o "import('json-dep/data.json'[^)]*)" .output/server/chunks/routes/index.mjsimport('json-dep/data.json')
The attribute is gone, not renamed. Nitro 2 transpiles .ts/.js/.tsx/.jsx with
esbuild({ target: "es2019" }), and esbuild silently strips import attributes for every esXXXX
target — es2025 included. The gate is whether the named target shipped attributes, not how recent
the year is:
esbuild.transformSync('const p = import("x.json", { with: { type: "json" } })', { loader: 'ts', target: 'es2019' })
// -> const p = import("x.json");
esbuild.transformSync(same, { loader: 'ts', target: 'es2019', supported: { 'import-attributes': true } })
// -> const p = import("x.json", { with: { type: "json" } });es2019 dropped · es2022 dropped · es2024 dropped · es2025 dropped
esnext kept · node18 kept · node20 kept · chrome120 kept
.mjs files are not matched by that plugin's loader map, which is why case 2 above had to be
written as .mjs to demonstrate the other bug in isolation. Runtime result is the same 500.
cd 4-nitro-v3 && npm install && npm run build
grep -o "import('json-dep/data.json'[^)]*)" .output/server/_chunks/index.mjs
npm start # then: curl localhost:3000import('json-dep/data.json', { assert: { type: 'json' } })
HTTP 500 TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]
The attribute-key defect is unchanged on the rollup builder. Two things are different on v3: the
TypeScript defect is gone (oxc preserves the attributes), and rollup is no longer the default
builder — rolldown is, and it emits with correctly, so 4-nitro-v3/nitro.config.ts opts in with
builder: 'rollup'.
- Nitro — set
output.importAttributesKey: "with"in the Rollup output config (src/rollup/config.tsonv2;src/build/rollup/config.tsand the rollup branch ofsrc/build/vite/bundler.tsonmain). Every runtime Nitro supports acceptswith; none of the modern ones acceptassert. - Nitro 2 — pass
supported: { "import-attributes": true }to the esbuild plugin so thees2019target stops stripping them. @nuxt/icon— set the same key from the plugin it already installs, viaoutputOptions, so users are covered on Nitro versions that predate the fix.