Skip to content

Navigation Menu

Sign in
Sign up

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 when using prebuilt binaries.

npm Node License: MIT

FeaturesQuick StartInstallationUsageAPIMappingCLIDownstream PackagesPerformance

⚙️ 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 - Built with N-API (no WebAssembly)

⚡ Quick Start

npm install lua-state
const { LuaState } = require("lua-state");
const lua = new LuaState();
lua.setGlobal("x", 10);
const result = lua.eval("return x * 2");
console.log(result); // 20
lua.close();

Lua runs synchronously in the same thread as Node.js and blocks the event loop during execution. This means long-running Lua code will block all JavaScript execution.

📦 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 automatically falls back to building the official Lua sources (the same as --mode=official).

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

lua.getVersion(); // "Lua 5.4.8" or "LuaJIT 2.1.0-beta3"

Evaluate Lua Code

lua.eval("return 2 + 2"); // 4
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 }");
lua.getGlobal("config"); // { debug: true, port: 8080 }
lua.getGlobal("config.port"); // 8080
lua.getGlobal("config.missing"); // undefined (path exists but value is missing)
lua.getGlobal("missing"); // null (global variable does not exist)

Call Functions Both Ways

// Call Lua from JS
lua.eval("function add(a, b) return a + b end");
const add = lua.getGlobal("add");
add(5, 7); // 12
// Call JS from Lua
lua.setGlobal("add", (a, b) => a + b);
lua.eval("return add(3, 4)"); // 12
// JS function with multiple returns
lua.setGlobal("getUser", () => ["Alice", 30]);
lua.eval("name, age = getUser()");
lua.getGlobal("name"); // "Alice"
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
`);
success; // false
err.message; // "Something went wrong"

Get Table Length

lua.eval("items = { 1, 2, 3 }");
lua.getLength("items"); // 3

File Execution

-- config.lua
return {
 title = "My App",
 features = { "auth", "api", "db" }
}
const config = lua.evalFile("config.lua");
config.title; // "My App"

Lua Errors

// All errors are instances of LuaError
// Syntax error
try {
 lua.eval("return 1+");
} catch (err) {
 err instanceof LuaError; // true
 err.message; // [string "return 1+"]:1: unexpected symbol near <eof>
}
// String error
try {
 lua.eval('error("foo")');
} catch (err) {
 err.message; // [string "error(\"foo\")"]:1: foo
 err.stack; // Lua-style stack trace (not a JavaScript stack)
}
// Table error (non-string)
try {
 lua.eval('error({ foo = "bar" })');
} catch (err) {
 err.message; // ""
 err.cause; // { foo: "bar" }
}

🕒 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 part of API - 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
close() void Close Lua VM

⚠️ Note on close():
Lua VM memory is not managed by the JavaScript garbage collector.
It is recommended to call close() when the instance is no longer needed to avoid holding native memory.
Calling close() multiple times has no effect.
Any method call after close() will throw an error.

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 preserved during conversion.

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 (not converted back to Date)
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 POJO (array-like tables are NOT converted to JavaScript arrays)
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.

⚠️ When Lua returns multiple values, they are returned as an array in JavaScript.

🧩 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

build 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 build [options]

Options:

The build system is based on node-gyp and supports flexible integration with existing Lua installations.

Option Description Default
-m, --mode download, official, source, system download
--skip-if-exists Skip build if a binary already exists false
-v, --version Lua version for download build 5.4.8
--source-dir, --include-dirs, --libraries Custom paths for source/system builds -
--prebuild [path] Build into standard prebuilds/ layout -
--out <path> Copy binary to an exact path (low-level) -

Examples:

# Rebuild Lua 5.2.4 from official sources
npx lua-state build --mode=official --version=5.2.4
# Build into prebuilds/ structure for the current platform
npx lua-state build --mode=source --source-dir=deps/lua-5.2.1/src --prebuild
# Build into a custom base directory (instead of cwd)
npx lua-state build --mode=source --source-dir=deps/lua-5.2.1/src --prebuild ./dist
# Copy binary to an exact path
npx lua-state build --out ./prebuilds/linux-x64/lua-state.glibc.node
# Rebuild with system Lua
npx lua-state build --mode=system --libraries=-llua5.4 --include-dirs=/usr/include/lua5.4
# Rebuild with system or prebuilt LuaJIT
npx lua-state build --mode=system --libraries=-lluajit-5.1 --include-dirs=/usr/include/luajit-2.1
# Rebuild with custom lua sources
npx lua-state build --mode=source --source-dir=deps/lua-5.1/src
# Skip the build if a binary already exists (used by the npm install hook)
npx lua-state build --skip-if-exists

💡 --prebuild generates the standard prebuilds/{platform}-{arch}/lua-state[.glibc|.musl].node path for the current platform (Linux binaries get a glibc/musl tag). The result is compatible with node-gyp-build and prebuildify. Has priority over --out.

💡 By default build always produces a fresh binary. --skip-if-exists only skips the rebuild when build/Release/lua-state.node is already present; --out/--prebuild still copy the existing binary.

💡 In download mode, if no prebuilt binary exists for your platform/architecture/Lua version, the build automatically falls back to official mode - it downloads the matching Lua sources and compiles them (requires a C++ toolchain).

⚠️ 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

📦 Downstream Packages

Most downstream packages use lua-state only during the build - to generate schemas, serialize data, or run code-generation against the Lua runtime - so install it as a devDependency. Nothing native ships to consumers: the published artifact is plain generated output, with no binary and no runtime dependency on lua-state.

If your deliverable also embeds the binary at runtime (e.g. a custom Lua version), follow the steps below: build with --prebuild, load through node-gyp-build, and forward the copied types.

Building the binary

Use the build command with --prebuild to compile a specific Lua version from official sources into the standard prebuilds/ layout for the current platform:

{
 "scripts": {
 "build:lua": "lua-state build --mode=official --version=5.2.1 --prebuild"
 }
}

For a vendored/patched Lua source tree use --mode=source --source-dir=deps/lua-5.2.1/src; LuaJIT requires --mode=system (see CLI notes).

This produces prebuilds/{platform}-{arch}/lua-state[.glibc|.musl].node, compatible with what node-gyp-build expects at load time.

Loading the binary

Since the generated layout matches what node-gyp-build expects, loading is a one-liner (__dirname is the package root where package.json and prebuilds/ live):

// index.js
module.exports = require("node-gyp-build")(__dirname);

Forwarding types (TypeScript)

node-gyp-build returns any, so forward the copied declarations to keep full typing. Wire everything through a src/lua-state/ module:

src/lua-state/
├── lua-state.d.ts # copied from lua-state types
└── index.ts # loads the binary + forwards types

1. Copy the self-contained declarations (the main case is lua-state in devDependencies):

cp node_modules/lua-state/types/lua-state.d.ts src/lua-state/

⚠️ tsc does not emit .d.ts files, so copy the declaration into your build output as well. Otherwise export type * from './lua-state' breaks for consumers of the published package:

"scripts": {
 "build": "tsc && cp src/lua-state/lua-state.d.ts dist/lua-state/"
}

2. Load the binary and cast it to the module's shape (path.resolve(__dirname, '..', '..') is the package root where node-gyp-build looks for prebuilds/):

// src/lua-state/index.ts
const path = require("node:path");
import type * as LuaTypes from "./lua-state";
const binding = require("node-gyp-build")(
 path.resolve(__dirname, "..", ".."),
) as typeof LuaTypes;
export const LuaState = binding.LuaState;
export const LuaError = binding.LuaError;
export type LuaState = LuaTypes.LuaState;
export type LuaError = LuaTypes.LuaError;
export type * from "./lua-state";

The explicit export type aliases merge the class types with the local const exports; export type * forwards the remaining type-only exports (LuaStateOptions, LuaPrimitive, LuaValue, LuaFunction, LuaTable, LuaLibName).

Consumers get a fully typed API: import { LuaState, LuaError, type LuaStateOptions } from '../lua-state'.

For distributing a single consolidated .d.ts, pipe the declarations through @microsoft/api-extractor.

🌍 Environment Variables

These variables can be used for CI/CD or custom build scripts.

Variable Description Default
LUA_STATE_MODE Build mode (download, official, source, system) download
LUA_STATE_SKIP_IF_EXISTS Skip build if binary already exists 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) -
LUA_STATE_PREBUILD Build into standard prebuilds/ layout -
LUA_STATE_OUT Copy binary to an exact path (low-level) -

🔍 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

lua-state uses native N-API bindings and provides low-overhead communication between JavaScript and Lua.

Performance depends heavily on the type of data being exchanged:

  • Primitive values are extremely fast
  • Flat objects are moderately fast
  • Deep or large object graphs are significantly more expensive to serialize

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

About

Run real Lua (5.1–5.5 & LuaJIT) in Node.js

Topics

Resources

Contributing

Stars

16 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /