Skip to main content

AI does help me write code, but when I'm writing for you here, I only write my own words, not even using autofill. I would rather lose some time than lose your trust. - Scott

v1.9 - An Affected Graph is Here

· 5 min read
Scott
Lead Developer: bun-workspaces

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:

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.lock for all other dependencies in a workspace's package.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.