npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lua-state

v1.3.0

Published

Native Lua & LuaJIT bindings for Node.js (5.1-5.5)

Downloads

258

Readme

lua-state

npm Node License: MIT

Native Lua & LuaJIT bindings for Node.js. Create Lua VMs, execute code, share values between languages - no compiler required when using 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 - 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 limits the depth of nested C calls (LUAI_MAXCCALLS, default 200) in every supported Lua version and in LuaJIT. Deeply recursive chains of JS↔Lua calls (e.g. a Lua function calling a JS function that calls back into Lua) can hit this limit and raise Lua's stack overflow error — regardless of the Lua version used.

🧩 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

💡 Library availability depends on the Lua version: bit32 is available in Lua 5.2–5.3, utf8 in Lua 5.3+, and in Lua 5.1 coroutine is part of the base library. Unknown library names are silently ignored.

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 |

LuaStateError Class

Errors thrown by the JavaScript API layer (not from Lua execution) are represented as LuaStateError instances. For example, calling a method on a closed LuaState.

Properties

| Property | Type | Description | | --------- | ---------------------- | ------------------------------------------------------------- | | name | "LuaStateError" | Error name | | message | string | Error message | | code | string | Machine-readable error code, e.g. "ERR_LUA_STATE_CLOSED" |

🔄 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

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 | | --download-dir <path> | Directory to store downloaded sources | npm cache | | --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 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_DOWNLOAD_DIR | Directory to store downloaded Lua sources | npm cache | | LUA_STATE_DEBUG | Build with debugging enabled | false | | LUA_STATE_PREBUILD | Build into standard prebuilds/ layout | - | | LUA_STATE_OUT | Copy binary to an exact path (low-level) | - | | LUA_STATE_REPOSITORY | GitHub repository for prebuilt binaries | quaternion/node-lua-state |

🔍 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