Pipeline extension API
The pipeline extension API lets you hook into rk build and rk dev from your roku-config.ts. You can use it for the following scenarios:
- Running generators before a build
- Uploading source maps after a build
- Adding custom esbuild plugins at named points in the tooling plugin chain
To use the API, define a pipeline object with hooks and plugins and add it to your roku-config.ts.
Importing and declaring the pipeline
The following code demonstrates how to import and declare the pipeline extension (alternatively, you can define pipeline inline directly in roku-config.ts).
import type { RokuConfig } from "@roku-sdk/rsg-ts/roku-config";
import { pipeline } from "./roku-pipeline";
export default {
app: {
entryPoint: "src/index.ts"
},
pipeline
} satisfies RokuConfig;Hook context types
Hooks receive a context object. The following import brings in the context types:
import type {
BuildHookContext,
PostBuildHookContext,
DeployHookContext,
DeployHookDevice,
ManifestEntries,
RokuPipeline
} from "@roku-sdk/rsg-ts/roku-config";The following interfaces define the context types:
interface BuildHookContext {
config: CommonBuildConfig; // merged config: defaults -> roku-config.ts -> command-line flags
isRebuild: boolean; // false on first build, true on later watch cycles
}
interface PostBuildHookContext extends BuildHookContext {
result: esbuild.BuildResult;
bundleDir: string; // absolute path
pkgDir: string; // absolute path
}
interface DeployHookContext extends PostBuildHookContext {
device: DeployHookDevice;
}
// Narrow on `kind === "rce"` before using `ip` as an address: a Roku Cloud Emulator
// device has none, and its `ip` is the empty string.
type DeployHookDevice =
| { kind?: "lan"; id?: string; ip: string; name?: string } // id: from `rk device ls`, or `host:<ip>` for `--host`
| { kind: "rce"; id: string; ip: string; name?: string }; // name: `rce:<name>`; ip: always ""
// rk always sets `kind` and `id`; id is empty when no registered device was resolved.
// They are optional on the LAN variant only so that a context built as `{ ip, name }`
// (in a hook's own unit tests, say) still type-checks.Pipeline types
The following types define the pipeline, its plugin slots, and its hooks:
type BuildPluginSlot = "workspaceResolve" | "images" | "assets" | "fonts" | "solid" | "hermes";
type StartDevPluginSlot = BuildPluginSlot | "sideload";
type PositionFor<TSlot extends string> =
| "first"
| "last"
| { before: TSlot }
| { after: TSlot };
interface PipelinePlugin<TSlot extends string> {
plugin: esbuild.Plugin;
position: PositionFor<TSlot>;
}
interface BuildPipeline {
preBuild?: (ctx: BuildHookContext) => Promise<void> | void;
transformManifest?: (entries: ManifestEntries) => ManifestEntries | Promise<ManifestEntries>;
postBuild?: (ctx: PostBuildHookContext) => Promise<void> | void;
plugins?: PipelinePlugin<BuildPluginSlot>[];
}
interface StartDevPipeline {
preBuild?: (ctx: BuildHookContext) => Promise<void> | void;
transformManifest?: (entries: ManifestEntries) => ManifestEntries | Promise<ManifestEntries>;
postBuild?: (ctx: PostBuildHookContext) => Promise<void> | void;
postDeploy?: (ctx: DeployHookContext) => Promise<void> | void;
plugins?: PipelinePlugin<StartDevPluginSlot>[];
}
interface RokuPipeline {
build?: BuildPipeline;
startDev?: StartDevPipeline;
}Built-in plugin chain
The build chain order is as follows: workspaceResolve -> images -> assets -> fonts -> solid -> hermes.
The rk dev chain order is as follows: workspaceResolve -> images -> assets -> fonts -> solid -> hermes -> sideload. The hermes slot uses debug-upload for JavaScript by default, or hermes-compile when --compile-ts (or compileTs: true in config) enables local bytecode compilation. The --no-sideload flag omits the sideload step.
The following rules apply to slots:
- The
sideloadslot exists only inrk dev. - Build pipeline typing does not allow
sideload. - If you use an unknown slot, config loading fails with an error.
- Multiple plugins targeting the same position keep declaration order.
Hook execution order
The hooks run in a different order for rk build, a single rk dev run, and rk dev --watch.
rk build
The rk build command runs the following steps:
- Load and merge the config.
- Clean the output directory.
- Run
pipeline.build.preBuild. - Run the esbuild build (plus custom injected plugins).
- Assemble the manifest (
pipeline.build.transformManifestruns before the manifest values are converted to strings and written). - Run the tooling build-end plugins (Hermes or debug).
- Run
pipeline.build.postBuild. - Package the app (when you use
--package).
rk dev (single run)
A single rk dev run follows these steps:
- Load and merge the config.
- Clean the output directory.
- Run
pipeline.startDev.preBuild. - Run the esbuild build (plus custom injected plugins).
- Assemble the manifest (
pipeline.startDev.transformManifestruns before the manifest values are converted to strings and written). - Run the tooling build-end plugins (Hermes or debug, plus sideload).
- Run
pipeline.startDev.postBuild. - Run
pipeline.startDev.postDeploy(only after a successful sideload).
rk dev --watch
Each watch cycle runs the following steps:
- Run
preBuild. - Build.
- Run
transformManifest(before the manifest is converted to strings and written). - Run
postBuild. - Run
postDeploy(after a successful sideload).
The isRebuild value is false for the first cycle and true for all later cycles.
Error behavior
The pipeline handles hook exceptions according to the execution context:
- Non-watch mode (
rk build,rk dev): Hook exceptions abort the command. - Watch mode (
rk dev --watch): The pipeline logs hook failures for that cycle, and watch continues. - Shell strings: Hooks are function-based only. Shell-string hook syntax is not supported.
transformManifest
The transformManifest hook runs during manifest assembly, after the pipeline merges base entries from pkg/manifest and roku-config.ts but before it converts the values to strings and writes them. The following code shows the hook type:
type ManifestEntries = RokuManifestEntries & Record<string, string | number | boolean>;
transformManifest?: (entries: ManifestEntries) => ManifestEntries | Promise<ManifestEntries>;The following notes apply to the hook:
- The
transformManifesthook runs on every build cycle, includingrk dev --watchrebuilds. - Throwing from the hook aborts the current build cycle.
- The hook can return
build_versionas a number; tooling zero-pads it during manifest serialization.
Example
See baseline-app-pipeline-test and its roku-pipeline.ts for more information about a complete app that configures every hook, including startDev hooks and plugins at each supported position. The following example shows a pipeline with build and startDev hooks:
import { execSync } from "node:child_process";
import type { RokuPipeline } from "@roku-sdk/rsg-ts/roku-config";
export const pipeline: RokuPipeline = {
build: {
transformManifest: entries => ({
...entries,
build_version: Number(process.env.CI_BUILD_NUMBER ?? entries.build_version ?? 1)
}),
preBuild: ({ isRebuild }) => {
console.log("build preBuild", { isRebuild });
},
postBuild: ({ bundleDir }) => {
execSync(`echo uploading maps from ${bundleDir}`, { stdio: "inherit" });
},
plugins: [
{
plugin: {
name: "my-build-plugin",
setup(build) {
build.onStart(() => console.log("my-build-plugin onStart"));
}
},
position: { before: "solid" }
}
]
},
startDev: {
postDeploy: ({ device }) => {
console.log("deployed to", device.name ?? device.id);
}
}
};Understanding recent changes
The following change affects hooks that use the device. The DeployHookContext.device field is now DeployHookDevice. It used to be { ip: string; name?: string }. It now also carries kind ("lan" or "rce") and id (the device's id in rk device ls). A Roku Cloud Emulator device (kind: "rce") does not have an address of its own; therefore, its ip is always "", and a postDeploy hook that talks to the device by IP must check kind first. On the LAN variant, kind and id are optional; therefore, existing code that builds a { ip, name } context keeps compiling, and narrowing on device.kind === "rce" works either way.
Related documentation
- App configuration options (roku-config.ts): Describes the
roku-config.tsoptions. - rk build: Describes how to build an app without deploying it to a device.
- rk dev: Describes how to sideload an app to a device.
- Roku Cloud Emulator devices: Describes how to register, start, and use RCE devices.
- baseline-app-pipeline-test: Contains a complete app that configures every hook.
Updated about 5 hours ago