🚀

Getting started
Project layout, the <component> root tag, and the compile CLI.

A flash-theater app is a normal Roku app project with one addition: a src/ tree containing .thr (and optionally .flsh) files alongside your ordinary hand-written Roku project files (manifest, images, a bootstrap Main.brs). The flash-theater compile CLI reads everything under src/ and produces a complete, ready-to-zip Roku project under out/ — plain SceneGraph .xml and .brs, nothing exotic at runtime.

Project layout

src/ is 100% hand-written — it's the only directory you ever touch. out/ is 100% generated, mirroring src/'s exact structure — safe to delete, and it's wiped clean and rebuilt from scratch on every non---check compile. Nothing hand-written ever lives in out/, so gitignoring it wholesale is always safe.

apps/my-app/
apps/my-app/
├── flash-theater.config.json   # optional — tooling config, sibling to src/ and out/
├── src/                        # 100% hand-written — the only directory you edit
│   ├── manifest
│   ├── images/
│   ├── source/Main.brs
│   └── components/
│       └── HomeScreen/HomeScreen.thr
└── out/                        # 100% generated — gitignored, wiped + rebuilt every compile
    ├── manifest
    ├── images/
    ├── source/Main.brs
    └── components/
        └── HomeScreen/HomeScreen.xml
        └── HomeScreen/HomeScreen.brs

An optional flash-theater.config.json at the app root (a sibling of src//out/, not inside src/) can rename either directory or exclude paths from compilation entirely:

flash-theater.config.json
{
  "designResolution": "hd",
  "srcDir": "src",
  "outDir": "out",
  "exclude": ["components/Experimental/**"]
}

The compile CLI

There's no glob/pattern argument — compile always processes the whole project, the same way tsc reads tsconfig.json with no arguments rather than being told a file list every time.

terminal
flash-theater compile                       # compile the whole project (src/ -> out/)
flash-theater compile --check                # validate only, write nothing
flash-theater compile --src-dir a --out-dir b

Packaging: flash-theater zip

Zipping out/ into a sideload-ready package is also the compiler's job, not something every app needs its own script for. flash-theater zip reads an already-compiled out/ (or out-<env>/, see Environments) and writes dist/<appName>.zip, where <appName> defaults to your package.json's "name".

terminal
flash-theater compile                       # first, build out/
flash-theater zip                            # then, zip out/ into dist/<name>.zip
flash-theater zip --app-name custom-name     # override the zip's base name

Every .thr file has the same shape

An optional <script> region (declarations — see Reactive state and Statements & expressions) followed by a mandatory <component> tag wrapping the template markup, which is always valid XML with one or more top-level children — no forced single-root wrapper.

Widget.thr
<script>
  ...declarations (field, derived, state, functions, ...)...
</script>

<component>
  ...children (valid XML, one or more top-level siblings)...
</component>

<component extends="..."> controls what SceneGraph base class the file compiles to — Group by default, or e.g. Scene for an app's root entry point. Here's the actual root of apps/sample-app, a real Scene-rooted component compiled from .thr like everything else in the app:

MainScene.thr
<script>
public function setup() {
  m.top.backgroundColor = "0x101010FF"
  router.setRouting([
    { path: "splash", component: "SplashScreen" },
    { path: "browse", component: "Shell", children: [
      { path: "", component: "HomeScreen" }
    ] }
  ])
  router.navigate("/splash")
}
</script>

<component extends="Scene">
<FlashTheaterRouterOutlet id="rootOutlet" />
</component>

The DSL is case-sensitive

BrightScript itself is case-insensitive, but .thr/.flsh deliberately are not — two identifiers differing only in case are different identifiers in DSL source (the compiled .brs output still folds case like any BrightScript program does at runtime). Every bare identifier you write must resolve to exactly one of: a real local in scope (a parameter, an assignment target, a loop variable), a declared binding (field/derived/state/read/watch/stream/function name), or m/a recognized BrightScript builtin. Anything else is a compile error, not a silent pass-through — there's no "probably a BrightScript local used elsewhere."

Sideloading to a real device

Each sample app's build:roku script runs flash-theater compile && flash-theater zip; a separate sideload script installs the resulting dist/ zip onto a dev Roku over the network via kopytko-roku-device. Sideloading needs a native Node install (not WSL — it can't reach a LAN device) and the device's current IP, which can drift across sessions.

⚠️ Not (yet) supported

  • ○ A fully-generated Main.brs — one hand-written bootstrap line is still required per app.
  • ○ A shared, package-size-aware generated runtime file strategy for apps approaching Roku's 4MB package limit.
  • ○ @import/kopytko-packager integration for pulling in external component libraries.

Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.