Skip to content

Commit

Permalink
feat: abstract file watching backend (#22)
Browse files Browse the repository at this point in the history
* feat: abstract backend

* feat: abstract backend

* feat: abstract backend

* feat: abstract backend

* feat: abstract backend

* feat: abstract backend
  • Loading branch information
gajus authored Mar 20, 2023
1 parent aca931d commit 095316c
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 46 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Refer to recipes:
* [Handling the `AbortSignal`](#handling-the-abortsignal)
* [Tearing down project](#tearing-down-project)
* [Throttling `spawn` output](#throttling-spawn-output)
* [Using custom file watching backend](#using-custom-file-watching-backend)

||Turbowatch|Nodemon|
|---|---|---|
Expand Down Expand Up @@ -489,6 +490,23 @@ worker:dev: 2fb02d72 > [18:48:37.408] 95ms debug @utilities #waitFor: Waiting f

However, this means that some logs might come out of order. To disable this feature, set `{ throttleOutput: { delay: 0 } }`.

### Using custom file watching backend

By default, Turbowatch uses `fs.watch` on MacOS (Node.js v19.1+) and fallsback to [chokidar](https://github.com/paulmillr/chokidar) on other platforms. However, you can override this behaviour and even implement your own file change detection logic.

```ts
import {
watch,
TurboWatcher,
} from 'turbowatch';
export default watch({
Watcher: TurboWatcher,
project: __dirname,
triggers: [],
});
```

### Logging

Turbowatch uses [Roarr](https://github.com/gajus/roarr) logger.
Expand Down
11 changes: 3 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"micromatch": "^4.0.5",
"p-retry": "^4.6.2",
"roarr": "^7.14.6",
"semver": "^7.3.8",
"serialize-error": "^11.0.0",
"throttle-debounce": "^5.0.0",
"yargs": "^17.7.1",
Expand Down
24 changes: 24 additions & 0 deletions src/backends/ChokidarWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FileWatchingBackend } from './FileWatchingBackend';
import * as chokidar from 'chokidar';

export class ChokidarWatcher extends FileWatchingBackend {
private chokidar: chokidar.FSWatcher;

public constructor(project: string) {
super();

this.chokidar = chokidar.watch(project);

this.chokidar.on('ready', () => {
this.emit('ready');
});

this.chokidar.on('all', (event, filename) => {
this.emit('change', { filename });
});
}

public close() {
return this.chokidar.close();
}
}
30 changes: 30 additions & 0 deletions src/backends/FSWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable canonical/filename-match-regex */

import { FileWatchingBackend } from './FileWatchingBackend';
import fs, { type FSWatcher as NativeFSWatcher } from 'node:fs';

export class FSWatcher extends FileWatchingBackend {
private fsWatcher: NativeFSWatcher;

public constructor(project: string) {
super();

this.fsWatcher = fs.watch(
project,
{
recursive: true,
},
(eventType, filename) => {
this.emit('change', { filename });
},
);

setImmediate(() => {
this.emit('ready');
});
}

public async close() {
this.fsWatcher.close();
}
}
21 changes: 21 additions & 0 deletions src/backends/FileWatchingBackend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
/* eslint-disable @typescript-eslint/method-signature-style */

import { type FileChangeEvent } from '../types';
import { EventEmitter } from 'node:events';

interface BackendEventEmitter {
on(event: 'ready', listener: () => void): this;
on(event: 'change', listener: ({ filename }: FileChangeEvent) => void): this;
}

export abstract class FileWatchingBackend
extends EventEmitter
implements BackendEventEmitter
{
public constructor() {
super();
}

public abstract close(): Promise<void>;
}
44 changes: 44 additions & 0 deletions src/backends/TurboWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable canonical/filename-match-regex */

import { Logger } from '../Logger';
import { ChokidarWatcher } from './ChokidarWatcher';
import { FileWatchingBackend } from './FileWatchingBackend';
import { FSWatcher } from './FSWatcher';
import { platform } from 'node:os';
import * as semver from 'semver';

const log = Logger.child({
namespace: 'TurboWatcher',
});

const isMacOs = () => {
return platform() === 'darwin';
};

export class TurboWatcher extends FileWatchingBackend {
private backend: FileWatchingBackend;

public constructor(project: string) {
super();

if (semver.gte(process.version, '19.1.0') && isMacOs()) {
log.info('using native FSWatcher');
this.backend = new FSWatcher(project);
} else {
log.info('using native ChokidarWatcher');
this.backend = new ChokidarWatcher(project);
}

this.backend.on('ready', () => {
this.emit('ready');
});

this.backend.on('change', (event) => {
this.emit('change', event);
});
}

public close() {
return this.backend.close();
}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export { ChokidarWatcher } from './backends/ChokidarWatcher';
export { FileWatchingBackend } from './backends/FileWatchingBackend';
export { FSWatcher } from './backends/FSWatcher';
export { type ChangeEvent, type Expression } from './types';
export { watch } from './watch';
9 changes: 3 additions & 6 deletions src/subscribe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,13 @@ it('removes duplicates', async () => {

subscription.trigger([
{
event: 'add',
path: '/foo',
filename: '/foo',
},
{
event: 'add',
path: '/foo',
filename: '/foo',
},
{
event: 'add',
path: '/bar',
filename: '/bar',
},
]);

Expand Down
23 changes: 11 additions & 12 deletions src/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { generateShortId } from './generateShortId';
import { Logger } from './Logger';
import {
type ActiveTask,
type ChokidarEvent,
type FileChangeEvent,
type Subscription,
type Trigger,
} from './types';
Expand All @@ -18,7 +18,7 @@ export const subscribe = (trigger: Trigger): Subscription => {

let first = true;

let eventQueue: ChokidarEvent[] = [];
let fileChangeEventQueue: FileChangeEvent[] = [];

const handleSubscriptionEvent = async () => {
if (trigger.abortSignal?.aborted) {
Expand Down Expand Up @@ -88,27 +88,26 @@ export const subscribe = (trigger: Trigger): Subscription => {
}
}

// TODO differentiate between "add", "unlink" and "change" events
const affectedPaths: string[] = [];

const event = {
files: eventQueue
.filter(({ path }) => {
if (affectedPaths.includes(path)) {
files: fileChangeEventQueue
.filter(({ filename }) => {
if (affectedPaths.includes(filename)) {
return false;
}

affectedPaths.push(path);
affectedPaths.push(filename);
return true;
})
.map(({ path }) => {
.map(({ filename }) => {
return {
name: path,
name: filename,
};
}),
};

eventQueue = [];
fileChangeEventQueue = [];

if (trigger.initialRun && reportFirst) {
log.trace('initial run...');
Expand Down Expand Up @@ -205,8 +204,8 @@ export const subscribe = (trigger: Trigger): Subscription => {
});
}
},
trigger: async (events: readonly ChokidarEvent[]) => {
eventQueue.push(...events);
trigger: async (events: readonly FileChangeEvent[]) => {
fileChangeEventQueue.push(...events);

await handleSubscriptionEvent();
},
Expand Down
12 changes: 8 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// cspell:words idirname imatch iname wholename

import { type FileWatchingBackend } from './backends/FileWatchingBackend';
import { type ProcessOutput } from 'zx';

/* eslint-disable @typescript-eslint/sort-type-union-intersection-members */
Expand Down Expand Up @@ -125,24 +126,27 @@ export type Trigger = {
throttleOutput: Throttle;
};

export type WatcherConstructable = new (project: string) => FileWatchingBackend;

/**
* @property project absolute path to the directory to watch
*/
export type ConfigurationInput = {
readonly Watcher?: WatcherConstructable;
readonly debounce?: Debounce;
readonly project: string;
readonly triggers: readonly TriggerInput[];
};

export type Configuration = {
readonly Watcher: WatcherConstructable;
readonly debounce: Debounce;
readonly project: string;
readonly triggers: readonly TriggerInput[];
};

export type ChokidarEvent = {
event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
path: string;
export type FileChangeEvent = {
filename: string;
};

/**
Expand All @@ -160,7 +164,7 @@ export type Subscription = {
expression: Expression;
initialRun: boolean;
teardown: () => Promise<void>;
trigger: (events: readonly ChokidarEvent[]) => Promise<void>;
trigger: (events: readonly FileChangeEvent[]) => Promise<void>;
};

export type TurbowatchController = {
Expand Down
Loading

0 comments on commit 095316c

Please sign in to comment.