Skip to main content

Orchestrating a Multi-Language Monorepo with fyrer

fyrer is language-agnostic — it runs any shell command, so it works equally well with Rust, Go, TypeScript, Python, or any other toolchain. This guide walks through a realistic multi-language monorepo configuration inspired by the acme-corp example in the fyrer repository.

Example monorepo structure

my-monorepo/
├── fyrer.yml
├── apps/
│ ├── api/ # Rust (Cargo)
│ ├── web/ # TypeScript (Bun)
│ ├── worker/ # Go
│ └── cli/ # Python
└── packages/
├── ui/ # TypeScript shared library
├── shared/ # Rust shared library
└── core/ # Go shared library

Shared packages (ui, shared, core) are built first; the apps that depend on them are built after. fyrer's dependency graph handles the ordering automatically.

Complete fyrer.yml

Below is the full configuration for the acme-corp example, annotated with comments explaining each package:

version: 1

cache:
provider: local

# Global env vars — inherited by every package and task
env:
NODE_ENV: development
LOG_LEVEL: info

packages:
# ───────────────────────────── TypeScript / Bun ─────────────────────────────

# Shared UI component library — built once, consumed by web
- name: ui
root: ./packages/ui
env:
NODE_ENV: production
env_file: .env
tasks:
build:
cmd: bun build src/index.ts --outdir dist
inputs:
- src/**
- package.json
- tsconfig.json
outputs:
- dist/**
ignore:
- node_modules/**
cache: true
watch:
cmd: bun build src/index.ts --outdir dist --watch
inputs:
- src/**
outputs:
- dist/**
ignore:
- node_modules/**
cache: false
watch: true # polls inputs and restarts on change (300ms poll, 300ms debounce)
env:
NODE_ENV: development

# Frontend web app — depends on ui:build
- name: web
root: ./apps/web
env:
PORT: "3000"
API_URL: http://localhost:8080
env_file: .env
tasks:
dev:
cmd: bun --watch src/index.ts
depends_on:
- ui:build
inputs:
- src/**
- package.json
ignore:
- node_modules/**
- dist/**
cache: false
persistent: true
env:
NODE_ENV: development
build:
cmd: bun build src/index.ts --outdir dist
depends_on:
- ui:build
inputs:
- src/**
- package.json
outputs:
- dist/**
ignore:
- node_modules/**
cache: true
env:
NODE_ENV: production

# ────────────────────────────── Rust / Cargo ───────────────────────────────

# Shared Rust library — built first, consumed by api
- name: shared
root: ./packages/shared
tasks:
build:
cmd: cargo build --release
inputs:
- src/**
- Cargo.toml
- Cargo.lock
outputs:
- target/release/libshared.rlib
ignore:
- target/**
cache: true

# Rust HTTP API — depends on shared:build
- name: api
root: ./apps/api
env_file: .env
tasks:
build:
cmd: cargo build --release
depends_on:
- shared:build
inputs:
- src/**
- Cargo.toml
- Cargo.lock
outputs:
- target/release/api
ignore:
- target/**
cache: true
dev:
cmd: cargo run
depends_on:
- shared:build
inputs:
- src/**
- Cargo.toml
ignore:
- target/**
cache: false
persistent: true

# ──────────────────────────────── Go ──────────────────────────────────────

# Shared Go library — compiled as an archive
- name: core
root: ./packages/core
tasks:
build:
cmd: go build -buildmode=archive -o target/core.a .
inputs:
- "*.go"
- go.mod
outputs:
- target/core.a
cache: true

# Go background worker — depends on core:build
- name: worker
root: ./apps/worker
env:
ADDR: ":9000"
tasks:
build:
cmd: go build -o bin/worker .
depends_on:
- core:build
inputs:
- "*.go"
- go.mod
outputs:
- bin/worker
ignore:
- bin/**
cache: true
dev:
cmd: go run .
depends_on:
- core:build
inputs:
- "*.go"
- go.mod
ignore:
- bin/**
cache: false
persistent: true

# ──────────────────────────────── Python ──────────────────────────────────

# Python CLI — bundle, lint, docs generation, and a persistent serve task
- name: cli
root: ./apps/cli
env_file: .env
tasks:
bundle:
cmd: python3 cli.py bundle
inputs:
- cli.py
- .env
outputs:
- dist/**
ignore:
- dist/**
- __pycache__/**
cache: true
lint:
cmd: python3 cli.py lint
inputs:
- cli.py
outputs:
- dist/lint-report.txt
ignore:
- dist/**
- __pycache__/**
cache: true
docs:
cmd: python3 gen-docs.py
cwd: scripts
inputs:
- gen-docs.py
outputs:
- ../docs/**
ignore:
- ../docs/**
- __pycache__/**
cache: true
serve:
cmd: python3 cli.py serve
depends_on:
- cli:bundle
inputs:
- cli.py
ignore:
- dist/**
cache: false
persistent: true
env:
PORT: "8010"
timeout-demo:
cmd: python3 cli.py slow
timeout: 2s
inputs:
- cli.py
ignore:
- __pycache__/**
cache: false
i

Tasks with watch: true are watched by polling their inputs globs every 300ms (debounced 300ms). When inputs change, the task is restarted automatically and its dependents are marked Stale (visible as in the TUI). watch cannot be combined with cache.

Running the whole stack

# Build everything (first run: compiles; subsequent runs: mostly cached)
fyrer run build

# Start all dev servers
fyrer run dev

# Preview what would run without executing anything
fyrer plan build

On the first fyrer run build, all packages compile from scratch. On subsequent runs, only packages whose inputs have changed are rebuilt — the rest are restored from the local cache and reported as ⚡ Cached.

Cross-language dependencies

depends_on works across languages without any special configuration. In the example above:

  • worker:build (Go) depends on core:build (Go)
  • api:build (Rust) depends on shared:build (Rust)
  • web:build and web:dev (TypeScript) both depend on ui:build (TypeScript)
  • cli:serve (Python) depends on cli:bundle (Python)

fyrer doesn't know or care what language a task uses — it resolves the dependency graph and runs each command in order. You can freely add cross-language dependencies the same way: just reference package:task in depends_on.

Per-package tool setup

Each package uses its own toolchain (bun, cargo, go, python3). fyrer does not install tools — it assumes every required binary is already available on your PATH when fyrer run is executed.

i

Install all required toolchains (Rust / Cargo, Go, Bun, Python 3, etc.) on your machine or CI runner before running fyrer. fyrer will fail with a command not found error for any package whose toolchain is missing.

Use fyrer list to verify that all packages and tasks are correctly detected after editing fyrer.yml. It's a fast, read-only check that reports every package name and its available tasks without executing anything.