X Tutup
Skip to content

Latest commit

 

History

History
722 lines (553 loc) · 20.5 KB

File metadata and controls

722 lines (553 loc) · 20.5 KB

Using the brs-node Library

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.

Table of Contents


Installation

Install the package via npm:

npm install brs-node

Or using yarn:

yarn add brs-node

Using in Node.js Applications

Basic Setup

Import the library in your Node.js application:

const brs = require("brs-node");

Or using ES6 imports (TypeScript):

import * as brs from "brs-node";

Executing BrightScript Files

The library provides several functions to execute BrightScript code. The main workflow involves:

  1. Creating a payload from your BrightScript files
  2. Registering a callback to handle output and events
  3. Executing the payload

Complete Example

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);
    }
})();

Executing BrightScript from In-Memory Files

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);
    }
})();

SceneGraph App Example

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
})();

Using the REPL Interpreter

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);
})();

Handling Callbacks

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 communication

Working with SharedArrayBuffer

For 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);

Using in Tests

The brs-node library is excellent for testing BrightScript code. Here are common patterns used in the project's test suite.

Setting up Jest Tests

Install Jest and set up your test environment:

npm install --save-dev jest

Testing with E2E Helper Functions

Create 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");
    });
});

Testing Best Practices

1. Reset File System Between Tests

beforeEach(() => {
    brs.BrsDevice.fileSystem.resetMemoryFS();
});

2. Use Fake Timers for Time-Dependent Tests

const fakeTimer = require("@sinonjs/fake-timers");

let clock;

beforeEach(() => {
    clock = fakeTimer.install({
        now: 1547072370937,
        toFake: ["Date", "performance"]
    });
});

afterEach(() => {
    clock.uninstall();
});

3. Test with Deep Links

test("handles deep link parameters", async () => {
    const deepLink = new Map([
        ["contentId", "12345"],
        ["mediaType", "movie"]
    ]);

    await execute([resourceFile("deeplink.brs")], {}, deepLink);

    // Assert expected behavior
});

API Reference

Core Functions

registerCallback(callback, sharedBuffer?)

Registers a callback function to receive interpreter messages and events.

Parameters:

  • callback: (message: any, data?: any) => void - Function to handle messages
  • sharedBuffer?: SharedArrayBuffer - Optional shared buffer for worker threads

Example:

brs.registerCallback((message) => {
    console.log("Received:", message);
});

createPayloadFromFiles(files, device, deepLink?, root?, ext?)

Creates an execution payload from BrightScript files.

Parameters:

  • files: string[] - Array of file paths to execute
  • device: DeviceInfo - Device configuration object
  • deepLink?: Map<string, string> - Deep link parameters
  • root?: string - Root directory for pkg:/ volume
  • ext?: string - Root directory for ext1:/ volume

Returns: AppPayload

createPayloadFromFileMap(fileMap, device, deepLink?)

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 values
  • device: DeviceInfo - Device configuration object
  • deepLink?: 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 package

executeFile(payload, options?)

Executes a BrightScript application payload.

Parameters:

  • payload: AppPayload - Application payload to execute
  • options?: object - Execution options

Returns: Promise<{ exitReason: string, cipherText?: ArrayBuffer, iv?: Uint8Array }>

getReplInterpreter(options)

Creates a REPL interpreter instance for interactive execution.

Parameters:

  • options: { device: DeviceInfo, root?: string, ext?: string, extZip?: ArrayBuffer }

Returns: Promise<ReplInterpreter>

executeLine(line, interpreter)

Executes a single line of BrightScript code in the REPL interpreter.

Parameters:

  • line: string - BrightScript code to execute
  • interpreter: ReplInterpreter - REPL interpreter instance

File System API

BrsDevice.fileSystem.resetMemoryFS()

Resets the in-memory file system to a clean state.


Additional Resources

Support

X Tutup