@lvcabral/brs-engine
BrightScript Engine - AI Agent Instructions
Install
agr install @lvcabral/brs-engine --target copilotWrites 1 file into .github/copilot-instructions.md, pinned to git-f9a5a8d1.
- .github/copilot-instructions.md
Document
BrightScript Engine - AI Agent Instructions
Project Overview
brs-engine is a TypeScript-based interpreter for Roku's BrightScript language that runs on browsers, Node.js, and Electron. It simulates Roku's runtime environment, including the Draw 2D API and SceneGraph framework, enabling Roku app development and testing outside of Roku hardware.
Key Architecture:
- Monorepo structure with three packages:
packages/browser(web/Electron) published asbrs-engine,packages/node(CLI/server) published asbrs-node, andpackages/scenegraphpublished asbrs-scenegraph - Web Worker architecture: Browser package runs interpreter in a Web Worker (
brs.worker.js) with API library (brs.api.js) for host communication - Lexer → Parser → Interpreter pipeline: Code flows through
src/core/lexer→src/core/parser→src/core/interpreter - Component System: BrightScript objects live in
src/core/brsTypes/components; SceneGraph nodes live insrc/extensions/scenegraph/nodes - Extension System: SceneGraph (and other future features) are plug-in extensions implementing
BrsExtensioninterface (src/core/extensions.ts); registered withregisterExtension() - Device Simulation:
BrsDevice(insrc/core/device/) maintains simulated Roku device state (registry, file system, device info) - Roku OS Version: Currently synchronized with Roku OS 15.x features and APIs
Recent Major Improvements
SceneGraph Extension Architecture (v2.x)
- Third package:
packages/scenegraph(published asbrs-scenegraphv0.1.0+) is a standalone addon that owns the XML component parser,RoSGScreen, all nodes, and Task execution helpers - Extension system:
BrsExtensioninterface (src/core/extensions.ts) is the plug-in contract;registerExtension()wires extensions into every new interpreter instance without forking the core - BrightScriptExtension class: The entry point for the SceneGraph addon (
src/extensions/scenegraph/index.ts), implementsBrsExtension - SGRoot singleton (
src/extensions/scenegraph/SGRoot.ts): Holds the active scene,m.global, focus, threads, timers, and animations - SupportedExtension enum:
SupportedExtension.SceneGraph("brs-scenegraph"),SDK1andBrightSignextensions are planned
SceneGraph Rendezvous & Threads (v2.1.0)
- Real Rendezvous: SceneGraph now implements thread updates similar to Roku's Rendezvous pattern for safe cross-thread field access
- Multiple roMessagePort in Main thread: Tasks and Main thread can each own multiple ports
- Task debugger support: Task threads can be paused at breakpoints via the MicroDebugger
RoSGNode Refactoring & Field System
- Abstract base class:
RoSGNodeis abstract; all nodes must extend eitherRoSGNodeor the concreteNodeclass - Field type validation:
Field.canAcceptValue()validates typed arrays (intarray,floatarray,boolarray,stringarray,colorarray,timearray) and converts string values to the correct type - Field aliases: Nodes support multiple comma-separated field aliases; aliases fire observers correctly and support child change propagation
- System fields are protected: Cannot be removed via
removeField()or added viasetFields(); useaddFields()for new fields - setValue vs setValueSilent:
setValue()triggers observers;setValueSilent()does not (used during initialization) setValue()signature: Does NOT create a field if it does not already exist (prevents accidental field creation on assignment)
New SceneGraph Nodes (v2.0 – v2.1)
- Animation system:
Animation,ParallelAnimation,SequentialAnimation,FloatFieldInterpolator,ColorFieldInterpolator,Vector2DFieldInterpolator - Panel nodes:
PanelSet,Panel,ListPanel,GridPanel,OverhangPanelSetScene,Overhang - Standard dialogs:
StandardKeyboardDialog,StandardProgressDialog,StdDlgContentArea,StdDlgProgressItem,StdDlgTitleArea - Input nodes:
PinPad,VoiceTextEditBox,ScrollableText,ScrollingLabel - Grid nodes:
PosterGrid,MaskGroup - Other:
InfoPane,RSGPalette,ChannelStore
Content Handling Pattern
- Standardized content caching: All list/grid nodes follow the
refreshContent()pattern - setValue override: Content fields processed in
setValue(), triggeringrefreshContent() - Performance optimization: Content cached once, never re-fetched per frame
File System Improvements
- Case preservation: Writeable volumes (
tmp:,cachefs:) preserve original case - Shared volumes:
tmp:andcachefs:are shared among threads - Pinned
@zenfs/corev2.5.0: Using synchronous config for reliability
Type System Enhancements
- Uninitialized validation: Raises type mismatch error when passing
Uninitializedto non-dynamic function parameters - Double support: Added
dflag toParseJson()for parsing todoubletype - Type coercion: Typed functions return
0when no return statement is hit (user functions only) - UTF-8 BOM support:
Lexernow correctly handles files with UTF-8 BOM - DRM detection:
roDeviceInfo.getDrmInfoEx()populated in browser environments
API and Library Improvements
- Worker as Blob: Browser API supports loading the Worker bundle from a
Blob - Log level management: Device data and interpreter output now have configurable log levels
- LexerParser module: Separated lexer and parser into reusable module
- Screenshot API:
getScreenshot()method returns full-resolutionImageData
Development Workflows
Build & Test
npm run build # Build all three packages (outputs to packages/*/lib and packages/*/bin)
npm run build:web # Build browser + scenegraph packages and launch dev server
npm run build:node # Build Node.js package only
npm run build:cli # Build Node.js + scenegraph packages
npm run build:sg # Build scenegraph extension package only
npm run test # Run Vitest tests
npm run start # Start webpack dev server for browser package
Key Build Details
- Webpack bundles TypeScript using
ts-loaderwith separate configs per package - ifdef-loader enables conditional compilation with
/// #if BROWSER,/// #if DEBUG,/// #else,/// #endifdirectives - Browser package creates two bundles:
brs.worker.js(interpreter) andbrs.api.js(host API) - Node package creates three:
brs.cli.js(CLI),brs.ecp.js(ECP server),brs.node.js(library) - SceneGraph package creates two bundles:
brs-sg.js(browser/Worker) andbrs-sg.node.js(Node.js library) - SceneGraph build also assembles
assets/common.zip(fonts, locale, images forcommon:/volume) and copies it to the browser and node packages - Lint/format gate: run
npm run lintandnpm run prettier:writebefore committing; both must be clean. ESLint enforcesno-case-declarations— when aswitchcase/defaultdeclares a binding (let/const/function/class), wrap that case body in braces (case X: { const y = ...; break; }) so the binding doesn't leak into sibling cases. ESLint also enforcesunicorn/prefer-string-raw: useString.raw`...`instead of string literals with escaped backslashes ("C:\\folder"→String.raw`C:\folder`,"\\d+"→String.raw`\d+`) — auto-fixable witheslint --fix. It only targets plain string literals (template literals and a lone trailing"\\"are left alone). Andunicorn/prefer-string-replace-all: use.replaceAll(/…/g, …)instead of.replace(/…/g, …)for global replacements (also auto-fixable; thegflag stays becausereplaceAllrequires it).
Running the CLI
# After building
./packages/node/bin/brs.cli.js path/to/app.zip
./packages/node/bin/brs.cli.js path/to/script.brs
Code Patterns & Conventions
BrightScript Type System
- All BrightScript values implement
BrsTypefromsrc/core/brsTypes/BrsType.ts - Primitive types:
BrsBoolean,BrsString,Int32,Int64,Float,Doubleinsrc/core/brsTypes/ - Components (Roku objects like
roArray,roAssociativeArray): ExtendBrsComponentinsrc/core/brsTypes/components/ - Nodes (SceneGraph like
Group,RowList): Extend base node classes insrc/extensions/scenegraph/nodes/ - Callable functions: Wrap native TypeScript functions using
Callableclass fromsrc/core/brsTypes/Callable.ts
Example adding a stdlib function:
export const MyFunction = new Callable("myFunction", {
signature: {
args: [{ name: "input", type: ValueKind.String }],
returns: ValueKind.String,
},
impl: (_interpreter, input: BrsString) => {
return new BrsString(input.value.toUpperCase());
},
});
SceneGraph Node Implementation
- RoSGNode is now abstract: All SceneGraph nodes extend
RoSGNode(abstract) orNode(concrete base class) - Fields are declared in
defaultFields: FieldModel[]array withname,type,value, optionalalwaysNotify, optionalhidden - Field system improvements: Supports typed arrays (
intarray,floatarray,boolarray,stringarray,colorarray,timearray) - Lazy per-node allocation (large content trees):
hiddendefault fields (ContentNode metadata) are not materialized up front — they live in a shared per-class spec and are built on first access viaresolveField/hasNodeField; a node's ~70 method Callables are built on demand viaBrsComponent.buildMethods()/ensureMethods()(RoSGNode methods are prototype getters, the per-nodeRoHttpAgentis lazy). UseresolveField/hasNodeFieldfor by-name lookups that must see hidden metadata, and never treat a method getter as an identity-stable field. See the "Per-node memory" section of.claude/CLAUDE.md. Coverage:test/extensions/scenegraph/{HiddenFields,LazyMethods}.test.js - System fields are protected: Cannot be removed via
removeField()or added viasetFields(); useaddFields()for new fields - setValue vs setValueSilent:
setValue()triggers observers,setValueSilent()does not (used during initialization) - Custom rendering overrides
renderNode()method, receivesIfDraw2Dcontext - Field observers use
observeField()to watch changes - Node lifecycle:
init()called on creation,deinit()on destruction - See
src/extensions/scenegraph/nodes/RowList.tsandsrc/extensions/scenegraph/nodes/ZoomRowList.tsas complex examples with focus handling and child rendering
SceneGraph Rendering Architecture
Node Hierarchy and Base Classes
All SceneGraph nodes inherit from RoSGNode (src/extensions/scenegraph/components/RoSGNode.ts), which provides:
- Field management: Dynamic field registration with
Fieldclass, aliases, observers, and notifications - Child management: Parent-child relationships via
ifSGNodeChildreninterface (appendChild, removeChild, etc.) - Focus system: Focus chain tracking via
ifSGNodeFocus(hasFocus, setFocus, isInFocusChain) - Bounding rectangles: Three coordinate spaces tracked in every node:
rectLocal: Node's own coordinate space (relative to itself)rectToParent: Transformed to parent's coordinate spacerectToScene: Transformed to root Scene coordinate space
- Standard fields: All nodes have
id,focusedChild,focusable,changefields by default
Group (src/extensions/scenegraph/nodes/Group.ts) extends Node (which extends RoSGNode) and is the base for all visual/renderable nodes:
- Transform fields:
translation[x,y],rotation(degrees),scale[x,y],scaleRotateCenter[x,y],opacity(0-1),visible(boolean) - Layout fields:
width,height,clippingRectfor cropping child rendering - Child rendering:
renderChildren()recursively renders child nodes with inherited transforms - Drawing utilities: Helper methods for text (drawText, drawTextWrap, breakTextIntoLines, ellipsizeLine) and images (drawImage, loadBitmap)
- Coordinate transforms:
inheritParentTransformandinheritParentOpacityapply parent values to children - Caching:
isDirtyflag tracks when text measurements need recalculation,cachedLinesstores text layout
Node Type Categories
-
Container Nodes (manage children, no direct visual output):
Group: Basic container with transformsLayoutGroup: Auto-layout children in rows/columnsScene: Root node, sets screen resolution (SD/HD/FHD), background color/image, dialog management
-
Visual Leaf Nodes (render content, usually have no children):
Label: Single/multi-line text with alignment, wrapping, ellipsizationPoster: Image display with scaling modes (noScale, scaleToFit, scaleToZoom), 9-patch supportRectangle: Filled rectangle with color and optional rotationBusySpinner: Animated loading indicator
-
Interactive Container Nodes (visual + focus + children):
ArrayGrid: Grid of items with focus management and scrolling (base for grids)RowList: Horizontal rows of items, each row is a scrollable list (fully implemented with row titles, focus feedback)ZoomRowList: Advanced row list with zoom animations and configurable row heightsPosterGrid: Grid of poster images with focus handlingLayoutGroup: Auto-layout container with horizontal/vertical arrangement and alignmentMarkupList/MarkupGrid: Similar to above with markup text supportLabelList,CheckList,RadioButtonList: Specialized list typesButtonGroup,Button: Interactive button controlsKeyboard,MiniKeyboard,TextEditBox,PinPad,VoiceTextEditBox: Input controlsPanelSet,Panel,ListPanel,GridPanel: Panel-based navigation layout
-
Animation Nodes:
Animation,ParallelAnimation,SequentialAnimation: Animation container nodesFloatFieldInterpolator,ColorFieldInterpolator,Vector2DFieldInterpolator: Field interpolators
-
Special Nodes:
ContentNode: Data-only node (no rendering), holds metadata for lists/gridsTask: Background thread execution (runs BrightScript in separate Worker)Timer: Interval/timeout eventsVideo,Audio,SoundEffect: Media playbackDialog,KeyboardDialog,StandardDialog,StandardKeyboardDialog,StandardProgressDialog: Modal overlaysFont: Font resource definitionRSGPalette: Color palette resourceChannelStore: In-channel purchasingMaskGroup: Masked rendering groupInfoPane,Overhang,OverhangPanelSetScene: UI chrome nodesScrollingLabel,ScrollableText: Scrolling text nodes
Rendering Pipeline and Flow
SceneGraph Rendering Architecture Overview:
The SceneGraph rendering system is triggered by roSGScreen (the display component) and flows through a hierarchy of renderNode() method calls. The entry point is always Scene.renderNode(), which then recursively calls renderNode() on child nodes. This is fundamentally different from a typical DOM-based rendering system - nodes must explicitly implement renderNode() to participate in the rendering pipeline.
Key Principle: Nodes that want custom rendering behavior MUST override renderNode(), not invent new methods like renderContent(). Group's base renderNode() only calls renderChildren() - it has no concept of "content rendering".
Initialization (SceneGraph bootstrap):
- Component XML parsing (
src/extensions/scenegraph/parser/ComponentDefinition.ts):- Scan
pkg:/components/for.xmlfiles - Parse XML with
xmldoclibrary intoComponentDefinitionobjects - Build inheritance tree (components can extend other components or built-in types)
- Extract
<interface>(fields/functions),<children>(initial child nodes),<script>tags
- Scan
- Node factory (
src/extensions/scenegraph/factory/NodeFactory.ts):createNode()instantiates nodes from string type names- Built-in types registered in
NodeFactory.tsswitch statement - Custom components use
ComponentDefinitionto create nodes with inherited fields
- Environment setup:
- Each component gets its own
Environment(scope) for BrightScript functions init()function called after node creation if defined in component script- Field observers registered, initial field values set
- Each component gets its own
Frame Render Cycle (triggered by roSGScreen display update):
-
roSGScreen.renderFrame() initiates the cycle:
- Gets the Scene node from
sgRoot.scene - Calls
scene.renderNode(interpreter, [0, 0], 0, 1.0, draw2D) - This is the ONLY entry point to the rendering system
- Gets the Scene node from
-
Scene.renderNode() called with:
interpreter: Active interpreter instanceorigin: [x, y] position in screen coordinates (starts at [0, 0])angle: Accumulated rotation from parent chain (starts at 0)opacity: Accumulated opacity from parent chain (starts at 1.0)draw2D:IfDraw2Dinterface for canvas drawing
-
Scene-specific rendering:
- Clears canvas with
backgroundColor - Draws
backgroundURIimage if set (scaled to screen resolution) - Calls
renderChildren()to process child nodes
- Clears canvas with
-
Group.renderNode() recursion (for each child):
- Visibility check: Skip if
visiblefield is false (ALWAYS check this first in custom renderNode) - Transform calculation:
- Get node's
translationfield:nodeTrans = this.getTranslation() - Calculate draw position:
drawTrans = [nodeTrans[0] + origin[0], nodeTrans[1] + origin[1]] - If parent has
angle, rotate translation vector:rotateTranslation(nodeTrans, angle)
- Get node's
- Accumulate transforms:
rotation = parentAngle + this.getRotation()opacity = parentOpacity * this.getOpacity()
- Custom node rendering (nodes override renderNode for this):
- Simple visual nodes (Label, Poster, Rectangle): Call IfDraw2D methods directly
- Complex nodes (ArrayGrid, RowList, ZoomRowList): Implement full custom rendering logic
- Container nodes: Just call
renderChildren()to delegate to children
- Bounding rect updates:
updateBoundingRects(rect, origin, rotation): UpdatesrectLocal,rectToParent,rectToScene- Used for hit testing, collision detection, debugging
- Recurse to children:
renderChildren(interpreter, drawTrans, rotation, opacity, draw2D) - Parent rect propagation:
updateParentRects(origin, angle)updates parent's bounding rects
- Visibility check: Skip if
Key Rendering Concepts:
- Coordinate space transformations: Every node maintains three rect representations for different use cases (local calculations, parent-relative layout, screen-absolute hit testing)
- Transform inheritance: Children accumulate parent transforms (translation, rotation, opacity) at render time
- Depth-first traversal: Parents render before children, ensuring proper z-ordering
- Canvas-based drawing: All drawing operations use HTML5 Canvas 2D context via
IfDraw2Dinterface - Lazy evaluation: Bounding rects and transforms calculated during render pass, not on field changes
Implementing Custom renderNode():
When creating a custom node that needs to render visual content, follow this pattern (see ArrayGrid.ts, RowList.ts, ZoomRowList.ts):
renderNode(interpreter: Interpreter, origin: number[], angle: number, opacity: number, draw2D?: IfDraw2D) {
// 1. ALWAYS check visibility first
if (!this.isVisible()) {
return;
}
// 2. Calculate transforms
const nodeTrans = this.getTranslation();
const drawTrans = nodeTrans.slice();
drawTrans[0] += origin[0];
drawTrans[1] += origin[1];
const rotation = angle + this.getRotation();
opacity = opacity * this.getOpacity();
// 3. Do your custom rendering here
// - Use draw2D methods for drawing
// - Access cached data (e.g., this.content)
// - Create/update item components
// - Call itemComp.renderNode() for child items
// 4. Update bounding rectangles
this.rectToScene = { x: drawTrans[0], y: drawTrans[1], width: ..., height: ... };
this.rectToParent = { x: nodeTrans[0], y: nodeTrans[1], width: ..., height: ... };
// 5. Render children (if any non-content children exist)
this.renderChildren(interpreter, drawTrans, rotation, opacity, draw2D);
// 6. Update parent rects and mark clean
this.updateParentRects(origin, angle);
this.isDirty = false;
}
Common Mistakes to Avoid:
- ❌ Creating methods like
renderContent()- Group doesn't call them - ❌ Calling
getFieldValue()repeatedly in render loop - cache data inrefreshContent()orsetValue() - ❌ Forgetting visibility check - causes rendering of invisible nodes
- ❌ Not calling
updateParentRects()and settingisDirty = false- breaks bounding box calculations - ❌ Not handling the case when content is empty - can cause crashes
Drawing Interface (IfDraw2D)
IfDraw2D interface (src/core/brsTypes/interfaces/IfDraw2D.ts) provides BrightScript ifDraw2D API:
- Canvas management:
doClearCanvas(),getContext(),getCanvas(),getRgbaCanvas() - Basic shapes:
doDrawLine(),doDrawPoint(),doDrawRect(),doDrawRotatedRect() - Text rendering:
doDrawText()with font, color, alignment, rotation support - Image drawing:
doDrawObject(): Draw bitmap at positiondoDrawScaledObject(): Draw with scale factorsdoDrawRotatedObject(): Draw with rotation around center pointdoDrawTransformedObject(): Combined scale + rotation + translationdoDrawCroppedBitmap(): Draw portion of bitmap (for sprites, tiling)
- Collision detection:
collision()helper for RectRect, RectCircle, CircleCircle
BrsDraw2D Components (implement IfDraw2D for off-screen rendering):
- RoBitmap (
src/core/brsTypes/components/RoBitmap.ts): In-memory image with alpha channel, supports 9-patch borders - RoRegion (
src/core/brsTypes/components/RoRegion.ts): Sub-region of bitmap for sprite sheets, tiling - RoScreen (
src/core/brsTypes/components/RoScreen.ts): Double-buffered main screen, SwapBuffers for frame display - RoCompositor (
src/core/brsTypes/components/RoCompositor.ts): Layer compositor with sprites, z-ordering, collision
Canvas Pooling: createNewCanvas() and releaseCanvas() manage reusable canvas contexts to avoid GC pressure
Content Handling Pattern
ArrayGrid/RowList/ZoomRowList Content Processing: Nodes that display dynamic content from ContentNode trees follow this pattern:
- Content Field in setValue() Method (note:
set()method is deprecated, usesetValue()):
setValue(index: string, value: BrsType, alwaysNotify?: boolean, kind?: FieldKind, sync?: boolean) {
const fieldName = index.toLowerCase();
if (fieldName === "content") {
// First, store the field value
super.setValue(index, value, alwaysNotify, kind);
// Clear existing item components
this.itemComps.length = 0; // or this.rowItemComps.length = 0
// Process content into cache
this.refreshContent();
// Set initial focus if needed
if (this.content.length > 0 && this.focusIndex < 0) {
this.focusIndex = 0;
}
return;
}
super.setValue(index, value, alwaysNotify, kind, sync);
}
- refreshContent() Method:
protected refreshContent() {
// Clear content cache
this.content.length = 0;
// Get content field value
const contentNode = this.getFieldValue("content");
if (!(contentNode instanceof ContentNode)) {
return;
}
// Extract children into flat array
const children = contentNode.getNodeChildren();
this.content = children.filter((child) => child instanceof ContentNode) as ContentNode[];
// Initialize tracking arrays (focus, scroll, etc.)
for (let i = 0; i < this.content.length; i++) {
this.rowFocus[i] = this.rowFocus[i] ?? 0;
this.rowScrollOffset[i] = this.rowScrollOffset[i] ?? 0;
}
}
- Use Cached Content in renderNode():
renderNode(...) {
// Use cached content, NOT getFieldValue("content")
if (this.content.length === 0) {
return;
}
for (let i = 0; i < this.content.length; i++) {
const contentItem = this.content[i]; // Already a ContentNode
// Render using cached data
}
}
Key Points:
- ALWAYS call
super.setValue()first to store the field value - NEVER call
getFieldValue("content")orgetNodeChildren()in render methods - use cachedthis.contentarray - Process content once in
refreshContent(), use cache everywhere else - This prevents infinite loops and improves performance
- The
setValue()method is the modern approach;set()is deprecated but maintained for compatibility
Performance and Caching
- Text measurement caching: Group.isDirty + cachedLines avoid re-measuring text on every frame
- Bitmap texture management:
TextureManager(global singleton) caches loaded images by URI; a separate global registry insrc/core/device/Graphics.ts(gated byBrsDevice.tracking) tracks all live bitmaps/fonts for ther2d2-bitmapsdebug query - Lazy bounding rect updates: Only recalculated during render pass when transforms change
- Conditional rendering: Nodes check
visiblefield early to skip invisible subtrees - Transform accumulation: Transforms calculated incrementally down tree, not recalculated from root
Component Lifecycle
- Creation:
createNode()orcreateChild()instantiates node, sets initial fields - Initialization:
init()BrightScript function called if defined in component - Field changes: Observers notified,
onChangecallbacks invoked - Rendering:
renderNode()called every frame if visible - Focus changes:
onKeyEvent()called when node has focus and receives key press - Destruction:
deinit()called, observers removed, children destroyed recursively
Event Handling
- Key events:
Scene.handleOnKeyEvent()walks focus chain from focused node up to Scene- Each node's
onKeyEvent()BrightScript function called if defined - If returns
true, event consumed; iffalse, bubbles to parent - Built-in nodes (like Group) have
handleKey()method for default behavior
- Each node's
- Field observers:
observeField()registers callback (function name or message port) for field changes - Timer events: Timer node posts messages to message port on interval/timeout
Conditional Compilation
Use preprocessing directives for platform-specific code:
/// #if BROWSER
// Browser-only code (uses Web APIs)
/// #else
// Node.js code (uses Node APIs)
/// #endif
Common pattern: src/core/index.ts uses /// #if BROWSER to set up Worker onmessage vs Node.js postMessage mock
Communication Patterns
- Browser: Host ↔ Worker via
postMessage()with typed payloads (AppPayload,TaskPayload) - Shared memory:
SharedArrayBufferfor control signals between host and Worker (key events, display state) - Events:
BrsDevice.sharedArrayholds inter-thread communication buffer (seesrc/core/device/BrsDevice.ts)
File System Architecture
- Virtual FS:
FileSystemclass (src/core/device/FileSystem.ts) uses@zenfs/corefor in-memory file system - Roku volumes:
pkg:/,tmp:/,cachefs:/,ext1:/(external) simulated as mount points - Zip packages auto-extracted to
pkg:/on execution
Testing Patterns
- Vitest tests in
test/mirror source structure - End-to-end tests in
test/e2e/run full BrightScript files - Simulator tests in
test/simulator/contain.brsfiles exercising runtime features - Use
execute(source)helper from interpreter tests to run BrightScript code snippets
Common Tasks
Adding a New BrightScript Component
- Create file in
src/core/brsTypes/components/Ro<ComponentName>.ts - Extend
BrsComponent, implement required interfaces (e.g.,IfArray,IfAssociativeArray) - Register in
src/core/stdlib/CreateObject.tsfactory function - Add tests in
test/brsTypes/components/
Adding a New SceneGraph Node
- Create file in
src/extensions/scenegraph/nodes/<NodeName>.ts - Extend appropriate base (
Node,Group,ArrayGrid, etc.) - Define
defaultFieldswith all Roku-documented fields - Implement
renderNode()for rendering if visual node - Register in
src/extensions/scenegraph/factory/NodeFactory.tsnode registry - Export from
src/extensions/scenegraph/nodes/index.ts
Working with the Debugger
- Developer vs production mode: The Micro Debugger and all debug instrumentation are gated behind
debugOnCrash(CLI--debug/ APIoptions.debugOnCrash), via theBrsDevice.trackingflag. Production is the default. Encrypted.bpkpackages always run in production mode. - Micro Debugger (developer mode only): Set breakpoints with the
STOPstatement in BrightScript. In production mode the debugger is disabled, soSTOPexits the app (EXIT_BRIGHTSCRIPT_STOP) and break requests are ignored. - Debug API: Call
debug("break")from host to pause interpreter - Console integration:
printstatements route throughBrsDevice.stdout - Gating new instrumentation: counters/registries that exist only for debug commands (
bscs,sgnodes,stats, ther2d2-bitmapstexture registry) must be gated behindBrsDevice.trackingso they add no overhead in production.
Important Constraints
- No
eval(): BrightScript'sEval()not implemented (documented inlimitations.md) - Task threads limited to 10 per app (see
limitations.md) m.globalis shared: Changes tom.globalare now properly shared across Task threads- Video/Audio lifecycle: Must call
.stop()before destroying player objects or playback continues - CORS: Web apps need CORS proxy for cross-origin
roUrlTransfercalls (configurable inDeviceInfo.corsProxy) - System fields protection: Cannot remove system fields or use
setFields()to add new fields (useaddFields()instead) - Field type validation: Field assignment now validates types (e.g.,
intarray,floatarray) and converts values appropriately
Key Files to Reference
- Core entry:
src/core/index.ts- Interpreter initialization and main execution loop - API surface:
src/api/index.ts- Browser package's public API (seedocs/engine-api.md) - Type definitions:
src/core/brsTypes/BrsType.ts- Foundation of type system - Device state:
src/core/device/BrsDevice.ts- Registry, file system, device info singleton - Manifest parsing:
src/core/common.ts-parseManifest()and device info types - Extension contract:
src/core/extensions.ts-BrsExtensioninterface andregisterExtension() - SceneGraph entry:
src/extensions/scenegraph/index.ts-BrightScriptExtensionclass, exports all SG symbols - SGRoot singleton:
src/extensions/scenegraph/SGRoot.ts- Active scene,m.global, focus, threads, timers - SceneGraph bootstrap:
src/extensions/scenegraph/parser/ComponentDefinition.ts- Component XML parsing and node tree building - Node factory:
src/extensions/scenegraph/factory/NodeFactory.ts-createNode()registry for all built-in node types - Serializer:
src/extensions/scenegraph/factory/Serializer.ts- Node serialization for cross-thread messaging - Lexer/Parser module:
src/core/LexerParser.ts- Separated lexer and parser functions for reusability - Field system:
src/extensions/scenegraph/nodes/Field.ts- Field model, type validation, and conversion logic
Project-Specific Quirks
modkeyword conflict: Cannot usemodas variable name (BrightScript operator vs identifier)- Memory info:
roAppMemoryMonitoronly accurate in Node.js and Chromium (uses non-standardperformance.memory) - Prettier config: Use 4-space tabs, 120 char line width (see
package.json) - Branch naming: Current development branch is
master - Platform detection: Use
BrsDevice.deviceInfo.customFeaturesarray to check host-defined capabilities (e.g.,"touch_controls") - Virtual File System: Uses
@zenfs/corewith case-insensitive file system; writeable volumes preserve original case - Type coercion: Functions automatically convert between Integer and Float types when needed
- Typed returns: User functions with typed returns automatically return
0if no return statement is hit - SceneGraph CLI flag: The Node.js CLI loads the SceneGraph extension by default; pass
--no-sgto skip it - Node.js library: Manually register SceneGraph extension with
registerExtension(() => new BrightScriptExtension())frombrs-scenegraph
Documentation
- Limitations: See
docs/limitations.mdfor unsupported features and known issues - Customization: See
docs/customization.mdfor DeviceInfo config and manifest options - Extensions: See
docs/extensions.mdfor details on the extension system andbrs-scenegraphintegration - Contributing: See
docs/contributing.mdfor PR guidelines
Trust
Not scanned yet. Artifacts are graded after they are crawled, so a recently discovered one may have no result for a while.
Versions
git-f9a5a8d19ccb2026-08-04