Documentation menu

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:

ValueMeaning
robloxRoblox, on the client or the server
roblox_serverRoblox, server only
luneneeds the Lune runtime
luteneeds the Lute runtime
sealneeds the Seal runtime, which loads .luau files only
zuneneeds the Zune runtime
luauportable 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".

ValueMeaning
luauplain Luau. This is the default, and Ember leaves it out of the file.
alloyAlloy. embr publish runs alloy build first.
loomLoom. embr publish runs loom build first.
luwuLuwu, 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.

TargetID
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). A roblox_server project also takes roblox packages, as wally's realm rule does. A lune project also takes roblox packages, because Lune runs Roblox code through its @lune/roblox DOM. Lute stays strict.
Project languageTakes packages in
luauLuau
alloyAlloy, Luau
loomLoom, Luau (Loom code imports Luau through a .d.loom file)
luwuLuwu, Luau
Project environmentTakes packages for
robloxroblox, luau
roblox_serverroblox_server, roblox, luau
lunelune, roblox, luau
lute, seal, zuneits own environment, luau
luauluau

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):

  1. An entry on the dependency that pulled it in, which beats everything the package itself says
  2. ember.toml [target] main
  3. pesde.toml [target] lib
  4. Rojo default.project.json tree $path
  5. Conventional locations: init.luau, init.lua, src/init.*, lib/init.*
  6. 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 entry is 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.