paulmillr / chokidar

Minimal and efficient cross-platform file watching library
https://paulmillr.com
MIT License
11.04k stars 586 forks source link

Add docs on migrating globs to chokidar v4 #1350

Closed wartab closed 2 months ago

wartab commented 2 months ago

It's good to see that chokidar is trying to get rid of dependencies. However I suspect that removing glob support is going to affect many projects. I checked the diff between 3.6.0 and 4.0.0 and the only changes I saw was the change of some examples, but they don't seem to be equivalent (except for the last?): watcher.add(['new-file-2', 'new-file-3', '**/other-file*']); -> watcher.add(['new-file-2', 'new-file-3']);

await watcher.unwatch('new-file*'); -> await watcher.unwatch('new-file');

ignored: '*.txt', -> ignored: (file) => file.endsWith('.txt'),

It would be nice to have a part of the documentation that tells people how it is recommended to migrate away from using globs.

Should we always use ignored? How would you migrate examples such as:

let scssWatcher = chokidar.watch("./**/*.scss");
let assetWatcher = chokidar.watch("./assets/**/*");
paulmillr commented 2 months ago

Pull requests are appreciated.

Some facts:

  1. Regular expressions can be used in filters, such as /.+\.txt$/
  2. Nodejs now natively supports globs: const { glob } = require('node:fs/promises');
  3. Adding some dir and globstars “all its files” is the same as adding the dir and using filters
  4. Unwatching can be implemented using listing all watched files, filtering them.

it’s more complicated, but the community part which uses globs will come up with shortcuts and solutions.

felipecrs commented 2 months ago
  1. Regular expressions can be used in filters, such as /.+.txt$/

image

paulmillr commented 2 months ago

ignored: (path) => RE.test(path)

felipecrs commented 2 months ago

Oh, yes. That's what I'm doing. :)

And of course, anyone can just npm install glob and continue using it as a filter as function.

davidwessman commented 2 months ago

Worked great to switch from glob to folder + filter 🚀 Nice to drop all those dependencies, thank you! 🙂

bxt commented 2 months ago

Hmm I struggled around an hour already on this. I just had the very basic case of watching all files with a certain extension. Let me first share what does not work:

// obviously broken, only works in v3:
const watcher = watch('**/*.gql');

// broken, because also ignores "." already
const watcher = watch('.', { ignore: (file) => !file.endsWith('.gql') });

// broken, "Error: EMFILE: too many open files, watch"
const watcher = watch('.');
watcher.on('change', (path) => {
  if (!file.endsWith('.gql')) return;
  // ...
});

The best (?) solution I found so far, if you are willing to use globby:

import globby from 'globby';
const watcher = watch(await globby('**/*.gql'));

There are of course a bunch of other glob packages. If you are able to use the experimental node v22 glob:

import { glob } from 'node:fs/promises';
const watcher = watch(await Array.fromAsync(glob('**/*.gql')));

Is this working the same as the old code? No idea, but at least it seems to work.

What also seems to work is using the second stats argument for the filter function, it is not in the TS types though:

const watcher = watch('.', {
  // @ts-expect-error Doesn't have types for second argument
  ignored: (file, stats) => stats && stats.isFile() && !file.endsWith('.gql'),
});

I'm not sure if this is better in any way.

paulmillr commented 2 months ago

@bxt why doesn’t this work exactly? ignore: (file) => !file.endsWith('.gql')

bxt commented 2 months ago

@paulmillr Maybe I'm doing it wrong, but:

const watcher = watch('.', {
  ignored: (file) => {
    console.log(`file: ${file}`);
    return !file.endsWith('.gql');
  },
});

This prints file: . and nothing else. So it already ignores the current directoy and does not look further. If I un-ignore . is stops at the immediate child directories, as none on their name end in .gql either. The solution would be to not ignore any directories, which is what I did with ignored: (file, stats) => stats && stats.isFile() && !file.endsWith('.gql') but it seems a bit complicated as well.

felipecrs commented 2 months ago

Maybe the . is the issue? Suggestion:

const watcher = watch(process.cwd(), {
  ignored: (file) => !file.endsWith('.gql'),
});
paulmillr commented 2 months ago

What about return file !== "." && !file.endsWith('.gql');?

paulmillr commented 2 months ago

Yeah, I just realized that subdirs will also need to be excluded. So, using second argument is the proper way to do this:

const watcher = watch('.', {
  ignored: (file, stats) => stats && stats.isFile() && !file.endsWith('.gql'),
});

As for typescript errors with this, 4.0.1 will fix it.

universse commented 2 months ago

Hi I'm writing a tool that let users configure their own watch patterns. To migrate this, users now have to write a bunch of extra logic for ignoring files. On the other hand, using RegExp is not as ergonomic. What do you recommend?

// user code
export default config =  {
  entries: ['folder-1/**/*.json', 'folder-2/*/file.json'] // tool also supports yaml, json5, user only wants json
}
paulmillr commented 2 months ago

