From 8536dce98959aa4bb20a0a5bd9334d9b60d0331e Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Fri, 6 Feb 2026 07:59:55 +0000 Subject: [PATCH] feat(oxfmt): Support glob for CLI paths (#18976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #13705 - Support glob patterns like `*.js`, `**/*.ts`, and `{a,b}.js` in CLI paths - Globs are pre-expanded to make `ignore` crate respects `.gitignore` - Existing cases that don't use glob will NOT be affected in terms of performance This also fixes bug that `oxfmt a.js a.js` formats the same file 2 times 🙈 --- Cargo.lock | 1 + apps/oxfmt/Cargo.toml | 1 + apps/oxfmt/src/cli/command.rs | 6 +- apps/oxfmt/src/cli/walk.rs | 138 ++++++++++++----- .../__snapshots__/exclude_nested.test.ts.snap | 27 ++++ .../cli/exclude_nested/exclude_nested.test.ts | 12 ++ .../__snapshots__/glob_patterns.test.ts.snap | 146 ++++++++++++++++++ .../cli/glob_patterns/fixtures/lib/util.js | 1 + .../test/cli/glob_patterns/fixtures/src/a.js | 1 + .../test/cli/glob_patterns/fixtures/src/b.js | 1 + .../test/cli/glob_patterns/fixtures/src/c.ts | 1 + .../cli/glob_patterns/fixtures/src/{d}.js | 1 + .../cli/glob_patterns/glob_patterns.test.ts | 91 +++++++++++ ...no_error_on_unmatched_pattern.test.ts.snap | 21 +++ .../no_error_on_unmatched_pattern.test.ts | 12 ++ .../website_formatter/src/snapshots/cli.snap | 2 +- .../src/snapshots/cli_terminal.snap | 7 +- 17 files changed, 428 insertions(+), 41 deletions(-) create mode 100644 apps/oxfmt/test/cli/glob_patterns/__snapshots__/glob_patterns.test.ts.snap create mode 100644 apps/oxfmt/test/cli/glob_patterns/fixtures/lib/util.js create mode 100644 apps/oxfmt/test/cli/glob_patterns/fixtures/src/a.js create mode 100644 apps/oxfmt/test/cli/glob_patterns/fixtures/src/b.js create mode 100644 apps/oxfmt/test/cli/glob_patterns/fixtures/src/c.ts create mode 100644 apps/oxfmt/test/cli/glob_patterns/fixtures/src/{d}.js create mode 100644 apps/oxfmt/test/cli/glob_patterns/glob_patterns.test.ts diff --git a/Cargo.lock b/Cargo.lock index a945fbaec84f4..b707540f8b0a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2529,6 +2529,7 @@ dependencies = [ "oxc_span", "phf", "rayon", + "rustc-hash", "serde", "serde_json", "simdutf8", diff --git a/apps/oxfmt/Cargo.toml b/apps/oxfmt/Cargo.toml index 058b1137d101e..411ce82804e5d 100644 --- a/apps/oxfmt/Cargo.toml +++ b/apps/oxfmt/Cargo.toml @@ -45,6 +45,7 @@ json-strip-comments = { workspace = true } miette = { workspace = true } phf = { workspace = true, features = ["macros"] } rayon = { workspace = true } +rustc-hash = { workspace = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/apps/oxfmt/src/cli/command.rs b/apps/oxfmt/src/cli/command.rs index 98355e786ea9e..47ea4e3f9afc8 100644 --- a/apps/oxfmt/src/cli/command.rs +++ b/apps/oxfmt/src/cli/command.rs @@ -28,9 +28,11 @@ pub struct FormatCommand { pub ignore_options: IgnoreOptions, #[bpaf(external)] pub runtime_options: RuntimeOptions, - /// Single file, single path or list of paths. + /// Single file, path or list of paths. + /// Glob patterns are also supported. + /// (Be sure to quote them, otherwise your shell may expand them before passing.) + /// Exclude patterns with `!` prefix like `'!**/fixtures/*.js'` are also supported. /// If not provided, current working directory is used. - /// Glob is supported only for exclude patterns like `'!**/fixtures/*.js'`. // `bpaf(fallback)` seems to have issues with `many` or `positional`, // so we implement the fallback behavior in code instead. #[bpaf(positional("PATH"), many, guard(validate_paths, PATHS_ERROR_MESSAGE))] diff --git a/apps/oxfmt/src/cli/walk.rs b/apps/oxfmt/src/cli/walk.rs index e12a2cced823d..b6fba794ae40c 100644 --- a/apps/oxfmt/src/cli/walk.rs +++ b/apps/oxfmt/src/cli/walk.rs @@ -3,7 +3,11 @@ use std::{ sync::mpsc, }; -use ignore::gitignore::{Gitignore, GitignoreBuilder}; +use ignore::{ + gitignore::{Gitignore, GitignoreBuilder}, + overrides::OverrideBuilder, +}; +use rustc_hash::FxHashSet; use crate::core::{FormatFileStrategy, utils::normalize_relative_path}; @@ -23,8 +27,10 @@ impl Walk { // // Classify and normalize specified paths // - let mut target_paths = vec![]; + let mut target_paths = FxHashSet::default(); + let mut glob_patterns = vec![]; let mut exclude_patterns = vec![]; + for path in paths { let path_str = path.to_string_lossy(); @@ -35,26 +41,38 @@ impl Walk { continue; } - // Otherwise, treat as target path + // Normalize `./` prefix + let normalized = + if let Some(stripped) = path_str.strip_prefix("./") { stripped } else { &path_str }; - if path.is_absolute() { - target_paths.push(path.clone()); + // Separate glob patterns from concrete paths + if is_glob_pattern(normalized, cwd) { + glob_patterns.push(normalized.to_string()); continue; } - // NOTE: `.` and cwd behave differently, need to normalize - let path = if path_str == "." { + // Resolve full path for concrete paths + let full_path = if path.is_absolute() { + path.clone() + } else if normalized == "." { + // NOTE: `.` and cwd behave differently, need to normalize cwd.to_path_buf() - } else if let Some(stripped) = path_str.strip_prefix("./") { - cwd.join(stripped) } else { - cwd.join(path) + cwd.join(normalized) }; - target_paths.push(path); + target_paths.insert(full_path); } - // Default to cwd if no target paths are provided - if target_paths.is_empty() { - target_paths.push(cwd.to_path_buf()); + + // Expand glob patterns and add to target paths + // NOTE: See `expand_glob_patterns()` for why we pre-expand globs here + if !glob_patterns.is_empty() { + target_paths.extend(expand_glob_patterns(cwd, &glob_patterns)?); + } + + // Default to `cwd` if no positive paths were specified. + // Exclude patterns alone should still walk, but unmatched globs should not. + if target_paths.is_empty() && glob_patterns.is_empty() { + target_paths.insert(cwd.to_path_buf()); } // @@ -182,27 +200,7 @@ impl Walk { true }); - let inner = inner - // Do not follow symlinks like Prettier does. - // See https://github.com/prettier/prettier/pull/14627 - .follow_links(false) - // Include hidden files and directories except those we explicitly skip above - .hidden(false) - // Do not respect `.ignore` file - .ignore(false) - // Do not search upward - // NOTE: Prettier only searches current working directory - .parents(false) - // Also do not respect globals - .git_global(false) - // But respect downward nested `.gitignore` files - // NOTE: Prettier does not: https://github.com/prettier/prettier/issues/4081 - .git_ignore(true) - // Also do not respect `.git/info/exclude` - .git_exclude(false) - // Git is not required - .require_git(false) - .build_parallel(); + let inner = apply_walk_settings(&mut inner).build_parallel(); Ok(Some(Self { inner })) } @@ -248,6 +246,51 @@ fn is_ignored(matchers: &[Gitignore], path: &Path, is_dir: bool, check_ancestors false } +/// Check if a path string looks like a glob pattern. +/// Glob-like characters are also valid path characters on some environments. +/// If the path actually exists on disk, it is treated as a concrete path. +/// e.g. `{config}.js`, `[id].tsx` +fn is_glob_pattern(s: &str, cwd: &Path) -> bool { + let has_glob_chars = s.contains('*') || s.contains('?') || s.contains('[') || s.contains('{'); + has_glob_chars && !cwd.join(s).exists() +} + +// NOTE: Why pre-expand globs? +// An alternative approach would be: +// - to always walk the entire `cwd` +// - and filter by both concrete paths and glob patterns +// +// However, this would be inefficient for common use cases +// like `oxfmt src/a.js` or pre-commit hooks that specify only staged files. +// +// Pre-expanding globs allows us to walk only the necessary paths. +// And this only happens if glob patterns are specified. +// +// NOTE: Why not use `ignore::Overrides` in the main walk? +// `ignore::Overrides` have the highest priority in the `ignore` crate, +// so files matching the glob would be collected even if they're in `.gitignore`! +/// Expand glob patterns to concrete file paths. +fn expand_glob_patterns(cwd: &Path, patterns: &[String]) -> Result, String> { + let mut ob = OverrideBuilder::new(cwd); + for pattern in patterns { + ob.add(pattern).map_err(|e| format!("Invalid glob pattern `{pattern}`: {e}"))?; + } + let overrides = ob.build().map_err(|e| format!("Failed to build glob overrides: {e}"))?; + + let mut builder = ignore::WalkBuilder::new(cwd); + builder.overrides(overrides); + + let mut paths = vec![]; + for entry in apply_walk_settings(&mut builder).build().flatten() { + // Use `!is_dir()` instead of `is_file()` to handle symlinks correctly + if entry.file_type().is_some_and(|ft| !ft.is_dir()) { + paths.push(entry.into_path()); + } + } + + Ok(paths) +} + fn load_ignore_paths(cwd: &Path, ignore_paths: &[PathBuf]) -> Result, String> { // If specified, resolve absolute paths and check existence if !ignore_paths.is_empty() { @@ -272,6 +315,31 @@ fn load_ignore_paths(cwd: &Path, ignore_paths: &[PathBuf]) -> Result &mut ignore::WalkBuilder { + builder + // Do not follow symlinks like Prettier does. + // See https://github.com/prettier/prettier/pull/14627 + .follow_links(false) + // Include hidden files and directories except those we explicitly skip + .hidden(false) + // Do not respect `.ignore` file + .ignore(false) + // Do not search upward + // NOTE: Prettier only searches current working directory + .parents(false) + // Also do not respect globals + .git_global(false) + // But respect downward nested `.gitignore` files + // NOTE: Prettier does not: https://github.com/prettier/prettier/issues/4081 + .git_ignore(true) + // Also do not respect `.git/info/exclude` + .git_exclude(false) + // Git is not required + .require_git(false) +} + // --- struct WalkBuilder { diff --git a/apps/oxfmt/test/cli/exclude_nested/__snapshots__/exclude_nested.test.ts.snap b/apps/oxfmt/test/cli/exclude_nested/__snapshots__/exclude_nested.test.ts.snap index a9ab8f3fbce82..1cf0b4185d9a2 100644 --- a/apps/oxfmt/test/cli/exclude_nested/__snapshots__/exclude_nested.test.ts.snap +++ b/apps/oxfmt/test/cli/exclude_nested/__snapshots__/exclude_nested.test.ts.snap @@ -212,3 +212,30 @@ Finished in ms on 1 files using 1 threads. --------------------" `; + +exports[`exclude_nested > should handle glob include with glob exclude 1`] = ` +"-------------------- +arguments: --check * !**/error.js +working directory: exclude_nested/fixtures +exit code: 0 +--- STDOUT --------- +Checking formatting... + +All matched files use the correct format. +Finished in ms on 3 files using 1 threads. +--- STDERR --------- + +-------------------- +-------------------- +arguments: --check foo/**/*.js !**/bar/* +working directory: exclude_nested/fixtures +exit code: 0 +--- STDOUT --------- +Checking formatting... + +All matched files use the correct format. +Finished in ms on 1 files using 1 threads. +--- STDERR --------- + +--------------------" +`; diff --git a/apps/oxfmt/test/cli/exclude_nested/exclude_nested.test.ts b/apps/oxfmt/test/cli/exclude_nested/exclude_nested.test.ts index e61618bf25f57..ad8e391c8fe90 100644 --- a/apps/oxfmt/test/cli/exclude_nested/exclude_nested.test.ts +++ b/apps/oxfmt/test/cli/exclude_nested/exclude_nested.test.ts @@ -45,4 +45,16 @@ describe("exclude_nested", () => { const snapshot = await runAndSnapshot(fixturesDir, testCases); expect(snapshot).toMatchSnapshot(); }); + + it("should handle glob include with glob exclude", async () => { + const testCases = [ + // Glob include all .js, glob exclude error.js + ["--check", "*", "!**/error.js"], + // Glob include foo/**/*.js, glob exclude bar directory + ["--check", "foo/**/*.js", "!**/bar/*"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); }); diff --git a/apps/oxfmt/test/cli/glob_patterns/__snapshots__/glob_patterns.test.ts.snap b/apps/oxfmt/test/cli/glob_patterns/__snapshots__/glob_patterns.test.ts.snap new file mode 100644 index 0000000000000..f81d360178540 --- /dev/null +++ b/apps/oxfmt/test/cli/glob_patterns/__snapshots__/glob_patterns.test.ts.snap @@ -0,0 +1,146 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`glob_patterns > should deduplicate files specified both directly and via glob 1`] = ` +"-------------------- +arguments: --check src/a.js src/*.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/a.js (ms) +src/b.js (ms) +src/{d}.js (ms) + +Format issues found in above 3 files. Run without \`--check\` to fix. +Finished in ms on 3 files using 1 threads. +--- STDERR --------- + +--------------------" +`; + +exports[`glob_patterns > should expand basic wildcard pattern 1`] = ` +"-------------------- +arguments: --check src/*.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/a.js (ms) +src/b.js (ms) +src/{d}.js (ms) + +Format issues found in above 3 files. Run without \`--check\` to fix. +Finished in ms on 3 files using 1 threads. +--- STDERR --------- + +--------------------" +`; + +exports[`glob_patterns > should expand brace expansion pattern 1`] = ` +"-------------------- +arguments: --check src/{a,b}.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/a.js (ms) +src/b.js (ms) + +Format issues found in above 2 files. Run without \`--check\` to fix. +Finished in ms on 2 files using 1 threads. +--- STDERR --------- + +-------------------- +-------------------- +arguments: --check src/{a}.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/a.js (ms) + +Format issues found in above 1 files. Run without \`--check\` to fix. +Finished in ms on 1 files using 1 threads. +--- STDERR --------- + +--------------------" +`; + +exports[`glob_patterns > should expand recursive glob pattern and exclude 1`] = ` +"-------------------- +arguments: --check **/*.js !util.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/a.js (ms) +src/b.js (ms) +src/{d}.js (ms) + +Format issues found in above 3 files. Run without \`--check\` to fix. +Finished in ms on 3 files using 1 threads. +--- STDERR --------- + +--------------------" +`; + +exports[`glob_patterns > should handle mix of concrete paths and glob patterns 1`] = ` +"-------------------- +arguments: --check src/c.ts src/*.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/a.js (ms) +src/b.js (ms) +src/c.ts (ms) +src/{d}.js (ms) + +Format issues found in above 4 files. Run without \`--check\` to fix. +Finished in ms on 4 files using 1 threads. +--- STDERR --------- + +--------------------" +`; + +exports[`glob_patterns > should respect .gitignore for directories when expanding globs 1`] = ` +"-------------------- +arguments: --check **/*.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/a.js (ms) +src/b.js (ms) +src/{d}.js (ms) + +Format issues found in above 3 files. Run without \`--check\` to fix. +Finished in ms on 3 files using 1 threads. +--- STDERR --------- + +--------------------" +`; + +exports[`glob_patterns > should treat glob-like filename as concrete path when it exists on disk 1`] = ` +"-------------------- +arguments: --check src/{d}.js +working directory: glob_patterns/fixtures +exit code: 1 +--- STDOUT --------- +Checking formatting... + +src/{d}.js (ms) + +Format issues found in above 1 files. Run without \`--check\` to fix. +Finished in ms on 1 files using 1 threads. +--- STDERR --------- + +--------------------" +`; diff --git a/apps/oxfmt/test/cli/glob_patterns/fixtures/lib/util.js b/apps/oxfmt/test/cli/glob_patterns/fixtures/lib/util.js new file mode 100644 index 0000000000000..9071bf5852ca1 --- /dev/null +++ b/apps/oxfmt/test/cli/glob_patterns/fixtures/lib/util.js @@ -0,0 +1 @@ +class X { diff --git a/apps/oxfmt/test/cli/glob_patterns/fixtures/src/a.js b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/a.js new file mode 100644 index 0000000000000..bf87686953b57 --- /dev/null +++ b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/a.js @@ -0,0 +1 @@ +const a=1 diff --git a/apps/oxfmt/test/cli/glob_patterns/fixtures/src/b.js b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/b.js new file mode 100644 index 0000000000000..c4211af792671 --- /dev/null +++ b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/b.js @@ -0,0 +1 @@ +const b=2 diff --git a/apps/oxfmt/test/cli/glob_patterns/fixtures/src/c.ts b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/c.ts new file mode 100644 index 0000000000000..bee2ddb2b48eb --- /dev/null +++ b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/c.ts @@ -0,0 +1 @@ +const c:number=3 diff --git a/apps/oxfmt/test/cli/glob_patterns/fixtures/src/{d}.js b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/{d}.js new file mode 100644 index 0000000000000..11ca84472a507 --- /dev/null +++ b/apps/oxfmt/test/cli/glob_patterns/fixtures/src/{d}.js @@ -0,0 +1 @@ +const d=4 diff --git a/apps/oxfmt/test/cli/glob_patterns/glob_patterns.test.ts b/apps/oxfmt/test/cli/glob_patterns/glob_patterns.test.ts new file mode 100644 index 0000000000000..ba92bc9be01f3 --- /dev/null +++ b/apps/oxfmt/test/cli/glob_patterns/glob_patterns.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { join } from "node:path"; +import { writeFile, rm } from "node:fs/promises"; +import { runAndSnapshot } from "../utils"; + +const fixturesDir = join(import.meta.dirname, "fixtures"); + +describe("glob_patterns", () => { + it("should expand basic wildcard pattern", async () => { + const testCases = [ + // src/*.js should match src/a.js and src/b.js, but not src/c.ts + ["--check", "src/*.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); + + it("should expand recursive glob pattern and exclude", async () => { + const testCases = [ + // Should match all .js files except util.js + ["--check", "**/*.js", "!util.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); + + it("should expand brace expansion pattern", async () => { + const testCases = [ + // {a,b}.js should match a.js and b.js + ["--check", "src/{a,b}.js"], + ["--check", "src/{a}.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); + + it("should handle mix of concrete paths and glob patterns", async () => { + const testCases = [ + // Mix of concrete path and glob + ["--check", "src/c.ts", "src/*.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); + + it("should deduplicate files specified both directly and via glob", async () => { + const testCases = [ + // src/a.js is specified both directly and via glob - should only appear once + ["--check", "src/a.js", "src/*.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); + + it("should treat glob-like filename as concrete path when it exists on disk", async () => { + const testCases = [ + // `{d}.js` contains glob-like `{` character but actually exists on disk. + // It should be treated as a concrete path, not as a brace expansion pattern. + // Without the existence check, `{d}` would expand to `d` and try to match + // `d.js` which does NOT exist, resulting in no files being found. + ["--check", "src/{d}.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); + + it("should respect .gitignore for directories when expanding globs", async () => { + const gitignorePath = join(fixturesDir, ".gitignore"); + + try { + // Create .gitignore that ignores the "lib/" directory + await writeFile(gitignorePath, "lib/\n"); + + const testCases = [ + // **/*.js should NOT include lib/util.js (directory is gitignored) + ["--check", "**/*.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + } finally { + await rm(gitignorePath, { force: true }); + } + }); +}); diff --git a/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/__snapshots__/no_error_on_unmatched_pattern.test.ts.snap b/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/__snapshots__/no_error_on_unmatched_pattern.test.ts.snap index b39b2e614ab17..08bc362565d69 100644 --- a/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/__snapshots__/no_error_on_unmatched_pattern.test.ts.snap +++ b/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/__snapshots__/no_error_on_unmatched_pattern.test.ts.snap @@ -24,6 +24,27 @@ Expected at least one target file --------------------" `; +exports[`no_error_on_unmatched_pattern > should handle unmatched glob patterns 1`] = ` +"-------------------- +arguments: --check __nonexistent__/**/*.js +working directory: no_error_on_unmatched_pattern/fixtures +exit code: 2 +--- STDOUT --------- + +--- STDERR --------- +Expected at least one target file +-------------------- +-------------------- +arguments: --check --no-error-on-unmatched-pattern __nonexistent__/**/*.js +working directory: no_error_on_unmatched_pattern/fixtures +exit code: 0 +--- STDOUT --------- + +--- STDERR --------- +No files found matching the given patterns. +--------------------" +`; + exports[`no_error_on_unmatched_pattern > should ignore ignored paths even when explicitly passed 1`] = ` "-------------------- arguments: --check --no-error-on-unmatched-pattern ignored-by-config/bad.js diff --git a/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/no_error_on_unmatched_pattern.test.ts b/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/no_error_on_unmatched_pattern.test.ts index 3c73d4ee58968..cf267db04c800 100644 --- a/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/no_error_on_unmatched_pattern.test.ts +++ b/apps/oxfmt/test/cli/no_error_on_unmatched_pattern/no_error_on_unmatched_pattern.test.ts @@ -15,6 +15,18 @@ describe("no_error_on_unmatched_pattern", () => { expect(snapshot).toMatchSnapshot(); }); + it("should handle unmatched glob patterns", async () => { + const testCases = [ + // Glob pattern that matches nothing - should error without flag + ["--check", "__nonexistent__/**/*.js"], + // With flag - should not error + ["--check", "--no-error-on-unmatched-pattern", "__nonexistent__/**/*.js"], + ]; + + const snapshot = await runAndSnapshot(fixturesDir, testCases); + expect(snapshot).toMatchSnapshot(); + }); + // When a file path inside an ignored directory is passed directly via CLI, it should still be ignored. // This is important for tools like lint-staged that pass explicit file paths. it("should ignore ignored paths even when explicitly passed", async () => { diff --git a/tasks/website_formatter/src/snapshots/cli.snap b/tasks/website_formatter/src/snapshots/cli.snap index 805c427dfa5d4..6197d4ed079c2 100644 --- a/tasks/website_formatter/src/snapshots/cli.snap +++ b/tasks/website_formatter/src/snapshots/cli.snap @@ -56,7 +56,7 @@ search: false ## Available positional items: - _`PATH`_ — - Single file, single path or list of paths. If not provided, current working directory is used. Glob is supported only for exclude patterns like `'!**/fixtures/*.js'`. + Single file, path or list of paths. Glob patterns are also supported. (Be sure to quote them, otherwise your shell may expand them before passing.) Exclude patterns with `!` prefix like `'!**/fixtures/*.js'` are also supported. If not provided, current working directory is used. diff --git a/tasks/website_formatter/src/snapshots/cli_terminal.snap b/tasks/website_formatter/src/snapshots/cli_terminal.snap index a733bd0d0453b..30e038ccfee3c 100644 --- a/tasks/website_formatter/src/snapshots/cli_terminal.snap +++ b/tasks/website_formatter/src/snapshots/cli_terminal.snap @@ -30,9 +30,10 @@ Runtime Options --threads=INT Number of threads to use. Set to 1 for using only 1 CPU core. Available positional items: - PATH Single file, single path or list of paths. If not provided, current - working directory is used. Glob is supported only for exclude patterns - like `'!**/fixtures/*.js'`. + PATH Single file, path or list of paths. Glob patterns are also supported. + (Be sure to quote them, otherwise your shell may expand them before + passing.) Exclude patterns with `!` prefix like `'!**/fixtures/*.js'` + are also supported. If not provided, current working directory is used. Available options: -h, --help Prints help information