From b6db266c9ee5aa582500107891f8c81ac97d7071 Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Tue, 25 Jul 2023 11:11:22 -0400 Subject: [PATCH] fix(core): handle complex glob patterns within rust (#18242) --- Cargo.lock | 45 +++- .../shared/reference/project-configuration.md | 15 ++ e2e/nx-run/src/cache.test.ts | 48 +++- packages/nx/Cargo.toml | 2 + packages/nx/migrations.json | 6 - .../update-16-5-4/update-output-globs.spec.ts | 77 ------ .../update-16-5-4/update-output-globs.ts | 54 ---- .../nx/src/native/cache/expand_outputs.rs | 2 +- packages/nx/src/native/hasher.rs | 2 +- packages/nx/src/native/utils/glob.rs | 234 ++++++++++++++++-- packages/nx/src/native/walker.rs | 14 +- packages/nx/src/native/watch/types.rs | 4 +- .../src/native/workspace/get_config_files.rs | 9 +- .../workspace/get_nx_workspace_files.rs | 2 +- 14 files changed, 322 insertions(+), 192 deletions(-) delete mode 100644 packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts delete mode 100644 packages/nx/src/migrations/update-16-5-4/update-output-globs.ts diff --git a/Cargo.lock b/Cargo.lock index 4dd7491663b52..4275246ff42b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.14" @@ -155,7 +164,7 @@ checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", "once_cell", - "regex-automata", + "regex-automata 0.1.10", "serde", ] @@ -740,7 +749,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.20", "bstr", "fnv", "log", @@ -1067,7 +1076,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -1343,7 +1352,9 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "once_cell", "rayon", + "regex", "swc_common", "swc_ecma_ast", "swc_ecma_dep_graph", @@ -1652,13 +1663,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.2", "memchr", - "regex-syntax", + "regex-automata 0.3.3", + "regex-syntax 0.7.4", ] [[package]] @@ -1667,7 +1679,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.28", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-syntax 0.7.4", ] [[package]] @@ -1676,6 +1699,12 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + [[package]] name = "rustc-hash" version = "1.1.0" diff --git a/docs/shared/reference/project-configuration.md b/docs/shared/reference/project-configuration.md index aa16a5233bd8a..9331f12df6690 100644 --- a/docs/shared/reference/project-configuration.md +++ b/docs/shared/reference/project-configuration.md @@ -328,6 +328,21 @@ But if the above is not possible, globs (parsed by the [GlobSet](https://docs.rs } ``` +More advanced patterns can be used to exclude files and folders in a single line + +```json +{ + "targets": { + "build-js": { + "outputs": ["{workspaceRoot}/dist/libs/!(cache|.next)/**/*.{js,map}"] + }, + "build-css": { + "outputs": ["{workspaceRoot}/dist/libs/mylib/**/!(secondary).css"] + } + } +} +``` + ### dependsOn Targets can depend on other targets. This is the relevant portion of the configuration file: diff --git a/e2e/nx-run/src/cache.test.ts b/e2e/nx-run/src/cache.test.ts index cbebdc873770d..af2a0a044c718 100644 --- a/e2e/nx-run/src/cache.test.ts +++ b/e2e/nx-run/src/cache.test.ts @@ -1,10 +1,12 @@ import { cleanupProject, + directoryExists, listFiles, newProject, readFile, rmDist, runCLI, + tmpProjPath, uniq, updateFile, updateJson, @@ -157,18 +159,23 @@ describe('cache', () => { updateProjectConfig(mylib, (c) => { c.targets.build = { executor: 'nx:run-commands', - outputs: ['{workspaceRoot}/dist/*.{txt,md}'], + outputs: ['{workspaceRoot}/dist/!(.next)/**/!(z|x).(txt|md)'], options: { commands: [ 'rm -rf dist', 'mkdir dist', - 'echo a > dist/a.txt', - 'echo b > dist/b.txt', - 'echo c > dist/c.txt', - 'echo d > dist/d.txt', - 'echo e > dist/e.txt', - 'echo f > dist/f.md', - 'echo g > dist/g.html', + 'mkdir dist/apps', + 'mkdir dist/.next', + 'echo a > dist/apps/a.txt', + 'echo b > dist/apps/b.txt', + 'echo c > dist/apps/c.txt', + 'echo d > dist/apps/d.txt', + 'echo e > dist/apps/e.txt', + 'echo f > dist/apps/f.md', + 'echo g > dist/apps/g.html', + 'echo h > dist/.next/h.txt', + 'echo x > dist/apps/x.txt', + 'echo z > dist/apps/z.md', ], parallel: false, }, @@ -183,7 +190,10 @@ describe('cache', () => { // Rerun without touching anything const rerunWithUntouchedOutputs = runCLI(`build ${mylib}`); expect(rerunWithUntouchedOutputs).toContain('local cache'); - const outputsWithUntouchedOutputs = listFiles('dist'); + const outputsWithUntouchedOutputs = [ + ...listFiles('dist/apps'), + ...listFiles('dist/.next').map((f) => `.next/${f}`), + ]; expect(outputsWithUntouchedOutputs).toContain('a.txt'); expect(outputsWithUntouchedOutputs).toContain('b.txt'); expect(outputsWithUntouchedOutputs).toContain('c.txt'); @@ -191,14 +201,20 @@ describe('cache', () => { expect(outputsWithUntouchedOutputs).toContain('e.txt'); expect(outputsWithUntouchedOutputs).toContain('f.md'); expect(outputsWithUntouchedOutputs).toContain('g.html'); + expect(outputsWithUntouchedOutputs).toContain('.next/h.txt'); + expect(outputsWithUntouchedOutputs).toContain('x.txt'); + expect(outputsWithUntouchedOutputs).toContain('z.md'); // Create a file in the dist that does not match output glob - updateFile('dist/c.ts', ''); + updateFile('dist/apps/c.ts', ''); // Rerun const rerunWithNewUnrelatedFile = runCLI(`build ${mylib}`); expect(rerunWithNewUnrelatedFile).toContain('local cache'); - const outputsAfterAddingUntouchedFileAndRerunning = listFiles('dist'); + const outputsAfterAddingUntouchedFileAndRerunning = [ + ...listFiles('dist/apps'), + ...listFiles('dist/.next').map((f) => `.next/${f}`), + ]; expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('a.txt'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('b.txt'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('c.txt'); @@ -206,6 +222,11 @@ describe('cache', () => { expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('e.txt'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('f.md'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('g.html'); + expect(outputsAfterAddingUntouchedFileAndRerunning).toContain( + '.next/h.txt' + ); + expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('x.txt'); + expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('z.md'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('c.ts'); // Clear Dist @@ -214,7 +235,8 @@ describe('cache', () => { // Rerun const rerunWithoutOutputs = runCLI(`build ${mylib}`); expect(rerunWithoutOutputs).toContain('read the output from the cache'); - const outputsWithoutOutputs = listFiles('dist'); + const outputsWithoutOutputs = listFiles('dist/apps'); + expect(directoryExists(`${tmpProjPath()}/dist/.next`)).toBe(false); expect(outputsWithoutOutputs).toContain('a.txt'); expect(outputsWithoutOutputs).toContain('b.txt'); expect(outputsWithoutOutputs).toContain('c.txt'); @@ -223,6 +245,8 @@ describe('cache', () => { expect(outputsWithoutOutputs).toContain('f.md'); expect(outputsWithoutOutputs).not.toContain('c.ts'); expect(outputsWithoutOutputs).not.toContain('g.html'); + expect(outputsWithoutOutputs).not.toContain('x.txt'); + expect(outputsWithoutOutputs).not.toContain('z.md'); }); it('should use consider filesets when hashing', async () => { diff --git a/packages/nx/Cargo.toml b/packages/nx/Cargo.toml index 286a0e7b450ad..dc4e73bd37c46 100644 --- a/packages/nx/Cargo.toml +++ b/packages/nx/Cargo.toml @@ -13,8 +13,10 @@ hashbrown = { version = "0.14.0", features = ["rayon"] } ignore = '0.4' ignore-files = "1.3.0" itertools = "0.10.5" +once_cell = "1.18.0" napi = { version = '2.12.6', default-features = false, features = ['anyhow', 'napi4', 'tokio_rt'] } napi-derive = '2.9.3' +regex = "1.9.1" rayon = "1.7.0" thiserror = "1.0.40" tokio = { version = "1.28.2", features = ["fs"] } diff --git a/packages/nx/migrations.json b/packages/nx/migrations.json index 6e74b4d901043..f287f739d45ba 100644 --- a/packages/nx/migrations.json +++ b/packages/nx/migrations.json @@ -77,12 +77,6 @@ "version": "16.2.0-beta.0", "description": "Remove outputPath from run commands", "implementation": "./src/migrations/update-16-2-0/remove-run-commands-output-path" - }, - "16.5.4-update-output-globs": { - "cli": "nx", - "version": "16.5.4-beta.0", - "description": "Update outdated non-standard globs to unix standard", - "implementation": "./src/migrations/update-16-5-4/update-output-globs" } } } diff --git a/packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts b/packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts deleted file mode 100644 index 6798e3ad7bb8a..0000000000000 --- a/packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createTreeWithEmptyWorkspace } from '../../generators/testing-utils/create-tree-with-empty-workspace'; -import { TargetConfiguration } from '../../config/workspace-json-project-json'; -import { - addProjectConfiguration, - readProjectConfiguration, -} from '../../generators/utils/project-configuration'; -import updateOutputsGlobs from './update-output-globs'; -import { readJson, updateJson } from '../../generators/utils/json'; -import { NxJsonConfiguration } from '../../config/nx-json'; - -describe('update output globs', () => { - it('should update output globs', () => { - const tree = createTreeWithEmptyWorkspace(); - const targets: Record = { - build: { - outputs: ['{options.outputPath}', 'dist/apps/my-app/*.(js|map|ts)'], - }, - lint: {}, - test: { - outputs: ['dist/apps/my-app/main.(js|map|ts)'], - }, - run: { - outputs: ['dist/apps/my-app'], - }, - }; - addProjectConfiguration(tree, 'my-app', { - root: 'apps/my-app', - targets, - }); - - updateJson(tree, 'nx.json', (json) => { - json.targetDefaults = { - lint: { - outputs: ['dist/apps/my-app', '*.(js|map|ts)'], - }, - }; - return json; - }); - - updateOutputsGlobs(tree); - - const migratedTargets = readProjectConfiguration(tree, 'my-app').targets; - expect(migratedTargets).toMatchInlineSnapshot(` - { - "build": { - "outputs": [ - "{options.outputPath}", - "dist/apps/my-app/*.{js,map,ts}", - ], - }, - "lint": {}, - "run": { - "outputs": [ - "dist/apps/my-app", - ], - }, - "test": { - "outputs": [ - "dist/apps/my-app/main.{js,map,ts}", - ], - }, - } - `); - - const nxJson = readJson(tree, 'nx.json'); - expect(nxJson.targetDefaults).toMatchInlineSnapshot(` - { - "lint": { - "outputs": [ - "dist/apps/my-app", - "*.{js,map,ts}", - ], - }, - } - `); - }); -}); diff --git a/packages/nx/src/migrations/update-16-5-4/update-output-globs.ts b/packages/nx/src/migrations/update-16-5-4/update-output-globs.ts deleted file mode 100644 index 47803469a4c3f..0000000000000 --- a/packages/nx/src/migrations/update-16-5-4/update-output-globs.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Tree } from '../../generators/tree'; -import { - getProjects, - updateProjectConfiguration, -} from '../../generators/utils/project-configuration'; -import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available'; -import { TargetConfiguration } from '../../config/workspace-json-project-json'; -import { updateJson } from '../../generators/utils/json'; -import { NxJsonConfiguration } from '../../config/nx-json'; - -function replaceOutput(output: string) { - // replace {projectRoot}/folder/*.(js|map|ts) to {projectRoot}/folder/*.{js,map,ts} - const regex = /\(([^)]+)\)/g; - return output.replace(regex, (match, group1) => { - let replacements = group1.split('|').join(','); - return `{${replacements}}`; - }); -} - -export default async function updateOutputsGlobs(tree: Tree) { - for (const [projectName, projectConfiguration] of getProjects( - tree - ).entries()) { - for (const [targetName, targetConfiguration] of Object.entries( - projectConfiguration.targets ?? {} - )) { - if (!Array.isArray(targetConfiguration.outputs)) { - continue; - } - - targetConfiguration.outputs = - targetConfiguration.outputs.map(replaceOutput); - } - updateProjectConfiguration(tree, projectName, projectConfiguration); - } - - if (tree.exists('nx.json')) { - updateJson(tree, 'nx.json', (json) => { - for (const [, targetConfiguration] of Object.entries( - json.targetDefaults ?? {} - )) { - if (!Array.isArray(targetConfiguration.outputs)) { - continue; - } - - targetConfiguration.outputs = - targetConfiguration.outputs.map(replaceOutput); - } - return json; - }); - } - - await formatChangedFilesWithPrettierIfAvailable(tree); -} diff --git a/packages/nx/src/native/cache/expand_outputs.rs b/packages/nx/src/native/cache/expand_outputs.rs index b039b92ba12d2..e203bdf44c80a 100644 --- a/packages/nx/src/native/cache/expand_outputs.rs +++ b/packages/nx/src/native/cache/expand_outputs.rs @@ -19,7 +19,7 @@ pub fn expand_outputs(directory: String, entries: Vec) -> anyhow::Result return Ok(existing_paths); } - let glob_set = build_glob_set(not_found)?; + let glob_set = build_glob_set(¬_found)?; let found_paths = nx_walker_sync(directory) .filter_map(|path| { if glob_set.is_match(&path) { diff --git a/packages/nx/src/native/hasher.rs b/packages/nx/src/native/hasher.rs index 0b3fd9e3080b8..911deb54250c4 100644 --- a/packages/nx/src/native/hasher.rs +++ b/packages/nx/src/native/hasher.rs @@ -43,7 +43,7 @@ fn hash_files_matching_globs( directory: String, glob_patterns: Vec, ) -> anyhow::Result> { - let glob_set = build_glob_set(glob_patterns)?; + let glob_set = build_glob_set(&glob_patterns)?; let mut hashes = nx_walker(directory, move |receiver| { let mut collection: Vec = Vec::new(); diff --git a/packages/nx/src/native/utils/glob.rs b/packages/nx/src/native/utils/glob.rs index 1fd47db978bb9..f874beb9be5e0 100644 --- a/packages/nx/src/native/utils/glob.rs +++ b/packages/nx/src/native/utils/glob.rs @@ -1,56 +1,260 @@ use globset::{GlobBuilder, GlobSet, GlobSetBuilder}; +use once_cell::sync::Lazy; +use regex::Regex; +use std::fmt::Debug; +use std::path::Path; +use tracing::trace; -pub(crate) fn build_glob_set(globs: Vec) -> anyhow::Result { - let mut glob_set_builder = GlobSetBuilder::new(); - for glob in globs { - let glob = GlobBuilder::new(&glob) +pub struct NxGlobSetBuilder { + included_globs: GlobSetBuilder, + excluded_globs: GlobSetBuilder, +} + +impl NxGlobSetBuilder { + pub fn new>(globs: &[S]) -> anyhow::Result { + let mut glob_set_builder = NxGlobSetBuilder { + included_globs: GlobSetBuilder::new(), + excluded_globs: GlobSetBuilder::new(), + }; + let mut globs: Vec<&str> = globs.iter().map(|s| s.as_ref()).collect(); + globs.sort(); + for glob in globs { + glob_set_builder.add(glob)?; + } + Ok(glob_set_builder) + } + + pub fn add(&mut self, glob: &str) -> anyhow::Result<&mut NxGlobSetBuilder> { + let negated = glob.starts_with('!'); + let glob_string = glob.strip_prefix('!').unwrap_or(glob).to_string(); + + let glob_string = if glob_string.ends_with('/') { + format!("{}**", glob_string) + } else { + glob_string + }; + + let glob = GlobBuilder::new(&glob_string) .literal_separator(true) .build() .map_err(anyhow::Error::from)?; - glob_set_builder.add(glob); + + if negated { + self.excluded_globs.add(glob); + } else { + self.included_globs.add(glob); + } + + Ok(self) + } + + pub fn build(&self) -> anyhow::Result { + Ok(NxGlobSet { + excluded_globs: self.excluded_globs.build()?, + included_globs: self.included_globs.build()?, + }) } +} - glob_set_builder.build().map_err(anyhow::Error::from) +pub struct NxGlobSet { + included_globs: GlobSet, + excluded_globs: GlobSet, +} +impl NxGlobSet { + pub fn is_match>(&self, path: P) -> bool { + self.included_globs.is_match(path.as_ref()) && !self.excluded_globs.is_match(path.as_ref()) + } +} + +pub(crate) fn build_glob_set + Debug>(globs: &[S]) -> anyhow::Result { + let result = globs + .iter() + .map(|s| convert_glob(s.as_ref())) + .collect::>>()? + .into_iter() + .flatten() + .collect::>(); + + trace!(?globs, ?result, "converted globs to result"); + + NxGlobSetBuilder::new(&result)?.build() +} + +// path/!(cache)/** +static NEGATIVE_DIR_REGEX: Lazy = Lazy::new(|| Regex::new(r"!\{(.*?)}").unwrap()); +// path/**/(subdir1|subdir2)/*.(js|ts) +static MULTI_PATTERNS_REGEX: Lazy = Lazy::new(|| Regex::new(r"\((.*?)\)").unwrap()); + +/// Converts a glob string to a list of globs +/// e.g. `path/!(cache)/**` -> `path/**`, `!path/cache/**` +fn convert_glob(glob: &str) -> anyhow::Result> { + // If there are no negations or multiple patterns, return the glob as is + if !glob.contains('!') && !glob.contains('|') && !glob.contains('(') { + return Ok(vec![glob.to_string()]); + } + + let glob = MULTI_PATTERNS_REGEX.replace_all(glob, |caps: ®ex::Captures| { + format!("{{{}}}", &caps[1].replace('|', ",")) + }); + + let mut globs: Vec = Vec::new(); + + globs.push(NEGATIVE_DIR_REGEX.replace_all(&glob, "*").into()); + + let matches: Vec<_> = NEGATIVE_DIR_REGEX.find_iter(&glob).collect(); + + if matches.len() == 1 { + globs.push(format!("!{}", glob.replace('!', ""))); + } else { + for matched in matches { + let a = glob.replace(matched.as_str(), "*"); + globs.push(format!("!{}", a.replace('!', ""))); + } + } + + Ok(globs) } #[cfg(test)] mod test { use super::*; + use std::assert_eq; + + #[test] + fn should_convert_globs() { + let full_convert = + convert_glob("dist/!(cache|cache2)/**/!(README|LICENSE).(js|ts)").unwrap(); + assert_eq!( + full_convert, + [ + "dist/*/**/*.{js,ts}", + "!dist/*/**/{README,LICENSE}.{js,ts}", + "!dist/{cache,cache2}/**/*.{js,ts}", + ] + ); + + let no_dirs = convert_glob("dist/**/!(README|LICENSE).(js|ts)").unwrap(); + assert_eq!( + no_dirs, + ["dist/**/*.{js,ts}", "!dist/**/{README,LICENSE}.{js,ts}"] + ); + + let no_files = convert_glob("dist/!(cache|cache2)/**/*.(js|ts)").unwrap(); + assert_eq!( + no_files, + ["dist/*/**/*.{js,ts}", "!dist/{cache,cache2}/**/*.{js,ts}"] + ); + + let no_extensions = convert_glob("dist/!(cache|cache2)/**/*.js").unwrap(); + assert_eq!( + no_extensions, + ["dist/*/**/*.js", "!dist/{cache,cache2}/**/*.js"] + ); + + let no_patterns = convert_glob("dist/**/*.js").unwrap(); + assert_eq!(no_patterns, ["dist/**/*.js",]); + } #[test] fn should_detect_package_json() { - let glob_set = build_glob_set(vec![String::from("packages/*/package.json")]).unwrap(); + let glob_set = build_glob_set(&["packages/*/package.json"]).unwrap(); assert!(glob_set.is_match("packages/nx/package.json")) } #[test] fn should_not_detect_deeply_nested_package_json() { - let glob_set = build_glob_set(vec![String::from("packages/*/package.json")]).unwrap(); + let glob_set = build_glob_set(&["packages/*/package.json"]).unwrap(); assert!(!glob_set.is_match("packages/nx/test-files/package.json")) } #[test] fn should_detect_deeply_nested_package_json() { - let glob_set = build_glob_set(vec![String::from("packages/**/package.json")]).unwrap(); + let glob_set = build_glob_set(&["packages/**/package.json"]).unwrap(); assert!(glob_set.is_match("packages/nx/test-files/package.json")) } #[test] fn should_detect_node_modules() { - let glob_set = build_glob_set(vec![String::from("**/node_modules")]).unwrap(); + let glob_set = build_glob_set(&["**/node_modules"]).unwrap(); assert!(glob_set.is_match("node_modules")); assert!(glob_set.is_match("packages/nx/node_modules")); } #[test] fn should_not_detect_root_plugin_configs() { - let glob_set = build_glob_set(vec![ - // String::from("!(Cargo.toml)"), - String::from("*/**/Cargo.toml"), - ]) - .unwrap(); + let glob_set = build_glob_set(&["*/**/Cargo.toml"]).unwrap(); assert!(glob_set.is_match("packages/a/Cargo.toml")); assert!(glob_set.is_match("a/Cargo.toml")); assert!(!glob_set.is_match("Cargo.toml")) } + + #[test] + fn should_handle_negated_globs() { + let glob_set = build_glob_set(&["!nested/ignore/", "nested/"]).unwrap(); + assert!(!glob_set.is_match("file.map")); + assert!(!glob_set.is_match("nested/ignore/file.js")); + assert!(!glob_set.is_match("another-nested/nested/file.ts")); + assert!(glob_set.is_match("nested/file.js")); + assert!(glob_set.is_match("nested/nested/file.ts")); + + let glob_set = build_glob_set(&["nested/", "!nested/*.{css,map}"]).unwrap(); + assert!(glob_set.is_match("nested/file.js")); + assert!(glob_set.is_match("nested/file.ts")); + assert!(!glob_set.is_match("nested/file.css")); + assert!(!glob_set.is_match("nested/file.map")); + + let glob_set = build_glob_set(&["!nested/**/ignore/", "nested/**"]).unwrap(); + assert!(glob_set.is_match("nested/nested/file.js")); + assert!(!glob_set.is_match("nested/ignore/file.ts")); + assert!(!glob_set.is_match("nested/nested/ignore/file.ts")); + } + + #[test] + fn should_handle_multiple_globs() { + let glob_set = build_glob_set(&["nested/", "doesnt-exist/"]).unwrap(); + assert!(glob_set.is_match("nested/file.js")); + assert!(!glob_set.is_match("file.js")); + } + + #[test] + fn should_handle_complex_patterns() { + // let glob_set = + // build_glob_set(&["dist/!(cache|cache2)/**/!(README|LICENSE).(txt|md)"]).unwrap(); + // + // // matches + // assert!(glob_set.is_match("dist/nested/file.txt")); + // assert!(glob_set.is_match("dist/nested/file.md")); + // // no matches + // assert!(!glob_set.is_match("dist/file.txt")); + // assert!(!glob_set.is_match("dist/cache/nested/README.txt")); + // assert!(!glob_set.is_match("dist/nested/LICENSE.md")); + // assert!(!glob_set.is_match("dist/cache/file.txt")); + // assert!(!glob_set.is_match("dist/cache2/file.txt")); + // assert!(!glob_set.is_match("dist/cache2/README.txt")); + // assert!(!glob_set.is_match("dist/LICENSE.md")); + // assert!(!glob_set.is_match("dist/README.txt")); + + let glob_set = build_glob_set(&["dist/*.(js|ts)"]).unwrap(); + // matches + assert!(glob_set.is_match("dist/file.js")); + assert!(glob_set.is_match("dist/file.ts")); + //no matches + assert!(!glob_set.is_match("dist/file.txt")); + assert!(!glob_set.is_match("dist/nested/file.js")); + + let glob_set = build_glob_set(&["dist/**/!(main).(js|ts)"]).unwrap(); + // matches + assert!(glob_set.is_match("dist/file.js")); + //no matches + assert!(!glob_set.is_match("dist/main.js")); + + let glob_set = build_glob_set(&["dist/!(main|cache)/"]).unwrap(); + // matches + assert!(glob_set.is_match("dist/nested/")); + // no matches + assert!(!glob_set.is_match("dist/main.js")); + assert!(!glob_set.is_match("dist/file.js")); + assert!(!glob_set.is_match("dist/cache/")); + assert!(!glob_set.is_match("dist/main/")); + } } diff --git a/packages/nx/src/native/walker.rs b/packages/nx/src/native/walker.rs index d017b7db7b656..0f79f292c9f6b 100644 --- a/packages/nx/src/native/walker.rs +++ b/packages/nx/src/native/walker.rs @@ -17,11 +17,8 @@ where { let base_dir: PathBuf = directory.as_ref().into(); - let ignore_glob_set = build_glob_set(vec![ - String::from("**/node_modules"), - String::from("**/.git"), - ]) - .expect("These static ignores always build"); + let ignore_glob_set = + build_glob_set(&["**/node_modules", "**/.git"]).expect("These static ignores always build"); // Use WalkDir instead of ignore::WalkBuilder because it's faster WalkDir::new(&base_dir) @@ -47,11 +44,8 @@ where let directory = directory.as_ref(); let nx_ignore = directory.join(".nxignore"); - let ignore_glob_set = build_glob_set(vec![ - String::from("**/node_modules"), - String::from("**/.git"), - ]) - .expect("These static ignores always build"); + let ignore_glob_set = + build_glob_set(&["**/node_modules", "**/.git"]).expect("These static ignores always build"); let mut walker = WalkBuilder::new(directory); walker.hidden(false); diff --git a/packages/nx/src/native/watch/types.rs b/packages/nx/src/native/watch/types.rs index 88fa2b0f2df4e..4a4453a3ebc22 100644 --- a/packages/nx/src/native/watch/types.rs +++ b/packages/nx/src/native/watch/types.rs @@ -2,8 +2,6 @@ use napi::bindgen_prelude::*; use std::path::PathBuf; use tracing::trace; -use watchexec_events::filekind::ModifyKind::Name; -use watchexec_events::filekind::RenameMode; use watchexec_events::{Event, Tag}; #[napi(string_enum)] @@ -96,6 +94,8 @@ impl From<&Event> for WatchEventInternal { #[cfg(not(target_os = "macos"))] { use watchexec_events::filekind::FileEventKind; + use watchexec_events::filekind::ModifyKind::Name; + use watchexec_events::filekind::RenameMode; match event_kind { FileEventKind::Create(_) => EventType::create, diff --git a/packages/nx/src/native/workspace/get_config_files.rs b/packages/nx/src/native/workspace/get_config_files.rs index 518cc3b60827f..13ed96dd7b49c 100644 --- a/packages/nx/src/native/workspace/get_config_files.rs +++ b/packages/nx/src/native/workspace/get_config_files.rs @@ -1,7 +1,6 @@ -use crate::native::utils::glob::build_glob_set; +use crate::native::utils::glob::{build_glob_set, NxGlobSet}; use crate::native::utils::path::Normalize; use crate::native::walker::nx_walker; -use globset::GlobSet; use napi::JsObject; use std::collections::hash_map::Entry; @@ -19,7 +18,7 @@ pub fn get_project_configurations( where ConfigurationParser: Fn(Vec) -> napi::Result>, { - let globs = build_glob_set(globs)?; + let globs = build_glob_set(&globs)?; let config_paths: Vec = nx_walker(workspace_root, move |rec| { let mut config_paths: HashMap = HashMap::new(); for (path, _) in rec { @@ -38,7 +37,7 @@ where pub fn insert_config_file_into_map( path: PathBuf, config_paths: &mut HashMap, - globs: &GlobSet, + globs: &NxGlobSet, ) { if globs.is_match(&path) { let parent = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf(); @@ -78,7 +77,7 @@ mod test { #[test] fn should_insert_config_files_properly() { let mut config_paths: HashMap = HashMap::new(); - let globs = build_glob_set(vec!["**/*".into()]).unwrap(); + let globs = build_glob_set(&["**/*"]).unwrap(); insert_config_file_into_map(PathBuf::from("project.json"), &mut config_paths, &globs); insert_config_file_into_map(PathBuf::from("package.json"), &mut config_paths, &globs); diff --git a/packages/nx/src/native/workspace/get_nx_workspace_files.rs b/packages/nx/src/native/workspace/get_nx_workspace_files.rs index af90b926ec34c..92edc5d536c5c 100644 --- a/packages/nx/src/native/workspace/get_nx_workspace_files.rs +++ b/packages/nx/src/native/workspace/get_nx_workspace_files.rs @@ -112,7 +112,7 @@ fn create_root_map( type WorkspaceData = (HashSet, Vec); fn get_file_data(workspace_root: &str, globs: Vec) -> anyhow::Result { - let globs = build_glob_set(globs)?; + let globs = build_glob_set(&globs)?; let (projects, file_data) = nx_walker(workspace_root, move |rec| { let mut projects: HashMap = HashMap::new(); let mut file_hashes: Vec = vec![];