Step-by-step guide for adding new built-in JavaScript modules to mikrojs. Use this skill whenever the user wants to add a new JS-level module (not a raw C module), create a higher-level API wrapper around native C bindings, add a new "mikro/xxx" import, or asks about the TypeScript-to-bytecode pipeline, esbuild bundling, qjsc compilation, or how built-in modules are registered.
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Step-by-step guide for adding new built-in JavaScript modules to mikrojs. Use this skill whenever the user wants to add a new JS-level module (not a raw C module), create a higher-level API wrapper around native C bindings, add a new "mikro/xxx" import, or asks about the TypeScript-to-bytecode pipeline, esbuild bundling, qjsc compilation, or how built-in modules are registered.
Adding a Built-in JS Module to mikrojs
Built-in JS modules provide higher-level APIs on top of native C modules. They're written in TypeScript, bundled with esbuild, compiled to bytecode with qjsc, and embedded in the firmware. Users import them as mikrojs/xxx.
There are two types:
Core builtins (mikrojs/xxx) - part of every firmware, compiled via CMake
Driver/board builtins (@mikrojs/driver-xxx) - registered via MIK_REGISTER_BUILTIN, only in firmware that includes the driver
Pipeline Overview
TypeScript source (.ts)
-> esbuild bundle + minify (.js) [scripts/bundle-runtime.js]
-> qjsc bytecode compile (.h) [scripts/compile-bytecode.sh]
-> #include in builtins.cpp [core] or MIK_REGISTER_BUILTIN [driver]
-> Available at import("mikro/xxx") or import("@mikrojs/driver-xxx")
The pipeline is driven by the mikrojs_generate_bytecode() CMake function, which handles both the bundle and compile steps.
// packages/@mikrojs/native/runtime/example/example.tsimport * as native from'native:example'import {typeResult, err, ok} from'mikro/result'exportfunctionread(channel: number): Result<number, {type: 'ReadFailed'}> {
const result = native.read(channel)
if (!result.ok) returnerr({type: 'ReadFailed'asconst})
returnok(result.value)
}
Key rules:
Import native bindings from native:xxx (internal, not user-facing)
Import other builtins from mikrojs/xxx
Both native:* and mikrojs/* are marked external by esbuild
Keep code minimal; every byte becomes firmware flash
2. If needed: create the native C module
Follow the add-native-module skill to create the native:xxx native module with MIK_REGISTER_MODULE().
3. Add to the RUNTIME_MODULES list
packages/@mikrojs/native/CMakeLists.txt and packages/@mikrojs/firmware/components/mikrojs/CMakeLists.txt both call mikrojs_generate_bytecode() with the module list:
Add your module name to the MODULES list in both files.
4. Include in builtins.cpp
packages/@mikrojs/native/src/builtins.cpp - Add the include and table entry:
// Add with the other gen/ includes (alphabetical order)#include"gen/mikrojs_example.h"// Add to the builtins[] table (alphabetical order)staticconstmik_builtin_t builtins[] = {
// ...
{"mikro/example", mikrojs_example_bytecode, mikrojs_example_bytecode_size},
// ...
{NULL, NULL, 0},
};
5. Add type declarations (optional but recommended)
Create type declarations so TypeScript users get autocomplete.