Feature Flags
iRaceDeck ships three plugins — the Elgato Stream Deck plugin, the Mirabox VSD Craft plugin, and the Ulanzi Deck plugin. They share most code, but there are two kinds of reason a feature might need gating: a genuine hardware difference (only Stream Deck+ has a touch strip), or a temporary in-development kill-switch. Icon rendering itself no longer differs by host — since issue #642, every plugin rasterizes its SVG icons to PNG in-plugin (@iracedeck/rasterizer, wrapping @resvg/resvg-js) before sending pixels to the device, so the old QT5-vs-QT6.7+ SVG engine split that used to justify most of these flags is gone. See Architecture and .claude/rules/svg-platform-compatibility.md (in-repo) for what changed.
Feature flags let us gate those features at build time: unsupported code is stripped from the bundle, and Property Inspector controls that would have no effect are hidden. Flags also provide a lightweight way for contributors to test in-development features locally without shipping them to everyone, or to kill a risky in-development pipeline quickly.
How the flags are structured
Section titled “How the flags are structured”Each plugin has a committed platform-features.json:
packages/iracing-plugin-stream-deck/platform-features.jsonpackages/iracing-plugin-mirabox/platform-features.jsonpackages/iracing-plugin-ulanzi/platform-features.json
The file has one top-level key, features — product-level flags. (A capabilities key existed before issue #642 to track raw SVG engine support; it was retired once icon rendering moved to in-plugin PNG rasterization, since no code branches on host SVG engine capability anymore.)
Example (Mirabox):
{ "features": { "dialFeedback": false, "profiles": false, "pngRasterization": true }}Where the flags take effect
Section titled “Where the flags take effect”The plugin build pipeline reads the merged flags once and fans them out to three places:
- Bundle code —
@rollup/plugin-replacesubstitutes__FEATURE_DIAL_FEEDBACK__and__FEATURE_PNG_RASTERIZATION__withtrue/falseat compile time. Terser then drops unreachable branches, so disabled code doesn’t ship.pngRasterizationgates a single call,initializeRasterizer(...)in each plugin’splugin.ts— when it’sfalse, the call (and everything it would have pulled in) is dropped.profilesis not substituted here — it has no compile-time constant, so it never tree-shakes anything (see the table below). - Property Inspector HTML — the same flags are passed into EJS templates as
locals.platform. Controls wrapped in<% if (locals.platform?.features?.dialFeedback !== false) { %>disappear from the compiled HTML.profilesgates PI content the same way — the “Stream Deck Profiles” accordion and the Race Admin / Camera Focus car-selector sections.pngRasterizationgates no PI content — it only guards a plugin-startup call. - Runtime
config.json— the merged flags are written tocom.iracedeck.sd.core.sdPlugin/bin/config.jsonas afeatureFlagsfield. Readable at runtime viagetFeatureFlag("pngRasterization")/getPlatformFeatures()from@iracedeck/deck-coreif a dynamic check is ever needed. This is the only way to readprofiles, since it skips the compile-time path entirely.
Overriding flags locally
Section titled “Overriding flags locally”For local testing — without editing committed files — create feature-flags.local.json at the repo root:
{ "features": { "pngRasterization": false }}Each plugin’s build deep-merges this file on top of its own committed platform-features.json. Any keys you don’t include fall through to the committed values. The file is listed in .gitignore, so it never lands in a commit.
You must rebuild for the override to take effect (pnpm build or restart pnpm watch:*). There’s no runtime reload — flags are baked into the bundle.
Always restart the watcher after editing a flag file. Rollup resolves the flags once when its config module loads, and that resolution is held for the lifetime of the watcher — editing platform-features.json or feature-flags.local.json mid-watch will trigger a rebuild but the output will still reflect the flag values from watcher startup.
Unknown keys in the local file are ignored with a warning during the build — watch the console for [platform-features] feature-flags.local.json has unknown keys (ignored): … to catch typos.
A committed feature-flags.local.json.example at the repo root documents the shape; copy it if you’d like a starting point.
Typical use cases
Section titled “Typical use cases”- Test a Mirabox-only scenario on your Stream Deck build. Set
features.dialFeedback: falsein the local file, rebuild Stream Deck — the touch-strip feedback code and controls disappear from your Stream Deck build too. Flip it back and they return. - Compare PNG rasterization against the raw SVG path. Set
features.pngRasterization: falsein the local file and rebuild — the plugin falls back to sending SVG data URIs straight to the host, exactly as every build did before issue #642. Useful for isolating whether a rendering issue is in the rasterizer or elsewhere. - Develop a beta feature locally without shipping it. (Once issue #363 lands.) Commit the feature with its flag defaulting to
falseeverywhere. Testers opt in viafeature-flags.local.json.
Current flags
Section titled “Current flags”| Flag | Stream Deck | Mirabox | Ulanzi | Purpose |
|---|---|---|---|---|
dialFeedback |
true |
false |
false |
Stream Deck+ touch-strip feedback + touch-tap input — only Elgato hardware has a plugin-facing touch strip |
profiles |
true |
false |
false |
Stream Deck Profiles PI accordion + profile switching (Race Admin selector, Camera Focus) — Elgato-only, since Mirabox/Ulanzi hosts have no profile system. Runtime-only: read via getFeatureFlag("profiles") / locals.platform, with no __FEATURE_*__ compile-time constant (unlike the other two flags) |
pngRasterization |
true |
true |
true |
Temporary kill-switch for in-plugin PNG rasterization (@iracedeck/rasterizer, issue #642) — true everywhere; force it false locally to fall back to raw SVG data URIs |
Adding a new flag
Section titled “Adding a new flag”Short version (see the in-repo rule .claude/rules/platform-feature-flags.md for full details):
- Add the flag to all three
platform-features.jsonfiles with the correct per-platform default. - Add it to the
PlatformFeatureFlagsinterface inpackages/deck-core/src/plugin-config.ts. - If the flag needs to tree-shake bundle code (most do), declare the
__FEATURE_*__ambient global in each of the three plugins’ ownsrc/platform-features.d.tsand add a replace entry in all three pluginrollup.config.mjsfiles, then seed the default intest-setup.tsand cover both thetrueandfalsepaths withvi.stubGlobal. A flag that only gates a PI control or an infrequent runtime check — likeprofiles— can skip all of this and just readgetFeatureFlag(...)/locals.platform?.features?.…at the call site. - Gate the affected code (plugin init,
deck-core, or an action file) and/or PI partials (locals.platform?.features?.yourFlag !== false).

