🌎

Environments
env.<name>, environments/<name>.config.json, manifestOverrides, --env.

An environment is a named, opt-in build profile — different builds of the same app (staging/production) that need different build-time variables (API keys, service URLs), a patched manifest, or a different set of bundled files, without a second copy of the source tree. With no environment active, a build behaves exactly as it did before this feature existed — this is entirely additive.

Selecting an environment

--env <name> selects environments/<name>.config.json (a dedicated folder, sibling to src/). A FLASH_THEATER_ENV environment variable is honored as a fallback whenever --env isn't passed — specifically so it flows through npm run build:roku's compile && zip chain with zero package.json changes:

shell
FLASH_THEATER_ENV=staging npm run build:roku
# equivalently, passed directly to the compiler itself:
flash-theater compile --env staging

An active environment writes to out-<env>/ instead of out/. flash-theater zip --env <name> (see Getting started) then names its output dist/<app>-<env>-<version>.zip instead of dist/<app>.zip — different environments' builds never clobber each other or the plain build.

environments/<name>.config.json

All four keys are optional, but — unlike the base flash-theater.config.json — an unrecognized top-level key is a hard error: designResolution/srcDir/outDir stay base-config-only, never per-environment.

environments/staging.config.json
{
  "variables": {
    "apiBaseUrl": { "value": "https://staging.api.example.com" },
    "apiKey": { "fromEnv": "STAGING_API_KEY" }
  },
  "manifestOverrides": {
    "title": "My App (Staging)"
  },
  "exclude": ["images/production-only/**"],
  "include": ["images/staging-only/**"]
}
  • variables — each name resolves to a plain string: a literal (value) or a build-time bash/CI variable (fromEnv, failing the build if unset). Readable from DSL code as env.<name> below.
  • manifestOverrides — a partial patch on top of the base src/manifest: each key is upserted when writing out-<env>/manifest. src/manifest itself is never touched.
  • exclude — glob patterns added on top of the base config's own exclude, for this environment only.
  • include — glob patterns exempted from exclude (base's or this environment's) for this environment only — lets a base config permanently exclude e.g. images/staging-only/** from the plain build, while each environment's own include pulls its own subtree back in.

env.<name> — reading a declared variable

Resolved the same structural way theme.a.b is — not a schemaless, free-form scan like store/router — because an environment's whole variable set is known from its own config file at compile time. Using env.* with no active environment, or referencing an undeclared name, is an ordinary compile error, not a runtime bug:

EnvDemo.thr (excerpt)
derived apiBaseUrlLabel: string = "API: " + env.apiBaseUrl

Local overrides

An optional, git-ignored environments/<name>.local.config.json, same shape, sitting beside the committed file — picked up automatically whenever that environment is selected, no extra flag. Lets a developer point apiBaseUrl at localhost or supply a personal sandbox key without touching the committed config:

environments/staging.local.config.json (git-ignored)
{
  "variables": {
    "apiBaseUrl": { "value": "http://localhost:3000" }
  }
}

variables/manifestOverrides are merged key-by-key with the local file winning on any conflict; exclude/include are concatenated, committed patterns first.

Reference implementation — apps/environments-demo

Every mechanism on this page has a router-mounted, scaled chapter in apps/environments-demo — 2 chapters (/variable-reads and /overrides-and-manifest), reachable with REWIND/FAST-FORWARD once compiled and sideloaded. Unlike every other chapter app, this one has no meaningful "plain" (no active environment) build at all — every chapter reads env.*, which is a hard compile error with no active environment — so its own package.json defaults FLASH_THEATER_ENV to dev via a small scripts/with-env.mjs wrapper whenever the caller hasn't already set one. Chapter 1 reads three declared variables (a URL-shaped apiBaseUrl, a feature-flag-shaped enableBetaFeatures, and a fromEnv-sourced buildLabel) and narrates the closed-set validation errors that can't be demonstrated live (a build that hit either one would never have produced a running app). Chapter 2 proves manifestOverrides actually patched the shipped manifest by reading the title back at runtime with roAppInfo.GetTitle(), proves include/exclude actually changed which files were packaged by reading two environment-only placeholder files back with ReadAsciiFile, and narrates local overrides (environments/dev.local.config.json.example is this app's own copy-paste starting point). See findings/environments-demo-app.md for what each chapter covers and findings/demo-app-conventions.md for the app-structure convention it follows.

⚠️ Not (yet) supported

  • ○ Non-string variable values (numbers, booleans, nested groups) — every env.<name> value is a plain string.
  • ○ Per-environment designResolution/srcDir/outDir — those stay base-config-only, app-wide settings.
  • ○ env.* in the site's in-browser ThrPlayground — there is no real process.env/environment-file concept to demo against client-side.

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