v1.9 - An Affected Graph is Here

bun-workspaces 1.9 is a major milestone for the project, thanks to the new debuggable affected graph!
Read on for more details, or get straight into the documentation.
More documentation resources:
- Affected Overview
- CLI:
- TS/JS API:
- Configuration
- Project Root Configuration (sets default base git ref for comparison)
- Workspace Configuration (sets inputs for affected detection)
- Workspace Inputs (configuring affected detection inputs)
- Workspace Dependencies (how workspaces depend on each other)
Graphs?
The "graph" term hasn't been explicitly used with bun-workspaces primitives and features very much, but it's a useful
concept that still applies.
The Project Graph
In bun-workspaces, you could say that the project graph is simply the structure of your
workspaces and how they depend on each other, each workspace being a node and each dependency being an edge.
bun-workspaces does not perform source code analysis to determine the graph, since with Bun,
workspaces must be explicitly declared in package.json dependencies like "my-workspace-name": "workspace:*"
for imports/exports to work between them.
This makes determining the project graph fast and computationally cheap.
The Affected Graph
The affected graph is simply only the part of the project graph that has been meaningfully changed.
If one can determine which workspaces are affected by some code change, that can be very helpful for orchestrating DevOps tasks, such as running builds for only those changed workspaces.
What is a Meaningful Change?
Inputs: The Source of Changes
By default, a workspace is considered to have a set of inputs.
When not configured, the default inputs are:
- All git-trackable files in the workspace's directory
- All workspace dependencies. If a workspace dependency is affected for any reason, its dependents are also considered affected.
- The version resolved in
bun.lockfor all other dependencies in a workspace'spackage.json.
Inputs can be configured per script as well.
Here's an example of a workspace config that configures its inputs:
// path/to/your/workspace/bw.workspace.ts
import { defineWorkspaceConfig } from "bun-workspaces/config";
export default defineWorkspaceConfig({
alias: "my-web-app",
tags: ["frontend"],
// All input config is optional
defaultInputs: {
// default: all git-trackable files in the workspace directory
files: [
"src/**/*.{ts,tsx}",
"!src/**/*.test.{ts,tsx}",
"/tsconfig.json", // relative to project root
],
// treat all tagged libs like dependencies
workspacePatterns: ["tag:lib"],
// treat only the "react" package as affect-able
externalDependencies: ["react"],
},
scripts: {
// configure inputs per script
test: {
inputs: {
files: ["src/**/*.{ts,tsx}"],
},
},
},
});
Detecting Changes
There are two main ways to determine affected workspaces:
Using git diff (default)
The default behavior of affected features is to use the git diff between the current HEAD and the default base ref in git.
The default base ref is main except when overridden via the project root config or a environment variable BW_AFFECTED_BASE_REF_DEFAULT.
Uncommitted changes (staged, unstaged, untracked) are considered a part of the diff by default. Files that are gitignored are never considered a part of the diff.
# Uses diff of current HEAD against the default base ref
bw ls-affected
# Compare specific refs (can also pass commit SHA, tag, etc.)
bw ls-affected --base=my-branch-a --head=my-branch-b
# Ignore uncommitted changes
bw ls-affected --ignore-uncommitted # (staged, unstaged and untracked)
bw ls-affected --ignore-untracked
bw ls-affected --ignore-unstaged
bw ls-affected --ignore-staged
# Ignore changes to workspace dependencies derived from package.json files
bw ls-affected --ignore-workspace-deps
# Ignore changes to external dependencies (e.g. npm packages)
bw ls-affected --ignore-external-deps
Using a list of changed files
You can bypass using git entirely and simply pass a list of changed files that match workspace inputs.
bw ls-affected --files="packages/example/**/*.ts packages/example/my-file.js"
Debuggability
You can use the --explain flag to get detailed reasoning for the affected workspaces.
# Detailed output about every changed file and dependency
bw ls-affected --explain --detailed
This can give you output such as this when workspace-b depends on workspace-a, whose my-script.ts file is changed:
Workspace: workspace-a
Path: packages/workspace-a
Changed input files:
- my-script.ts (input: "my-script.ts")
Affected dependencies: (none)
Changed external dependencies: (none)
Workspace: workspace-b
Path: packages/workspace-b
Changed input files: (none)
Affected dependencies:
- @sandbox/workspace-a
chain: @sandbox/workspace-b --[package]-> @sandbox/workspace-a
Changed external dependencies: (none)
Running Affected Scripts
You can run a script across all affected workspaces with the bw run-affected command.
It essentially marries the options of bw ls-affected and bw run.
bw run-affected my-script --base=my-branch --ignore-uncommitted
API Parity
As usual, the TypeScript API is in parity with the CLI.
Determine affected workspaces via git:
import { createFileSystemProject } from "bun-workspaces";
// Initialize Project, by default at process.cwd()
const myProject = createFileSystemProject();
// Workspace results contain details about each workspace,
// including whether it is affected and why
const { workspaceResults } = await project.determineAffectedWorkspaces({
// Optional, the script to run to determine affected workspaces
// When not provided, based on workspaces' default inputs
script: "my-script",
diffSource: "git",
diffOptions: {
// Optional, defaults to main if default base ref not configured
baseRef: "my-branch-a",
// Optional, defaults to current HEAD if not provided
headRef: "my-branch-b",
// Optional means of ignoring uncommitted changes
// gitignored files are never included in a diff
ignoreUntracked: false, // files that may be tracked but aren't
ignoreStaged: false,
ignoreUnstaged: false,
// Ignores untracked, staged, and unstaged
ignoreUncommitted: false,
},
// Ignore workspace dependencies when determining affected workspaces
ignoreWorkspaceDependencies: false,
// Ignore changes external dependencies (e.g. react, lodash) lock versions
ignoreExternalDependencies: false,
});
Determine affected workspaces via list of files:
import { createFileSystemProject } from "bun-workspaces";
const myProject = createFileSystemProject();
const { workspaceResults } = await project.determineAffectedWorkspaces({
// Bypass git and pass a list of changed files that match workspace inputs
diffSource: "fileList",
changedFiles: ["src/**/*.ts", "something.txt"],
});
Run a script across affected workspaces:
import { createFileSystemProject } from "bun-workspaces";
const myProject = createFileSystemProject();
// Returns the same output and summary as project.runScriptAcrossWorkspaces
const { output, summary } = await project.runAffectedWorkspaceScript({
// About the same options as project.determineAffectedWorkspaces
affectedOptions: {
diffSource: "git",
diffOptions: {
baseRef: "my-branch-a",
headRef: "my-branch-b",
},
},
// About the same options as project.runScriptAcrossWorkspaces
scriptOptions: {
script: "my-script",
},
});
Conclusion
This is an exciting and big improvement to bun-workspaces I've wanted since day one, but I wanted
to ensure that the right foundation was in place for it first.
This is just one of several features to come that add advanced monorepo management capabilities to bun-workspaces
without changing the fact that you can use the package right away without any configuration.
This continues to reinforce my hypothesis that powerful monorepos can be achieved with more lightweight tooling that works with, rather than against, native tooling.
