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

wasmley

v3.2.0

Published

Lua 5.4 runtime compiled to WebAssembly - run Lua in the browser with OpenAL audio, WebGL graphics, HTTP fetch, and 15+ embedded libraries and JIT

Readme

🌙 Wasmley

The official Lua runtime compiled to WebAssembly — run Lua in the browser at near-native speed with OpenAL audio, WebGL graphics, and HTTP fetch.

Named after Roberto Ierusalimschy, the creator of Lua (with a WASM twist).

npm version License: MIT


What's New in 3.0

  • 🔊 OpenAL Audio — Play sounds, music, and generate tones
  • 🎮 WebGL 2.0 — Full GPU-accelerated graphics with shaders
  • 🖼️ Canvas Graphics — Simple 2D drawing API
  • 🌐 Enhanced HTTP — Response headers, timeouts, request cancellation
  • 📦 15+ Embedded Libraries — JSON, functional programming, OOP, and more

Features

| Feature | Description | |---------|-------------| | ✅ Full Lua 5.4 | The real PUC-Rio implementation | | 🔊 OpenAL Audio | Play PCM audio, control volume/pitch | | 🎮 WebGL 2.0 | Shaders, buffers, textures | | 🖼️ Canvas 2D | Rectangles, circles, lines, text | | 🌐 HTTP Fetch | GET/POST with headers, timeout, cancel | | 📚 15+ Libraries | json, lume, moses, inspect, etc. | | ⚡ Near-native | WASM performance |


Installation

npm install wasmley

Or via CDN:

<script src="https://unpkg.com/wasmley/lua.js"></script>

Quick Start

High-Level API (Recommended)

const { create } = require('wasmley');

const lua = await create({
  print: console.log,
  printErr: console.error
});

lua.run(`
  print("Hello from Lua!")
  
  local json = require("json")
  print(json.encode({hello = "world"}))
`);

Browser with Audio

<script src="lua.js"></script>
<script>
async function main() {
  const lua = await LuaModule({ print: console.log });
  const L = lua._luaL_newstate();
  lua._luaL_openselectedlibs(L, 0xFFFFFFFF, 0);
  lua._luaL_openpurelibs(L);
  lua._luaL_openhttp(L);
  lua._luaL_openaudio(L);
  
  // Run Lua code that plays audio
}
main();
</script>

Audio API

-- Initialize OpenAL
audio.init()

-- Generate a 440Hz sine wave tone
local sampleRate = 44100
local duration = 0.5
local freq = 440
local samples = {}

for i = 0, sampleRate * duration - 1 do
    local t = i / sampleRate
    local value = math.floor(math.sin(2 * math.pi * freq * t) * 32767)
    local lo = value % 256
    local hi = math.floor(value / 256) % 256
    if hi < 0 then hi = hi + 256 end
    if lo < 0 then lo = lo + 256 end
    table.insert(samples, string.char(lo, hi))
end

local pcmData = table.concat(samples)
local buffer = audio.newBuffer(pcmData, 1, 44100, 16)
local source = audio.play(buffer, false, 1.0, 1.0)

-- Control playback
audio.setVolume(source, 0.5)
audio.setPitch(source, 1.2)
audio.pause(source)
audio.resume(source)
audio.stop(source)

-- Cleanup
audio.quit()

HTTP API

Simple GET

http.get("https://api.example.com/data", function(res)
    print("Status: " .. res.status)
    print("Data: " .. res.data)
    print("Content-Type: " .. (res.headers["content-type"] or "unknown"))
end)

POST JSON

http.postJson("https://api.example.com/users",
    { name = "Alice", email = "[email protected]" },
    function(res)
        print("Created: " .. res.data)
    end
)

Full Options

local reqId = http.fetch("https://api.example.com/data", {
    method = "POST",
    body = '{"key": "value"}',
    headers = {
        ["Content-Type"] = "application/json",
        ["Authorization"] = "Bearer token123"
    },
    timeout = 5000,  -- 5 seconds
    onsuccess = function(res)
        print("Status: " .. res.status)
        print("OK: " .. tostring(res.ok))
        -- res.headers is a table with lowercase keys
    end,
    onerror = function(err)
        print("Error: " .. err.statusText)
    end
})

-- Cancel request
http.cancel(reqId)

WebGL API

gl.init("#canvas")
gl.viewport(0, 0, 800, 600)
gl.clearColor(0.1, 0.1, 0.2, 1.0)

-- Create shader program
local vertexShader = [[
  attribute vec2 a_position;
  void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
  }
]]

