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
-
Ship
brs.worker.js,brs.api.js, andbrs-sg.jstogether under thelib/folder. -
Copy
assets/common.zipfrom this package to replace the default one, providing SceneGraph fonts and resources. -
Declare the extension in
DeviceInfo.extensionswhen you callbrs.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); -
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. -
The worker calls
importScripts("./brs-sg.js"), findsBrightScriptExtension, and callsregisterExtensionbefore 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-nodeCLI registers the SceneGraph extension by default (from thebrs-sg.node.jsbundle shipped in itsbin/folder). Pass--no-sgto 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 againstbin/):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/commonand merge them with the extension overrides undersrc/extensions/scenegraph/common(extension files win on name collisions). - Zip the merged tree to
packages/scenegraph/assets/common.zip, which mirrors Roku'scommon:/volume. - In the development environment it overwrites
packages/browser/assets/common.zipandpackages/node/assets/common.zipso 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
-
Implement
BrsExtension– the interface lives insrc/core/extensions.tsand 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 newCreateObjectfactories or global functions.onBeforeExecute– called for each payload (apps and tasks) before execution. Use it to prepare resources, parse manifests, or updateBrsDevicestate.updateSourceMap– push extra files into the debugger/source map.tick– receive a callback on each interpreter tick (good for polling or background work).execTask– handleTaskthread payloads if your extension owns custom task nodes.
-
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 editingDeviceInfo.extensions. -
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 emitslib/brs-sg.js(browser) andlib/brs-sg.node.js(Node) as a reference implementation. -
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 defaultcommon:/volume. -
Document activation – explain how hosts should copy your bundle next to
brs.worker.jsor how they should callregisterExtension. The worker only loads modules that you add to theextensionsarray inside the payload.
Recommended workflow
- Use the
brs-scenegraphrepository layout as a starting point (Webpack config + TypeScript build that targets both runtimes). - Keep
brs-engineas a dependency or peer dependency so that your extension always compiles against the sameBrsExtensioninterface. - 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.