The manifest
[target]
Last updated October 23, 2026
The [target] table says where the package's code runs, what language it
ships, and what goes into the archive at publish. A package that ships for
several environments or languages writes
one [[target]] table for each.
[target]
environment = "roblox"
language = "luau"
main = "src/init.luau"
includes = ["src", "LICENSE"]
excludes = ["src/tests"]
workspace = ["packages/*"]
environment#
Required. Where the code runs:
| Value | Meaning |
|---|---|
roblox | Roblox, on the client or the server |
roblox_server | Roblox, server only |
lune | needs the Lune runtime |
lute | needs the Lute runtime |
seal | needs the Seal runtime, which loads .luau files only |
zune | needs the Zune runtime |
luau | portable Luau with no runtime API; it runs in every environment |
Two old names still parse in every manifest and lockfile. shared is the
old name of roblox, and server is the old name of roblox_server. Ember
writes the new names. The roblox_server packages still install into
packages/server, so Rojo project paths do not change.
Foreign ecosystems translate automatically: pesde roblox → roblox and
roblox_server → roblox_server; wally realms shared → roblox and
server → roblox_server. Each environment installs into
its own output folder. The
Runtimes page compares the seven.
language#
Optional. What the shipped files are written in. It is separate from
environment, so an Alloy package for Lune sets environment = "lune" and
language = "alloy".
| Value | Meaning |
|---|---|
luau | plain Luau. This is the default, and Ember leaves it out of the file. |
alloy | Alloy. embr publish runs alloy build first. |
loom | Loom. embr publish runs loom build first. |
luwu | Luwu, which runs only on a Luwu host. |
Any other value fails the manifest parse. Languages covers the build step and Alloy projects.
Several targets#
A package can ship one release for each target. Write one [[target]]
table for each target, in place of the one [target] table:
[[target]]
environment = "roblox"
main = "lib/init.luau"
includes = ["lib"]
[[target]]
environment = "roblox"
language = "alloy"
main = "build/init.luau"
includes = ["src", "build"]
[[target]]
environment = "luau"
language = "luwu"
main = "luwu/init.luwu"
includes = ["luwu"]
The first target is the project's own. Ember uses its environment and language when it installs the project's dependencies.
Target IDs#
Each target has an ID. For Luau, the ID is the environment. For another language, the ID is the environment, a dash, and the language.
| Target | ID |
|---|---|
environment = "roblox" | roblox |
environment = "roblox", language = "alloy" | roblox-alloy |
environment = "roblox_server", language = "loom" | roblox_server-loom |
environment = "luau", language = "luwu" | luau-luwu |
Two targets with the same ID are an error.
Publish several targets#
embr publish uploads one archive for each target.
Each archive holds the files, the main, and the build output of its own
target. --target <id> publishes only that target. Repeat it to publish
more than one:
embr publish --target roblox-alloy
embr publish --target roblox --target luau-luwu
Which target a project gets#
embr add and embr install pick one target of each dependency. They
take the target with the project's environment and language first. Then
they take the Luau target of the project's environment. So an Alloy project
for Roblox gets roblox-alloy when the package ships it, and roblox when
it does not.
The command stays embr add scope/name, because Ember picks the target.
The package page has a button for each target, and a line under its install
box names the projects that get the open target.
What a project installs#
Every project with a [target] gets an install rule from its own
environment and language. So runtimes and languages do not reach into each
other. In a manifest with several targets, the first target sets the rule.
A manifest with no [target] sets no rule.
- Language. A project takes packages in its own language, or in plain Luau. A Luau project takes no Alloy, Loom, or Luwu package. An Alloy project takes Alloy and Luau packages.
- Environment. A project takes packages for its own environment, or
portable Luau (
luau). Aroblox_serverproject also takesrobloxpackages, as wally's realm rule does. Aluneproject also takesrobloxpackages, because Lune runs Roblox code through its@lune/robloxDOM. Lute stays strict.
| Project language | Takes packages in |
|---|---|
luau | Luau |
alloy | Alloy, Luau |
loom | Loom, Luau (Loom code imports Luau through a .d.loom file) |
luwu | Luwu, Luau |
| Project environment | Takes packages for |
|---|---|
roblox | roblox, luau |
roblox_server | roblox_server, roblox, luau |
lune | lune, roblox, luau |
lute, seal, zune | its own environment, luau |
luau | luau |
A mismatch is an install error. Ember checks every package it resolves, transitive packages included, and the error names the rule:
This Alloy project for roblox takes Alloy or Luau packages for roblox or portable Luau; acme/fs@1.0.0 is a Luau package for lune. Set `target` on the dependency to take it on purpose
This Luau project for roblox takes Luau packages for roblox or portable Luau; acme/x@1.0.0 is a Loom package for roblox
You can cross an environment on purpose with
target on a direct dependency.
Ember then checks the packages under it against the environment that
target names. You cannot cross a language, because a project cannot run
a language that it does not read.
After an install, an Alloy project runs alloy build in each installed
Alloy package. See Alloy projects.
main#
Optional entry point, e.g. src/init.luau. Not needed on a
workspace root, which has no code of its own. When a
package ships without main, installers find the entry in this order
(extensions are stripped in link files, since Luau string requires reject
them):
- An
entryon the dependency that pulled it in, which beats everything the package itself says - ember.toml
[target] main - pesde.toml
[target] lib - Rojo
default.project.jsontree$path - Conventional locations:
init.luau,init.lua,src/init.*,lib/init.* - A guess from what the package ships: the file, when it ships exactly
one, or the one named after the package when it ships several. Test,
example and docs folders don't count toward either. The install says
which file it picked, and
entryis how you pick a different one.
Nothing conclusive → the package still installs, with a warning and no link
file. That happens when a package ships several Luau files and none carries
its name, where guessing would be a coin flip; state an entry and it
links.
For an Alloy or Loom package, main must name the .luau file that the
build writes. embr publish refuses the package
otherwise.
includes and excludes#
These shape publishing only — which files go into the archive. Entries
are plain relative paths (a directory covers everything under it) or globs
(src/*; * stays within one path segment, ** crosses). includes
absent means "everything sensible": the default walk minus VCS, build,
and output directories (.git, .ember-staging, .ember-patch, ember.lock,
target, node_modules, and the packages-out folders never ship).
excludes subtracts from whatever was selected. ember.toml always ships.
The singular spellings include and exclude are accepted aliases;
write the plural forms.
workspace#
Member globs, relative to this manifest. Having any makes this a
workspace root: * stays within one path segment, ** crosses, a
! prefix subtracts matches, and a literal . includes the root itself.
Every matched directory must contain an ember.toml — anything else is an
error. pesde's spelling workspace_members is accepted as a parsing
alias; write workspace. The full behavior — live links between members,
publish-time version rewriting — lives in Workspaces.