Why You Should Use npm for Luau

For those who’d prefer, a full version of the article is available on Medium.

Note that this article is intended for developers who are using packages in some way (using any package manager or even git submodules).

Why You Should Use npm for Luau

Discover how to improve your Luau development experience by using a powerful package manager like npm.


Lately, I have been working a lot more in the Luau open-source ecosystem. I have packaged up and published projects such as React-Lua and Jest-Lua on the npm registry.

To understand why I did that, I will answer the question that lead me down that path: how can Luau development become more efficient?

Improved Luau Development

I will present a few improvements I made to the Luau ecosystem. Those gave me the ability to write Luau code more efficiently.

I wrote this article assuming that code is written on the file system, not Roblox Studio. The most notable tool to do that is Rojo. This is the foundation of the Roblox ecosystem, as it unlocks a broad set of tools like:

  • code editors (like VS Code)
  • version control (like git)
  • code formatter (like StyLua)
  • code analysis (like selene and luau-lsp, the latter giving the analysis you would get inside Roblox Studio)

Normalizing Module Imports

Importing Luau or Lua modules differs across platforms. Most Lua environments import modules with something similar to a file path. On Roblox, it is a whole different story.

When using the file system, importing modules using Roblox instances is tedious. It requires the developer to keep a mental model of how the files are mapped into the Roblox data-model tree. In some cases, it may be trivial. In other cases, files are spread out in the data-model. Also, do not forget that files named init.lua affect the relative imports.

-- Where is MyModule? It could be somewhere like this
local MyModule = require(script.Parent.Parent.MyModule)

-- Maybe one less `.Parent` if the file is named `init.lua`?
local MyModule = require(script.Parent.MyModule)

-- Or maybe we're better off getting it from the root of the data-model?
local MyModule = require(game:GetService("ReplicatedStorage").Packages.MyModule)

-- Then what if the intermediate container is called `Modules`?
local MyModule = require(game:GetService("ReplicatedStorage").Modules.MyModule)

This is one of the main reasons why I implemented darklua’s convert_require rule. I can now import modules in a way that fits the environment where I write code: using file paths. I provide those file paths as strings to the built-in require function and darklua converts them to fit Roblox.

-- Where is MyModule?

-- Just point to the file and darklua will figure out 
-- how to get the get the correct Instance 
local MyModule = require("../MyModule") 

Reusing Code

This is the step where code becomes a reusable unit of logic, which often takes the name of a package, library or module. At this step, the development experience gets significantly better, since packages can be used by other packages to create even more useful ones.

The perfect candidate to manage packages is npm. You may think it is only a JavaScript package manager, but as long as there is a package.json, you can publish, resolve and download dependencies. For example, there are already packages using TypeScript, c++ or WebAssembly.

You probably use (or heard about) Wally. It does provide the basic set of features you would expect from a package manager, but some features are not as advanced as npm like:

  • the packages search
  • the package display page does not show the authors, the README.md or the package content

Other features simply do not exist in Wally:

  • support file dependencies (to depend on packages on the file system)
  • a dashboard to manage collaborators on specific packages
  • download metrics for packages
  • use different registries based on package scopes
  • support “workspace” structured projects, where multiple locally dependent packages can be maintained easily
  • support peer dependencies
  • display the dependencies that need funding
  • display vulnerabilities reported by dependencies
  • unpublish packages that are published by accident that no other package depends on
  • transfer ownership (using the command line or the website)

This is what I like about npm: it gives all the enterprise-ready functionality today. No need to build (or fund) existing features in other package managers. I can focus on writing Luau packages now.

Standardizing Dependency Imports

The Luau language can be configured to import modules using a set of aliases defined in the .luaurc configuration. To cite the document where the aliases feature was added in the language:

Aliases can be used to bind an absolute or relative path to a convenient, case-insensitive name that can be required directly.

In order for my packages to properly require other packages, I have standardized the module imports to use this alias @pkg. To import a dependency, I can now simply use the @pkg prefix followed by the dependency name:

local React = require("@pkg/react")

Generating the @pkg Alias Content

To import a dependency like @pkg/dependency-name, it implies that the @pkg alias content contains a dependency-name.luau file.

The folder where npm stores all the installed dependencies (named node_modules) has to be processed with npmluau. This tool is published on npm, which makes it very simple to add to a Luau package:

npm install --save-dev npmluau

It is the glue between npm and Luau (I did not bother coming up with an original name as you can see). npmluau will generate a folder named .luau-aliases inside the node_modules with the structure needed to import modules by their names. The @pkg alias can now be set to node_modules/.luau-aliases.

With that all set up, code analysis with luau-lsp can trace npm dependencies. For Roblox developers, darklua can convert those into Roblox imports.

How to Get Started

As you can see, Luau is now fully ready to get packaged up by the flexible and powerful npm.

If you want to get started, I have created a project generator that will generate the necessary configuration files to analyze, auto-format, bundle to a single file, build a Roblox model, run tests with Jest-Lua and automate GitHub releases.

Wondering what is currently available to use? Visit my awesome-luau package list.

If you would like to support my work, I have just created a page on ko-fi where you can contribute. If you prefer, GitHub sponsors are also available in my projects under Sea of Voices.

Useful Links

10 Likes