BrightScript Engine Extensions

July 25, 2026 · View on GitHub

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.

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.

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:

    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:

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

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

See the Node.js library guide 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.

  • 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.