From 4acfee4200a49ee5a01093d1b75fc6f16ef29c18 Mon Sep 17 00:00:00 2001 From: Maurice Schmicking <17197791+mschmicking@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:26:41 +0200 Subject: [PATCH] docs: split the reference out of the README into docs/ The README was 353 lines, 219 of them a single API code block. That block was also the worst part of it: one continuous listing of JSDoc comments, which cannot be linked to, scanned, or read on a phone. Splits it into docs/ and rewrites the reference as real prose with per-method sections, grouped by what you are trying to do rather than by declaration order. Adds the type-conversion tables and the stack-index rules, which were previously only discoverable by reading the tests. docs/api.md full reference, with its own table of contents docs/migrating-to-2.0.md the breaking-changes table and what to check docs/troubleshooting.md build failures, including the Windows node-gyp and npm-12-blocks-scripts cases users will actually hit docs/development.md layout, CI, and the release process README keeps what someone needs to decide whether to use this and get it running: what it is, install, quick start, links, caveats. Now 100 lines with a table of contents. Documentation links are absolute GitHub URLs so they also work when the README is rendered on npmjs.com, where relative links break. docs/ is not in the files allowlist, so the tarball does not grow. Co-Authored-By: Claude Opus 5 --- README.md | 332 +++++------------------------------- docs/api.md | 351 +++++++++++++++++++++++++++++++++++++++ docs/development.md | 55 ++++++ docs/migrating-to-2.0.md | 43 +++++ docs/troubleshooting.md | 71 ++++++++ 5 files changed, 559 insertions(+), 293 deletions(-) create mode 100644 docs/api.md create mode 100644 docs/development.md create mode 100644 docs/migrating-to-2.0.md create mode 100644 docs/troubleshooting.md diff --git a/README.md b/README.md index 07ed53b..10a8a53 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ Lua and [LuaFileSystem](https://github.com/lunarmodules/luafilesystem) are compi the addon, so there is no system Lua to install and nothing to configure — `npm install` builds everything from source on Linux, macOS and Windows, on both x64 and ARM64. +- [Installation](#installation) +- [Quick start](#quick-start) +- [Documentation](#documentation) +- [Examples](#examples) +- [How it works](#how-it-works) +- [Caveats](#caveats) +- [License](#license) + ## Installation ``` @@ -18,19 +26,11 @@ The addon is compiled at install time, so you need a working C/C++ toolchain: - **macOS** — the Xcode Command Line Tools (`xcode-select --install`) - **Windows** — the "Desktop development with C++" workload from Visual Studio Build Tools -Nothing else. Earlier versions needed you to build and install LuaJIT yourself on Linux; that is -no longer the case. +Nothing else. Node.js 18 or newer. -> **Windows with Visual Studio 2026:** `node-gyp` 11.x cannot detect that version and fails with -> `find VS unknown version "undefined"`. It is what npm bundles on Node.js 20 and 22. Install -> node-gyp 12 and point npm at it — note the explicit `@12`, since `@latest` still resolves to -> 11.x on those Node versions: -> -> ``` -> npm install -g node-gyp@12 -> set npm_config_node_gyp=%APPDATA%\npm\node_modules\node-gyp\bin\node-gyp.js -> npm install node-lua-runner -> ``` +If the install fails, see +[troubleshooting](https://github.com/mschmicking/node-lua-runner/blob/master/docs/troubleshooting.md) +— the common ones are Visual Studio 2026 on Windows and npm 12 blocking build scripts. ## Quick start @@ -55,300 +55,46 @@ lua.DoString('print("2 + 3 = " .. add(2, 3))'); lua.Close(); ``` -## Breaking changes in 2.0.0 - -2.0.0 is a maintenance release that makes the package build and run on current Node.js. It fixes -several long-standing bugs, and those fixes change behaviour: - -| Change | Before | Now | -|---|---|---| -| `SetField(index, key, value)` | Wrote the *key* into the field, ignoring the value | Writes the value, and resolves a relative `index` before pushing | -| `LoadFile` / `LoadString` | Executed the chunk, identical to `DoFile` / `DoString` | Compile and push the chunk without running it; call `Call(0, 0)` to run it | -| Lua booleans read into JS | Arrived as the numbers `1` and `0` | Arrive as `true` and `false` | -| `Push(3.5)` | Truncated to `3` | Keeps the fractional value | -| `AddPackagePath` | Appended without a separator, corrupting `package.path`, so `require` usually failed | Appends correctly, and no longer breaks on paths containing quotes | -| `ToValue` on a table | Only converted correctly when the table was at the top of the stack | Works at any stack index, including nested tables | -| Calling a method after `Close()` | Use-after-free | Throws | -| `Resume(args)` | Returned `undefined` | Returns the Lua status code | - -Other things worth knowing if you are upgrading: - -- **LuaJIT has been replaced by stock Lua 5.1.5.** Windows builds used to link LuaJIT 2.0.3, so - Windows users lose JIT compilation. In exchange, macOS on Apple Silicon and Linux on ARM64 work - at all, which they previously did not. The Lua C API and language are unchanged. -- **`require('lfs')` now works on every platform.** It used to be a Windows-only prebuilt DLL - loaded through an `LUA_CPATH` hack; LuaFileSystem is now compiled into the addon. -- Node.js 18 or newer is required. +## Documentation -## About - -Built on the [Lua 5.1 C API](https://www.lua.org/manual/5.1/manual.html). The binding uses -[Node-API](https://nodejs.org/api/n-api.html), which is ABI-stable — a compiled build keeps -working across future Node.js releases instead of breaking on each major version. - -**Based on:** -- nodelua ( https://github.com/brettlangdon/NodeLua ) -- node-luajit ( https://github.com/whtiehack/node-luajit ) -- LuaFileSystem ( https://github.com/lunarmodules/luafilesystem ) - -**Features:** -- Low-level API mapping closely onto the Lua C API -- Synchronous only +- [**API reference**](https://github.com/mschmicking/node-lua-runner/blob/master/docs/api.md) — + every method, type conversion, and how stack indices behave +- [**Migrating to 2.0**](https://github.com/mschmicking/node-lua-runner/blob/master/docs/migrating-to-2.0.md) + — what changed and what to check in existing code +- [**Troubleshooting**](https://github.com/mschmicking/node-lua-runner/blob/master/docs/troubleshooting.md) + — build and install problems +- [**Development**](https://github.com/mschmicking/node-lua-runner/blob/master/docs/development.md) + — layout, CI, and how releases are cut +- [**Changelog**](https://github.com/mschmicking/node-lua-runner/blob/master/CHANGELOG.md) ## Examples -- [Simple](https://github.com/mschmicking/node-lua-runner/blob/master/examples/simple/index.js) -- [Using the lua require function](https://github.com/mschmicking/node-lua-runner/blob/master/examples/lua_require/index.js) +- [Simple](https://github.com/mschmicking/node-lua-runner/blob/master/examples/simple/index.js) — + running code, registering a JavaScript function, reading globals +- [Using `require`](https://github.com/mschmicking/node-lua-runner/blob/master/examples/lua_require/index.js) + — loading Lua modules from disk - [Using LuaFileSystem](https://github.com/mschmicking/node-lua-runner/tree/master/examples/lua_lfs) -## API - -```javascript - -const nodelua = require('node-lua-runner'); - -var lua = new nodelua.LuaState(); - - -/** - * [Add a path to the lua package.path variable. Set a root path for lua require (see example)] - * @param {String} path - */ -lua.AddPackagePath(__dirname); - - -/** - * [Loads and runs the given file] - * @type {String} file - * @throws {Exception} - */ -lua.DoFile(__dirname + "/test.lua"); - - -/** - * [Loads and runs the given string] - * @type {String} str - * @throws {Exception} - */ -lua.DoString("print('Hello world!')"); - - -/** - * [Compiles the given file and pushes it onto the stack WITHOUT running it. - * Use Call to run it.] - * @type {String} file - * @throws {Exception} - */ -lua.LoadFile(__dirname + "/test.lua"); - - -/** - * [Compiles the given string and pushes it onto the stack WITHOUT running it. - * Use Call to run it.] - * @type {String} str - * @throws {Exception} - */ -lua.LoadString("print('Hello world!')"); - - -/** - * [Sets the function f as the new value of global name. - * Arguments are read off the Lua stack with ToValue; the return value is the - * number of results the function pushed.] - * @param {String} name [name of the global in lua] - * @param {Function} f [function to set] - */ -lua.RegisterFunction('add', function() { - var a = lua.ToValue(1); - var b = lua.ToValue(2); - lua.Pop(2); - lua.Push(a + b); - return 1; -}); - - -/** - * [Pops a value from the stack and sets it as the new value of global name] - * @type {String} name - */ -lua.SetGlobal("myVar"); - - -/** - * [Pushes onto the stack the value of the global name] - * @type {String} name - */ -lua.GetGlobal('myVar'); - - -/** - * [Does the equivalent to t[k] = v, where t is the value at the given valid index. - * Unlike the raw C API, v is passed as an argument rather than taken from the - * top of the stack.] - * @type {Number} index - * @type {String} key - * @type {*} value - * @throws {Exception} [if the value at index is not a table] - */ -lua.SetField(index, "key", value); - - -/** - * [Pushes onto the stack the value t[key], where t is the value at the given valid index.] - * @type {Number} index - * @type {String} key - * @throws {Exception} [if the value at index is not a table] - */ -lua.GetField(index, "key"); - +## How it works -/** - * [Get value at the given acceptable index] - * @type {Number} index - * @return value - */ -var value = lua.ToValue(-1); +Built on the [Lua 5.1 C API](https://www.lua.org/manual/5.1/manual.html) through +[Node-API](https://nodejs.org/api/n-api.html), which is ABI-stable — a compiled build keeps working +across future Node.js releases instead of breaking on each major version. +The API is low-level and maps closely onto the C API, and it is synchronous throughout. -/** - * [Calls a function. Gets the function and arguments from the stack. Pushes the results onto the stack. See https://www.lua.org/manual/5.1/manual.html#pdf-pcall for more information] - * @type {Number} args - * @type {Number} results - * @throws {Exception} - */ -lua.Call(args, results); - - -/** - * [Yields a coroutine.] - * @type {Number} args - */ -lua.Yield(args); - - -/** - * [Starts and resumes a coroutine in a given thread.] - * @type {Number} args - * @return {Number} [status code] - */ -lua.Resume(args); - - -/** - * [Pushes a value n onto the stack] - * @type n - */ -lua.Push(5); - - -/** - * [Pops n elements from the stack. Default value is 1] - * @type {Number} n - */ -lua.Pop(); -lua.Pop(n); - - -/** - * [Returns the index of the top element in the stack. Because indices start at 1, this result is equal to the number of elements in the stack (and so 0 means an empty stack)] - * @return {Number} - */ -var size = lua.GetTop(); - - -/** - * [Accepts any acceptable index, or 0, and sets the stack top to this index. If the new top is larger than the old one, then the new elements are filled with nil. If index is 0, then all stack elements are removed] - * @type {Number} index - */ -lua.SetTop(index); - - -/** - * [Moves the top element into the given position (and pops it), without shifting any element (therefore replacing the value at the given position)] - * @type {Number} index - */ -lua.Replace(index); - - -/** - * [Returns the status of the state] - * @return {Number} [compare against nodelua.STATUS.*] - */ -lua.Status(); - - -/** - * [Controls the Lua garbage collector] - * @type {Number} what [one of nodelua.GC.*] - * @return {Number} - */ -lua.CollectGarbage(nodelua.GC.COLLECT); - - -/** - * [Destroys the Lua state and frees its memory. Safe to call more than once. - * Any further use of the state throws.] - */ -lua.Close(); - -``` - -### Constants - -```javascript -nodelua.INFO.VERSION // "Lua 5.1" -nodelua.INFO.VERSION_NUM // 501 -nodelua.INFO.COPYRIGHT -nodelua.INFO.AUTHORS - -nodelua.LUA.GLOBALSINDEX // pseudo-index of the globals table -nodelua.LUA.REGISTRYINDEX // pseudo-index of the registry - -nodelua.STATUS.YIELD -nodelua.STATUS.ERRRUN -nodelua.STATUS.ERRSYNTAX -nodelua.STATUS.ERRMEM -nodelua.STATUS.ERRERR - -nodelua.GC.STOP -nodelua.GC.RESTART -nodelua.GC.COLLECT -nodelua.GC.COUNT -nodelua.GC.COUNTB -nodelua.GC.STEP -nodelua.GC.SETPAUSE -nodelua.GC.SETSTEPMUL -``` +Descended from [NodeLua](https://github.com/brettlangdon/NodeLua) and +[node-luajit](https://github.com/whtiehack/node-luajit). ## Caveats This is a thin wrapper over the Lua C API, and it does not shield you from every way of misusing -that API. Some stack operations on values of an unexpected type raise an *unprotected* Lua error, -which aborts the process rather than throwing a JavaScript exception. `SetField` and `GetField` -guard against this explicitly; other methods do not. Keep track of what is on the stack. - -## Development - -``` -npm install -npm test -``` - -### Releasing - -Commits follow [Conventional Commits](https://www.conventionalcommits.org/); the pull request -title is what matters, since it becomes the squashed commit message. - -release-please keeps an open `chore(master): release x.y.z` pull request that accumulates merged -changes, works out the next version and rewrites `CHANGELOG.md`. Merging that pull request tags the -commit and publishes a GitHub Release, which is what triggers the npm publish. So merging the -release pull request is the single deliberate act that ships a version — nothing publishes on an -ordinary merge to `master`. - -Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) over OIDC, so -there is no npm token stored in this repository. +it. Some stack operations on a value of an unexpected type raise an *unprotected* Lua error, which +aborts the process rather than throwing a JavaScript exception. `SetField` and `GetField` guard +against this; other methods do not. Keep track of what is on the stack — see +[stack indices](https://github.com/mschmicking/node-lua-runner/blob/master/docs/api.md#stack-indices). ## License -ISC — see [LICENSE](LICENSE). The vendored Lua and LuaFileSystem sources are MIT; their -notices are in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md). +ISC — see [LICENSE](LICENSE). The vendored Lua and LuaFileSystem sources are MIT; their notices are +in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md). diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..10d34e9 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,351 @@ +# API reference + +A thin wrapper over the [Lua 5.1 C API](https://www.lua.org/manual/5.1/manual.html). If a method +looks unfamiliar, the corresponding C function in that manual is the authoritative description of +what it does to the stack. + +Every method is synchronous. + +- [Creating a state](#creating-a-state) +- [Running code](#running-code) +- [Loading without running](#loading-without-running) +- [Globals and fields](#globals-and-fields) +- [The stack](#the-stack) +- [Calling functions](#calling-functions) +- [Calling JavaScript from Lua](#calling-javascript-from-lua) +- [Coroutines](#coroutines) +- [Lifecycle and diagnostics](#lifecycle-and-diagnostics) +- [Constants](#constants) +- [Type conversion](#type-conversion) +- [Stack indices](#stack-indices) + +## Creating a state + +```javascript +const nodelua = require('node-lua-runner'); + +const lua = new nodelua.LuaState(); +``` + +Each `LuaState` owns an independent Lua interpreter with the standard library open and +`require('lfs')` available. States do not share globals, and callbacks registered on one are +invisible to the other. + +Call [`Close()`](#close) when you are done. If you do not, the interpreter is freed when the object +is garbage collected. + +## Running code + +### `DoString(code)` + +Compiles and runs a chunk of Lua. + +- `code` `{String}` +- Throws if the chunk fails to compile or raises an error. + +```javascript +lua.DoString('print("Hello world!")'); +``` + +### `DoFile(path)` + +Compiles and runs a file. + +- `path` `{String}` +- Throws if the file cannot be read, fails to compile, or raises an error. + +```javascript +lua.DoFile(__dirname + '/test.lua'); +``` + +### `AddPackagePath(path)` + +Appends a directory to Lua's `package.path` so `require` can find `.lua` files in it. + +- `path` `{String}` — a directory. Backslashes are normalised and a trailing separator is ignored. + +```javascript +lua.AddPackagePath(__dirname + '/lua'); +lua.DoString('local m = require("mymodule")'); +``` + +## Loading without running + +### `LoadString(code)` + +Compiles a chunk and pushes it onto the stack as a function **without running it**. Use +[`Call`](#callargs-results) to run it. + +- `code` `{String}` +- Throws if the chunk fails to compile. + +```javascript +lua.LoadString('counter = counter + 1'); +lua.Call(0, 0); // now it runs +``` + +### `LoadFile(path)` + +The same, for a file. + +- `path` `{String}` +- Throws if the file cannot be read or fails to compile. + +> Before 2.0.0 these two methods executed the chunk, behaving identically to `DoString` / `DoFile`. +> See [migrating to 2.0](migrating-to-2.0.md). + +## Globals and fields + +### `SetGlobal(name)` + +Pops the value on top of the stack and stores it in the global `name`. + +- `name` `{String}` + +```javascript +lua.Push(5); +lua.SetGlobal('myVar'); +``` + +### `GetGlobal(name)` + +Pushes the value of the global `name` onto the stack. + +- `name` `{String}` + +```javascript +lua.GetGlobal('myVar'); +const value = lua.ToValue(-1); +lua.Pop(1); +``` + +### `SetField(index, key, value)` + +Does `t[key] = value`, where `t` is the value at `index`. + +- `index` `{Number}` — stack index of the table. Relative indices are resolved before the value is + pushed, so `-1` means the table you are looking at. +- `key` `{String}` +- `value` `{*}` — converted per [type conversion](#type-conversion). +- Throws if the value at `index` is not a table. + +Unlike the raw C API, the value is passed as an argument rather than taken from the top of the +stack. + +```javascript +lua.DoString('t = {}'); +lua.GetGlobal('t'); +lua.SetField(-1, 'answer', 42); +lua.Pop(1); +``` + +### `GetField(index, key)` + +Pushes `t[key]` onto the stack, where `t` is the value at `index`. + +- `index` `{Number}` +- `key` `{String}` +- Throws if the value at `index` is not a table. + +```javascript +lua.GetField(nodelua.LUA.GLOBALSINDEX, 'a'); // same as GetGlobal('a') +lua.GetField(-1, 't'); +const t = lua.ToValue(-1); +lua.Pop(2); +``` + +## The stack + +### `Push(value)` + +Pushes a JavaScript value onto the stack. See [type conversion](#type-conversion). + +### `Pop([n])` + +Pops `n` elements. `n` defaults to `1`. + +### `ToValue(index)` + +Returns the value at `index` as a JavaScript value, leaving the stack unchanged. + +- `index` `{Number}` +- Returns `{*}` + +### `GetTop()` + +Returns the index of the top element, which equals the number of elements on the stack. `0` means +empty. + +- Returns `{Number}` + +### `SetTop(index)` + +Sets the stack top to `index`. Growing it fills the new slots with `nil`; `0` clears the stack. + +- `index` `{Number}` + +### `Replace(index)` + +Moves the top element into `index` and pops it, without shifting anything else. + +- `index` `{Number}` + +## Calling functions + +### `Call(args, results)` + +Calls a function. The function and its arguments are taken from the stack; the results are pushed +back onto it. Equivalent to `lua_pcall`. + +- `args` `{Number}` — how many arguments are on the stack +- `results` `{Number}` — how many results to keep +- Throws if the function raises an error. + +```javascript +lua.DoString('function join(a, b) return a .. "-" .. b end'); + +lua.GetGlobal('join'); +lua.Push('left'); +lua.Push('right'); +lua.Call(2, 1); + +console.log(lua.ToValue(-1)); // "left-right" +lua.Pop(1); +``` + +## Calling JavaScript from Lua + +### `RegisterFunction(name, fn)` + +Makes `fn` callable from Lua as the global `name`. + +- `name` `{String}` +- `fn` `{Function}` — receives **no JavaScript arguments**. Read the Lua arguments off the stack + with [`ToValue`](#tovalueindex), starting at index `1`. Push your results and return how many you + pushed. + +```javascript +lua.RegisterFunction('add', function () { + const a = lua.ToValue(1); + const b = lua.ToValue(2); + lua.Pop(2); + lua.Push(a + b); + return 1; // one result pushed +}); + +lua.DoString('print(add(2, 3))'); // 5 +``` + +Returning nothing (or a non-number) means no results. + +If the callback throws, the exception cannot travel through Lua's C frames, so it is left pending +and surfaces once control returns to JavaScript. Lua carries on with no results from the call. + +## Coroutines + +### `Yield(args)` + +Yields a coroutine. + +- `args` `{Number}` + +> Only meaningful inside a running coroutine. Calling it otherwise raises an unprotected Lua error, +> which aborts the process — see [caveats](#stack-indices). + +### `Resume(args)` + +Starts or resumes a coroutine. + +- `args` `{Number}` +- Returns `{Number}` — the Lua status code; compare against `nodelua.STATUS.*`. + +## Lifecycle and diagnostics + +### `Close()` + +Destroys the Lua state and frees its memory. Safe to call more than once. Any further use of the +state throws rather than crashing. + +### `Status()` + +Returns the state's status. + +- Returns `{Number}` — compare against `nodelua.STATUS.*`. `0` means runnable. + +### `CollectGarbage(what)` + +Controls the Lua garbage collector. + +- `what` `{Number}` — one of `nodelua.GC.*` +- Returns `{Number}` + +```javascript +lua.CollectGarbage(nodelua.GC.COLLECT); +const kb = lua.CollectGarbage(nodelua.GC.COUNT); +``` + +## Constants + +```javascript +nodelua.INFO.VERSION // "Lua 5.1" +nodelua.INFO.VERSION_NUM // 501 +nodelua.INFO.COPYRIGHT +nodelua.INFO.AUTHORS + +nodelua.LUA.GLOBALSINDEX // pseudo-index of the globals table +nodelua.LUA.REGISTRYINDEX // pseudo-index of the registry + +nodelua.STATUS.YIELD +nodelua.STATUS.ERRRUN +nodelua.STATUS.ERRSYNTAX +nodelua.STATUS.ERRMEM +nodelua.STATUS.ERRERR + +nodelua.GC.STOP +nodelua.GC.RESTART +nodelua.GC.COLLECT +nodelua.GC.COUNT +nodelua.GC.COUNTB +nodelua.GC.STEP +nodelua.GC.SETPAUSE +nodelua.GC.SETSTEPMUL +``` + +## Type conversion + +### Lua to JavaScript + +| Lua | JavaScript | +|---|---| +| `string` | `String` | +| `number` | `Number` | +| `boolean` | `Boolean` | +| `table` | `Object` — converted recursively, at any stack index | +| `nil` and everything else | `undefined` | + +Lua tables are always objects, never arrays. A table written as `{"a", "b"}` arrives as +`{ 1: 'a', 2: 'b' }`, keeping Lua's 1-based keys. + +### JavaScript to Lua + +| JavaScript | Lua | +|---|---| +| `String` | `string` (embedded NULs preserved) | +| `Number` | `number` (fractional values kept) | +| `Boolean` | `boolean` | +| `Object` | `table` — converted recursively | +| everything else | `nil` | + +Arrays are objects, so they become tables keyed by the **string** forms of their indices +(`"0"`, `"1"`, …), not 1-based Lua arrays. Build the table explicitly if that matters. + +## Stack indices + +Positive indices count from the bottom of the stack, negative from the top: `-1` is the top +element. `nodelua.LUA.GLOBALSINDEX` and `nodelua.LUA.REGISTRYINDEX` are pseudo-indices and are never +treated as relative. + +**This is a thin wrapper and it does not shield you from every misuse of the C API.** Some stack +operations on a value of an unexpected type raise an *unprotected* Lua error, which aborts the +process rather than throwing a JavaScript exception. `SetField` and `GetField` guard against this +explicitly; other methods do not. Keep track of what you have put on the stack. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..cd07fb5 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,55 @@ +# Development + +``` +npm install +npm test +``` + +`npm install` compiles the addon, including the vendored Lua and LuaFileSystem sources under +`vendor/`. `npm test` runs the suite with Node's built-in test runner. + +To rebuild without reinstalling: + +``` +npx node-gyp rebuild +``` + +## Layout + +| Path | Contents | +|---|---| +| `src/` | The Node-API binding. This is the code to change. | +| `vendor/lua/` | Lua 5.1.5, a verbatim upstream copy | +| `vendor/lfs/` | LuaFileSystem 1.8.0, a verbatim upstream copy | +| `test/` | Test suite and fixtures | +| `examples/` | Runnable examples, each with its own `index.js` | +| `binding.gyp` | Build definition for all platforms | + +`vendor/` is deliberately unmodified. Keeping it a straight copy means upgrading Lua is a file copy +rather than a merge, so please do not patch it — if something in there needs fixing, fix it +upstream or work around it in `src/`. + +## Continuous integration + +Every pull request builds and tests on Linux, macOS and Windows against Node 20, 22 and 24, and +runs CodeQL over both the C++ and the JavaScript. All of those must pass before a merge. + +CodeQL raises alerts inside `vendor/` because its path filters do not apply to compiled languages. +Findings there belong upstream; dismiss them rather than patching the vendored sources. + +## Releasing + +Commits follow [Conventional Commits](https://www.conventionalcommits.org/), and the **pull request +title** is what matters, since it becomes the squashed commit message. A title that does not parse +means a release that silently does not happen, so it is validated on every pull request. + +release-please keeps an open `chore(master): release x.y.z` pull request that accumulates merged +changes, works out the next version and rewrites `CHANGELOG.md`. Merging that pull request tags the +commit and publishes a GitHub Release, which is what triggers the npm publish. + +So merging the release pull request is the single deliberate act that ships a version — nothing +publishes on an ordinary merge to `master`. + +Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) over OIDC, so +there is no npm token stored in this repository, and published versions carry provenance linking +them to the commit and workflow run that built them. diff --git a/docs/migrating-to-2.0.md b/docs/migrating-to-2.0.md new file mode 100644 index 0000000..f1b0e3f --- /dev/null +++ b/docs/migrating-to-2.0.md @@ -0,0 +1,43 @@ +# Migrating to 2.0 + +2.0.0 is a maintenance release that makes the package build and run on current Node.js. It fixes +several long-standing bugs, and those fixes change behaviour. + +## Behaviour changes + +| Change | Before | Now | +|---|---|---| +| `SetField(index, key, value)` | Wrote the *key* into the field, ignoring the value | Writes the value, and resolves a relative `index` before pushing | +| `LoadFile` / `LoadString` | Executed the chunk, identical to `DoFile` / `DoString` | Compile and push the chunk without running it; call `Call(0, 0)` to run it | +| Lua booleans read into JS | Arrived as the numbers `1` and `0` | Arrive as `true` and `false` | +| `Push(3.5)` | Truncated to `3` | Keeps the fractional value | +| `AddPackagePath` | Appended without a separator, corrupting `package.path`, so `require` usually failed | Appends correctly, and no longer breaks on paths containing quotes | +| `ToValue` on a table | Only converted correctly when the table was at the top of the stack | Works at any stack index, including nested tables | +| Calling a method after `Close()` | Use-after-free | Throws | +| `Resume(args)` | Returned `undefined` | Returns the Lua status code | + +Most of these were broken enough that working 1.x code is unlikely to depend on them. The two worth +checking in your own code: + +- **`SetField`** did not do what its name says, so anyone using it either worked around it or gave + up on it. +- **`LoadFile` / `LoadString`** used to run the chunk. If you called one and relied on the side + effects, add a `Call(0, 0)`. + +## Environment changes + +- **LuaJIT has been replaced by stock Lua 5.1.5.** Windows builds used to link LuaJIT 2.0.3, so + Windows users lose JIT compilation. In exchange, macOS on Apple Silicon and Linux on ARM64 work + at all, which they previously did not. The Lua C API and the language are unchanged. +- **`require('lfs')` now works on every platform.** It used to be a Windows-only prebuilt DLL + loaded through an `LUA_CPATH` hack; LuaFileSystem is now compiled into the addon. +- **No system Lua is needed.** Linux previously required you to build and install LuaJIT by hand. +- **Node.js 18 or newer is required.** + +## Why the binding was rewritten + +1.x was built on NAN, which tracks V8's unstable C++ API and no longer compiles on current Node — +the build failed inside `nan.h` itself, before reaching any of this project's code. + +2.0.0 uses [Node-API](https://nodejs.org/api/n-api.html), which is ABI-stable. A compiled build +keeps working across future Node.js major versions instead of breaking on each one. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..86bff69 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,71 @@ +# Troubleshooting + +This package compiles from source at install time, so most problems are build problems. + +## Windows: `find VS unknown version "undefined"` + +``` +gyp ERR! find VS unknown version "undefined" found at "C:\Program Files\Microsoft Visual Studio\18\..." +gyp ERR! find VS could not find a version of Visual Studio 2017 or newer to use +``` + +`node-gyp` 11.x cannot detect Visual Studio 2026, and it is what npm bundles on Node.js 20 and 22. +Node.js 24 already ships a newer node-gyp and is unaffected. + +Install node-gyp 12 and point npm at it. Note the explicit `@12` — `@latest` still resolves to 11.x +on those Node versions, so it does not fix anything: + +``` +npm install -g node-gyp@12 +set npm_config_node_gyp=%APPDATA%\npm\node_modules\node-gyp\bin\node-gyp.js +npm install node-lua-runner +``` + +## npm blocks the build script + +``` +npm warn install-scripts node-lua-runner@2.0.0 (install: node-gyp rebuild) +``` + +npm 12 blocks install scripts by default, and this package needs its one to compile the addon. +Approve it: + +``` +npm install-scripts approve node-lua-runner +npm install +``` + +This affects every native addon, not just this package. + +## `Cannot find module '.../build/Release/nodelua'` + +The addon was not compiled. Usually the install script was blocked (see above) or the build failed +earlier in the log. Rebuild and read the output: + +``` +npx node-gyp rebuild +``` + +## The build cannot find a compiler + +You need a C/C++ toolchain: + +- **Linux** — `build-essential` (or your distribution's equivalent) and Python 3 +- **macOS** — the Xcode Command Line Tools (`xcode-select --install`) +- **Windows** — the "Desktop development with C++" workload from Visual Studio Build Tools + +## The process exits without an error + +Some stack operations on a value of an unexpected type raise an *unprotected* Lua error, which +aborts the process instead of throwing. `SetField` and `GetField` guard against this; other methods +do not. + +If a call vanishes without a JavaScript exception, check what was actually on the stack at that +point — usually an index is off by one, or a value was popped earlier than intended. See +[stack indices](api.md#stack-indices). + +## `require('lfs')` fails + +LuaFileSystem is compiled into the addon and registered in `package.preload`, so `require('lfs')` +should work on every platform. If it does not, you are almost certainly running 1.x, where it was +Windows-only. Check `require('node-lua-runner/package.json').version`.