Skip to content

The nexus command

nexus create <name> Start a new resource in the folder <name>
nexus dev Work on the UI in a browser, with a mock in place of the game
nexus dev --game Serve the UI to the game with hot reload
nexus build Build web/dist and the Lua bridge, and check fxmanifest.lua
nexus check Type-check, validate the contract, and report what Chromium 103 cannot run
nexus --help
nexus --version

dev, build and check run in the folder of a resource, next to fxmanifest.lua. Running them from inside web/ works too. In a resource made by nexus create they are also npm scripts: npm run dev, npm run build, npm run check.

Every command exits with 0 when it succeeded and 1 when it did not, and an error says what to do next. An unknown option is an error, not something that is ignored.

my_shop/
fxmanifest.lua
client/main.lua your client Lua
server/main.lua your server Lua
locales/en.json strings, read by Lua and by the mock
web/
contract.ts every message between the page and Lua
mock.ts what stands in for the game under `nexus dev`
screens/Main.nexus one file per screen
nexus-contract.d.ts generated: the types of `nui`
dist/ generated by `nexus build`: the page
nexus/ generated by `nexus build`: the Lua bridge
vite.config.ts
tsconfig.json
package.json

web/components/ and web/lib/ are common places for components and modules, but only web/screens/, web/contract.ts and web/mock.ts have a fixed meaning.

npm create nexus-ui <name> runs this command from the newest release, with nothing installed beforehand.

Copies the resource template into a new folder <name> and fills in the name. The name may use letters, digits, _ and -, and starts with a letter. The folder must not exist yet, or be empty.

The result works as it is: one screen, one call with its server handler, a mock and a locale. After npm install and npm run build, ensure <name> starts it, and /<name> in game opens the screen.

Starts the Vite dev server and prints its address, http://localhost:5173/ unless the port is taken.

In a browser, the page talks to a mock host instead of FiveM. A bar at the bottom of the window has:

  • one button per screen, which opens it with the props from web/mock.ts or closes it;
  • phone app and tablet app, when a screen has a surface: each shows or hides the app in a frame of the size LB gives it, 390 by 844 for the phone and 1280 by 800 for the tablet;
  • one button per world screen, under its name: it shows or hides the screen in a frame of its size, opened with the props under worlds in web/mock.ts. The mouse works in the frame as it does on a prop, the page draws its own cursor, and the keyboard is sent to it as the messages a display gets in game;
  • the buttons that components add with dev.action(label, fn) and the mock adds with action;
  • log, which shows what crossed the bridge: calls with their answer and duration, pushes, client messages and state updates.

Calls are answered by the handlers in web/mock.ts, and validated and rate-limited exactly as the server would. See The mock.

Saving a .nexus file mounts the open screen again with the props it had. A change to styles only swaps the stylesheet. After a full reload, the screens that were open are opened again.

A problem in web/contract.ts is printed in the terminal each time the file is saved.

The server stops with whatever started it. When npm run dev is ended from outside, by a task runner or a closed editor, Windows ends npm and leaves its children running. nexus dev watches the five processes above it and exits when one of them is gone, so the port is free again.

Option
--port <n> The port to listen on. Default: 5173.
--game See below.

Runs the same server for the page inside FiveM, so that hot reload works in game:

  1. It writes the Lua bridge into nexus/, as nexus build does.
  2. It changes ui_page in fxmanifest.lua to the address of the dev server. The original line is kept in a comment on the same line.
  3. It tells you to run refresh and ensure <name> in the server console, which loads the changed manifest.

While it runs, a change to web/contract.ts or to a <screen> tag updates nexus/contract.lua or nexus/screens.lua, and the terminal says to ensure the resource again, because Lua is only loaded when a resource starts. A contract or a screen that is broken at the moment leaves the last working bridge in place.

When you stop it with Ctrl+C, ui_page is put back. If the process was killed instead, the next nexus dev or nexus build puts it back.

With --game the port must be free, because FiveM is given one address. If it is taken, the command says so and suggests another.

The game and the dev server must run on the same machine, since the page is loaded from localhost. In game the page talks to the real Lua, not to the mock.

  1. Loads web/contract.ts. A contract the bridge could not enforce stops the build.

  2. Compiles every component. An error stops the build, with the file, the line, a code frame and what to write instead.

  3. Builds the page into web/dist for Chromium 103: newer syntax is rewritten, and each screen becomes its own file that is loaded when the screen is first opened. An app in LB Phone or LB Tablet is the same page, and so is a world screen, so a project with either builds once. What only world screens need is a file of its own, which a page without one never loads. Calls to dev.action are taken out.

  4. Writes nexus/client.lua, nexus/server.lua, nexus/contract.lua and nexus/screens.lua. A file that would not change is not touched.

  5. Checks fxmanifest.lua:

    • lua54 'yes';
    • ui_page 'web/dist/index.html';
    • files covers everything in web/dist;
    • nexus/contract.lua loads on both sides before the two runtimes;
    • nexus/screens.lua and then nexus/client.lua load on the client;
    • nexus/server.lua loads on the server;
    • no script list loads a bridge file on the wrong side.

    When something is missing, the build fails and prints the exact lines to add.

The build also runs the checks between apps, world screens, the contract and Lua that nexus check runs, and stops on an error among them. It lists the apps and the world screens it found.

The four files in nexus/ and the folder web/dist are what a release has to ship, next to your own Lua and fxmanifest.lua. Both are build output that the template’s .gitignore leaves out of the repository, so a release made from a fresh clone has to be built first. node_modules and the sources under web/ are not needed on a server.

Finds what is wrong without running anything, and prints every problem with its file, line, column and a code frame. It exits with 1 when there is at least one error, which makes it suitable for CI.

  • The contract: web/contract.ts loads and can be enforced.
  • Components: everything the compiler reports, such as unclosed tags and blocks, unknown directives, a bind: on something that cannot be bound.
  • Chromium 103: CSS and JavaScript that FiveM’s browser cannot run, in components and in plain .css, .ts and .js files under web/.
  • Apps: a screen with a surface that the contract gives props (an error: LB opens an app and passes nothing along), a surface that no Lua registers with Nexus.app (a warning: LB will not show it), and a Nexus.app for a surface no screen declares (an error). Lua is searched for the call, not parsed.
  • World screens: one without a size or with focus, keep-input, close or layer, a Nexus.open or Nexus.close with the name of a world screen, and a Nexus.world with a name that is not one. The last two are reported at their line in the Lua file. A name that Lua builds at run time is not seen.
  • Types: the scripts, the expressions in the templates, the props passed to components, and the .ts files under web/. A problem in a component is reported at its line in the .nexus file. Calls, pushes, client messages, state and the props of the screens the contract names are checked against it. The parameter of an inline handler is typed from its element and event: on:input={(event) => ...} on an <input> needs no annotation.
error web/screens/Shop.nexus:14:26: Property 'mesage' does not exist on type '{ message: string; }'. Did you mean 'message'? (TS2551)
12 | try {
13 | const result = await nui.call('shop:buy', { item: props.item, amount: amount.value });
> 14 | reply.value = result.mesage;
| ^
15 | } catch (error) {

Type checking uses the TypeScript installed in the resource (npm install --save-dev typescript) and the resource’s tsconfig.json. Its include and exclude decide which .ts files are checked, so a folder of scripts that run in Node can be left out. Built-ins are checked against what Chromium 103 has, whatever lib the tsconfig.json names. import.meta.glob and import.meta.env are typed as Vite provides them.

nexus check also brings web/nexus-contract.d.ts up to date.

What it does not check is listed in the roadmap.