Skip to content

fix(tasks): update styles watcher to support chokidar v4#141

Open
Hugoer wants to merge 2 commits into
Netcentric:mainfrom
Hugoer:Update-chokidar-usage-to-support-v4
Open

fix(tasks): update styles watcher to support chokidar v4#141
Hugoer wants to merge 2 commits into
Netcentric:mainfrom
Hugoer:Update-chokidar-usage-to-support-v4

Conversation

@Hugoer
Copy link
Copy Markdown
Member

@Hugoer Hugoer commented Mar 11, 2026

This PR fixes an issue with SCSS file watchers caused by Chokidar version > 3. In versions 4+, Chokidar no longer supports glob patterns directly.

Description

Chokidar v4 removed support for glob patterns in watch().
This PR updates the styles watcher to handle that breaking change by pre-resolving SCSS files via fast-glob before passing them to chokidar.

Related Issue

Fixes #138

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • I have read the CONTRIBUTING document.
  • All new and existing tests passed.

@easingthemes
Copy link
Copy Markdown
Member

Issues Found

  1. Missing glob import (Blocker)
    The diff adds await glob(pattern, ...) but there’s no visible require('fast-glob') or const { glob } = require('fast-glob') import at the top of tasks/styles.js. The existing file only imports path, log, generateEntries, and renderStyles. This will throw a ReferenceError: glob is not defined at runtime.
    Fix needed: Add const { glob } = require('fast-glob'); (or const glob = require('fast-glob');) at the top of the file.
  2. async Promise constructor anti-pattern (Minor)
    The promise callback is changed to new Promise(async (resolve) => { ... }). This is a well-known anti-pattern because:
    ∙ Exceptions thrown inside an async executor are not caught by the Promise — they become unhandled rejections instead of rejecting the returned promise
    ∙ The existing try/catch mitigates this partially, but any error from await glob(...) that escapes the catch block will be silently swallowed
    Suggested improvement: Either:
    ∙ Move the async logic before the Promise constructor, or
    ∙ Use async/await throughout and remove the new Promise wrapper entirely (preferred — the function could just be module.exports = async (config) => { ... })
  3. Watcher won’t detect new SCSS files (Design consideration)
    Since fast-glob resolves files once at startup, newly created SCSS files won’t be watched. The old glob-based approach (when it worked) would have picked up new files. This may be acceptable given the EMFILE constraints mentioned in Update chokidar usage to support v4 (glob patterns removal) #138, but it’s worth documenting as a known limitation.
  4. Downgrade vs. upgrade reasoning (Minor)
    The PR description says “downgraded chokidar from v5 to v4” but the title of Update chokidar usage to support v4 (glob patterns removal) #138 says “support v4.” The commit message says “update chokidar to version 4.0.3.” Since v5 existed before, this is technically a downgrade. Worth clarifying in the PR description whether v5 introduced other issues (the EMFILE problems mentioned in the issue?).
    Verdict
    Changes requested — The missing glob import (Rename template vars with fe-build one #1) is a runtime blocker that must be fixed before merge. The async Promise anti-pattern (Add package code  #2) is worth addressing. Items Order imports alphabetically #3 and stylelint.config.js #4 are informational.

@Hugoer Hugoer changed the title fix(tasks): adjust styles watcher implementation fix(tasks): update styles watcher to support chokidar v4 Apr 15, 2026
@Hugoer
Copy link
Copy Markdown
Member Author

Hugoer commented Apr 15, 2026

Thanks for reviewing!

changes made:

  1. Removed new Promise(async ...) anti-pattern — the exported function is now async directly, so errors from await glob(...) and other async ops are properly propagated.
  2. Replaced Promise.allSettled(...).then(...) with await in the non-watch branch — consistent async/await throughout.
  3. Added a comment documenting the SCSS watcher limitation (new files not detected at runtime) as Dragan suggested.

The glob import was already present on line 2, so nothing to add there.

Comment thread tasks/styles.js
absolute: true
});

