# BrightScript Engine Extensions

The BrightScript Simulation Engine now supports plug-in style extensions that can hook into the interpreter lifecycle without forking the core runtime. Extensions are regular JavaScript/TypeScript modules that implement the `BrsExtension` interface and register themselves with `registerExtension`. Each interpreter instance receives the registered extensions before executing BrightScript code, which allows you to add new host objects, preprocess app packages, or respond to worker ticks.

```ts
import { registerExtension, BrsExtension } from "brs-engine";

class MyExtension implements BrsExtension {
    name = "MyExtension";
    version = "1.0.0";

    onInit(interpreter) {
        // Set up custom CreateObject factories, global functions, etc.
    }

    async onBeforeExecute(interpreter, payload) {
        // Inspect payload.manifest, add files to the virtual FS, etc.
    }

    tick(interpreter) {
        // Receive periodic callbacks on the worker thread.
    }
}

registerExtension(() => new MyExtension());
```

> [!IMPORTANT]
>
> `registerExtension` lives in the **interpreter** bundle, not in the browser API bundle. It is exported at runtime by `brs-node` (and by `brs.worker.js`), but **not** by `brs-engine`'s `lib/brs.api.js`, which runs on the main thread. In the browser, an extension is therefore never registered from the page — declare it in `DeviceInfo.extensions` and let the worker load it, as described below.

## SceneGraph extension (`brs-scenegraph`)

The SceneGraph runtime ships as a standalone extension located under `packages/scenegraph`. It owns the XML component parser, `RoSGScreen`, nodes, and task execution helpers.

For how `Task` nodes read and write render‑owned nodes across threads — the direct render→task rendezvous channel and its design rationale — see [`scenegraph-rendezvous.md`](./scenegraph-rendezvous.md).

### Browser integrations

1. Ship `brs.worker.js`, `brs.api.js`, and `brs-sg.js` together under the `lib/` folder.
2. Copy `assets/common.zip` from this package to replace the default one, providing SceneGraph fonts and resources.
3. Declare the extension in `DeviceInfo.extensions` when you call `brs.initialize`. Each entry is a `[SupportedExtension, string]` pair where the string is the worker path to the bundle:

   ```ts
   import * as brs from "brs-engine";

   const overrides: Partial<brs.DeviceInfo> = {
       extensions: new Map([[brs.SupportedExtension.SceneGraph, "./brs-sg.js"]]),
   };

   await brs.initialize(overrides);
   ```

4. When an app package contains a `pkg:/components/` folder the packaging layer checks the map above and, if the extension is registered, pushes `{ moduleId: "brs-scenegraph", modulePath: "./brs-sg.js" }` into the worker payload.
5. The worker calls `importScripts("./brs-sg.js")`, finds `BrightScriptExtension`, and calls `registerExtension` before executing the app.

No extra glue code is required as long as `brs-sg.js` is served next to the worker bundle. If you want to preload the extension even when no components are present, call `registerExtension(() => new BrightScriptExtension())` manually in your host code.

### Node.js and CLI

- The `brs-node` CLI registers the SceneGraph extension by default (from the `brs-sg.node.js` bundle shipped in its `bin/` folder). Pass `--no-sg` to skip loading it.
- When embedding the Node.js library, the registration path depends on the execution model:
  - **Synchronous `executeFile`** — register the extension in-process before running:

    ```ts
    import { registerExtension } from "brs-node";
    const { BrightScriptExtension } = require("brs-node/bin/brs-sg.node.js");

    registerExtension(() => new BrightScriptExtension());
    ```

  - **Worker-based `executeApp`** — declare it on the payload instead; each worker thread is a fresh isolate and loads its own instance (paths resolve against `bin/`):

    ```ts
    payload.extensions = [SupportedExtension.SceneGraph];
    payload.device.extensions = new Map([[SupportedExtension.SceneGraph, "brs-sg.node.js"]]);
    ```

See the [Node.js library guide](./using-node-library.md) for the full comparison of the two models.

### Common volume assets

SceneGraph needs fonts, locale tables, and imagery that do not ship with the core interpreter. Run `npm run build` (or `npm run release`) inside `packages/scenegraph` so this package's assets build step runs. It will:

- Copy the base assets from `src/core/common` and merge them with the extension overrides under `src/extensions/scenegraph/common` (extension files win on name collisions).
- Zip the merged tree to `packages/scenegraph/assets/common.zip`, which mirrors Roku's `common:/` volume.
- In the development environment it overwrites `packages/browser/assets/common.zip` and `packages/node/assets/common.zip` so local builds of both packages ship the SceneGraph-aware asset bundle.

Note: In production environments you must copy `assets/common.zip` from this package to replace the default `common:/` volume.

## Creating your own extension

1. **Implement `BrsExtension`** – the interface lives in `src/core/extensions.ts` and exposes lifecycle hooks:
   - `name` – (required) unique identifier for your extension.
   - `version` – (required) semantic version string for your extension (e.g., "1.0.0").
   - `onInit` – called once per interpreter before any payload runs. Use it to register new `CreateObject` factories or global functions.
   - `onBeforeExecute` – called for each payload (apps and tasks) before execution. Use it to prepare resources, parse manifests, or update `BrsDevice` state.
   - `updateSourceMap` – push extra files into the debugger/source map.
   - `tick` – receive a callback on each interpreter tick (good for polling or background work).
   - `execTask` – handle `Task` thread payloads if your extension owns custom task nodes.

2. **Register a factory** – call `registerExtension(() => new MyExtension())` exactly once before you spin up interpreters. The factory pattern ensures each interpreter gets a fresh instance, and hosts can still gate which modules load by editing `DeviceInfo.extensions`.

3. **Bundle for each runtime** – browser workers expect an ES5 bundle that can be loaded through `importScripts`, while Node.js integrations can rely on CommonJS/ESM modules. The SceneGraph package emits `lib/brs-sg.js` (browser) and `lib/brs-sg.node.js` (Node) as a reference implementation.

4. **Distribute assets** – if your extension needs fonts, bitmaps, or configuration files, package them with your module in a common volume zip (e.g. `assets/common.zip`). Document how hosts should copy the zip to replace the default `common:/` volume.

5. **Document activation** – explain how hosts should copy your bundle next to `brs.worker.js` or how they should call `registerExtension`. The worker only loads modules that you add to the `extensions` array inside the payload.

## Recommended workflow

- Use the `brs-scenegraph` repository layout as a starting point (Webpack config + TypeScript build that targets both runtimes).
- Keep `brs-engine` as a dependency or peer dependency so that your extension always compiles against the same `BrsExtension` interface.
- Write integration tests using the Node.js runtime; you can spin up interpreters with mocks and assert that your hooks run when expected.
- Share your extension plans! The roadmap already includes Roku SDK1 and BrightSign compatibility layers built as extensions, so aligning your design with the shared system helps everyone.
