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

@gwigz/slua-tstl-plugin

v1.2.0

Published

TypeScriptToLua plugin for targeting Second Life's SLua runtime

Readme

@gwigz/slua-tstl-plugin

TypeScriptToLua plugin to provide better DX with SLua types.

Usage

Add the plugin to tstl.luaPlugins in your tsconfig.json:

{
  "tstl": {
    "luaTarget": "Luau",
    "luaPlugins": [{ "name": "@gwigz/slua-tstl-plugin" }],
  },
}

To enable output optimizations, pass optimize: true for all flags, or pick individual ones:

{
  "tstl": {
    "luaPlugins": [
      // all optimizations
      { "name": "@gwigz/slua-tstl-plugin", "optimize": true },
    ],
  },
}
{
  "tstl": {
    "luaPlugins": [
      // pick individual optimizations
      {
        "name": "@gwigz/slua-tstl-plugin",
        "optimize": {
          "compoundAssignment": true,
          "shortenTemps": true,
          "inlineLocals": true,
        },
      },
    ],
  },
}

What it does

  • Translates TypeScript patterns to native Luau/LSL equivalents (see below)
  • Automatically adjusts ll.* index arguments and return values from 0-based to 1-based
  • Optimizes self-reassignment array concat/spread to in-place table.extend
  • Handles adjusting Vector, Quaternion, and UUID casing
  • Validates luaTarget is set to Luau

Transforms

The plugin replaces TSTL lualib helpers with native Luau stdlib and LSL function calls for better performance and smaller output.

JSON

| TypeScript | Lua output | | --------------------- | -------------------- | | JSON.stringify(val) | lljson.encode(val) | | JSON.parse(str) | lljson.decode(str) |

For SL-typed JSON (preserving vector/quaternion/uuid), use lljson.slencode/lljson.sldecode directly.

Base64

| TypeScript | Lua output | | ----------- | ---------------------- | | btoa(str) | llbase64.encode(str) | | atob(str) | llbase64.decode(str) |

String methods

String methods are translated to LSL ll.* functions or Luau string.* stdlib calls:

| TypeScript | Lua output | | ---------------------- | ------------------------------------------------ | | str.toUpperCase() | ll.ToUpper(str) | | str.toLowerCase() | ll.ToLower(str) | | str.trim() | ll.StringTrim(str, STRING_TRIM) | | str.trimStart() | ll.StringTrim(str, STRING_TRIM_HEAD) | | str.trimEnd() | ll.StringTrim(str, STRING_TRIM_TAIL) | | str.indexOf(x) | (string.find(str, x, 1, true) or 0) - 1 | | str.indexOf(x, from) | (string.find(str, x, from + 1, true) or 0) - 1 | | str.includes(x) | string.find(str, x, 1, true) ~= nil | | str.startsWith(x) | string.find(str, x, 1, true) == 1 | | str.split(sep) | string.split(str, sep) | | str.repeat(n) | string.rep(str, n) | | str.substring(start) | string.sub(str, start + 1) | | str.substring(s, e) | string.sub(str, s + 1, e) | | str.replace(a, b) | ll.ReplaceSubString(str, a, b, 1) | | str.replaceAll(a, b) | ll.ReplaceSubString(str, a, b, 0) |

[!NOTE] str.indexOf(x, fromIndex) adjusts the fromIndex to 1-based (constant-folded for literals). str.startsWith(x, position) with a second argument falls through to TSTL's default handling. Similarly, str.split() with no separator is not transformed.

Array methods

| TypeScript | Lua output | | ------------------- | --------------------------------- | | arr.includes(val) | table.find(arr, val) ~= nil | | arr.indexOf(val) | (table.find(arr, val) or 0) - 1 |

[!NOTE] arr.indexOf(val, fromIndex) with a second argument falls through to TSTL's default handling.

Bitwise operators

TypeScript bitwise operators are automatically translated to bit32 library calls, since SLua does not support native Lua bitwise operators.

| TypeScript | Lua output | | ---------- | --------------------- | | a & b | bit32.band(a, b) | | a \| b | bit32.bor(a, b) | | a ^ b | bit32.bxor(a, b) | | a << b | bit32.lshift(a, b) | | a >> b | bit32.arshift(a, b) | | a >>> b | bit32.rshift(a, b) | | ~a | bit32.bnot(a) |

Compound assignments (&=, |=, ^=, <<=, >>=, >>>=) are also supported and desugar to the same bit32 calls.