local fragmentShader = [[
  precision mediump float;
  uniform vec4 u_color;
  void main() {
    gl_FragColor = u_color;
  }
]]

local program = gl.createProgramFromSource(vertexShader, fragmentShader)
gl.useProgram(program)

-- Create vertex buffer
local vbo = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vbo)
gl.bufferData(gl.ARRAY_BUFFER, {0, 0.5, -0.5, -0.5, 0.5, -0.5}, gl.STATIC_DRAW)

-- Draw
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 3)

Graphics API (2D Canvas)

gfx.init("#canvas")
gfx.clear(0.1, 0.1, 0.2, 1.0)

gfx.setColor(1, 0, 0, 1)  -- Red
gfx.rect(100, 100, 200, 150, true)  -- Filled rectangle

gfx.setColor(0, 1, 0, 1)  -- Green
gfx.circle(400, 300, 50, true)  -- Filled circle

gfx.setColor(1, 1, 1, 1)  -- White
gfx.text("Hello Wasmley!", 100, 50, "24px Arial")

Embedded Libraries

| Library | Description | |---------|-------------| | json | JSON encoder/decoder | | dkjson | Robust JSON with error handling | | inspect | Human-readable table dumps | | serpent | Serializer/pretty-printer | | lume | Game dev utilities | | classic | Tiny OOP library | | middleclass | Full-featured OOP | | moses | Functional programming | | fun | High-performance iterators | | flux | Tweening/animation | | schema | Data validation | | pl.* | Penlight utilities |


Building from Source

# Install dependencies
git clone https://github.com/Juicywoowowow/Wasmley
cd Wasmley

# Build
make

# Run dev server
make serve

License

MIT


Claude section!

Wasmley's Vertex JIT Compiler — Deep Dive

What Problem Does It Solve?

In normal WebGL/OpenGL, vertex data must be:

  1. Packed into binary — positions, colors, normals all as contiguous bytes in a specific format
  2. Type-converted — normalize byte values (0-255 → 0.0-1.0), convert floats to binary representation
  3. Stride-aware — know exactly how many bytes between each vertex
  4. Properly aligned — GPU expects data at specific byte offsets

Game developers normally do this manually:

-- Manual packing (tedious and error-prone)
local data = {}
for i, vertex in ipairs(vertices) do
  -- Pack position (3 floats = 12 bytes)
  table.insert(data, vertex.position[1])
  table.insert(data, vertex.position[2])
  table.insert(data, vertex.position[3])
  
  -- Pack color (4 normalized bytes = 4 bytes)
  table.insert(data, math.floor(vertex.color[1] / 255 * 255))
  table.insert(data, math.floor(vertex.color[2] / 255 * 255))
  table.insert(data, math.floor(vertex.color[3] / 255 * 255))
  table.insert(data, math.floor(vertex.color[4] / 255 * 255))
end

-- Upload to GPU
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)

-- Manually setup vertex pointers
local posLoc = gl.getAttribLocation(program, "position")
gl.enableVertexAttribArray(posLoc)
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 28, 0)  -- 28 = stride

local colorLoc = gl.getAttribLocation(program, "color")
gl.enableVertexAttribArray(colorLoc)
gl.vertexAttribPointer(colorLoc, 4, gl.UNSIGNED_BYTE, true, 28, 12)  -- 12 = offset

This is tedious, error-prone, and requires understanding:

  • Byte sizes for each type (float = 4 bytes, byte = 1 byte, etc.)
  • Memory alignment and stride
  • Type normalization rules
  • Offset calculations

What vjit Does:

vjit (Vertex JIT compiler) abstracts all of this away:

local format = vjit.format({
  { name = "position", type = "float", components = 3 },
  { name = "color", type = "nubyte", components = 4 }
})

vjit.bufferData(format, vertices, gl.ARRAY_BUFFER, gl.STATIC_DRAW)
vjit.setupAttribs(format, shaderProgram)

It automatically:

  1. Calculates stride — knows position is 12 bytes (3 floats), color is 4 bytes (4 ubytes)
  2. Calculates offsets — position starts at 0, color starts at 12
  3. Converts types — turns Lua tables into binary data with proper type conversion
  4. Normalizes values — converts 0-255 byte values to 0.0-1.0 float range
  5. Setups vertex pointers — calls glVertexAttribPointer with correct parameters

How It Works Internally:

Step 1: Format Definition

vjit.format({ ... })

