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

npm Node License: MIT

FeaturesQuick StartInstallationUsageAPIMappingCLIPerformance

⚙️ 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-state
const { 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 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 [success, err] = lua.eval(`
  local success, err = pcall(throwError);
  return success, err
`);
console.log(success); // false
console.log(err); // 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"

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.

  • 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

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

🌐 GitHub📦 npm