SkillAgentSearch skills...

Turbowatch

Extremely fast file change detector and task orchestrator for Node.js.

Install / Use

/learn @gajus/Turbowatch
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

[!WARNING]

Deprecated. You probably don't need turbowatch. Some alternatives: turbo watch and Tilt

Turbowatch 🏎

Extremely fast file change detector and task orchestrator for Node.js.

If you ever wanted something like Nodemon but more capable, then you are at the right place.

Basic usage:

npm install turbowatch
cat > turbowatch.ts <<'EOD'
import { defineConfig } from 'turbowatch';

export default defineConfig({
  project: __dirname,
  triggers: [
    {
      expression: ['match', '*.ts', 'basename'],
      name: 'build',
      onChange: async ({ spawn }) => {
        await spawn`tsc`;
      },
    },
  ],
});
EOD
npm exec turbowatch ./turbowatch.ts

Note See logging instructions to print logs that explain what Turbowatch is doing.

Refer to recipes:

||Turbowatch|Nodemon| |---|---|---| |Node.js interface (scriptable)|✅|❌<sup>1</sup>| |Graceful termination (teardown)|✅|❌<sup>2</sup>| |Scriptable child processes (zx)|✅|❌| |Retries|✅|❌| |Debounce|✅|❌| |Interruptible workflows|✅|❌| |Concurrent workflows|✅|❌| |Log grouping|✅|❌| |Bring-your-own backend|✅|❌| |Works with long-running processes|✅|✅| |Works with build utilities and REPLs|✅|✅| |Watch specific files or directories|✅|✅| |Ignoring specific files or directories|✅|✅| |Open source and available|✅|✅|

<sup><sup>1</sup> Undocumented</sup><br> <sup><sup>2</sup> Nodemon only provides the ability to send a custom signal to the worker.</sup><br>

API

Note defineConfig is used to export configuration for the consumption by turbowatch program. If you want to run Turbowatch programmatically, then use watch. The API of both methods is equivalent.

Turbowatch defaults are a good choice for most projects. However, Turbowatch has many options that you should be familiar with for advance use cases.

import {
  watch,
  type ChangeEvent,
} from 'turbowatch';

void watch({
  // Debounces triggers by 1 second.
  // Most multi-file spanning changes are non-atomic. Therefore, it is typically desirable to
  // batch together information about multiple file changes that happened in short succession.
  // Provide { debounce: { wait: 0 } } to disable debounce.
  debounce: {
    wait: 1000,
  },
  // The base directory under which all files are matched.
  // Note: This is different from the "root project" (https://github.com/gajus/turbowatch#project-root).
  project: __dirname,
  triggers: [
    {
      // Expression match files based on name.
      // https://github.com/gajus/turbowatch#expressions
      expression: [
        'allof',
        ['not', ['dirname', 'node_modules']],
        [
          'anyof',
          ['match', '*.ts', 'basename'],
          ['match', '*.tsx', 'basename'],
        ]
      ],
      // Indicates whether the onChange routine should be triggered on script startup.
      // Defaults to true. Set it to false if you would like onChange routine to not run until the first changes are detected.
      initialRun: true,
      // Determines what to do if a new file change is detected while the trigger is executing.
      // If {interruptible: true}, then AbortSignal will abort the current onChange routine.
      // If {interruptible: false}, then Turbowatch will wait until the onChange routine completes.
      // Defaults to true.
      interruptible: false,
      // Name of the trigger. Used for debugging
      // Must match /^[a-z0-9-_]+$/ pattern and must be unique.
      name: 'build',
      // Routine that is executed when file changes are detected.
      onChange: async ({ spawn }: ChangeEvent) => {
        await spawn`tsc`;
        await spawn`tsc-alias`;
      },
      // Routine that is executed when shutdown signal is received.
      onTeardown: async ({ spawn }) => {
        await spawn`rm -fr ./dist`;
      },
      // Label a task as persistent if it is a long-running process, such as a dev server or --watch mode.
      persistent: false,
      // Retry a task if it fails. Otherwise, watch program will throw an error if trigger fails.
      // Defaults to { retries: 3 }
      retry: {
        retries: 3,
      },
    },
  ],
});

Motivation

