Shared packages
You can import these packages in your own code. Here is what you can use where, and what each package gives you.
In code, the objects you see in the editor are called entities. They are the same thing.
Which packages you can import depends on where your code runs:
| Package | What it gives you | In a script | In a plugin | In a component extension |
|---|---|---|---|---|
@was/ecs | entities, components, the world | ✓ | ✓ | ✓ |
@was/engine | the component classes | ✓ | ✓ | ✓ |
@was/signals | reactive values | — | ✓ | ✓ |
@was/svdt | schemas and validation | ✓ | ✓ | — |
@was/utils | small helpers | ✓ | ✓ | — |
@was/ui | the editor's own component library | — | ✓ | ✓ |
@was/icons | the editor's icon set | — | ✓ | ✓ |
react | React 19 | — | ✓ | ✓ |
ctx.effectA script cannot import @was/signals directly. Use ctx.effect instead. It works the same way,
and it stops when its object goes away, so nothing is left running.
@was/signals: reactive values
A signal is a value that knows who reads it. When it changes, only the things that depend on it update.
import { signal, computed, effect, batch, untracked } from '@was/signals';
const score = signal(0);
const doubled = computed(() => score.value * 2);
const stop = effect(() => {
render(score.value); // runs again only when score changes
});
batch(() => {
score.value += 1;
score.value += 1; // effects run once, not twice
});
stop();
| Function | What it does |
|---|---|
signal(v) | a value that can be watched; read and write .value |
computed(fn) | a derived value, recalculated only when what it reads changes |
effect(fn) | runs now, and again whenever what it read changes; returns a stop function |
batch(fn) | groups writes, so watchers run once at the end |
untracked(fn) | reads a value without depending on it |
ref(obj) | makes a whole object reactive, down to every nested value |
snapshot(obj) | a plain, non-reactive copy |
raw(obj) | the underlying object, without tracking |
readonly(obj) | a view that cannot be written to |
For React panels, a companion package gives you useSignal, useComputed, useSignalEffect
and useLiveSignal. A component then re-renders from a signal with no extra wiring.
@was/ui: editor components
Build plugin panels and component extensions from the same components the editor uses. Your panels then look like part of the editor, instead of an embedded web page. The library has these components:
Accordion · Avatar · Button · Card · Checkbox · Chip · CloseButton
DimensionInput · Draggable · Dropdown · Flag · Icon · IconButton · Input
Menu · Modal · Outside · Panel · Popover · Portal · Render · Scroll
Search · Section · Segmented · Select · Skeleton · Switcher · Tabs
Toast · Toolbar · Tooltip
For icons, use @was/icons.
Panels can use only the utility classes that ship with the library. Arbitrary values like
text-[13px] are not included. For sizes outside the scale, use an inline style.
@was/ecs: entities and components
This package has Entity, Component and Space. With it, a plugin reads and changes a scene
through the full API, not a separate, limited one.
Space: the world
| Call | Returns |
|---|---|
getEntity(id) | one entity, or nothing |
hasEntity(id) | whether it exists |
getEntities() | all of them |
queryEntities(A, B, …) | every entity that has all of those components |
makeQueryEntities(A, B, …) | the same query, built once for repeated use |
createEntity(id?, components?) | a new entity, added to the world |
addEntity(…e) · removeEntity(…e) | adds an entity to the world or removes it |
getSystem(S) · hasSystem(S) | a system, or whether it exists |
execute() | runs one frame |
queryEntities to find entitiesAsking for "everything with a light and a transform" this way is fast.
getEntities() gives you the whole world, and you have to filter it yourself.
Entity: one object in the world
| Call | What it does |
|---|---|
getComponent(Type) | returns one component, or null |
getComponents(A, B) | returns several at once, in that order |
hasComponent(A, B) | checks whether it has all of them |
addComponent(…c) | adds components; a type it already has is ignored |
removeComponent(…c) | removes components, by class or by instance |
component(fn) | runs fn for every component, now and in the future |
clone() · clean() | copies the entity, or removes all its components |
.id · .components | its id, and its components keyed by type |
Component: the data
| Call | What it does |
|---|---|
.x or get('x') | reads a field; inside an effect, it also subscribes to it |
$data | the whole component as a plain object |
$rawData | the stored object, without subscribing; read-only |
update({ … }) | writes data. This is the only way to write. |
updateAt(path, value) | writes deep inside a large component without re-validating all of it |
version(field?) | a counter that goes up when something changes, so you can depend on a field without reading it |
reset(data?) | goes back to the defaults |
clone() | makes a copy |
version() for large listsReading a large value subscribes you to every item inside it. Depend on its version counter instead: you learn that it changed without watching every item. This is how a panel stays fast with a list of twenty thousand items.
@was/engine: component classes
Every component type is a class: TransformComponent, MaterialComponent, RigidBodyComponent
and the rest. Import the class and pass it to getComponent, hasComponent or query.
For the full list with every field, see the component reference.
@was/svdt: schemas
Use this package to describe the shape of your data and check it.
import { s, compile } from '@was/svdt';
const schema = s.object({ speed: s.f64(1), name: s.string('') });
const codec = compile(schema); // compile once, where you declare it, never on every call
const value = codec.parse(input);
Describe a shape
- Numbers:
s.f32 s.f64 s.i8 s.u8 s.i16 s.u16 s.i32 s.u32, each with a default. - Scalars:
s.bool,s.string,s.color,s.literal,s.enum,s.unknown,s.ref. - Vectors:
s.vec2,s.vec3,s.mat4. - Composites:
s.object,s.variant,s.array,s.record,s.union,s.preprocess,s.lazy.
You can chain these on any of them: .default(v), .optional(), .nullable(), .min(n),
.max(n), .int().
Use a compiled codec
| Call | What it does |
|---|---|
parse(input) | validates and normalises; throws on bad input |
safeParse(input) | the same, but returns success or an error instead of throwing |
parseAt(path, v) | validates one field without touching the rest |
equals(a, b) | deep comparison |
diff(a, b) | returns what changed |
apply(target, p) | applies a diff |
invert(p) | reverses a diff |
pack · unpack | converts to and from a compact binary form |
To inspect a schema itself instead of data, use introspect, keys and requiredKeys.
@was/utils
Small helpers: pick, omit, assign, keys, values, entries, capitalize, basename,
extname, dispose (combines cleanup functions into one), and MIME-type helpers for checking
file types.
Next: Glossary — the terms used across these docs.