Turbowatch
Extremely fast file change detector and task orchestrator for Node.js.
Install / Use
/learn @gajus/TurbowatchREADME
[!WARNING]
Deprecated. You probably don't need turbowatch. Some alternatives:
turbo watchand 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:
- Rebuilding assets when file changes are detected
- Restarting server when file changes are detected
- Retrying failing triggers
- Gracefully terminating Turbowatch
- Handling the
AbortSignal - Tearing down project
- Throttling
spawnoutput - Watching multiple scripts
- Using custom file watching backend
||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
defineConfigis used to export configuration for the consumption byturbowatchprogram. If you want to run Turbowatch programmatically, then usewatch. 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
node-connect
351.8kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
110.9kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
351.8kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
351.8kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