btest optimization

Comparisons of a bitwise AND against zero are automatically optimized to bit32.btest:

| TypeScript | Lua output | | --------------- | ----------------------- | | (a & b) !== 0 | bit32.btest(a, b) | | (a & b) === 0 | not bit32.btest(a, b) |

This works with !=, ==, and with the zero on either side (0 !== (a & b)).

ll.* index adjustment

SLua's ll.* functions use 1-based indexing (Lua convention), but TypeScript uses 0-based. The plugin automatically adjusts index arguments and return values based on @indexArg and @indexReturn JSDoc tags in the type definitions:

| TypeScript | Lua output | | -------------------------------- | ------------------------------------------------------------ | | ll.GetSubString("hello", 0, 2) | ll.GetSubString("hello", 1, 3) | | ll.GetSubString("hello", i, j) | ll.GetSubString("hello", i + 1, j + 1) | | ll.ListFindList(a, b) | ____tmp = ll.ListFindList(a, b); ____tmp and (____tmp - 1) |

  • @indexArg parameters get + 1 (constant-folded for literals)
  • @indexReturn wraps the result in a nil-safe ____tmp and (____tmp - 1) expression
  • Functions without these tags (e.g. ll.Say) are left unchanged

Array concat self-assignment

When an array is reassigned to itself with additional elements appended, the plugin emits table.extend (SLua's in-place append) instead of TSTL's __TS__ArrayConcat which allocates a new table:

| TypeScript | Lua output | | ---------------------------- | --------------------------------------- | | arr = arr.concat(b) | table.extend(arr, b) | | arr = arr.concat(b, c) | table.extend(table.extend(arr, b), c) | | arr = [...arr, ...b] | table.extend(arr, b) | | arr = [...arr, ...b, ...c] | table.extend(table.extend(arr, b), c) |

This optimization only applies when:

  • The expression is a statement (not const result = arr.concat(b))
  • The LHS is a simple identifier matching the receiver/first spread
  • All concat arguments / spread expressions are array-typed

Floor division

Math.floor(a / b) is translated to the native Luau floor division operator //:

| TypeScript | Lua output | | ------------------- | ---------- | | Math.floor(a / b) | a // b |

This only applies when the argument is directly a / expression. Math.floor(x) with a non-division argument is left as-is.

[!WARNING] JavaScript integer truncation idioms ~~x and x | 0 do not map cleanly to Luau. ~~x emits bit32.bnot(bit32.bnot(x)) and x | 0 emits bit32.bor(x, 0), neither of which preserves correct semantics for negative numbers (the bit32 library operates on unsigned 32-bit integers). Use math.floor(x) for floor truncation instead.

Passthrough arrow closures

Zero-parameter arrow functions that just call another zero-parameter function are collapsed to a direct function reference:

| TypeScript | Lua output | | ------------------------------------------- | ----------------------------------- | | LLTimers.once(1, () => patchNext()) | LLTimers:once(1, patchNext) | | LLEvents.on("on_rez", () => refreshUrl()) | LLEvents:on("on_rez", refreshUrl) |

This applies when:

  • The arrow has zero parameters
  • The body is a single call with zero arguments
  • The callee is a simple identifier (not a method call)
  • The callee's type signature has zero parameters (so extra args from the caller are harmlessly ignored)

Optimizations

Pass optimize: true to enable all optimizations, or pass an object to pick individual flags. All flags default to false when not specified.

filter

Inlines arr.filter(cb) as an ipairs loop instead of pulling in __TS__ArrayFilter.

Automatically disabled for files with more than one .filter() call, where the shared helper is results in a smaller script.

const result = arr.filter((x) => x > 0)
local function ____opt_fn_0(x)
    return x > 0
end
local ____opt_0 = {}
for _, ____opt_v_0 in ipairs(arr) do
    if ____opt_fn_0(____opt_v_0) then
        ____opt_0[#____opt_0 + 1] = ____opt_v_0
    end
end
local result = ____opt_0

compoundAssignment

Rewrites self-reassignment arithmetic to Luau compound assignment operators.

| TypeScript | Lua output | | ------------ | ---------- | | x = x + n | x += n | | x = x - 1 | x -= 1 | | x = x .. s | x ..= s |

Only currently only applies to simple identifiers.

floorMultiply

Reorders Math.floor((a / b) * c) to use the floor division operator, avoiding a math.floor call.

| TypeScript | Lua output | | ---------------------------------- | --------------------- | | Math.floor((used / limit) * 100) | used * 100 // limit |

Plain Math.floor(a / b) is always optimized to a // b regardless of this flag.

indexOf

Emits bare string.find / table.find for indexOf presence checks instead of the full (find or 0) - 1 pattern.

| TypeScript | Lua output | | ----------------------- | -------------------------------- | | s.indexOf(x) >= 0 | string.find(s, x, 1, true) | | s.indexOf(x) !== -1 | string.find(s, x, 1, true) | | s.indexOf(x) === -1 | not string.find(s, x, 1, true) | | arr.indexOf(x) >= 0 | table.find(arr, x) | | arr.indexOf(x) === -1 | not table.find(arr, x) |

Bare indexOf calls without a comparison will still emit (find or 0) - 1 to retain 0-index style responses.

shortenTemps

Shortens TSTL's destructuring temp names and collapses consecutive field accesses into multi-assignment.

const { a, b } = fn()

Default output:

local ____fn_result_0 = fn()
local a = ____fn_result_0.a
local b = ____fn_result_0.b

Optimized output:

local _r0 = fn()
local a, b = _r0.a, _r0.b

inlineLocals

Merges forward-declared local x with its first x = value assignment when there are no references to x in between.

Default output:

local x
x = 5

Optimized output:

local x = 5

numericConcat

Strips tostring() from number-typed (and string-typed) template literal interpolations, since Luau's .. operator handles numeric concatenation natively.

// count is number
const msg = `items: ${count}`

Default output:

local msg = "items: " .. tostring(count)

Optimized output:

local msg = "items: " .. count

Non-numeric types (booleans, any, etc.) still get wrapped in tostring().

defaultParams

Collapses default-parameter nil-checks into a single or expression.

function respondPoll(extraHtml = "") {
  // ...
}

Default output:

function respondPoll(extraHtml)
    if extraHtml == nil then
        extraHtml = ""
    end
end

Optimized output:

function respondPoll(extraHtml)
    extraHtml = extraHtml or ""
end

Safe for string and number defaults (both truthy in Lua). Not applied to false defaults.

Keeping output small

Some TypeScript patterns pull in large TSTL runtime helpers. Here are recommendations for keeping output lean:

Avoid delete on objects

The delete operator pulls in __TS__Delete, which depends on the entire Error class hierarchy (Error, TypeError, RangeError, etc.), __TS__Class, __TS__ClassExtends, __TS__New, and __TS__ObjectGetOwnPropertyDescriptors, roughly 150 lines of runtime code.

Instead, type your records to allow undefined and assign undefined (which compiles to nil):

// Bad
const cache: Record<string, Data> = {}
delete cache[key]

// Good, compiles to `cache[key] = nil`
const cache: Record<string, Data | undefined> = {}
cache[key] = undefined

To clear an entire record, use let and reassign instead of iterating with delete:

// Bad
for (const key of Object.keys(cache)) {
  delete cache[key]
}

// Good
let cache: Record<string, Data | undefined> = {}
// ...
cache = {}

Avoid Array.splice()

splice() pulls in __TS__ArraySplice and __TS__CountVarargs, roughly 75 lines. Rebuild the array instead:

// Bad
for (let i = items.length - 1; i >= 0; i--) {
  if (shouldRemove(items[i])) {
    items.splice(i, 1)
  }
}

// Good, compiles to simple table operations
let items: Item[] = []
const remaining: Item[] = []

for (const item of items) {
  if (!shouldRemove(item)) {
    remaining.push(item)
  }
}

items = remaining

Prefer for...in over Object.entries()

Object.entries() pulls in __TS__ObjectEntries. Use Object.keys() with indexing, or for...in which compiles directly to for key in pairs(obj):

// Pulls in __TS__ObjectEntries
for (const [key, value] of Object.entries(obj)) { ... }

// Compiles to `for key in pairs(obj)`, no helpers
for (const key in obj) {
  const value = obj[key]
}

Avoid Map and Set

TSTL's Map and Set polyfills add ~400 lines of runtime. Use plain Record<string, T> and arrays instead:

// Bad, ~400 lines of runtime
const lookup = new Map<string, UUID>()
const seen = new Set<string>()

// Good, plain Lua tables
const lookup: Record<string, UUID | undefined> = {}
const seen: Record<string, boolean> = {}

Build

bun run build