Moud

Packages#

A package is a Luau module someone published to Moud so other places can use it: a state machine, a camera rig, an inventory, a set of maths helpers. You install one by name and version, it is copied into your place, and your scripts require it like any other module.

Installing#

Project menu > Packages... opens the package window. It has three tabs:

  • Browse searches the catalog. Pick a package to see its owner, its versions, what it reaches, its description and every file of its source. Install copies the version you are looking at into the place.
  • Installed lists the packages this place has, with the version, what each reaches and whether its files still match what was installed. View shows the package in Browse again and Remove takes it out.
  • Publish sends one of your own folders as a package, see Publishing a package.

Installing time-format 1.2.0, a package that turns seconds into text for timers, writes three things:

place/
  place.toml                    the pin, under [packages]
  packages.lock                 each package's version, hash, permissions and per-file hashes
  shared/packages/time-format/  init.luau and the package's other files
toml
[packages]
time-format = "1.2.0 sha256:3f1c9a..."

The line in place.toml pins the exact version and a hash of its files. packages.lock keeps the same version and hash, what the package declared it reaches, and a hash of every file. Commit both with the rest of the place; they are what tells a teammate's copy which code it should have.

Requiring#

@pkg/name loads shared/packages/name/init.luau, and @pkg/name/part loads another file of the package. A package's own files reach each other the same way, require("@pkg/time-format/units"). The packages live under shared/, so the server and every client can require them. Here the round timer HUD shows its time through the package:

luau
-- client
local format = require("@pkg/time-format")
local replicated = require("@moud/replicated")

local hud = game.world:add("ScreenGui", { name = "RoundHud" })
local label = hud:add("TextLabel", {
    position = udim2(0.5, -80, 0, 12),
    size = udim2(0, 160, 0, 28),
    textColor = color(1, 1, 1),
    backgroundTransparency = 1,
})

local round = replicated.value("round")

round.changed:connect(function(state)
    label.text = format.clock(state.seconds)
end)

The code editor completes these paths, see Requiring modules. @pkg is also an alias in .luaurc, so luau-lsp finds the package and its types.

What a package can do#

A package is Luau source and nothing else, and it runs in the same sandbox as your own scripts.

Only .luau text files can be in a package: no Java, no mixins, no native libraries, no bytecode and no assets. A file that is not plain text, or starts like bytecode, is refused when it is published and again when it is installed.

A package runs where the script that requires it runs, and reaches exactly what that script reaches. It gets no extra globals and no file access, [trust] java gives it nothing more, and it cannot turn anything on in place.toml.

Installing only writes files. There are no install scripts or hooks, and the code first runs when one of your scripts requires it. A package also stays at the version you installed: a new version being published changes nothing in places that already use it. The Browse tab shows every file of a version as text before anything is written to your place.

Network access is declared#

A package that calls http.get, http.post, http.request or http.websocket has to say so when it is published. The catalog lists what it reaches, the Browse tab shows it next to the version, and the Installed tab shows it for every package you have.

Permission Lets the package call
http http.get, http.post, http.request
websocket http.websocket

It is checked three times:

  • When you publish, the editor reads the source and does not send a package that uses one of these calls without declaring it.
  • When you install, the editor reads the downloaded source again and refuses the same thing. A package that reaches http through _G, getfenv or by passing http around as a value is shown with a warning, since the source does not say what it calls.
  • While it runs, calling http.get or http.websocket with a function of the package on the call stack and without the matching permission is an error that names the package: http.get was called from package weather, which does not declare http.

Declaring a permission never turns anything on. Requests still need httpRequests = true in [features] (see Reaching websites), and they still only run on the server. The permission says what the package is allowed to use once your place allows it.

The running check follows the call stack, so a function the package hands to task.defer or task.delay runs later with no package code under it and is not caught. Keep httpRequests off in places that do not need it.

Changed files#

When a script requires a package, the engine hashes the package's files and compares the result with the pin in place.toml. If one file changed, the require fails:

package time-format was changed after it was installed and no longer matches the hash in
place.toml, reinstall it or move it out of shared/packages

The same happens for a folder under shared/packages with no pin, and for a pin whose folder is missing. Installing from the window again puts the right files back.

To change a package's code, move its folder out of shared/packages, for example to shared/time-format, and remove it from the Installed tab. It is then your own module, and you require it as @shared/time-format.

The hash is SHA-256 over every file in path order: the path, a zero byte, the text, a zero byte. A download whose files do not match the hash the catalog gave is not installed.

Versions, yanking and reports#

  • A version is three numbers, 1.4.2. Once a version is published it can never change. A fix is a new, higher version.
  • The owner can yank a version that should not be used. A yanked version cannot be installed from the Browse tab, but places that already pinned it keep working, because the catalog still serves it to them.
  • Anyone signed in can report a package from the Browse tab: malicious, broken, stolen, inappropriate or other. Moud staff read reports and can yank any version.

Publishing a package#

Put the package in a folder of your place with an init.luau that returns the module. The whole of time-format could be one file, shared/time-format/init.luau:

luau
local format = {}

function format.clock(seconds: number): string
    return string.format("%d:%02d", seconds // 60, seconds % 60)
end

function format.long(seconds: number): string
    local minutes = seconds // 60
    if minutes == 0 then
        return seconds .. " seconds"
    end
    return minutes .. " min " .. seconds % 60 .. " s"
end

return format

Then open the Publish tab:

Field
Folder the folder inside the place, like shared/time-format
Name lowercase letters, digits and dashes, 3 to 40 long
Version higher than every version already published
Description up to 200 characters
Reaches http, Reaches websockets the permissions, see Network access is declared

You need to be signed in to Moud. The first account to publish a name owns it, and only that account publishes later versions. A creator key with the publish scope that is not tied to one game can publish too, see Studios, keys and webhooks.

Limits#

  • At most 64 files and 256 KB of source per version.
  • File paths are lowercase letters, digits, - and _, in folders, ending in .luau.
  • 20 publishes an hour per account, 10 reports an hour per account.

Plugin packages#

The catalog also holds editor plugins. A plugin package is published, versioned, hashed, limited and reported exactly like a module, declares http and websocket the same way, and also starts from init.luau. Its kind, module or plugin, is set by the first publish of a name and never changes, so a later version of a module can not be published as a plugin. Plugins are found and installed from Plugins > Browse... rather than the Packages window, see Editor plugins.

The catalog routes#

Route
GET /v1/packages?q=&kind= search by name or description, kind is module or plugin
GET /v1/packages/{name} the package and its versions, newest first
GET /v1/packages/{name}/{version} one version with its files
POST /v1/packages publish, signed in or with a creator key, kind defaults to module
POST /v1/packages/{name}/{version}/yank yank or unyank, owner only
POST /v1/packages/{name}/report report, signed in

See The catalog api for signing in, errors and rate limits.