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[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:
-- 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
httpthrough_G,getfenvor by passinghttparound as a value is shown with a warning, since the source does not say what it calls. - While it runs, calling
http.getorhttp.websocketwith 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/packagesThe 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:
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 formatThen 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.
