# Installation

> Packages, the two ways to run Vela, the config file, and the ambient declaration.

Source: https://docs.astra-void.xyz/vela-rbxts/getting-started/installation/

Vela is a compile-time utility-class system for `roblox-ts` UI — React by default,
[Vide](https://centau.github.io/vide/) when the project says so. Write Tailwind-shaped class names
on Roblox host elements and your own components, and Vela rewrites them into Roblox properties and
helper instances before roblox-ts emits Luau.

It is a compiler over your `.tsx` files, not a component library. **Where that compiler runs is yours to pick** — a plugin inside `rbxtsc`, or a command that runs before it.

## Install the packages

```bash
pnpm add vela-rbxts
```

Then the ordinary roblox-ts stack, which Vela does not bring for you. Install the UI library you
actually write JSX with — React:

```bash
pnpm add @rbxts/react @rbxts/react-roblox @rbxts/services
```

…or [Vide](https://centau.github.io/vide/), which needs no Vela config of its own — the target is read off the `jsxFactory` your `tsconfig.json` already sets, see [the Vide guide](https://docs.astra-void.xyz/vela-rbxts/guides/vide.md):

```bash
pnpm add @rbxts/vide @rbxts/services
```

```bash
pnpm add -D roblox-ts typescript@5.9.3 @rbxts/compiler-types @rbxts/types
```

The `typescript@5.9.3` pin is deliberate: Vela calls into **your** TypeScript install, and 7.x fails
on the first file. Nothing enforces it.

> **pnpm needs `nodeLinker: hoisted`**
>
> Vela's runtime reaches your place *transitively*, and pnpm's isolated linker does not put it where roblox-ts and Rojo look. Set the linker once, at the workspace root:
>
> ```yaml title="pnpm-workspace.yaml"
> nodeLinker: hoisted
> ```
>
> npm and Yarn hoist by default. A nested `.npmrc` will not do it — pnpm reads `nodeLinker` only from the workspace root.

**What the TypeScript pin protects you from, and the rest of the version matrix**

Vela's host adapter resolves `typescript` from **your** install and calls into its API. TypeScript 7 moved enough of that API to break the call. A plain `typescript` install today gets 7.x and a build that dies on the first file:

```text title="rbxtsc output with typescript@7"
src/client/Panel.tsx:1:1 - error TS89000: [@vela-rbxts/rbxtsc-host] compiler-invocation-failed: Cannot read properties of undefined (reading 'Latest')
```

`5.9.3` is what the repo pins and tests against, and `6.x` also builds. No package declares a
`peerDependency`, so your package manager will not warn you, and the error names neither TypeScript
nor a version.

The rest of what Vela is developed against: **Node 24** in CI, **roblox-ts `^3.0.0`**,
`@rbxts/compiler-types ^3.0.0-types.0`, `@rbxts/types ^1.0.935`, `@rbxts/react ^17.3.7-ts.1`, and
`@rbxts/services ^1.6.0`.

**Why no runtime host appears in those commands**

An element Vela could not settle at compile time imports a **runtime host**: `@rbxts/vela-runtime` for React, `@rbxts/vela-runtime-vide` for Vide. Both ship as dependencies of `vela-rbxts`, and each declares *its* UI library as an optional peer. The host you do not emit for is one inert ModuleScript.

`@rbxts/services` matters beyond your own imports. The host reads `{ UserInputService, Workspace }` from it for viewport, input mode and rem, so it must resolve in the built place. `@rbxts/react-roblox` mounts a React tree, and Vide mounts itself.

## Choose how Vela runs

Two ways to run it, the same compiler either way. **The transformer** registers a plugin so the
lowering happens inside `rbxtsc`. **The CLI** lowers your sources into a second tree first, which
`rbxtsc` then compiles with no plugin.

They differ in where the work sits in your build:

| | Transformer | CLI |
|---|---|---|
| Setup | One `tsconfig.json` plugin entry | `rootDir`/`include` point at the generated tree |
| Build command | `rbxtsc` | `vela build && rbxtsc` |
| Watch | `rbxtsc -w` | `vela watch` beside `rbxtsc -w` |
| Sources `rbxtsc` sees | Yours | The generated mirror |
| Lowered output | Never written to disk | A reviewable tree you can read and diff |
| Needs a plugin-capable toolchain | Yes | No |
| Processes to keep running | One | Two |
| Available since | 0.1.0 | 0.11.0 |

Pick the transformer when `rbxtsc` is your build. Pick the CLI when a plugin is not an option, such
as a pinned toolchain or a build system driving `tsc` itself. It also fits when you want the lowered
sources as an artifact.

Everything after this section is shared: the config file, the type declaration, and the smoke test all
work the same on both.

### Track A — the transformer

Add one entry to `compilerOptions.plugins` in your existing `tsconfig.json`:

```json title="tsconfig.json"
{
  "compilerOptions": {
    "plugins": [{ "transform": "vela-rbxts/transformer" }]
  }
}
```

That is the entire tsconfig change — Vela requires no compiler option of its own. Without a project
yet, start from [roblox-ts's quick start](https://roblox-ts.com/docs/quick-start).

**Why the entry names a /transformer subpath, and what else goes in it**

`vela-rbxts/transformer` is a separate export from the package root: the root gives you `defineConfig` and types, the subpath is the callable roblox-ts consumes.

The plugin entry object is also where transformer options go: file filters, a diagnostic code base, a project root. All are covered in the [configuration reference](https://docs.astra-void.xyz/vela-rbxts/reference/config.md).

### Track B — the CLI

Point `rbxtsc` at the tree the CLI generates, and register no plugin:

```json title="tsconfig.json"
{
  "compilerOptions": {
    "rootDir": ".vela/src",
    "baseUrl": ".vela/src",
    "outDir": "out"
  },
  "include": [".vela/src"]
}
```

`vela build` mirrors `src` into `.vela/src`, transforming the `.tsx` files that use `className` and
copying everything else byte for byte. It writes a `.gitignore` beside the tree:

```bash title="Build"
npx vela build && npx rbxtsc -p tsconfig.json
```

If a plugin entry is still registered, or `rootDir` still points at `src`, `vela build` says so on
every run. The [CLI reference](https://docs.astra-void.xyz/vela-rbxts/reference/cli.md) has every flag.

**Edit src, never the generated tree**

The mirror is a build output: an edit inside `.vela/src` survives until the next run touches that file, with no warning when it disappears. Your editor and `tsc --noEmit` point at the generated tree too, but the language server still reads your real sources.

## Add `vela.config.ts`

Optional — with no config file Vela uses its default theme and infers the framework from
`tsconfig.json`. Add it when you want your own theme values:

```ts title="vela.config.ts"
import { defineConfig } from "vela-rbxts";

export default defineConfig({
  theme: {
    extend: {
      colors: {
        brand: {
          500: "Color3.fromRGB(59, 130, 246)",
          700: "Color3.fromRGB(29, 78, 216)",
        },
      },
      radius: {
        panel: "new UDim(0, 10)",
      },
    },
  },
});
```

Two things to get right. Every value is an **expression string in the roblox-ts dialect**, and it
goes under `theme.extend`. A top-level `theme.colors` replaces the whole registry *and* discards
`theme.extend.colors` silently. See the [theming guide](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md).

**Where the file goes, and where Vela looks for it**

Vela walks up from each source file's directory looking for `vela.config.ts` or `vela.config.json`, the `.ts` form winning when both exist. There are no `.js`, `.mjs` or `.cjs` variants and no `package.json` key.

Keep it at the project root, **outside** `rootDir` — Vela loads it with Node, so it is build tooling
rather than game code. Dropped into `src/`, roblox-ts sweeps it up and fails the build:

```text title="rbxtsc output"
src/vela.config.ts:1:30 - error TS roblox-ts: You cannot use modules directly under node_modules.

1 import { defineConfig } from "vela-rbxts";
                               ~~~~~~~~~~~~
```

Note that this is the opposite of the `.d.ts` file below, which *must* live inside `rootDir`.

One wrinkle at the root: a roblox-ts `tsconfig.json` includes only `src`, so typed ESLint reports `vela.config.ts` as outside the project. Widening `include` hands the file back to roblox-ts — use an ESLint override for that path, or the JSON form.

## Declare the `className` prop

Roblox host elements have no `className` prop in `@rbxts/types`. Vela augments `React.Attributes`
globally, which takes effect only if the package is imported for its side effect somewhere in your
program:

```ts title="src/vela-env.d.ts"
import "vela-rbxts";
```

Without it every `className` is a type error, even though Vela would still have processed it. Keep
the file inside `src` — under the CLI, `.d.ts` files are copied through unchanged.

> **Do not name the file src/vela-rbxts.d.ts**
>
> A roblox-ts project sets `baseUrl` to `src`, and TypeScript tries `baseUrl` before `node_modules` for a bare specifier. Under that name, `import "vela-rbxts"` resolves to the declaration file itself. The augmentation never loads, and `className` stays a type error with nothing pointing at the cause. Any other name works.

## Install the language server

Vela ships a language server, `vela-rbxts-lsp`, built from the same compiler crate the build uses.
No build needs it, but one class of mistake is reported **only** here: `text-*` on a `frame`,
`image-*` on a label, `placeholder-*` outside a `textbox`. The transform pipeline never checks any
of it, so those compile clean and write the wrong property onto the wrong instance.

In VS Code, install **Vela LSP**, id `astra-void.vela-rbxts-lsp`, publisher `astra-void`:

```bash title="VS Code"
code --install-extension astra-void.vela-rbxts-lsp
```

It needs VS Code `^1.91.0`, and that is the whole install — the marketplace serves a per-platform
VSIX with the server binary inside it. The extension activates on TypeScript and TSX documents: open
a `.tsx` file and the server starts.

**How your theme reaches the server**

The extension watches `vela.config.{ts,json}`, evaluates every match and pushes the result over the wire — the server never reads the file itself. The `.ts` form goes through **your project's** `typescript`, so the pin at the top of this page decides whether your editor resolves custom theme keys too.

**Any other editor**

The server speaks LSP over stdio and has no VS Code dependency. Add it to the project:

```bash
pnpm add -D @vela-rbxts/lsp
```

Then point your client's server command at it:

```bash title="Server command"
npx --package @vela-rbxts/lsp vela-rbxts-lsp
```

`--package` is not optional. The binary is named `vela-rbxts-lsp`, but the package owning it is
`@vela-rbxts/lsp`. `vela-rbxts-lsp` alone is the VS Code extension id, published as a VSIX. The
platform binary arrives as an optional dependency picked by `os`/`cpu`. Linux arm64 musl and Windows
on ARM have no prebuilt binary on either channel and need a source build.

**Nothing hands the server your config here.** Passing the resolved theme is the client's job,
through `initializationOptions`. Skip it and the server falls back to the default theme, so every
custom key reports `unknown-theme-key` against classes that build perfectly well.

[Editor setup](https://docs.astra-void.xyz/vela-rbxts/guides/editor-setup.md) has the payload shape, the method that updates it
after startup, and the full list of what the server does and does not implement.

## Build and serve

On track A there is no Vela-specific build step — compile with roblox-ts as usual:

```bash title="Build — track A"
npx rbxtsc
```

On track B, lower first. `vela build` exits non-zero when a file fails to compile, so `&&` is the right joint:

```bash title="Build — track B"
npx vela build && npx rbxtsc
```

Either way, sync `out/` into Studio with Rojo the same way you already do:

```bash title="Serve"
rojo serve
```

You need no Vela-specific Rojo mapping.

**Why the runtime import resolves with no project change**

Most of what Vela produces is a Roblox property or a helper instance emitted as a JSX child. The rest is an import of `@rbxts/vela-runtime`, one ModuleScript the whole place shares.

It resolves because roblox-ts only resolves a package whose scope directory is one of the project's `typeRoots`. `node_modules/@rbxts` is the one every roblox-ts project lists and every Rojo template maps to `ReplicatedStorage`, the same route `@rbxts/react` takes. This is what the `nodeLinker` note is about: the directory has to be real for either tool to walk into it.

## Check that it actually took

Two of the three ways this setup can fail are silent, so spend one build proving it worked. Put a
broken class on any `.tsx` element:

```tsx title="src/client/Smoke.tsx"
<frame className="bg-nope-500 p-4" />
```

Build and read the output, looking for a warning naming `nope-500` — track A routes it through
roblox-ts's numbered diagnostics, track B prints it itself:

```text title="Track A — rbxtsc output"
src/client/Smoke.tsx:4:28 - warning TS89000: [@vela-rbxts/compiler] unknown-theme-key: Unknown theme key "nope-500" for background color utility in className literal.

4   return <frame className="bg-nope-500 p-4" />;
                            ~~~~~~~~~~~
```

```text title="Track B — vela build output"
client/Smoke.tsx:4:28 - warning vela/compiler(unknown-theme-key): Unknown theme key "nope-500" for background color utility in className literal.
```

If you get the warning, everything is wired: Vela is running and diagnostics are reaching you. Delete
the file.

**No warning — what to check**

**`className` is also a type error in your editor.** The ambient declaration is not loading. Check that the `.d.ts` file exists inside `src` and is not named `src/vela-rbxts.d.ts`.

**`className` type-checks fine.** On track A, either the transformer is not running, or your roblox-ts does not expose `context.addDiagnostic` and is discarding every Vela warning. For the first, check that the `plugins` entry spells the `/transformer` subpath and the file ends in `.tsx`. The second makes broken classes look clean forever. On track B, check you ran `vela build` and that its header names the directory you expected.

**What the emitted file should look like**

Check the emitted file: `out/` on track A, `.vela/src` on track B. `p-4` should have become a `<uipadding>` child, and the `className` attribute should be gone. If the attribute survived, the element was skipped.

The padding values read as `__VelaRem.scale(new UDim(0, 16), 0)` rather than a bare `new UDim(0, 16)`, and the file grows an import of `@rbxts/vela-runtime`. That is [rem](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md#rem): `16` is the value at the base viewport, and the scaler follows the real one.

## Next steps

- [Your first component](https://docs.astra-void.xyz/vela-rbxts/getting-started/first-component.md) — write a panel, put it on
  screen, and read exactly what each class lowered to.
- [Editor setup](https://docs.astra-void.xyz/vela-rbxts/guides/editor-setup.md) — what the server provides, the three settings it contributes, sorting class names on save, and configuring a non-VS Code client.
- [The playground](https://docs.astra-void.xyz/vela-playground/) — compile class strings in the browser against the real
  compiler, with no project at all.
- [Vide](https://docs.astra-void.xyz/vela-rbxts/guides/vide.md) — what changes when the project emits for Vide instead of React,
  and the two limits that are inherent to it.
