This guide explains how to use the brs-node library in your Node.js applications and testing environments. The library provides programmatic access to the BrightScript Simulation Engine, allowing you to execute BrightScript code, run Roku applications, and test your BrightScript implementations.
- Using the brs-node Library
Install the package via npm:
npm install brs-nodeOr using yarn:
yarn add brs-nodeImport the library in your Node.js application:
const brs = require("brs-node");Or using ES6 imports (TypeScript):
import * as brs from "brs-node";The library provides several functions to execute BrightScript code. The main workflow involves:
- Creating a payload from your BrightScript files
- Registering a callback to handle output and events
- Executing the payload
const brs = require("brs-node");
const fs = require("fs");
const path = require("path");
// Register a callback to handle interpreter messages
brs.registerCallback((message, data) => {
if (typeof message === "string") {
const [messageType, content] = message.split(",", 2);
switch (messageType) {
case "print":
console.log(content);
break;
case "warning":
console.warn(content);
break;
case "error":
console.error(content);
break;
case "end":
console.log(`Execution finished: ${content}`);
break;
}
} else if (message instanceof Map) {
// Registry updates
console.log("Registry updated:", message);
}
});
// Define device configuration
const deviceData = {
developerId: "34c6fceca75e456f25e7e99531e2425c6c1de443",
friendlyName: "BrightScript Test Device",
deviceModel: "8000X",
clientId: "6c5bf3a5-b2a5-4918-824d-7691d5c85364",
RIDA: "f51ac698-bc60-4409-aae3-8fc3abc025c4",
countryCode: "US",
timeZone: "US/Eastern",
locale: "en_US",
clockFormat: "12h",
displayMode: "1080p",
customFeatures: [],
localIps: ["192.168.1.100"],
};
// Create payload from BrightScript files
const files = [
path.join(__dirname, "main.brs"),
path.join(__dirname, "lib", "utils.brs")
];
const payload = brs.createPayloadFromFiles(
files,
deviceData,
new Map(), // deepLink parameters (optional)
"/path/to/pkg/root", // root directory for pkg:/ (optional)
"/path/to/ext/root" // root directory for ext1:/ (optional)
);
// Execute the payload
(async () => {
try {
const result = await brs.executeFile(payload);
console.log(`Exit reason: ${result.exitReason}`);
// Handle encrypted package generation if password was provided
if (result.exitReason === "PACKAGED") {
console.log("Package encrypted successfully");
// Save the encrypted package
const encryptedData = new Uint8Array(result.cipherText);
fs.writeFileSync("app.bpk", encryptedData);
}
} catch (error) {
console.error("Execution failed:", error);
}
})();You can also create a payload from in-memory file content using createPayloadFromFileMap. This is useful when working with uploaded files, network resources, or dynamically generated content:
const brs = require("brs-node");
// Register callback (same as above)
brs.registerCallback((message) => {
// Handle messages...
});
// Create file map with Blob content
const fileMap = new Map();
// Add BrightScript source files
const mainBrsCode = `
sub Main()
print "Hello from in-memory BrightScript!"
print "Device model: "; CreateObject("roDeviceInfo").GetModel()
end sub
`;
fileMap.set("main.brs", new Blob([mainBrsCode], { type: "text/plain" }));
// Add manifest file
const manifestContent = `
title=My In-Memory App
major_version=1
minor_version=0
build_version=1
`;
fileMap.set("manifest", new Blob([manifestContent], { type: "text/plain" }));
// Add additional library file
const libCode = `
function GetAppVersion() as string
return "1.0.1"
end function
`;
fileMap.set("lib/utils.brs", new Blob([libCode], { type: "text/plain" }));
// Execute the in-memory files
(async () => {
try {
// Create payload from file map
const payload = await brs.createPayloadFromFileMap(fileMap, deviceData);
// Execute the payload
const result = await brs.executeFile(payload);
console.log(`Exit reason: ${result.exitReason}`);
} catch (error) {
console.error("Execution failed:", error);
}
})();For SceneGraph applications, organize files in the proper folder structure:
const fileMap = new Map();
// Manifest for SceneGraph app
const manifest = `
title=My SceneGraph App
major_version=1
minor_version=0
ui_resolutions=hd
splash_min_time=0
`;
fileMap.set("manifest", new Blob([manifest], { type: "text/plain" }));
// Main application source (executed)
const mainCode = `
sub Main()
print "SceneGraph app starting..."
screen = CreateObject("roSGScreen")
m.port = CreateObject("roMessagePort")
screen.setMessagePort(m.port)
scene = screen.CreateScene("MainScene")
screen.show()
print "Scene created and displayed"
' Event loop
while true
msg = wait(1000, m.port)
if msg <> invalid
if msg.isScreenClosed()
exit while
end if
else
exit while ' Timeout for demo
end if
end while
end sub
`;
fileMap.set("source/main.brs", new Blob([mainCode], { type: "text/plain" }));
// Scene component XML (packaged, not executed)
const sceneXml = `<?xml version="1.0" encoding="utf-8" ?>
<component name="MainScene" extends="Scene">
<children>
<Label id="titleLabel"
text="Hello SceneGraph!"
translation="[960, 540]"
horizAlign="center"
font="font:LargeSystemFont" />
</children>
<script type="text/brightscript" uri="MainScene.brs" />
</component>`;
fileMap.set("components/MainScene.xml", new Blob([sceneXml], { type: "text/xml" }));
// Scene component script (packaged, not executed)
const sceneBrs = `
function init()
print "MainScene component initialized"
m.titleLabel = m.top.findNode("titleLabel")
if m.titleLabel <> invalid
m.titleLabel.text = "Hello from Component!"
end if
end function
`;
fileMap.set("components/MainScene.brs", new Blob([sceneBrs], { type: "text/plain" }));
// Execute SceneGraph app
(async () => {
const payload = await brs.createPayloadFromFileMap(fileMap, deviceData);
const result = await brs.executeFile(payload);
// payload.pkgZip contains the complete app package
})();For interactive BrightScript execution or building a custom REPL:
const brs = require("brs-node");
(async () => {
// Get REPL interpreter instance
const replInterpreter = await brs.getReplInterpreter({
device: deviceData,
root: "/path/to/pkg/root", // optional
ext: "/path/to/ext/root", // optional
extZip: undefined // optional ArrayBuffer with zip data
});
// Execute single line
brs.executeLine("print \"Hello, World!\"", replInterpreter);
// Execute expression
brs.executeLine("? 2 + 2", replInterpreter);
// Get variable information
const globalVars = replInterpreter.formatVariables(0); // 0=global, 1=module, 2=function
console.log("Global variables:", globalVars);
// Access interpreter options
console.log("Root path:", replInterpreter.options.root);
})();The callback function receives all output and events from the interpreter:
brs.registerCallback((message, data) => {
if (typeof message === "string") {
// Parse message type and content
const parts = message.split(",");
const messageType = parts[0];
const content = parts.slice(1).join(",");
switch (messageType) {
case "print":
// Standard output
process.stdout.write(content);
break;
case "warning":
// Warning messages
console.warn(content);
break;
case "error":
// Error messages
console.error(content);
break;
case "start":
// Execution started
console.log("Execution started");
break;
case "end":
// Execution finished with reason
console.log(`Finished: ${content}`);
break;
case "debug":
// Debug events
console.log("Debug:", content);
break;
case "syslog":
// System log messages
console.log("SysLog:", content);
break;
}
} else if (message instanceof ImageData) {
// Screen buffer update (for ASCII rendering or canvas)
console.log(`Screen updated: ${message.width}x${message.height}`);
} else if (message instanceof Map) {
// Registry updates
console.log("Registry updated with", message.size, "entries");
}
}, sharedBuffer); // Optional SharedArrayBuffer for inter-thread communicationFor advanced use cases with worker threads (like the ECP server):
const { Worker } = require("worker_threads");
// Create shared buffer for communication
const dataBufferIndex = 128; // From brs.dataBufferIndex
const dataBufferSize = 128; // From brs.dataBufferSize
const length = dataBufferIndex + dataBufferSize;
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * length);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray.fill(-1);
// Register callback with shared buffer
brs.registerCallback(messageCallback, sharedBuffer);
// Use with worker threads
const worker = new Worker("./ecp-worker.js");
worker.postMessage(sharedBuffer);The brs-node library is excellent for testing BrightScript code. Here are common patterns used in the project's test suite.
Install Jest and set up your test environment:
npm install --save-dev jestCreate a helper module for end-to-end tests:
// test/helpers/E2EHelpers.js
const path = require("path");
const stream = require("stream");
const brs = require("brs-node");
brs.registerCallback(() => {}); // Suppress output
const deviceData = {
developerId: "34c6fceca75e456f25e7e99531e2425c6c1de443",
friendlyName: "Test Device",
deviceModel: "8000X",
firmwareVersion: "48G.04E05531A",
clientId: "test-client-id",
RIDA: "test-rida",
countryCode: "US",
timeZone: "US/Eastern",
locale: "en_US",
clockFormat: "12h",
displayMode: "1080p",
audioCodecs: ["mp3", "wav", "aac"],
videoFormats: new Map([
["codecs", ["mpeg4 avc", "vp9"]],
["containers", ["mp4", "mkv"]]
]),
customFeatures: [],
localIps: ["192.168.1.100"]
};
function resourceFile(...filenameParts) {
return path.join("test", "resources", ...filenameParts);
}
function createMockStreams() {
const stdout = Object.assign(new stream.PassThrough(), process.stdout);
const stderr = Object.assign(new stream.PassThrough(), process.stderr);
return {
stdout,
stderr,
stdoutSpy: jest.spyOn(stdout, "write").mockImplementation(() => {}),
stderrSpy: jest.spyOn(stderr, "write").mockImplementation(() => {})
};
}
async function execute(filenames, options = {}, deepLink) {
// Reset file system for clean test
brs.BrsDevice.fileSystem.resetMemoryFS();
const payload = brs.createPayloadFromFiles(filenames, deviceData);
if (deepLink) {
payload.deepLink = deepLink;
}
await brs.executeFile(payload, options);
}
function allArgs(jestMock) {
return jestMock.mock.calls
.reduce((allArgs, thisCall) => allArgs.concat(thisCall), []);
}
module.exports = {
deviceData,
resourceFile,
createMockStreams,
execute,
allArgs
};Use the helpers in your tests:
const { execute, createMockStreams, resourceFile, allArgs } = require("./helpers/E2EHelpers");
describe("BrightScript Components", () => {
let outputStreams;
beforeAll(() => {
outputStreams = createMockStreams();
});
afterEach(() => {
jest.resetAllMocks();
});
test("roArray operations", async () => {
await execute([resourceFile("components", "roArray.brs")], outputStreams);
expect(allArgs(outputStreams.stdout.write).map(arg => arg.trimEnd())).toEqual([
"array length: 4",
"last element: sit",
"first element: lorem",
"can delete elements: true"
]);
});
test("roAssociativeArray operations", async () => {
await execute([resourceFile("components", "roAssociativeArray.brs")], outputStreams);
const output = allArgs(outputStreams.stdout.write);
expect(output).toContain("AA size: 3");
expect(output).toContain("can delete elements: true");
});
});beforeEach(() => {
brs.BrsDevice.fileSystem.resetMemoryFS();
});const fakeTimer = require("@sinonjs/fake-timers");
let clock;
beforeEach(() => {
clock = fakeTimer.install({
now: 1547072370937,
toFake: ["Date", "performance"]
});
});
afterEach(() => {
clock.uninstall();
});test("handles deep link parameters", async () => {
const deepLink = new Map([
["contentId", "12345"],
["mediaType", "movie"]
]);
await execute([resourceFile("deeplink.brs")], {}, deepLink);
// Assert expected behavior
});Registers a callback function to receive interpreter messages and events.
Parameters:
callback: (message: any, data?: any) => void- Function to handle messagessharedBuffer?: SharedArrayBuffer- Optional shared buffer for worker threads
Example:
brs.registerCallback((message) => {
console.log("Received:", message);
});Creates an execution payload from BrightScript files.
Parameters:
files: string[]- Array of file paths to executedevice: DeviceInfo- Device configuration objectdeepLink?: Map<string, string>- Deep link parametersroot?: string- Root directory forpkg:/volumeext?: string- Root directory forext1:/volume
Returns: AppPayload
Creates an execution payload from a map of file paths and Blob content. This function automatically creates a ZIP package (in memory) containing all files and properly handles SceneGraph app structures.
Parameters:
fileMap: Map<string, Blob>- Map with file paths as keys and Blob content as valuesdevice: DeviceInfo- Device configuration objectdeepLink?: Map<string, string>- Deep link parameters
Returns: Promise<AppPayload> - Payload with pkgZip containing all files
File Handling Rules:
- Files without folder: Placed in
source/folder and executed as main source code - Files in
source/folder: Executed as main source code (including subfolders) - Files in other folders (e.g.,
components/,images/): Packaged in ZIP but not executed as source
SceneGraph Support:
- Component XML and BrightScript files in
components/folder are packaged for SceneGraph runtime - Only
source/folder BrightScript files are executed as main application code - Maintains proper separation between main source and component files
Examples:
Basic usage:
const fileMap = new Map();
fileMap.set("main.brs", new Blob([brightScriptCode], { type: "text/plain" }));
fileMap.set("manifest", new Blob([manifestContent], { type: "text/plain" }));
const payload = await brs.createPayloadFromFileMap(fileMap, deviceData);
const result = await brs.executeFile(payload);SceneGraph app structure:
const fileMap = new Map();
// Main application (executed)
fileMap.set("source/main.brs", new Blob([mainAppCode], { type: "text/plain" }));
// SceneGraph components (packaged, not executed directly)
fileMap.set("components/MyScene.xml", new Blob([sceneXmlCode], { type: "text/xml" }));
fileMap.set("components/MyScene.brs", new Blob([sceneBrsCode], { type: "text/plain" }));
// Assets (packaged as-is)
fileMap.set("images/icon.png", new Blob([iconData], { type: "image/png" }));
fileMap.set("manifest", new Blob([manifestContent], { type: "text/plain" }));
const payload = await brs.createPayloadFromFileMap(fileMap, deviceData);
// payload.pkgZip contains the complete app packageExecutes a BrightScript application payload.
Parameters:
payload: AppPayload- Application payload to executeoptions?: object- Execution options
Returns: Promise<{ exitReason: string, cipherText?: ArrayBuffer, iv?: Uint8Array }>
Creates a REPL interpreter instance for interactive execution.
Parameters:
options: { device: DeviceInfo, root?: string, ext?: string, extZip?: ArrayBuffer }
Returns: Promise<ReplInterpreter>
Executes a single line of BrightScript code in the REPL interpreter.
Parameters:
line: string- BrightScript code to executeinterpreter: ReplInterpreter- REPL interpreter instance
Resets the in-memory file system to a clean state.
- GitHub Issues: https://github.com/lvcabral/brs-engine/issues
- Slack: RokuCommunity