lua-state
v1.1.3
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.4) 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.4 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");
const lua = new LuaState();
lua.setGlobal("name", "World");
const result = lua.eval('return "Hello, " .. name');
console.log(result); // → "Hello, World"📦 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 result = lua.eval(`
local ok, err = pcall(throwError);
return { ok, err }
`);
console.log(result); // { 1: false, 2: "Error: Something went wrong" }Get 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"🕒 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
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
Core Methods
| Method | Description | Returns |
| ------------------------ | ------------------- | ------------------------------- |
| eval(code) | Execute Lua code | LuaValue |
| evalFile(path) | Run Lua file | LuaValue |
| setGlobal(name, value) | Set global variable | this |
| getGlobal(path) | Get global value | LuaValue \| null \| undefined |
| getLength(path) | Get length of table | number \| null \| undefined |
| getVersion() | Get Lua version | 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.4, 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
