Skip to content

quaternion/node-lua-state

Repository files navigation

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.

npm Node License: MIT

FeaturesQuick StartInstallationUsageAPIMappingCLIPerformance

⚙️ 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-state
const { 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 all

Call 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")); // 3

File 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.

  • await is not required and not supported - calls like lua.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 JS Date becomes a number in Lua, but that number won’t automatically
convert back into a Date when 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

install If you need to rebuild with a different Lua version or use your system Lua installation, you can do it with the included CLI tool:
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 system mode (cannot be built from source).

run

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

🌐 GitHub📦 npm