Skip to content

fix(cli): glob patterns ignored in chokidar v4#1131

Merged
aws-cdk-automation merged 4 commits intomainfrom
conroy/chokidar
Feb 10, 2026
Merged

fix(cli): glob patterns ignored in chokidar v4#1131
aws-cdk-automation merged 4 commits intomainfrom
conroy/chokidar

Conversation

@kaizencc
Copy link
Contributor

@kaizencc kaizencc commented Feb 10, 2026

Fixes #1146.

In CDK CLI v2.1103.0, we released #1059 which migrated our chokidar dependency from v3 to v4. We missed a breaking change in this migration: glob is no longer supported. This causes cdk watch in CDK CLI v2.1102.0 - v2.1105.0 to work improperly with include or exclude parameters that utilize globs -- including the default configuration that is include: ['**']. Using the default include causes cdk watch to not watch any files at all.

Since CDK CLI exposes an include and exclude property that users can set in their own cdk.json configuration, and we support globs, we must continue to do so. Therefore, this PR migrates globs into the new format that chokidar v4 understands - a includes path adn an ignored function.

This code was written with the help of AI, including all tests, and also inspired by similar updates to other projects like this one.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@codecov-commenter
Copy link

codecov-commenter commented Feb 10, 2026

Codecov Report

❌ Patch coverage is 92.17391% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.75%. Comparing base (750d9be) to head (5a70d68).

Files with missing lines Patch % Lines
packages/aws-cdk/lib/cli/convert-globs.ts 94.28% 6 Missing ⚠️
packages/aws-cdk/lib/cli/cdk-toolkit.ts 70.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    aws/aws-cdk-cli#1131      +/-   ##
==========================================
+ Coverage   87.70%   87.75%   +0.05%     
==========================================
  Files          72       73       +1     
  Lines       10116    10228     +112     
  Branches     1337     1371      +34     
==========================================
+ Hits         8872     8976     +104     
- Misses       1218     1226       +8     
  Partials       26       26              
Flag Coverage Δ
suite.unit 87.75% <92.17%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a regression in cdk watch after upgrading to chokidar v4 by translating CLI watch.include/watch.exclude glob patterns into chokidar v4-compatible inputs (non-glob watch paths + an ignored callback), and updating tests accordingly.

Changes:

  • Added convertGlobsToChokidarV4() to derive watchPaths and an ignored() function from include/exclude patterns.
  • Updated CdkToolkit.watch() to call chokidar.watch(watchPaths, { ignored, cwd }) instead of passing globs directly.
  • Added new unit tests for glob conversion and adjusted existing watch tests to assert function-based ignore behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
packages/aws-cdk/lib/cli/convert-globs.ts New glob-to-chokidar-v4 conversion logic (watch paths extraction + ignore callback).
packages/aws-cdk/lib/cli/cdk-toolkit.ts Uses the converter for cdk watch and adds a pre-ready logging skip based on ignored().
packages/aws-cdk/test/cli/convert-globs.test.ts New tests for the conversion helper.
packages/aws-cdk/test/cli/cdk-toolkit.test.ts Updates watch tests to assert against watchPaths + ignored function behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +10 to +13
if (pattern === '**') return '.';
if (pattern.startsWith('**/')) return '.';
// Remove glob parts: ./dir/**/* -> ./dir, src/** -> src
return pattern.replace(/\/\*\*.*$/, '').replace(/\/\*\.[^/]*$/, '') || '.';
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

