Skip to content

Commit

Permalink
Improve types, refactor constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Aug 27, 2024
1 parent 72dc4a9 commit 52b7f11
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 86 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
## Why?

Chokidar is more useful than raw `fs.watch` / `fs.watchFile` in 2024:
There are many reasons to prefer Chokidar to raw fs.watch / fs.watchFile in 2024:

- Events are properly reported:
- Events are properly reported
- macOS events report filenames
- events are not reported twice
- changes are reported as add / change / unlink instead of useless `rename`
Expand All @@ -22,7 +22,6 @@ Chokidar is more useful than raw `fs.watch` / `fs.watchFile` in 2024:
Chokidar relies on the Node.js core `fs` module, but when using
`fs.watch` and `fs.watchFile` for watching, it normalizes the events it
receives, often checking for truth by getting file stats and/or dir contents.

The `fs.watch`-based implementation is the default, which
avoids polling and keeps CPU usage down. Be advised that chokidar will initiate
watchers recursively for everything within scope of the paths that have been
Expand All @@ -48,6 +47,7 @@ Use it in your code:

```javascript
import chokidar from 'chokidar';

// One-liner for current directory
chokidar.watch('.').on('all', (event, path) => {
console.log(event, path);
Expand Down Expand Up @@ -237,7 +237,7 @@ values are arrays of the names of the items contained in each directory.
## CLI

If you need a CLI interface for your file watching, check out
[chokidar-cli](https://github.com/open-cli-tools/chokidar-cli), allowing you to
third party [chokidar-cli](https://github.com/open-cli-tools/chokidar-cli), allowing you to
execute a command on each change, or get a stdio stream of change events.

## Install Troubleshooting
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "chokidar",
"description": "Minimal and efficient cross-platform file watching library",
"version": "4.0.0-alpha",
"version": "4.0.0-beta",
"homepage": "https://github.com/paulmillr/chokidar",
"author": "Paul Miller (https://paulmillr.com)",
"contributors": [
Expand Down
6 changes: 3 additions & 3 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { open, stat, lstat, realpath as fsrealpath } from 'fs/promises';
import * as sysPath from 'path';
import { type as osType } from 'os';
import type { FSWatcher, WatchHelper, FSWInstanceOptions } from './index.js';
import { EntryInfo } from 'readdirp';

export type Path = string;

Expand Down Expand Up @@ -532,9 +533,8 @@ export default class NodeFsHandler {

let stream = this.fsw
._readdirp(directory, {
fileFilter: (entry) => wh.filterPath(entry),
directoryFilter: (entry) => wh.filterDir(entry),
depth: 0,
fileFilter: (entry: EntryInfo) => wh.filterPath(entry),
directoryFilter: (entry: EntryInfo) => wh.filterDir(entry),
})
.on(STR_DATA, async (entry) => {
if (this.fsw.closed) {
Expand Down
143 changes: 65 additions & 78 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { stat, readdir } from 'fs/promises';
import type { Stats } from 'fs';
import { EventEmitter } from 'events';
import * as sysPath from 'path';
import { readdirp } from 'readdirp';
import { readdirp, ReaddirpStream, ReaddirpOptions, EntryInfo } from 'readdirp';
// @ts-ignore
import normalizePath from 'normalize-path';

Expand Down Expand Up @@ -188,8 +188,6 @@ const getAbsolutePath = (path, cwd) => {
return sysPath.join(cwd, path);
};

const undef = (opts, key) => opts[key] === undefined;

/**
* Directory entry.
*/
Expand Down Expand Up @@ -277,18 +275,18 @@ export class WatchHelper {
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
}

entryPath(entry) {
entryPath(entry: EntryInfo): Path {
return sysPath.join(this.watchPath, sysPath.relative(this.watchPath, entry.fullPath));
}

filterPath(entry) {
filterPath(entry: EntryInfo): boolean {
const { stats } = entry;
if (stats && stats.isSymbolicLink()) return this.filterDir(entry);
const resolvedPath = this.entryPath(entry);
return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
}

filterDir(entry) {
filterDir(entry: EntryInfo): boolean {
return this.fsw._isntIgnored(this.entryPath(entry), entry.stats);
}
}
Expand All @@ -302,94 +300,82 @@ export class WatchHelper {
* .on('add', path => log('File', path, 'was added'))
*/
export class FSWatcher extends EventEmitter {
closed: boolean;
options: FSWInstanceOptions;
_watched: Map<string, DirEntry>;

_closers: Map<string, Array<any>>;
_ignoredPaths: Set<Matcher>;
_throttled: Map<ThrottleType, Map<any, any>>;
_symlinkPaths: Map<Path, string | boolean>;
_streams: Set<any>;
closed: boolean;
_symlinkPaths: Map<Path, string | boolean>;
_watched: Map<string, DirEntry>;

_pendingWrites: Map<any, any>;
_pendingUnlinks: Map<any, any>;
_readyCount: number;
_emitReady: () => void;
_closePromise: Promise<void>;
_closePromise?: Promise<void>;
_userIgnored?: MatchFunction;
_readyEmitted: boolean;
_emitRaw: () => void;
_boundRemove: () => void;
_boundRemove: (dir: string, item: string) => void;

_nodeFsHandler?: NodeFsHandler;

// Not indenting methods for history sake; for now.
constructor(_opts: ChokidarOptions) {
constructor(_opts: ChokidarOptions = {}) {
super();
this.closed = false;

const opts: Partial<FSWInstanceOptions> = {};
if (_opts) Object.assign(opts, _opts); // for frozen objects
this._watched = new Map();
this._closers = new Map();
this._ignoredPaths = new Set<Matcher>();
this._throttled = new Map();
this._symlinkPaths = new Map();
this._streams = new Set();
this.closed = false;
this._symlinkPaths = new Map();
this._watched = new Map();

// Set up default options.
if (undef(opts, 'persistent')) opts.persistent = true;
if (undef(opts, 'ignoreInitial')) opts.ignoreInitial = false;
if (undef(opts, 'ignorePermissionErrors')) opts.ignorePermissionErrors = false;
if (undef(opts, 'interval')) opts.interval = 100;
if (undef(opts, 'binaryInterval')) opts.binaryInterval = 300;
this._pendingWrites = new Map();
this._pendingUnlinks = new Map();
this._readyCount = 0;
this._readyEmitted = false;

// Always default to polling on IBM i because fs.watch() is not available on IBM i.
if (isIBMi) {
opts.usePolling = true;
}
const awf = _opts.awaitWriteFinish;
const DEF_AWF = { stabilityThreshold: 2000, pollInterval: 100 };
const opts: FSWInstanceOptions = {
// Defaults
persistent: true,
ignoreInitial: false,
ignorePermissionErrors: false,
interval: 100,
binaryInterval: 300,
followSymlinks: true,
usePolling: false,
// useAsync: false,
atomic: true, // NOTE: overwritten later (depends on usePolling)
..._opts,
// Change format
ignored: arrify(_opts.ignored),
awaitWriteFinish:
awf === true ? DEF_AWF : typeof awf === 'object' ? { ...DEF_AWF, ...awf } : false,
};

// Global override (useful for end-developers that need to force polling for all
// instances of chokidar, regardless of usage/dependency depth)
// Always default to polling on IBM i because fs.watch() is not available on IBM i.
if (isIBMi) opts.usePolling = true;
// Editor atomic write normalization enabled by default with fs.watch
if (opts.atomic === undefined) opts.atomic = !opts.usePolling;
// opts.atomic = typeof _opts.atomic === 'number' ? _opts.atomic : 100;
// Global override. Useful for developers, who need to force polling for all
// instances of chokidar, regardless of usage / dependency depth
const envPoll = process.env.CHOKIDAR_USEPOLLING;
if (envPoll !== undefined) {
const envLower = envPoll.toLowerCase();

if (envLower === 'false' || envLower === '0') {
opts.usePolling = false;
} else if (envLower === 'true' || envLower === '1') {
opts.usePolling = true;
} else {
opts.usePolling = !!envLower;
}
if (envLower === 'false' || envLower === '0') opts.usePolling = false;
else if (envLower === 'true' || envLower === '1') opts.usePolling = true;
else opts.usePolling = !!envLower;
}
const envInterval = process.env.CHOKIDAR_INTERVAL;
if (envInterval) {
opts.interval = Number.parseInt(envInterval, 10);
}

// Editor atomic write normalization enabled by default with fs.watch
if (undef(opts, 'atomic')) opts.atomic = !opts.usePolling;
if (opts.atomic) this._pendingUnlinks = new Map();

if (undef(opts, 'followSymlinks')) opts.followSymlinks = true;

if (undef(opts, 'awaitWriteFinish')) opts.awaitWriteFinish = false;
if ((opts.awaitWriteFinish as any) === true || typeof opts.awaitWriteFinish === 'object') {
// opts.awaitWriteFinish = {};
const awf = opts.awaitWriteFinish;
if (awf) {
this._pendingWrites = new Map();
const st = typeof awf === 'object' && awf.stabilityThreshold;
const pi = typeof awf === 'object' && awf.pollInterval;
opts.awaitWriteFinish = {
stabilityThreshold: st || 2000,
pollInterval: pi || 100,
};
}
}
if (opts.ignored) opts.ignored = arrify(opts.ignored);

if (envInterval) opts.interval = Number.parseInt(envInterval, 10);
// This is done to emit ready only once, but each 'add' will increase that?
let readyCalls = 0;
this._emitReady = () => {
readyCalls++;
Expand All @@ -401,12 +387,11 @@ export class FSWatcher extends EventEmitter {
}
};
this._emitRaw = (...args) => this.emit(EV.RAW, ...args);
this._readyEmitted = false;
this.options = opts as FSWInstanceOptions;

// Initialize with proper watcher.
this._nodeFsHandler = new NodeFsHandler(this);
this._boundRemove = this._remove.bind(this);

this.options = opts;
this._nodeFsHandler = new NodeFsHandler(this);
// You’re frozen when your heart’s not open.
Object.freeze(opts);
}
Expand Down Expand Up @@ -555,9 +540,12 @@ export class FSWatcher extends EventEmitter {
this._readyCount = 0;
this._readyEmitted = false;
this._watched.forEach((dirent) => dirent.dispose());
['closers', 'watched', 'streams', 'symlinkPaths', 'throttled'].forEach((key) => {
this[`_${key}`].clear();
});

this._closers.clear();
this._watched.clear();
this._streams.clear();
this._symlinkPaths.clear();
this._throttled.clear();

this._closePromise = closers.length
? Promise.all(closers).then(() => undefined)
Expand Down Expand Up @@ -826,8 +814,8 @@ export class FSWatcher extends EventEmitter {
}

/**
* Provides a set of common helpers and properties relating to symlink and glob handling.
* @param {Path} path file, directory, or glob pattern being watched
* Provides a set of common helpers and properties relating to symlink handling.
* @param {Path} path file or directory pattern being watched
* @returns {WatchHelper} object containing helpers for this path
*/
_getWatchHelpers(path: string): WatchHelper {
Expand All @@ -843,7 +831,6 @@ export class FSWatcher extends EventEmitter {
* @returns {DirEntry} the directory's tracking object
*/
_getWatchedDir(directory: string) {
if (!this._boundRemove) this._boundRemove = this._remove.bind(this);
const dir = sysPath.resolve(directory);
if (!this._watched.has(dir)) this._watched.set(dir, new DirEntry(dir, this._boundRemove));
return this._watched.get(dir);
Expand Down Expand Up @@ -971,10 +958,10 @@ export class FSWatcher extends EventEmitter {
list.push(closer);
}

_readdirp(root, opts) {
_readdirp(root: Path, opts?: Partial<ReaddirpOptions>) {
if (this.closed) return;
const options = { type: EV.ALL, alwaysStat: true, lstat: true, ...opts };
let stream = readdirp(root, options);
const options = { type: EV.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
let stream: ReaddirpStream | undefined = readdirp(root, options);
this._streams.add(stream);
stream.once(STR_CLOSE, () => {
stream = undefined;
Expand All @@ -991,8 +978,8 @@ export class FSWatcher extends EventEmitter {

/**
* Instantiates watcher with paths to be tracked.
* @param {String|Array<String>} paths file/directory paths and/or globs
* @param {Object=} options chokidar opts
* @param paths file/directory paths
* @param options chokidar opts
* @returns an instance of FSWatcher for chaining.
*/
export const watch = (paths: string | string[], options: ChokidarOptions) => {
Expand Down

0 comments on commit 52b7f11

Please sign in to comment.