you can use built-in glob from fs as mentioned above

universse commented 2 months ago

fs.glob is not available in LTS version while this package supposedly run on nodejs 14+.

paulmillr commented 2 months ago

Use any other third party library, such as micromatch.

millette commented 2 months ago

@universse LTS doesn't mean forever https://endoflife.date/nodejs

v14 expired 30 Apr 2023, v16 expired 11 Sep 2023 and v18 expires in 30 Apr 2025.

universse commented 2 months ago

@millette what I mean is fs.glob is experimental and unavailable in Node.js LTS version. Using fs.glob as a workaround requires end users to upgrade to >LTS versions of Node.js.

universse commented 2 months ago

Use any other third party library, such as micromatch.

for my use case, this requires creating a watcher for each glob pattern, unless I'm missing something.

entries.forEach(entry => {
  const watcher = watch(globParent(entry), {
    ignored: (file) => {
      return !micromatch.isMatch(file, entry)
    }
  })
})
wartab commented 2 months ago
import { glob } from 'node:fs/promises';
const watcher = watch(await glob('**/*.gql'));

Is this working the same as the old code? No idea, but at least it seems to work.

I don't think it works the same. IIRC the way I use it is to also detect newly created files. This will only track previously created files matching that pattern.

jakebailey commented 2 months ago

My understanding (please correct me if I'm wrong) is that the old chokidar accepted globs, but then just recursively watched everything and then filtered the events. With the new version, you can't pass in globs, but directories you pass in are still watched recursively (which would include new files), so it should be equivalent to watch dirs (e.g. a prefix on the glob) and then filter the events yourself. It wouldn't be equivalent to apply the globs and then pass them in.

So, for **/*.gql, it's equivalent to watch ., then filter paths that match **/*.gql (or more sussinctly, .endsWith(".gql")).

This sort of explainer (if correct) would be nice to have in a doc; I was certainly confused until I tested it out. But, maybe that's not actually how it worked (my globs are all like some/path/**/* where the new path to watch is obvious).

paulmillr commented 2 months ago

Pull requests are welcome.

jcharnley commented 2 months ago

Hmm I struggled around an hour already on this. I just had the very basic case of watching all files with a certain extension. Let me first share what does not work:

// obviously broken, only works in v3:
const watcher = watch('**/*.gql');

// broken, because also ignores "." already
const watcher = watch('.', { ignore: (file) => !file.endsWith('.gql') });

// broken, "Error: EMFILE: too many open files, watch"
const watcher = watch('.');
watcher.on('change', (path) => {
  if (!file.endsWith('.gql')) return;
  // ...
});

The best (?) solution I found so far, if you are willing to use globby:

import globby from 'globby';
const watcher = watch(await globby('**/*.gql'));

There are of course a bunch of other glob packages. If you are able to use the experimental node v22 glob:

import { glob } from 'node:fs/promises';
const watcher = watch(await glob('**/*.gql'));

Is this working the same as the old code? No idea, but at least it seems to work.

What also seems to work is using the second stats argument for the filter function, it is not in the TS types though:

const watcher = watch('.', {
  // @ts-expect-error Doesn't have types for second argument
  ignored: (file, stats) => stats && stats.isFile() && !file.endsWith('.gql'),
});

I'm not sure if this is better in any way.

how are you getting passed that TS error to access the stats ?

this seems to be working for me

ignored: ((file: string, stats: any | undefined) => {
      if (stats && stats.isFile() && !file.endsWith('.otf') && !file.endsWith('.key') && !file.endsWith('.k')) {
        return true;
      }
      return false;
    }) as (file: string, stats?: any) => boolean,
bxt commented 2 months ago

@jcharnley You can use any of the TS workarounds, I used a @ts-expect-error comment. In the end you can also wait for the next patch release, as a fix for the TypeScript issue is already on main: https://github.com/paulmillr/chokidar/commit/87e0740cbd21327c82f97d3fe08662a9ae55e6d1

VandeurenGlenn commented 2 months ago

@jcharnley

There are of course a bunch of other glob packages. If you are able to use the experimental node v22 glob:

import { glob } from 'node:fs/promises';
const watcher = watch(await glob('**/*.gql'));

is this working the same as the old code? No idea, but at least it seems to work.

That should not work at all because what is returned by glob is an AsyncGenerator.

bxt commented 2 months ago

@VandeurenGlenn You're right, you need to use await Array.fromAsync(glob('.*')) to convert it to an array.

I also created a PR to fix this in the docs: https://github.com/paulmillr/chokidar/pull/1365

Btw in the meantime some docs update upgrading were added 🎉 https://github.com/paulmillr/chokidar?tab=readme-ov-file#upgrading so maybe this issue can be closed.

aleclarson commented 1 month ago

For anyone writing a tool that needs ”watch mode” with glob support, I'd recommend a library I wrote made exactly for this use case. It's called jumpgen (https://github.com/alloc/jumpgen) and it's built on top of Chokidar.