Creates a schema describing vertex layout. It calculates:

  • Total stride (16 bytes: 12 for position + 4 for color)
  • Offset for each attribute
  • OpenGL type enum for each field
  • Normalization flags

Step 2: JIT Compilation When you call vjit.bufferData(format, vertices, ...), it:

  1. Generates code — Creates a function that converts Lua tables to binary
  2. Optimizes — Hardcodes offsets, types, and stride
  3. Executes — Runs the generated function on all vertices
  4. Uploads — Sends binary data to GPU via glBufferData

This is "JIT" because it compiles the conversion function at runtime based on the format you defined.

Step 3: Binary Packing The generated function does something like:

local function packVertex(vertex, buffer, offset)
  -- Position: 3 floats starting at offset 0
  buffer[offset] = vertex.position[1]      -- float
  buffer[offset + 4] = vertex.position[2]  -- float
  buffer[offset + 8] = vertex.position[3]  -- float
  
  -- Color: 4 normalized ubytes starting at offset 12
  buffer[offset + 12] = math.floor(vertex.color[1])  -- ubyte (0-255)
  buffer[offset + 13] = math.floor(vertex.color[2])  -- ubyte
  buffer[offset + 14] = math.floor(vertex.color[3])  -- ubyte
  buffer[offset + 15] = math.floor(vertex.color[4])  -- ubyte
end

Step 4: Auto Setup Vertex Pointers vjit.setupAttribs(format, shaderProgram) does:

local posLoc = gl.getAttribLocation(shaderProgram, "position")
gl.enableVertexAttribArray(posLoc)
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 16, 0)

local colorLoc = gl.getAttribLocation(shaderProgram, "color")
gl.enableVertexAttribArray(colorLoc)
gl.vertexAttribPointer(colorLoc, 4, gl.UNSIGNED_BYTE, true, 16, 12)

Type System:

vjit supports all OpenGL types:

  • float — 4 bytes, IEEE 754 floating point
  • byte/ubyte — 1 byte, signed/unsigned integer
  • short/ushort — 2 bytes, signed/unsigned integer
  • int/uint — 4 bytes, signed/unsigned integer
  • nbyte/nubyte — normalized byte (0-255 maps to 0.0-1.0)
  • nshort/nushort — normalized short

Dynamic Buffers:

For streaming data (particles, deformable meshes), vjit provides:

local dynBuffer = vjit.dynamicBuffer(format, maxVertices)

-- Add vertices dynamically
for x = 0, 100 do
  for y = 0, 100 do
    dynBuffer:add(x, y, 0, 255, 128, 64, 255)
  end
end

-- Upload to GPU when ready
dynBuffer:upload()

The :add() method packs each vertex on-the-fly without creating intermediate tables.

Performance Implications:

Without vjit:

  • Manually pack each vertex (slow, error-prone)
  • Type conversions scattered throughout code
  • Easy to make stride/offset mistakes
  • Hard to change vertex format (requires rewriting all packing code)

With vjit:

  • Compiled packing function (fast, no per-vertex overhead)
  • Type conversions happen once during format definition
  • Stride/offset calculated automatically
  • Change vertex format once, everything updates

Why This Matters for Wasmley:

Game developers expect:

  • Easy vertex data management
  • No manual byte packing
  • Type safety
  • Auto shader binding

vjit delivers all of that. It's the difference between:

-- Before: ~20 lines of tedious manual packing
vjit.bufferData(format, vertices, gl.ARRAY_BUFFER, gl.STATIC_DRAW)
vjit.setupAttribs(format, program)

-- After: 2 lines of clean, declarative code

Real-World Usage:

-- Define a complex vertex format
local format = vjit.format({
  { name = "position", type = "float", components = 3 },
  { name = "normal", type = "float", components = 3 },
  { name = "texCoord", type = "float", components = 2 },
  { name = "color", type = "nubyte", components = 4 }
})

-- Load 3D model (e.g., from obj file or procedurally generated)
local model = loadModel("character.obj")

-- Upload with one call
vjit.bufferData(format, model.vertices, gl.ARRAY_BUFFER, gl.STATIC_DRAW)
vjit.setupAttribs(format, shaderProgram)

-- Draw
gl.drawArrays(gl.TRIANGLES, 0, #model.vertices)

This is what separates toy runtimes from production game engines. vjit is the kind of QoL feature that game developers expect. It shows Wasmley isn't just "Lua in WASM"—it's a complete, thoughtful game development platform.

"Lua" means "moon" in Portuguese. Wasmley brings the moon to the web. 🌙