extractWatchPath() can return glob-containing paths (e.g., patterns like *.ts or src/* will not be stripped), which would be passed directly to chokidar.watch() even though v4 no longer supports globs. Consider hardening this to always return a real directory (typically . or a parent dir) for any pattern containing */?/[], using a glob-parent style algorithm (or an existing helper) so watch paths are never globs.

Suggested change
if (pattern === '**') return '.';
if (pattern.startsWith('**/')) return '.';
// Remove glob parts: ./dir/**/* -> ./dir, src/** -> src
return pattern.replace(/\/\*\*.*$/, '').replace(/\/\*\.[^/]*$/, '') || '.';
if (!pattern) return '.';
// Pure recursive patterns watch from current directory
if (pattern === '**') return '.';
if (pattern.startsWith('**/')) return '.';
// If there are no glob characters, the pattern itself is a concrete path
const firstGlobIndex = pattern.search(/[*?\[\]]/);
if (firstGlobIndex === -1) {
return pattern;
}
// Find the last path separator before the first glob character
const lastSlashBeforeGlob = pattern.lastIndexOf('/', firstGlobIndex);
// Glob in the first segment (e.g., "*.ts", "**/*.ts") => watch current directory
if (lastSlashBeforeGlob === -1) {
return '.';
}
const baseDir = pattern.slice(0, lastSlashBeforeGlob);
// If baseDir is empty (e.g., "*/foo"), fall back to current directory
return baseDir || '.';

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +39
function matchesIncludePattern(normalized: string, basename: string, pathParts: string[], pattern: string): boolean {
// Non-glob patterns: match if path starts with the pattern
if (!pattern.includes('*')) {
return normalized.startsWith(pattern + '/') || normalized === pattern;
}
// Glob patterns
if (pattern === '**') return true;
if (pattern.startsWith('**/*.')) {
return basename.endsWith(pattern.slice(4));
}
if (pattern.endsWith('/**/*')) {
const dir = pattern.slice(0, -5);
return !dir || normalized.startsWith(dir + '/');
}
if (pattern.startsWith('**/') && pattern.endsWith('/*')) {
// Match files directly in a directory anywhere (e.g., **/my-dir2/*)
const dirName = pattern.slice(3, -2);
return pathParts.includes(dirName);
}
return true;
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

matchesIncludePattern() returns true for any unhandled glob shape (line 38), which effectively disables include filtering for many valid patterns. For example, patterns like src/** or cdk*.json-style globs aren’t meaningfully evaluated here; and **/my-dir/* is treated as matching any depth under my-dir (since it only checks pathParts.includes(dirName)). To preserve the CLI’s documented glob support, it would be more reliable to evaluate includes via minimatch (already a dependency) against the normalized relative path and only fall back to permissive behavior when explicitly intended.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +66
function matchesExcludePattern(normalized: string, basename: string, pathParts: string[], pattern: string): boolean {
if (!pattern.includes('*')) return basename === pattern;
if (pattern.startsWith('**/*.')) return basename.endsWith(pattern.slice(4));
if (pattern === '**/.*') return basename.startsWith('.');
if (pattern === '**/.*/**') {
// Match files inside hidden directories
return pathParts.some(part => part.startsWith('.'));
}
if (pattern.startsWith('**/') && pattern.endsWith('/**')) {
return pathParts.includes(pattern.slice(3, -3));
}
if (pattern.startsWith('**/')) {
// Match directory name anywhere in path (e.g., **/my-dir2)
const dirName = pattern.slice(3);
return pathParts.includes(dirName);
}
if (pattern.endsWith('/**')) {
const dir = pattern.slice(0, -3);
return normalized === dir || normalized.startsWith(dir + '/');
}
if (pattern.endsWith('*')) return basename.startsWith(pattern.slice(0, -1));
return false;
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

matchesExcludePattern() does not correctly implement several exclude patterns that are used in repo templates/docs, because it (a) treats non-glob patterns as basename === pattern (so src/test / target won’t exclude a directory tree) and (b) doesn’t handle common globs like target/* or cdk*.json (a * in the middle). This means user-provided excludes may silently stop working. Suggest using minimatch for exclude matching (with appropriate options like dot) against the normalized path, and treating non-glob patterns as path prefixes (relative to cdk.json) rather than basenames.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +96
// Check include patterns - if file doesn't match any include, ignore it
if (includePatterns.length > 0 && stats?.isFile()) {
const matchesInclude = includePatterns.some(pattern =>
matchesIncludePattern(normalized, basename, pathParts, pattern),
);
if (!matchesInclude) return true;
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Include filtering is currently gated on stats?.isFile() (line 91). Since stats is optional in chokidar’s ignored callback, this can cause include patterns like **/*.js to be ignored entirely when chokidar calls ignored() without stats, leading to deployments being triggered by files outside the include set. Consider making include evaluation independent of stats (while still ensuring directories aren’t excluded prematurely), or configuring chokidar so stats is always available for ignore evaluation.

Copilot uses AI. Check for mistakes.
* ignored function that evaluates both include and exclude patterns.
*
* @param includePatterns - Array of glob patterns to include (e.g., ['**', 'src/*.ts'])
* @param excludePatterns - Array of glob patterns to exclude (e.g., ['node_modules', '**\/*.d.ts'])
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

JSDoc example for excludePatterns contains an escaped glob **\/*.d.ts (line 76). In a comment this escape isn’t needed and reads like a literal backslash in the pattern; consider updating it to **/*.d.ts to avoid confusion about the expected input format.

Suggested change
* @param excludePatterns - Array of glob patterns to exclude (e.g., ['node_modules', '**\/*.d.ts'])
* @param excludePatterns - Array of glob patterns to exclude (e.g., ['node_modules', '**/*.d.ts'])

Copilot uses AI. Check for mistakes.
Comment on lines +884 to +886
// Skip logging for ignored files during initial scan
if (filePath && ignored(filePath)) {
return;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

In the watch event handler, ignored(filePath) is called without a stats argument. With the current ignored implementation, this means include-based filtering (which depends on stats?.isFile()) won’t be applied here, so the “Skip logging for ignored files” behavior may diverge from what chokidar actually ignores/doesn’t ignore. If include filtering continues to depend on stats, consider either passing/deriving stats consistently, or adjusting ignored so it can make the same decision with only the path.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +89
const watchPaths = includePatterns.map(extractWatchPath);

const ignored = (path: string, stats?: any): boolean => {
const normalized = path.replace(/\\/g, '/');
const basename = path.split(/[/\\]/).pop() || '';
const pathParts = normalized.split('/');

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Patterns themselves aren’t normalized before matching (e.g., a user include like ./dir/**/* will produce a watchPath of ./dir, but event paths under cwd are typically dir/..., so include/exclude comparisons can fail due to the leading ./). Consider normalizing includePatterns/excludePatterns up-front (strip leading ./, convert \\ to /) so matching semantics are consistent across platforms and with chokidar’s cwd behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +35
import { convertGlobsToChokidarV4 } from '../../lib/cli/convert-globs';

describe('convertGlobsToChokidarV4', () => {
describe('watchPaths extraction', () => {
test('converts "**" to current directory', () => {
const { watchPaths } = convertGlobsToChokidarV4(['**'], []);
expect(watchPaths).toEqual(['.']);
});

test('converts "**/*.js" to current directory', () => {
const { watchPaths } = convertGlobsToChokidarV4(['**/*.js'], []);
expect(watchPaths).toEqual(['.']);
});

test('converts "src/*.ts" to src directory', () => {
const { watchPaths } = convertGlobsToChokidarV4(['src/*.ts'], []);
expect(watchPaths).toEqual(['src']);
});

test('converts "./dir/**/*" to ./dir', () => {
const { watchPaths } = convertGlobsToChokidarV4(['./dir/**/*'], []);
expect(watchPaths).toEqual(['./dir']);
});

test('handles multiple include patterns', () => {
const { watchPaths } = convertGlobsToChokidarV4(['src/**', 'lib/**'], []);
expect(watchPaths).toEqual(['src', 'lib']);
});

test('deduplicates identical paths', () => {
const { watchPaths } = convertGlobsToChokidarV4(['**/*.js', '**/*.ts'], []);
expect(watchPaths).toEqual(['.']);
});
});

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Test coverage here doesn’t cover several glob shapes that CDK documents/ships in templates (e.g., target/*, cdk*.json, src/test, *.ts, src/*, and patterns with a leading ./). Adding cases for these would help ensure the conversion preserves existing watch.exclude/watch.include behavior across common configurations, especially since the matcher is currently shape-specific.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +11
if (pattern === '**') return '.';
if (pattern.startsWith('**/')) return '.';
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not do: if (pattern.startsWith('**')) return '.'; to cover both cases?

@aws-cdk-automation aws-cdk-automation added this pull request to the merge queue Feb 10, 2026
Merged via the queue into main with commit 4f58bf1 Feb 10, 2026
36 checks passed
@aws-cdk-automation aws-cdk-automation deleted the conroy/chokidar branch February 10, 2026 21:27
kaizencc added a commit that referenced this pull request Feb 11, 2026
github-merge-queue bot pushed a commit that referenced this pull request Feb 11, 2026
Reverts #1131

There are additional glob patterns that the PR does not catch.

---

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache-2.0 license
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(cli): watch not working on MacOS ^2.1100.1

5 participants