To abstract the complexity of orchestrating file watching operations.

For context, we are using Turborepo. The reason this project came to be is because Turborepo does not have "watch" mode (issue #986).

At first, we attempted to use a combination of tsc --watch, concurrently and Nodemon, but started to run into things breaking left and right, e.g.

  • services restarting prematurely (before all the assets are built)
  • services failing to gracefully shutdown and then failing to start, e.g. because ports are in use

Furthermore, the setup for each workspace was repetitive and not straightforward, and debugging issues was not a great experience because you have many workspaces running in watch mode producing tons of logs. Many of the workspaces being dependencies of each other, this kept re-triggering watch operations causing the mentioned issues.

In short, it quickly became clear that we need the ability to have more control over the orchestration of what/when needs to happen when files change.

We started with a script. At first I added debounce. That improved things. Then I added graceful termination logic, which mostly made everything work. We still had occasional failures due to out-of-order events, but adding retry logic fixed that too... At the end, while we got everything to work, it took a lot of effort and it still was a collection of hacky scripts that are hard to maintain and debug, and that's how Turbowatch came to be –

Turbowatch is a toolbox for orchestrating and debugging file watching operations based on everything we learned along the way.

Note If you are working on a very simple project, i.e. just one build step or just one watch operation, then you don't need Turbowatch. Turbowatch is designed for monorepos or otherwise complex workspaces where you have dozens or hundreds of build steps that depend on each other (e.g. building and re-building dependencies, building/starting/stopping Docker containers, populating data, sending notifications, etc).

We also shared these learnings with Turborepo team in hopes that it will help to design an embedded file watching experience.

Use Cases

Turbowatch can be used to automate any sort of operations that need to happen in response to files changing, e.g.,

  • You can run (and conditionally restart) long-running processes (like your Node.js application)
  • You can build assets (like TypeScript and Docker images)

spawn

Turbowatch exposes spawn function that is an instance of zx. Use it to evaluate shell commands:

async ({ spawn }: ChangeEvent) => {
  await spawn`tsc`;
  await spawn`tsc-alias`;
},

The reason Turbowatch abstracts zx is to enable graceful termination of child-processes when triggers are configured to be interruptible.

Persistent tasks

Your setup may include tasks that are not designed to exit, e.g. next dev (starts Next.js in development mode).

It is important that these tasks are marked as persistent to distinguish them from tasks that run to completion as that changes how Turbowatch treats them.

||Persistent|Non-Persistent| |---|---|---| |Ignore FileChangeEvent if { interruptible: false }|✅|❌|

Expressions

Expressions are used to match files. The most basic expression is match – it evaluates as true if a glob pattern matches the file, e.g.

Match all files with *.ts extension:

['match', '*.ts', 'basename']

Expressions can be combined using allof and anyof, e.g.,

Match all files with *.ts or *.tsx extensions:

[
  'anyof', 
  ['match', '*.ts', 'basename'],
  ['match', '*.tsx', 'basename']
]

Finally, not evaluates as true if the sub-expression evaluated as false, i.e. inverts the sub-expression.

Match all files with *.ts extension, but exclude index.ts:

[
  'allof', 
  ['match', '*.ts', 'basename'],
  [
    'not',
    ['match', 'index.ts', 'basename']
  ]
]

This is the gist behind Turbowatch expressions. However, there are many more expressions. Inspect Expression type for further guidance.

type Expression =
  // Evaluates as true if all of the grouped expressions also evaluated as true.
  | ['allof', ...Expression[]]
  // Evaluates as true if any of the grouped expressions also evaluated as true.
  | ['anyof', ...Expression[]]
  // Evaluates as true if a given file has a matching parent directory.
  | ['dirname' | 'idirname', string]
  // Evaluates as true if a glob matches against the basename of the file.
  | ['match' | 'imatch', string, 'basename' | 'wholename']
  // Evaluates as true if the sub-expression evaluated as false, i.e. inverts the sub-expression.
  | ['not', Expression];

Note Turbowatch expressions are a subset of [Watchman

Related Skills

View on GitHub
GitHub Stars971
CategoryDevelopment
Updated23h ago
Forks25

Languages

TypeScript

Security Score

85/100

Audited on Apr 7, 2026

No findings