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/
├── 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:
{
"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.
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 bPackaging: 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".
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 nameEvery .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.
<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:
<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.