lua-state
v1.2.0
Published
Run real Lua (5.1-5.4 & LuaJIT) inside Node.js - native N-API bindings with prebuilt binaries and full TypeScript support.
Maintainers
Readme
lua-state - Native Lua & LuaJIT bindings for Node.js
Embed real Lua (5.1-5.5) and LuaJIT in Node.js with native N-API bindings. Create Lua VMs, execute code, share values between languages - no compiler required with prebuilt binaries.
⚙️ Features
- ⚡ Multiple Lua versions - Supports Lua 5.1–5.5 and LuaJIT
- 🧰 Prebuilt Binaries - Lua 5.4.8 included for Linux/macOS/Windows
- 🔄 Bidirectional integration - Call Lua from JS and JS from Lua
- 📦 Rich data exchange - Objects, arrays, functions in both directions
- 🎯 TypeScript-ready - Full type definitions included
- 🚀 Native performance - N-API bindings, no WebAssembly overhead
⚡ Quick Start
npm install lua-stateconst { LuaState } = require("lua-state");
// Create a real Lua VM inside Node.js
const lua = new LuaState();
// Expose a JS function to Lua
lua.setGlobal("getUser", () => ["Alice", 30]);
// Run Lua code that calls JS and returns values back to JS
const result = lua.eval(`
local name, age = getUser()
return {
greeting = "Hello, " .. name,
nextAge = age + 1
}
`);
console.log(result); // { greeting: "Hello, Alice", nextAge: 31 }Lua runs synchronously in the same thread as Node.js - no promises, no async bridge, no WASM.
📦 Installation
Prebuilt binaries are currently available for Lua 5.4.8 and downloaded automatically from GitHub Releases. If a prebuilt binary is available for your platform, installation is instant - no compilation required. Otherwise, it will automatically build from source.
Requires Node.js 18+, tar (system tool or npm package), and a valid C++ build environment (for node-gyp) if binaries are built from source.
Tip: if you only use prebuilt binaries you can reduce install size with
npm install lua-state --no-optional.
🧠 Basic Usage
const lua = new LuaState();Get Current Lua Version
console.log(lua.getVersion()); // "Lua 5.4.8"Evaluate Lua Code
console.log(lua.eval("return 2 + 2")); // 4
console.log(lua.eval('return "a", "b", "c"')); // ["a", "b", "c"]Share Variables
// JS → Lua
lua.setGlobal("user", { name: "Alice", age: 30 });
// Lua → JS
lua.eval("config = { debug = true, port = 8080 }");
console.log(lua.getGlobal("config")); // { debug: true, port: 8080 }
console.log(lua.getGlobal("config.port")); // 8080
console.log(lua.getGlobal("config.missing")); // undefined - path exists but field is missing
console.log(lua.getGlobal("missing")); // null - global variable does not exist at allCall Functions Both Ways
// Call Lua from JS
lua.eval("function add(a, b) return a + b end");
const add = lua.getGlobal("add");
console.log(add(5, 7)); // 12
// Call JS from Lua
lua.setGlobal("add", (a, b) => a + b);
console.log(lua.eval("return add(3, 4)")); // 12
// JS function with multiple returns
lua.setGlobal("getUser", () => ["Alice", 30]);
lua.eval("name, age = getUser()");
console.log(lua.getGlobal("name")); // "Alice"
console.log(lua.getGlobal("age")); // 30
// JS function that throws an error
lua.setGlobal("throwError", () => {
throw new Error("Something went wrong");
});
const [success, err] = lua.eval(`
local success, err = pcall(throwError);
return success, err
`);
console.log(success); // false
console.log(err); // Error: Something went wrongGet Table Length
lua.eval("items = { 1, 2, 3 }");
console.log(lua.getLength("items")); // 3File Execution
-- config.lua
return {
title = "My App",
features = { "auth", "api", "db" }
}const config = lua.evalFile("config.lua");
console.log(config.title); // "My App"Lua Errors
// Syntax error
try {
lua.eval("return 1+");
} catch (err) {
console.log(err instanceof LuaError); // true
console.log(err.message); // => [string "return 1+"]:1: unexpected symbol near <eof>
}
// String error
try {
lua.eval('error("foo")');
} catch (err) {
console.log(err instanceof LuaError); // true
console.log(err.message); // => "[string "error("foo")"]:1: foo"
console.log(err.stack); // Lua stack traceback
}
// Table error (non-string)
try {
lua.eval('error({ foo = "bar" })');
} catch (err) {
console.log(err instanceof LuaError); // true
console.log(err.message); // ""
console.log(err.cause); // { foo: "bar" }
console.log(err.stack); // Lua stack traceback
}🕒 Execution Model
All Lua operations in lua-state are synchronous by design. The Lua VM runs in the same thread as JavaScript, providing predictable and fast execution. For asynchronous I/O, consider isolating Lua VMs in worker threads.
awaitis not required and not supported - calls likelua.eval()block until completion- Lua coroutines work normally within Lua, but are not integrated with the JavaScript event loop
- Asynchronous bridging between JS and Lua is intentionally avoided to keep the API simple, deterministic, and predictable.
⚠️ Note: Lua 5.1 and LuaJIT have a small internal C stack, which may cause stack overflows when calling JS functions in very deep loops. Lua 5.1.1+ uses a larger stack and does not have this limitation.
🧩 API Reference
LuaState Class
Represents an isolated, synchronous Lua VM instance.
new LuaState(options?: {
libs?: string[] | null // Libraries to load, use null or empty array to load none (default: all)
})Available libraries: base, bit32, coroutine, debug, io, math, os, package, string, table, utf8
Methods
| Method | Returns | Description |
| ------------------------ | ------------------------------- | ------------------- |
| eval(code) | LuaValue | Execute Lua code |
| evalFile(path) | LuaValue | Run Lua file |
| setGlobal(name, value) | this | Set global variable |
| getGlobal(path) | LuaValue \| null \| undefined | Get global value |
| getLength(path) | number \| null \| undefined | Get length of table |
| getVersion() | string | Get Lua version |
LuaError Class
Errors thrown from Lua are represented as LuaError instances.
Properties
| Property | Type | Description |
| --------- | ---------------------- | ---------------------------------------------------------------------- |
| name | "LuaError" | Error name |
| message | string | Error message (empty if a non-string value was passed to error(...)) |
| stack | string \| undefined | Lua stack traceback (not a JavaScript stack trace) |
| cause | unknown \| undefined | Value passed to error(...) when it is not a string |
🔄 Type Mapping (JS ⇄ Lua)
When values are passed between JavaScript and Lua, they’re automatically converted according to the tables below. Circular references are supported internally and won’t cause infinite recursion.
JavaScript → Lua
| JavaScript Type | Becomes in Lua | Notes |
| --------------- | -------------- | --------------------------------------------------------------------------- |
| string | string | UTF-8 encoded |
| number | number | 64-bit double precision |
| boolean | boolean | |
| Date | number | Milliseconds since Unix epoch |
| undefined | nil | |
| null | nil | |
| Function | function | Callable from Lua |
| Object | table | Recursively copies enumerable fields. Non-enumerable properties are ignored |
| Array | table | Indexed from 1 in Lua |
| BigInt | string | |
Lua → JavaScript
| Lua Type | Becomes in JavaScript | Notes |
| ---------- | --------------------- | ----------------------------------- |
| string | string | UTF-8 encoded |
| number | number | 64-bit double precision |
| boolean | boolean | |
| nil | null | |
| table | object | Converts to plain JavaScript object |
| function | function | Callable from JS |
⚠️ Note: Conversion is not always symmetrical - for example,
a JSDatebecomes a number in Lua, but that number won’t automatically
convert back into aDatewhen returned to JS.
🧩 TypeScript Support
This package provides full type definitions for all APIs.
You can optionally specify the expected Lua value type for stronger typing and auto-completion:
import { LuaState } from "lua-state";
const lua = new LuaState();
const anyValue = lua.eval("return { x = 1 }"); // LuaValue | undefined
const numberValue = lua.eval<number>("return 42"); // number🧰 CLI
npx lua-state install [options]Options:
The build system is based on node-gyp and supports flexible integration with existing Lua installations.
| Option | Description | Default |
| ----------------------------------------------- | ----------------------------------------- | ---------- |
| -m, --mode | download, source, or system | download |
| -f, --force | Force rebuild | false |
| -v, --version | Lua version for download build | 5.4.8 |
| --source-dir, --include-dirs, --libraries | Custom paths for source/system builds | - |
Examples:
# Rebuild with Lua 5.2.4
npx lua-state install --force --version=5.2.4
# Rebuild with system Lua
npx lua-state install --force --mode=system --libraries=-llua5.4 --include-dirs=/usr/include/lua5.4
# Rebuild with system or prebuilt LuaJIT
npx lua-state install --force --mode=system --libraries=-lluajit-5.1 --include-dirs=/usr/include/luajit-2.1
# Rebuild with custom lua sources
npx lua-state install --force --mode=source --source-dir=deps/lua-5.1/src⚠️ Note: LuaJIT builds are only supported in
systemmode (cannot be built from source).
Run a Lua script file or code string with the CLI tool:
npx lua-state run [file]Options:
| Option | Description | Default |
| ----------------------- | --------------------------------------- | ------- |
| -c, --code <code> | Lua code to run as string | - |
| --json | Output result as JSON | false |
| -s, --sandbox [level] | Run in sandbox mode (light, strict) | - |
Examples:
# Run a Lua file
npx lua-state run script.lua
# Run Lua code from string
npx lua-state run --code "print('Hello, World!')"
# Run and output result as JSON
npx lua-state run --code "return { name = 'Alice', age = 30 }" --json
# Run in sandbox mode (light restrictions)
npx lua-state run --sandbox light script.lua
# Run in strict sandbox mode (heavy restrictions)
npx lua-state run --sandbox strict script.lua🌍 Environment Variables
These variables can be used for CI/CD or custom build scripts.
| Variable | Description | Default |
| ----------------------- | ------------------------------------------- | ---------- |
| LUA_STATE_MODE | Build mode (download, source, system) | download |
| LUA_STATE_FORCE_BUILD | Force rebuild | false |
| LUA_VERSION | Lua version (for download mode) | 5.4.8 |
| LUA_SOURCE_DIR | Lua source path (for source mode) | - |
| LUA_INCLUDE_DIRS | Include directories (for system mode) | - |
| LUA_LIBRARIES | Library paths (for system mode) | - |
🔍 Compared to other bindings
| Package | Lua versions | TypeScript | API Style | Notes | | ------------- | -------------------- | ---------- | ------------------- | -------------------------------------------- | | fengari | 5.2 (WASM) | ❌ | Pure JS | Browser-oriented, slower | | lua-in-js | 5.3 (JS interpreter) | ✅ | Pure JS | No native performance | | wasmoon | 5.4 (WASM) | ✅ | Async/Promise | Node/Browser compatible | | node-lua | 5.1 | ❌ | Native (legacy NAN) | Outdated, Linux-only | | lua-native | 5.4 (N-API) | ✅ | Native N-API | Active project, no multi-version support | | lua-state | 5.1–5.5, LuaJIT | ✅ | Native N-API | Multi-version, prebuilt binaries, modern API |
⚡ Performance
Benchmarked on Lua 5.4.8 (Ryzen 7900X, Debian Bookworm, Node.js 24):
| Benchmark | Iterations | Time (ms) | | ------------------------ | ---------- | --------- | | Lua: pure computation | 1,000,000 | ≈ 3.8 | | JS → Lua calls | 50,000 | ≈ 4.3 | | Lua → JS calls | 50,000 | ≈ 6.4 | | JS → Lua data transfer | 50,000 | ≈ 135.0 | | Lua → JS data extraction | 50,000 | ≈ 62.5 |
To run the benchmark locally:
npm run bench
🧪 Quality Assurance
Each native binary is built and tested automatically before release.
The test suite runs JavaScript integration tests to ensure stable behavior across supported systems.
🪪 License
MIT License © quaternion
