warden.config.ts
Optional per-repo config for app builds, saved batches and affected e2e suites.
Optional, at the repo root. Without it, warden detects a single Expo project from app.json / app.config.*.
Setup
Add @delacour/warden as a dev dependency, then wrap the config in defineConfig. Your editor gets autocompletion, the defaults and docs on hover, and type errors for typos:
bun add -d @delacour/wardenwarden runs the file itself (Bun transpiles it), so it needs no build step. It looks for warden.config.ts, .mts, .js, .mjs and then warden.config.json, walking up from the cwd. Keep only one of them per directory. JSON configs still work but have no autocompletion.
Instead of an object, the default export can be a sync function. It receives { configDir, env }:
import { defineConfig } from "@delacour/warden/config";
export default defineConfig(({ env }) => ({
projects: [{ name: "app", bundleId: { ios: env.IOS_BUNDLE_ID ?? "com.example.app" } }],
}));If you'd rather not install the package, use a type-only import. It is erased before the file runs:
import type { WardenConfig } from "@delacour/warden/config";
export default { batches: { smoke: { platform: "ios", cmd: ["true"] } } } satisfies WardenConfig;The section types are exported too: WardenProject, WardenBatchPreset, WardenE2eSuite, WardenE2eFlow.
Projects
import { defineConfig } from "@delacour/warden/config";
export default defineConfig({
projects: [{
name: "salient",
root: "apps/salient/app",
bundleId: { ios: "com.example.salient", android: "com.example.salient" },
fingerprint: { command: "bun run --silent fingerprint:{platform}" },
eas: { profile: "development-simulator", workflow: ".eas/workflows/dev-build.yml", trigger: false },
build: { ios: "bunx expo run:ios --no-install --no-bundler" }
}]
});Prop
Type
The file needs at least one of projects, batches and e2e. With only batches, app builds fall back to detecting app.json / app.config.*.
Batches
batches maps a name to a saved warden batch invocation, run with warden batch <name>. Each field mirrors the flag of the same name. Flags you pass on the command line override the preset, and -- <cmd> overrides cmd. ios and android can't be used as names.
import { defineConfig } from "@delacour/warden/config";
export default defineConfig({
projects: [{ name: "salient", root: "apps/salient/app", bundleId: { ios: "com.example.salient" } }],
batches: {
"salient-e2e": {
project: "salient",
platform: "ios", count: 5, max: 5, profile: "iphone-17",
ports: ["8091:20"], app: true, retry: 1,
env: { E2E_SESSION_FILE: "e2e-artifacts/batch/session.json" },
serve: "bun scripts/e2e/run-ios.ts --session",
serveReady: "file:e2e-artifacts/batch/session.json",
serveTimeout: "20m",
jobsFrom: { command: "bun scripts/e2e/select-flows.ts --list --offline" },
cmd: ["bun", "scripts/e2e/run-ios.ts", "--attach", "{job}", "--device", "{udid}"]
}
}
});The preset's cwd is its project's root, or the config file's directory when there is no project. Serve, the jobs and jobsFrom.command run there, and relative paths in the preset resolve against it.
Prop
Type
e2e
e2e maps a suite name to the flows warden affected selects and warden e2e runs. See Affected e2e for how selection works. Every path and glob is relative to the config file.
import { defineConfig } from "@delacour/warden/config";
export default defineConfig({
e2e: {
mobile: {
flowsDir: "flows",
runner: ["<flow-runner>", "run", "{flowPath}", "--device", "{udid}"],
platform: "ios",
routerRoot: "apps/mobile/src/app",
maxDepth: 3,
runAll: ["bun.lock", "apps/mobile/ios/**"],
flows: {
"qa-login": { entries: ["apps/mobile/src/app/(auth)/login.tsx"] },
"store-*": { required: false }
}
}
}
});Prop
Type
flows.<id|glob>:
Prop
Type