Nested namespaces are broken at run time
Author: Chaoses-IbCreated Sep 2, 2026Updated Sep 2, 2026
During migrating from wasm-bindgen to napi, I encountered a problem about nested namespaces. napi doc says #[napi] mod namespaces cannot be nested, but doesn't clarify what about #[napi(namespace = "...")]:
#[napi] mod name { ... }— export an inline Rust module as a namespace. Every child item that also carries#[napi]is exported inside it (nested napi modules are not supported). Add#[napi(js_name = "...")]on themodto rename the namespace object.#[napi(namespace = "...")]on individual functions, classes, impl blocks, enums, consts, and type aliases — registers that item underexports.<namespace>; apply the same namespace to a class and itsimplblocks. Seenamespacein the attributes reference.
And nested namespaces like #[napi(namespace = "a.b")] actually work for the generated index.js/index.d.ts, but not for the runtime. It looks the runtime will mount it literally like exports["a.b"]. Adding a #[napi(module_exports)] hook can work around this problem. But it looks better to fix this in napi?
By the way, cross-namespace type reference cannot generate correct index.d.ts. Adding manual TS types like #[napi(ts_type = "a.MyType")] can fix it. Is this expected or a bug too?
The workaround hook:
#[napi(module_exports)]
unsafe fn restructure_namespaces(
env: &napi::bindgen_prelude::Env,
mut exports: napi::bindgen_prelude::Object,
) -> napi::Result<()> {
use napi::bindgen_prelude::{JsObjectValue, Object};
for key in Object::keys(&exports)? {
if !key.contains('.') {
continue;
}
let parts: Vec<&str> = key.split('.').collect();
let Some(flat) = exports.get::<Object>(key.as_str())? else {
continue;
};
let mut current = exports;
for part in &parts[..parts.len() - 1] {
let next = match current.get::<Object>(*part)? {
Some(next) => next,
None => {
let next = Object::new(env)?;
current.set(*part, &next)?;
next
}
};
current = next;
}
current.set(parts[parts.len() - 1], &flat)?;
exports.delete_named_property(key.as_str())?;
}
Ok(())
}Source: napi-rs/napi-rs