| name | adding-builtin-module |
| description | Scaffolds and registers a new Node.js-compatible built-in module in the skeleton. Use when asked to add a new node:X module, implement a new built-in, or create a new native+JS module pair. |
Adding a New Built-in Module
Step-by-step guide for adding a new Node.js-compatible built-in module to the skeleton.
Architecture
Built-in modules use a hybrid native+JS pattern:
- Rust native bridge: Low-level operations via
#[rquickjs::module], registered as __wasm_rquickjs_builtin/<name>_native
- JS wrapper: High-level Node.js-compatible API surface, imports from the native bridge
Checklist
1. Create the Rust native module
File: crates/wasm-rquickjs/skeleton/src/builtin/<name>.rs
use rquickjs::module::ModuleDef;
use rquickjs::prelude::*;
pub const <NAME>_JS: &str = include_str!("<name>.js");
pub const REEXPORT_JS: &str = r#"export { default } from 'node:<name>'; export * from 'node:<name>';"#;
pub const WIRE_JS: &str = "";
#[rquickjs::module]
mod native_module {
#[rquickjs::function]
pub fn example_function() -> String {
"hello".to_string()
}
}
pub use native_module::js_native_module;
2. Create the JS wrapper
File: crates/wasm-rquickjs/skeleton/src/builtin/<name>.js
import { example_function } from '__wasm_rquickjs_builtin/<name>_native';
const result = example_function();
export { result };
export default { result };
3. Add dependencies (if needed)
Edit crates/wasm-rquickjs/skeleton/Cargo.toml_:
- Always use
default-features = false for crates that may pull in C/native libraries
- Use pure-Rust backends (e.g.,
rust_backend feature) for wasm32-wasip2 compatibility
4. Register in mod.rs
File: crates/wasm-rquickjs/skeleton/src/builtin/mod.rs
Four places need updating:
a) Add the module declaration (top of file):
mod <name>;
b) Add to add_module_resolvers — register the internal native path and public names:
.with_module("__wasm_rquickjs_builtin/<name>_native")
.with_module("node:<name>")
.with_module("<name>")
c) Add to module_loader (ModuleLoader) — map native path to Rust module:
.with_module("__wasm_rquickjs_builtin/<name>_native", <name>::js_native_module)
d) Add to module_loader (BuiltinLoader) — map public names to JS constants:
.with_module("node:<name>", <name>::<NAME>_JS)
.with_module("<name>", <name>::REEXPORT_JS)
e) (Optional) Add to wire_builtins — if the module needs to attach globals:
writeln!(result, "{}", <name>::WIRE_JS).unwrap();
5. Update README.md
Add the new module to the supported APIs list in README.md.
Critical Gotchas
#[rquickjs::module(rename = ...)] does NOT affect JS imports
The rename attribute (e.g., rename = "camelCase") only affects Rust-side naming. JavaScript import statements must always use the original Rust snake_case names. Importing the camelCase version causes "Could not find export" errors.
u32 return value truncation
rquickjs may deliver u32 as signed i32 to JavaScript (e.g., 0xFFFFFFFF → -1). In the JS wrapper, apply >>> 0:
const result = native_fn() >>> 0;
CJS default export immutability
Use Object.defineProperty with writable: false, configurable: false on default export objects to emulate Node.js behavior where module constants are immutable.
WASM-compatible dependencies
Always use default-features = false for crates that may pull in C libraries. Use pure-Rust backends to ensure wasm32-wasip2 target compatibility.
No platform-conditional code
The skeleton is always compiled to wasm32-wasip1. Never write conditional code that checks for unix/windows/macOS or any other host platform (e.g., #[cfg(unix)], #[cfg(windows)], #[cfg(target_os = "...")], process.platform === "win32", path.sep === "\\", etc.). Such checks are meaningless in the WASM target and add dead code complexity.
node:http transport rule
Never use a loopback transport for node:http. Every node:http client request MUST go through wasi:http (the native Rust NodeHttpClientRequest). Do NOT add any fallback that bypasses wasi:http by creating direct node:net socket connections for loopback/localhost addresses.
Testing
- Define an example in
examples/runtime/ (JS file + WIT interface pair)
- Add tests in
tests/runtime.rs that use the example
- Run:
cargo test --test runtime <module> -- --nocapture
If also adding node_compat coverage, use the fixing-node-compat-test skill for that workflow.