A small, fast serial port library for Node.js with a Zig-backed native core. Prebuilt native binaries for all platforms are bundled into the package, so you probably can just install and go without any build process after install.
npm install @liminal-machines-co/serialimport { SerialPort, ReadlineParser } from "@liminal-machines-co/serial";
const port = new SerialPort({ path: "/dev/ttyUSB0", baudRate: 115200 });
await port.open();
port.pipe(new ReadlineParser()).on("data", (line) => console.log(line));
await port.write("AT\r\n");
const ports = await SerialPort.list(); // [{ path, portType }, ...]A SerialPort is a Node Readable, so everything you know about streams
applies. See examples/ for runnable scripts.
List ports without writing any code:
npx liminal-serial list # table of available ports
npx liminal-serial list --json # machine-readable JSONPATH TYPE
/dev/cu.usbserial-1420 cu
/dev/cu.Bluetooth-Incoming-Port cu
Transform streams you compose onto a port with .pipe():
ReadlineParser— split on a delimiter (default\n)ByteLengthParser— fixed-sizelength-byte framesInterByteTimeoutParser— emit after an inter-byte gapRegexParser— split on a pattern
MockSerialPort is a drop-in for SerialPort, so
you can test your serial logic with no device attached:
import { MockSerialPort } from "@liminal-machines-co/serial";
const port = new MockSerialPort({ path: "/dev/mock", baudRate: 9600 });
port.mockReply("PING", "PONG\n");
await port.open();
await port.write("PING"); // emits "PONG\n" on 'data'
port.getWrittenData(); // Buffer of everything written
port.simulateFault("disconnect");| Platform | Enumeration | Serial IO |
|---|---|---|
| macOS (arm64, x64) | ✅ | ✅ |
| Linux (x64, arm64) | ✅ | ✅ |
| Windows (x64) | ⏳ planned | ⏳ planned (loads; methods throw) |
Windows cross-compiles and loads today (its import library is generated from
node_api.def), but native serial IO and port enumeration aren't wired up yet —
a great first contribution if you're on Windows.
You'll need Zig 0.16.0 and Node ≥ 18.
npm install
npm run build:native # build the addon for your host -> prebuilds/
npm run build:prebuilds # cross-compile every targetReleases go out via npm version + a git tag — see RELEASING.md.
Architecture, decisions, and conventions live in CLAUDE.md.
Tests run on the Bun runner in three suites:
npm test # unit: mock + parsers, pure JS, no hardware
npm run test:integration # native addon over a socat PTY pair (needs socat + a host build)
npm run test:hardware # against a real device (opt-in, see below)
npm run typecheck:test # type-check the test sources-
Unit (
src/**/*.test.ts) — mock and parsers -
Integration (
test/integration/) — drives the real native binding through a virtual serial pair made withsocat, and checks the line config (dataBits/parity/stopBits) reaches the device viastty. Runnpm run build:nativefirst; skips automatically ifsocator a binary is missing. -
Hardware (
test/hardware/) — an opt-in suite that talks to a real device flashed with the firmware inarduino/test-device/. Point it at a port to run it:SERIAL_TEST_PORT=/dev/cu.usbmodem1101 npm run test:hardware
With
SERIAL_TEST_PORTunset it skips, so it never runs in CI or by accident. It exercises the firmware protocol (PING→PONG, ID, ECHO, LINES, BINARY) through the publicSerialPortAPI.
src/napi/*.zig— the native addon.root.zigregisters the module;serial_port_posix.zigimplementsNativeSerialPort(background read thread → threadsafe function; async-work Promises for write/drain);enumerate.ziglists ports by scanning/dev.src/*.ts— the JS layer:SerialPort(aReadable),MockSerialPort, parsers, and option validation.index.js— loads the right prebuilt.nodevianode-gyp-build.- Line config uses ZigEmbeddedGroup/serial; N-API headers come from node-api-headers.
MIT