const watcher = chokidar.watch(files, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a huge static list of paths with glob why not use something like this which translates to the previous behavior?

chokidar.watch(config.general.sourcesPath, {
  ignored: (filePath, stats) => stats?.isFile() && !filePath.endsWith(`.${config.general.sourceKey}.scss`),
  ignoreInitial: true
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw the issue description after I left my comment. The why of the solution I think should be clearly stated also in the PR description in addition to the link to the issue.

The current proposal could seem to be the only alternative with chokidar.

Comment thread tasks/styles.js
// Note: fast-glob resolves files once at startup, so newly created SCSS files
// won't be picked up by the watcher without a restart. This is an accepted
// tradeoff to avoid EMFILE errors with recursive directory watching.
const files = await glob(pattern, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency and testing please try to reuse the generateEntries() util also for the watcher part.

The bellow example seem to work as intended:

     const entries = generateEntries(config, 'scss');

      // Build reverse map: absolute src path -> dest key
      const srcToDest = Object.fromEntries(
        Object.entries(entries).map(([dest, src]) => [src, dest])
      );

      const watcher = chokidar.watch(Object.values(entries), {
        ignoreInitial: true
      });

      watcher.on('all', (event, file) => {
        const destFile = srcToDest[file];
        if (!destFile) return;

        config.stylelint.failOnError = false;

        renderStyles(file, destFile, config);
      });

Copy link
Copy Markdown
Member

@easingthemes easingthemes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the async/await refactor is a real improvement and the fast-glob → array approach is the right shape for chokidar's post-v3 contract. Reuse of fast-glob (already used in utils/generateEntries.js) keeps the codebase consistent, and the inline comment about the new-file watcher limitation is the right call.

Left a few inline questions/suggestions — only the chokidar v5→v4 rationale (in package.json) is something I'd like clarified before merge; the rest are nice-to-haves or follow-ups.

Comment thread package.json
"autoprefixer": "^10.4.20",
"babel-loader": "9.1.2",
"chokidar": "^5.0.0",
"chokidar": "^4.0.3",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why downgrade to v4 specifically rather than staying on ^5.0.0?

main is on chokidar ^5.0.0. Both v4 and v5 dropped glob support in watch(), so the file-array fix in this PR works equally well on either. The PR title says "support chokidar v4" but this is technically a downgrade from what's currently on main.

Given the previous attempt was reverted (f5df235), reviewers will want the rationale spelled out — Node engine constraint? Aligning with the sass transitive? Something v5 broke? Or alternatively, keep ^5.0.0 since the new code is forward-compatible.

This is the one item I'd like resolved before merge.

Comment thread tasks/styles.js
const files = await glob(pattern, {
cwd: config.general.sourcesPath,
absolute: true
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty-array edge case. If glob(...) returns [] (e.g., a fresh project with no .source.scss files yet), chokidar.watch([]) is called and silently does nothing — while the log on line 11 still says "Watcher Sass / autoprefixer running...". A one-liner would help DX:

if (!files.length) {
  log(__filename, 'No SCSS sources found to watch', '', 'warn');
}

Comment thread tasks/styles.js
renderStyles(file, destFile, config);
});

} catch (e) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading catch message now that the try is wider. 'Something is missing, you need install dev dependencies for this.' made sense when require('chokidar') was the only failure mode. Now this also catches errors from await glob(...) (bad cwd, permissions, etc.).

Either narrow the try to just the require('chokidar'), or generalize the message — something like 'Failed to start SCSS watcher' would survive future refactors.

Comment thread tasks/styles.js
.basename(file)
.replace(config.general.sourceKey, config.general.bundleKey);

const destFile = path.join(relativePath, fileName);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing, out of scope but trivial while you're here: config.stylelint.failOnError = false is set inside the 'all' event handler, so it re-mutates config on every file-change event. It should be set once before watcher.on(...) is registered. Not introduced by this PR — just noticed it while reviewing. Up to you whether to roll it in or leave for a follow-up.

Comment thread tasks/styles.js
Promise.allSettled(promises).then((results) => {
log(__filename, 'Styles done', '', 'info');
resolve();
module.exports = async (config) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression test? This is the second attempt at this fix after f5df235 was reverted. A small jest test that mocks fast-glob + chokidar and asserts chokidar.watch is called with an array (not a glob string) would lock in the behavior and prevent a third revert cycle. Happy to leave this as a follow-up issue if you'd prefer to keep this PR tight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update chokidar usage to support v4 (glob patterns removal)

3 participants